1 /*
2     SPDX-FileCopyrightText: 2012 Jasem Mutlaq <mutlaqja@ikarustech.com>
3 
4     SPDX-License-Identifier: GPL-2.0-or-later
5 */
6 
7 #include "capture.h"
8 
9 #include "captureadaptor.h"
10 #include "kstars.h"
11 #include "kstarsdata.h"
12 #include "Options.h"
13 #include "rotatorsettings.h"
14 #include "sequencejob.h"
15 #include "placeholderpath.h"
16 #include "skymap.h"
17 #include "ui_calibrationoptions.h"
18 #include "auxiliary/QProgressIndicator.h"
19 #include "auxiliary/ksmessagebox.h"
20 #include "ekos/manager.h"
21 #include "ekos/auxiliary/darklibrary.h"
22 #include "scriptsmanager.h"
23 #include "fitsviewer/fitsdata.h"
24 #include "fitsviewer/fitsview.h"
25 #include "indi/driverinfo.h"
26 #include "indi/indifilter.h"
27 #include "indi/clientmanager.h"
28 #include "oal/observeradd.h"
29 
30 #include <basedevice.h>
31 
32 #include <ekos_capture_debug.h>
33 
34 #define MF_TIMER_TIMEOUT    90000
35 #define GD_TIMER_TIMEOUT    60000
36 #define MF_RA_DIFF_LIMIT    4
37 
38 // Wait 3-minutes as maximum beyond exposure
39 // value.
40 #define CAPTURE_TIMEOUT_THRESHOLD  180000
41 
42 // Current Sequence File Format:
43 #define SQ_FORMAT_VERSION 2.1
44 // We accept file formats with version back to:
45 #define SQ_COMPAT_VERSION 2.0
46 
47 namespace Ekos
48 {
Capture()49 Capture::Capture()
50 {
51     setupUi(this);
52 
53     qRegisterMetaType<Ekos::CaptureState>("Ekos::CaptureState");
54     qDBusRegisterMetaType<Ekos::CaptureState>();
55 
56     new CaptureAdaptor(this);
57     QDBusConnection::sessionBus().registerObject("/KStars/Ekos/Capture", this);
58     QPointer<QDBusInterface> ekosInterface = new QDBusInterface("org.kde.kstars", "/KStars/Ekos", "org.kde.kstars.Ekos",
59             QDBusConnection::sessionBus(), this);
60 
61     // Connecting DBus signals
62     QDBusConnection::sessionBus().connect("org.kde.kstars", "/KStars/Ekos", "org.kde.kstars.Ekos", "newModule", this,
63                                           SLOT(registerNewModule(QString)));
64 
65     // ensure that the mount interface is present
66     registerNewModule("Mount");
67 
68     KStarsData::Instance()->userdb()->GetAllDSLRInfos(DSLRInfos);
69 
70     if (DSLRInfos.count() > 0)
71     {
72         qCDebug(KSTARS_EKOS_CAPTURE) << "DSLR Cameras Info:";
73         qCDebug(KSTARS_EKOS_CAPTURE) << DSLRInfos;
74     }
75 
76     dirPath = QUrl::fromLocalFile(QDir::homePath());
77 
78     //isAutoGuiding   = false;
79 
80     rotatorSettings.reset(new RotatorSettings(this));
81 
82     pi = new QProgressIndicator(this);
83 
84     progressLayout->addWidget(pi, 0, 4, 1, 1);
85 
86     seqFileCount = 0;
87     //seqWatcher		= new KDirWatch();
88     seqTimer = new QTimer(this);
89     connect(seqTimer, &QTimer::timeout, this, &Ekos::Capture::captureImage);
90 
91     connect(startB, &QPushButton::clicked, this, &Ekos::Capture::toggleSequence);
92     connect(pauseB, &QPushButton::clicked, this, &Ekos::Capture::pause);
93     connect(darkLibraryB, &QPushButton::clicked, DarkLibrary::Instance(), &QDialog::show);
94     connect(temperatureRegulationB, &QPushButton::clicked, this, &Ekos::Capture::showTemperatureRegulation);
95 
96     startB->setIcon(QIcon::fromTheme("media-playback-start"));
97     startB->setAttribute(Qt::WA_LayoutUsesWidgetRect);
98     pauseB->setIcon(QIcon::fromTheme("media-playback-pause"));
99     pauseB->setAttribute(Qt::WA_LayoutUsesWidgetRect);
100 
101     filterManagerB->setIcon(QIcon::fromTheme("view-filter"));
102     filterManagerB->setAttribute(Qt::WA_LayoutUsesWidgetRect);
103 
104     filterWheelS->addItem("--");
105 
106     connect(captureBinHN, static_cast<void (QSpinBox::*)(int)>(&QSpinBox::valueChanged), captureBinVN, &QSpinBox::setValue);
107 
108     connect(cameraS, static_cast<void (QComboBox::*)(const QString &)>(&QComboBox::activated), this,
109             &Ekos::Capture::setDefaultCCD);
110     connect(cameraS, static_cast<void (QComboBox::*)(int)>(&QComboBox::activated), this, &Ekos::Capture::checkCCD);
111 
112     connect(liveVideoB, &QPushButton::clicked, this, &Ekos::Capture::toggleVideo);
113 
114     guideDeviationTimer.setInterval(GD_TIMER_TIMEOUT);
115     connect(&guideDeviationTimer, &QTimer::timeout, this, &Ekos::Capture::checkGuideDeviationTimeout);
116 
117     connect(clearConfigurationB, &QPushButton::clicked, this, &Ekos::Capture::clearCameraConfiguration);
118 
119     darkB->setChecked(Options::autoDark());
120     connect(darkB, &QAbstractButton::toggled, this, [this]()
121     {
122         Options::setAutoDark(darkB->isChecked());
123     });
124 
125     connect(filterWheelS, static_cast<void (QComboBox::*)(const QString &)>(&QComboBox::activated), this,
126             &Ekos::Capture::setDefaultFilterWheel);
127     connect(filterWheelS, static_cast<void (QComboBox::*)(int)>(&QComboBox::activated), this,
128             &Ekos::Capture::checkFilter);
129 
130     connect(restartCameraB, &QPushButton::clicked, [this]()
131     {
132         restartCamera(cameraS->currentText());
133     });
134 
135     connect(cameraTemperatureS, &QCheckBox::toggled, [this](bool toggled)
136     {
137         if (currentCCD)
138         {
139             QVariantMap auxInfo = currentCCD->getDriverInfo()->getAuxInfo();
140             auxInfo[QString("%1_TC").arg(currentCCD->getDeviceName())] = toggled;
141             currentCCD->getDriverInfo()->setAuxInfo(auxInfo);
142         }
143     });
144 
145     connect(filterEditB, &QPushButton::clicked, this, &Ekos::Capture::editFilterName);
146 
147     connect(captureFilterS, static_cast<void(QComboBox::*)(const QString &)>(&QComboBox::currentIndexChanged),
148             [ = ]()
149     {
150         updateHFRThreshold();
151     });
152     connect(previewB, &QPushButton::clicked, this, &Ekos::Capture::captureOne);
153     connect(loopB, &QPushButton::clicked, this, &Ekos::Capture::startFraming);
154 
155     //connect( seqWatcher, SIGNAL(dirty(QString)), this, &Ekos::Capture::checkSeqFile(QString)));
156 
157     connect(addToQueueB, &QPushButton::clicked, this, &Ekos::Capture::addJob);
158     connect(removeFromQueueB, &QPushButton::clicked, this, &Ekos::Capture::removeJobFromQueue);
159     connect(queueUpB, &QPushButton::clicked, this, &Ekos::Capture::moveJobUp);
160     connect(queueDownB, &QPushButton::clicked, this, &Ekos::Capture::moveJobDown);
161     connect(selectFileDirectoryB, &QPushButton::clicked, this, &Ekos::Capture::saveFITSDirectory);
162     connect(queueSaveB, &QPushButton::clicked, this, static_cast<void(Ekos::Capture::*)()>(&Ekos::Capture::saveSequenceQueue));
163     connect(queueSaveAsB, &QPushButton::clicked, this, &Ekos::Capture::saveSequenceQueueAs);
164     connect(queueLoadB, &QPushButton::clicked, this, static_cast<void(Ekos::Capture::*)()>(&Ekos::Capture::loadSequenceQueue));
165     connect(resetB, &QPushButton::clicked, this, &Ekos::Capture::resetJobs);
166     connect(queueTable->selectionModel(), &QItemSelectionModel::currentRowChanged, this, &Ekos::Capture::selectedJobChanged);
167     connect(queueTable, &QAbstractItemView::doubleClicked, this, &Ekos::Capture::editJob);
168     connect(queueTable, &QTableWidget::itemSelectionChanged, this, &Ekos::Capture::resetJobEdit);
169     connect(setTemperatureB, &QPushButton::clicked, [&]()
170     {
171         if (currentCCD)
172             currentCCD->setTemperature(cameraTemperatureN->value());
173     });
174     connect(coolerOnB, &QPushButton::clicked, [&]()
175     {
176         if (currentCCD)
177             currentCCD->setCoolerControl(true);
178     });
179     connect(coolerOffB, &QPushButton::clicked, [&]()
180     {
181         if (currentCCD)
182             currentCCD->setCoolerControl(false);
183     });
184     connect(cameraTemperatureN, &QDoubleSpinBox::editingFinished, setTemperatureB,
185             static_cast<void (QPushButton::*)()>(&QPushButton::setFocus));
186     connect(captureTypeS, static_cast<void (QComboBox::*)(int)>(&QComboBox::activated), this, &Ekos::Capture::checkFrameType);
187     connect(resetFrameB, &QPushButton::clicked, this, &Ekos::Capture::resetFrame);
188     connect(calibrationB, &QPushButton::clicked, this, &Ekos::Capture::openCalibrationDialog);
189     connect(rotatorB, &QPushButton::clicked, rotatorSettings.get(), &Ekos::Capture::show);
190 
191     connect(scriptManagerB, &QPushButton::clicked, this, &Ekos::Capture::handleScriptsManager);
192 
193     addToQueueB->setIcon(QIcon::fromTheme("list-add"));
194     addToQueueB->setAttribute(Qt::WA_LayoutUsesWidgetRect);
195     removeFromQueueB->setIcon(QIcon::fromTheme("list-remove"));
196     removeFromQueueB->setAttribute(Qt::WA_LayoutUsesWidgetRect);
197     queueUpB->setIcon(QIcon::fromTheme("go-up"));
198     queueUpB->setAttribute(Qt::WA_LayoutUsesWidgetRect);
199     queueDownB->setIcon(QIcon::fromTheme("go-down"));
200     queueDownB->setAttribute(Qt::WA_LayoutUsesWidgetRect);
201     selectFileDirectoryB->setIcon(QIcon::fromTheme("document-open-folder"));
202     selectFileDirectoryB->setAttribute(Qt::WA_LayoutUsesWidgetRect);
203     queueLoadB->setIcon(QIcon::fromTheme("document-open"));
204     queueLoadB->setAttribute(Qt::WA_LayoutUsesWidgetRect);
205     queueSaveB->setIcon(QIcon::fromTheme("document-save"));
206     queueSaveB->setAttribute(Qt::WA_LayoutUsesWidgetRect);
207     queueSaveAsB->setIcon(QIcon::fromTheme("document-save-as"));
208     queueSaveAsB->setAttribute(Qt::WA_LayoutUsesWidgetRect);
209     resetB->setIcon(QIcon::fromTheme("system-reboot"));
210     resetB->setAttribute(Qt::WA_LayoutUsesWidgetRect);
211     resetFrameB->setIcon(QIcon::fromTheme("view-refresh"));
212     resetFrameB->setAttribute(Qt::WA_LayoutUsesWidgetRect);
213     calibrationB->setIcon(QIcon::fromTheme("run-build"));
214     calibrationB->setAttribute(Qt::WA_LayoutUsesWidgetRect);
215     rotatorB->setIcon(QIcon::fromTheme("kstars_solarsystem"));
216     rotatorB->setAttribute(Qt::WA_LayoutUsesWidgetRect);
217 
218     addToQueueB->setToolTip(i18n("Add job to sequence queue"));
219     removeFromQueueB->setToolTip(i18n("Remove job from sequence queue"));
220 
221     fileDirectoryT->setText(Options::fitsDir());
222 
223     ////////////////////////////////////////////////////////////////////////
224     /// Settings
225     ////////////////////////////////////////////////////////////////////////
226     // #0 Start Guide Deviation Check
227     startGuiderDriftS->setChecked(Options::enforceStartGuiderDrift());
228     connect(startGuiderDriftS, &QCheckBox::toggled, [ = ](bool checked)
229     {
230         Options::setEnforceStartGuiderDrift(checked);
231     });
232 
233     // #1 Abort Guide Deviation Check
234     limitGuideDeviationS->setChecked(Options::enforceGuideDeviation());
235     connect(limitGuideDeviationS, &QCheckBox::toggled, [ = ](bool checked)
236     {
237         Options::setEnforceGuideDeviation(checked);
238     });
239 
240     // #2 Guide Deviation Value
241     limitGuideDeviationN->setValue(Options::guideDeviation());
242     connect(limitGuideDeviationN, &QDoubleSpinBox::editingFinished, [ = ]()
243     {
244         Options::setGuideDeviation(limitGuideDeviationN->value());
245     });
246 
247     // 3. Autofocus HFR Check
248     limitFocusHFRS->setChecked(Options::enforceAutofocus());
249     connect(limitFocusHFRS, &QCheckBox::toggled, [ = ](bool checked)
250     {
251         Options::setEnforceAutofocus(checked);
252         if (checked == false)
253             isInSequenceFocus = false;
254     });
255 
256     // 4. Autofocus HFR Deviation
257     limitFocusHFRN->setValue(Options::hFRDeviation());
258     connect(limitFocusHFRN, &QDoubleSpinBox::editingFinished, [ = ]()
259     {
260         Options::setHFRDeviation(limitFocusHFRN->value());
261     });
262 
263     // 5. Autofocus temperature Check
264     limitFocusDeltaTS->setChecked(Options::enforceAutofocusOnTemperature());
265     connect(limitFocusDeltaTS, &QCheckBox::toggled, [ = ](bool checked)
266     {
267         Options::setEnforceAutofocusOnTemperature(checked);
268         if (checked == false)
269             isTemperatureDeltaCheckActive = false;
270     });
271 
272     // 6. Autofocus temperature Delta
273     limitFocusDeltaTN->setValue(Options::maxFocusTemperatureDelta());
274     connect(limitFocusDeltaTN, &QDoubleSpinBox::editingFinished, [ = ]()
275     {
276         Options::setMaxFocusTemperatureDelta(limitFocusDeltaTN->value());
277     });
278 
279     // 7. Refocus Every Check
280     limitRefocusS->setChecked(Options::enforceRefocusEveryN());
281     connect(limitRefocusS, &QCheckBox::toggled, [ = ](bool checked)
282     {
283         Options::setEnforceRefocusEveryN(checked);
284     });
285 
286     // 8. Refocus Every Value
287     limitRefocusN->setValue(static_cast<int>(Options::refocusEveryN()));
288     connect(limitRefocusN, &QDoubleSpinBox::editingFinished, [ = ]()
289     {
290         Options::setRefocusEveryN(static_cast<uint>(limitRefocusN->value()));
291     });
292 
293     // 9. File settings: filter name
294     fileFilterS->setChecked(Options::fileSettingsUseFilter());
295     connect(fileFilterS, &QCheckBox::toggled, [ = ](bool checked)
296     {
297         Options::setFileSettingsUseFilter(checked);
298     });
299 
300     // 10. File settings: duration
301     fileDurationS->setChecked(Options::fileSettingsUseDuration());
302     connect(fileDurationS, &QCheckBox::toggled, [ = ](bool checked)
303     {
304         Options::setFileSettingsUseDuration(checked);
305     });
306 
307     // 11. File settings: timestamp
308     fileTimestampS->setChecked(Options::fileSettingsUseTimestamp());
309     connect(fileTimestampS, &QCheckBox::toggled, [ = ](bool checked)
310     {
311         Options::setFileSettingsUseTimestamp(checked);
312     });
313 
314     QCheckBox * const checkBoxes[] =
315     {
316         limitGuideDeviationS,
317         limitRefocusS,
318         limitGuideDeviationS,
319     };
320     for (const QCheckBox * control : checkBoxes)
321         connect(control, &QCheckBox::toggled, this, &Ekos::Capture::setDirty);
322 
323     QDoubleSpinBox * const dspinBoxes[]
324     {
325         limitFocusHFRN,
326         limitFocusDeltaTN,
327         limitGuideDeviationN,
328     };
329     for (const QDoubleSpinBox * control : dspinBoxes)
330         connect(control, static_cast<void (QDoubleSpinBox::*)(double)>(&QDoubleSpinBox::valueChanged), this,
331                 &Ekos::Capture::setDirty);
332 
333     connect(fileUploadModeS, static_cast<void (QComboBox::*)(int)>(&QComboBox::activated), this, &Ekos::Capture::setDirty);
334     connect(fileRemoteDirT, &QLineEdit::editingFinished, this, &Ekos::Capture::setDirty);
335 
336     m_ObserverName = Options::defaultObserver();
337     observerB->setIcon(QIcon::fromTheme("im-user"));
338     observerB->setAttribute(Qt::WA_LayoutUsesWidgetRect);
339     connect(observerB, &QPushButton::clicked, this, &Ekos::Capture::showObserverDialog);
340 
341     // Exposure Timeout
342     captureTimeout.setSingleShot(true);
343     connect(&captureTimeout, &QTimer::timeout, this, &Ekos::Capture::processCaptureTimeout);
344 
345     // Post capture script
346     connect(&m_CaptureScript, static_cast<void (QProcess::*)(int exitCode, QProcess::ExitStatus status)>(&QProcess::finished),
347             this, &Ekos::Capture::scriptFinished);
348     connect(&m_CaptureScript, &QProcess::errorOccurred,
349             [this](QProcess::ProcessError error)
350     {
351         Q_UNUSED(error)
352         appendLogText(m_CaptureScript.errorString());
353         scriptFinished(-1, QProcess::NormalExit);
354     });
355     connect(&m_CaptureScript, &QProcess::readyReadStandardError,
356             [this]()
357     {
358         appendLogText(m_CaptureScript.readAllStandardError());
359     });
360     connect(&m_CaptureScript, &QProcess::readyReadStandardOutput,
361             [this]()
362     {
363         appendLogText(m_CaptureScript.readAllStandardOutput());
364     });
365 
366     // Remote directory
367     connect(fileUploadModeS, static_cast<void (QComboBox::*)(int)>(&QComboBox::activated), this,
368             [&](int index)
369     {
370         fileRemoteDirT->setEnabled(index != 0);
371     });
372 
373     customPropertiesDialog.reset(new CustomProperties());
374     connect(customValuesB, &QPushButton::clicked, [&]()
375     {
376         customPropertiesDialog.get()->show();
377         customPropertiesDialog.get()->raise();
378     });
379     connect(customPropertiesDialog.get(), &CustomProperties::valueChanged, [&]()
380     {
381         const double newGain = getGain();
382         if (captureGainN && newGain >= 0)
383             captureGainN->setValue(newGain);
384         const int newOffset = getOffset();
385         if (newOffset >= 0)
386             captureOffsetN->setValue(newOffset);
387     });
388 
389     flatFieldSource = static_cast<FlatFieldSource>(Options::calibrationFlatSourceIndex());
390     flatFieldDuration = static_cast<FlatFieldDuration>(Options::calibrationFlatDurationIndex());
391     wallCoord.setAz(Options::calibrationWallAz());
392     wallCoord.setAlt(Options::calibrationWallAlt());
393     targetADU = Options::calibrationADUValue();
394     targetADUTolerance = Options::calibrationADUValueTolerance();
395 
396     fileDirectoryT->setText(Options::captureDirectory());
397     connect(fileDirectoryT, &QLineEdit::textChanged, [&]()
398     {
399         Options::setCaptureDirectory(fileDirectoryT->text());
400     });
401 
402     if (Options::remoteCaptureDirectory().isEmpty() == false)
403     {
404         fileRemoteDirT->setText(Options::remoteCaptureDirectory());
405     }
406     connect(fileRemoteDirT, &QLineEdit::editingFinished, [&]()
407     {
408         Options::setRemoteCaptureDirectory(fileRemoteDirT->text());
409     });
410 
411     // Keep track of TARGET transfer format when changing CCDs (FITS or NATIVE). Actual format is not changed until capture
412     connect(
413         captureFormatS, static_cast<void (QComboBox::*)(int)>(&QComboBox::activated), this,
414         [&](uint index)
415     {
416         if (currentCCD)
417             currentCCD->setTargetTransferFormat(static_cast<ISD::CCD::TransferFormat>(index));
418         Options::setCaptureFormatIndex(index);
419     });
420 
421     // Load FIlter Offets
422     //loadFilterOffsets();
423 
424     //Note:  This is to prevent a button from being called the default button
425     //and then executing when the user hits the enter key such as when on a Text Box
426     QList<QPushButton *> qButtons = findChildren<QPushButton *>();
427     for (auto &button : qButtons)
428         button->setAutoDefault(false);
429 
430     //This Timer will update the Exposure time in the capture module to display the estimated download time left
431     //It will also update the Exposure time left in the Summary Screen.
432     //It fires every 100 ms while images are downloading.
433     downloadProgressTimer.setInterval(100);
434     connect(&downloadProgressTimer, &QTimer::timeout, this, &Ekos::Capture::setDownloadProgress);
435 
436     DarkLibrary::Instance()->setCaptureModule(this);
437     m_DarkProcessor = new DarkProcessor(this);
438     connect(m_DarkProcessor, &DarkProcessor::newLog, this, &Ekos::Capture::appendLogText);
439     connect(m_DarkProcessor, &DarkProcessor::darkFrameCompleted, this, &Ekos::Capture::setCaptureComplete);
440 }
441 
~Capture()442 Capture::~Capture()
443 {
444     qDeleteAll(jobs);
445 }
446 
setDefaultCCD(QString ccd)447 void Capture::setDefaultCCD(QString ccd)
448 {
449     Options::setDefaultCaptureCCD(ccd);
450 }
451 
setDefaultFilterWheel(QString filterWheel)452 void Capture::setDefaultFilterWheel(QString filterWheel)
453 {
454     Options::setDefaultCaptureFilterWheel(filterWheel);
455 }
456 
addCCD(ISD::GDInterface * newCCD)457 void Capture::addCCD(ISD::GDInterface * newCCD)
458 {
459     ISD::CCD * ccd = static_cast<ISD::CCD *>(newCCD);
460 
461     if (CCDs.contains(ccd))
462         return;
463 
464     CCDs.append(ccd);
465 
466     cameraS->addItem(ccd->getDeviceName());
467 
468     DarkLibrary::Instance()->addCamera(newCCD);
469 
470     if (Filters.count() > 0)
471         syncFilterInfo();
472 
473     checkCCD();
474 
475     emit settingsUpdated(getPresetSettings());
476 }
477 
addGuideHead(ISD::GDInterface * newCCD)478 void Capture::addGuideHead(ISD::GDInterface * newCCD)
479 {
480     QString guiderName = newCCD->getDeviceName() + QString(" Guider");
481 
482     if (cameraS->findText(guiderName) == -1)
483     {
484         cameraS->addItem(guiderName);
485         CCDs.append(static_cast<ISD::CCD *>(newCCD));
486     }
487 }
488 
addFilter(ISD::GDInterface * newFilter)489 void Capture::addFilter(ISD::GDInterface * newFilter)
490 {
491     foreach (ISD::GDInterface * filter, Filters)
492     {
493         if (filter->getDeviceName() == newFilter->getDeviceName())
494             return;
495     }
496 
497     filterWheelS->addItem(newFilter->getDeviceName());
498 
499     Filters.append(static_cast<ISD::Filter *>(newFilter));
500 
501     filterManagerB->setEnabled(true);
502 
503     int filterWheelIndex = 1;
504     if (Options::defaultCaptureFilterWheel().isEmpty() == false)
505         filterWheelIndex = filterWheelS->findText(Options::defaultCaptureFilterWheel());
506 
507     if (filterWheelIndex < 1)
508         filterWheelIndex = 1;
509 
510     checkFilter(filterWheelIndex);
511     filterWheelS->setCurrentIndex(filterWheelIndex);
512 
513     emit settingsUpdated(getPresetSettings());
514 }
515 
pause()516 void Capture::pause()
517 {
518     if (m_State != CAPTURE_CAPTURING)
519     {
520         // Ensure that the pause function is only called during frame capturing
521         // Handling it this way is by far easier than trying to enable/disable the pause button
522         // Fixme: make pausing possible at all stages. This makes it necessary to separate the pausing states from CaptureState.
523         appendLogText(i18n("Pausing only possible while frame capture is running."));
524         qCInfo(KSTARS_EKOS_CAPTURE) << "Pause button pressed while not capturing.";
525         return;
526     }
527     pauseFunction = nullptr;
528     m_State         = CAPTURE_PAUSE_PLANNED;
529     emit newStatus(Ekos::CAPTURE_PAUSE_PLANNED);
530     appendLogText(i18n("Sequence shall be paused after current exposure is complete."));
531     pauseB->setEnabled(false);
532 
533     startB->setIcon(QIcon::fromTheme("media-playback-start"));
534     startB->setToolTip(i18n("Resume Sequence"));
535 }
536 
toggleSequence()537 void Capture::toggleSequence()
538 {
539     if (m_State == CAPTURE_PAUSE_PLANNED || m_State == CAPTURE_PAUSED)
540     {
541         startB->setIcon(
542             QIcon::fromTheme("media-playback-stop"));
543         startB->setToolTip(i18n("Stop Sequence"));
544         pauseB->setEnabled(true);
545 
546         m_State = CAPTURE_CAPTURING;
547         emit newStatus(Ekos::CAPTURE_CAPTURING);
548 
549         appendLogText(i18n("Sequence resumed."));
550 
551         // Call from where ever we have left of when we paused
552         if (pauseFunction)
553             (this->*pauseFunction)();
554     }
555     else if (m_State == CAPTURE_IDLE || m_State == CAPTURE_ABORTED || m_State == CAPTURE_COMPLETE)
556     {
557         start();
558     }
559     else
560     {
561         abort();
562     }
563 }
564 
registerNewModule(const QString & name)565 void Capture::registerNewModule(const QString &name)
566 {
567     qCDebug(KSTARS_EKOS_CAPTURE) << "Registering new Module (" << name << ")";
568     if (name == "Mount" && mountInterface == nullptr)
569     {
570         mountInterface = new QDBusInterface("org.kde.kstars", "/KStars/Ekos/Mount",
571                                             "org.kde.kstars.Ekos.Mount", QDBusConnection::sessionBus(), this);
572 
573     }
574 }
575 
576 /**
577  * @brief Start the execution of the Capture::SequenceJob list #jobs.
578  *
579  * Starting the execution of the Capture::SequenceJob list selects the first job
580  * from the ist that may be executed and starts to prepare the job (@see prepareJob()).
581  *
582  * Several factors determine, which of the jobs will be selected:
583  * - First, the list is searched to find the first job that is marked as idle or aborted.
584  * -  If none is found, it is checked whether ignoring job progress is set. If yes,
585  *    all jobs are are reset (@see reset()) and the first one from the list is selected.
586  *    If no, the user is asked whether the jobs should be reset. If the user declines,
587  *    starting is aborted.
588  */
start()589 void Capture::start()
590 {
591     //    if (darkSubCheck->isChecked())
592     //    {
593     //        KSNotification::error(i18n("Auto dark subtract is not supported in batch mode."));
594     //        return;
595     //    }
596 
597     // Reset progress option if there is no captured frame map set at the time of start - fixes the end-user setting the option just before starting
598     ignoreJobProgress = !capturedFramesMap.count() && Options::alwaysResetSequenceWhenStarting();
599 
600     if (queueTable->rowCount() == 0)
601     {
602         if (addJob() == false)
603             return;
604     }
605 
606     SequenceJob * first_job = nullptr;
607 
608     for (auto &job : jobs)
609     {
610         if (job->getStatus() == SequenceJob::JOB_IDLE || job->getStatus() == SequenceJob::JOB_ABORTED)
611         {
612             first_job = job;
613             break;
614         }
615     }
616 
617     // If there are no idle nor aborted jobs, question is whether to reset and restart
618     // Scheduler will start a non-empty new job each time and doesn't use this execution path
619     if (first_job == nullptr)
620     {
621         // If we have at least one job that are in error, bail out, even if ignoring job progress
622         for (auto &job : jobs)
623         {
624             if (job->getStatus() != SequenceJob::JOB_DONE)
625             {
626                 appendLogText(i18n("No pending jobs found. Please add a job to the sequence queue."));
627                 return;
628             }
629         }
630 
631         // If we only have completed jobs and we don't ignore job progress, ask the end-user what to do
632         if (!ignoreJobProgress)
633             if(KMessageBox::warningContinueCancel(
634                         nullptr,
635                         i18n("All jobs are complete. Do you want to reset the status of all jobs and restart capturing?"),
636                         i18n("Reset job status"), KStandardGuiItem::cont(), KStandardGuiItem::cancel(),
637                         "reset_job_complete_status_warning") != KMessageBox::Continue)
638                 return;
639 
640         // If the end-user accepted to reset, reset all jobs and restart
641         for (auto &job : jobs)
642             job->resetStatus();
643 
644         first_job = jobs.first();
645     }
646     // If we need to ignore job progress, systematically reset all jobs and restart
647     // Scheduler will never ignore job progress and doesn't use this path
648     else if (ignoreJobProgress)
649     {
650         appendLogText(i18n("Warning: option \"Always Reset Sequence When Starting\" is enabled and resets the sequence counts."));
651         for (auto &job : jobs)
652             job->resetStatus();
653     }
654 
655     // Refocus timer should not be reset on deviation error
656     if (m_DeviationDetected == false && m_State != CAPTURE_SUSPENDED)
657     {
658         // start timer to measure time until next forced refocus
659         startRefocusEveryNTimer();
660     }
661 
662     // Only reset these counters if we are NOT restarting from deviation errors
663     // So when starting a new job or fresh then we reset them.
664     if (m_DeviationDetected == false)
665     {
666         ditherCounter     = Options::ditherFrames();
667         inSequenceFocusCounter = Options::inSequenceCheckFrames();
668     }
669 
670     m_DeviationDetected = false;
671     m_SpikesDetected = 0;
672 
673     m_State = CAPTURE_PROGRESS;
674     emit newStatus(Ekos::CAPTURE_PROGRESS);
675 
676     startB->setIcon(QIcon::fromTheme("media-playback-stop"));
677     startB->setToolTip(i18n("Stop Sequence"));
678     pauseB->setEnabled(true);
679 
680     setBusy(true);
681 
682     if (limitGuideDeviationS->isChecked() && autoGuideReady == false)
683         appendLogText(i18n("Warning: Guide deviation is selected but autoguide process was not started."));
684     if (limitFocusHFRS->isChecked() && m_AutoFocusReady == false)
685         appendLogText(i18n("Warning: in-sequence focusing is selected but autofocus process was not started."));
686     if (limitFocusDeltaTS->isChecked() && m_AutoFocusReady == false)
687         appendLogText(i18n("Warning: temperature delta check is selected but autofocus process was not started."));
688 
689     if (currentCCD->getTelescopeType() != ISD::CCD::TELESCOPE_PRIMARY)
690     {
691         connect(KSMessageBox::Instance(), &KSMessageBox::accepted, this, [ = ]()
692         {
693             KSMessageBox::Instance()->disconnect(this);
694             currentCCD->setTelescopeType(ISD::CCD::TELESCOPE_PRIMARY);
695             prepareJob(first_job);
696         });
697         connect(KSMessageBox::Instance(), &KSMessageBox::rejected, this, [ = ]()
698         {
699             KSMessageBox::Instance()->disconnect(this);
700             prepareJob(first_job);
701         });
702 
703         KSMessageBox::Instance()->questionYesNo(i18n("Are you imaging with %1 using your primary telescope?",
704                                                 currentCCD->getDeviceName()),
705                                                 i18n("Telescope Type"), 10, true);
706     }
707     else
708         prepareJob(first_job);
709 }
710 
711 /**
712  * @brief Stop, suspend or abort the currently active job.
713  * @param targetState
714  */
stop(CaptureState targetState)715 void Capture::stop(CaptureState targetState)
716 {
717     retries         = 0;
718     //seqTotalCount   = 0;
719     //seqCurrentCount = 0;
720 
721     captureTimeout.stop();
722 
723     ADURaw.clear();
724     ExpRaw.clear();
725 
726     if (activeJob)
727     {
728         if (activeJob->getStatus() == SequenceJob::JOB_BUSY)
729         {
730             QString stopText;
731             switch (targetState)
732             {
733                 case CAPTURE_IDLE:
734                     stopText = i18n("CCD capture stopped");
735                     break;
736 
737                 case CAPTURE_SUSPENDED:
738                     stopText = i18n("CCD capture suspended");
739                     break;
740 
741                 default:
742                     stopText = i18n("CCD capture aborted");
743                     break;
744             }
745             emit captureAborted(activeJob->getExposure());
746             KSNotification::event(QLatin1String("CaptureFailed"), stopText);
747             appendLogText(stopText);
748             activeJob->abort();
749             if (activeJob->isPreview() == false)
750             {
751                 int index = jobs.indexOf(activeJob);
752                 QJsonObject oneSequence = m_SequenceArray[index].toObject();
753                 oneSequence["Status"] = "Aborted";
754                 m_SequenceArray.replace(index, oneSequence);
755                 emit sequenceChanged(m_SequenceArray);
756             }
757         }
758 
759         // In case of batch job
760         if (activeJob->isPreview() == false)
761         {
762             activeJob->disconnect(this);
763         }
764         // or preview job in calibration stage
765         else if (calibrationStage == CAL_CALIBRATION)
766         {
767             activeJob->disconnect(this);
768             activeJob->setPreview(false);
769             currentCCD->setUploadMode(activeJob->getUploadMode());
770         }
771         // or regular preview job
772         else
773         {
774             currentCCD->setUploadMode(activeJob->getUploadMode());
775             jobs.removeOne(activeJob);
776             // Delete preview job
777             delete (activeJob);
778             activeJob = nullptr;
779 
780             emit newStatus(targetState);
781         }
782     }
783 
784     // Only emit a new status if there is an active job or if capturing is suspended.
785     // The latter is necessary since suspending clears the active job, but the Capture
786     // module keeps the control.
787     if (activeJob != nullptr || m_State == CAPTURE_SUSPENDED)
788         emit newStatus(targetState);
789 
790     // stop focusing if capture is aborted
791     if (m_State == CAPTURE_FOCUSING && targetState == CAPTURE_ABORTED)
792         emit abortFocus();
793 
794     calibrationStage = CAL_NONE;
795     m_State            = targetState;
796 
797     // Turn off any calibration light, IF they were turned on by Capture module
798     if (currentDustCap && dustCapLightEnabled)
799     {
800         dustCapLightEnabled = false;
801         currentDustCap->SetLightEnabled(false);
802     }
803     if (currentLightBox && lightBoxLightEnabled)
804     {
805         lightBoxLightEnabled = false;
806         currentLightBox->SetLightEnabled(false);
807     }
808 
809     if (meridianFlipStage == MF_NONE || meridianFlipStage >= MF_COMPLETED)
810         secondsLabel->clear();
811     disconnect(currentCCD, &ISD::CCD::newImage, this, &Ekos::Capture::processData);
812     disconnect(currentCCD, &ISD::CCD::newExposureValue, this,  &Ekos::Capture::setExposureProgress);
813     //    disconnect(currentCCD, &ISD::CCD::previewFITSGenerated, this, &Ekos::Capture::setGeneratedPreviewFITS);
814     disconnect(currentCCD, &ISD::CCD::ready, this, &Ekos::Capture::ready);
815 
816     // In case of exposure looping, let's abort
817     if (currentCCD->isFastExposureEnabled())
818         targetChip->abortExposure();
819 
820     imgProgress->reset();
821     imgProgress->setEnabled(false);
822 
823     fullImgCountOUT->setText(QString());
824     currentImgCountOUT->setText(QString());
825     exposeOUT->setText(QString());
826     m_isFraming = false;
827 
828     setBusy(false);
829 
830     if (m_State == CAPTURE_ABORTED || m_State == CAPTURE_SUSPENDED)
831     {
832         startB->setIcon(
833             QIcon::fromTheme("media-playback-start"));
834         startB->setToolTip(i18n("Start Sequence"));
835         pauseB->setEnabled(false);
836     }
837 
838     //foreach (QAbstractButton *button, queueEditButtonGroup->buttons())
839     //button->setEnabled(true);
840 
841     seqTimer->stop();
842 
843     activeJob = nullptr;
844     // meridian flip may take place if requested
845     setMeridianFlipStage(MF_READY);
846 }
847 
848 //void Capture::sendNewImage(const QString &filename, ISD::CCDChip * myChip)
849 //{
850 //    if (activeJob && (myChip == nullptr || myChip == targetChip))
851 //    {
852 //        activeJob->setProperty("filename", filename);
853 //        emit newImage(activeJob);
854 //        // We only emit this for client/both images since remote images already send this automatically
855 //        if (currentCCD->getUploadMode() != ISD::CCD::UPLOAD_LOCAL && activeJob->isPreview() == false)
856 //        {
857 //            emit newSequenceImage(filename, m_GeneratedPreviewFITS);
858 //            m_GeneratedPreviewFITS.clear();
859 //        }
860 //    }
861 //}
862 
setCamera(const QString & device)863 bool Capture::setCamera(const QString &device)
864 {
865     // Do not change camera while in capture
866     if (m_State == CAPTURE_CAPTURING)
867         return false;
868 
869     for (int i = 0; i < cameraS->count(); i++)
870         if (device == cameraS->itemText(i))
871         {
872             cameraS->setCurrentIndex(i);
873             checkCCD(i);
874             return true;
875         }
876 
877     return false;
878 }
879 
camera()880 QString Capture::camera()
881 {
882     if (currentCCD)
883         return currentCCD->getDeviceName();
884 
885     return QString();
886 }
887 
checkCCD(int ccdNum)888 void Capture::checkCCD(int ccdNum)
889 {
890     // Do not update any camera settings while capture is in progress.
891     if (m_State == CAPTURE_CAPTURING)
892         return;
893 
894     if (ccdNum == -1)
895     {
896         ccdNum = cameraS->currentIndex();
897 
898         if (ccdNum == -1)
899             return;
900     }
901 
902     if (ccdNum < CCDs.count())
903     {
904         // Check whether main camera or guide head only
905         currentCCD = CCDs.at(ccdNum);
906 
907         targetChip = nullptr;
908         if (cameraS->itemText(ccdNum).right(6) == QString("Guider"))
909         {
910             useGuideHead = true;
911             targetChip   = currentCCD->getChip(ISD::CCDChip::GUIDE_CCD);
912         }
913 
914         if (targetChip == nullptr)
915         {
916             useGuideHead = false;
917             targetChip   = currentCCD->getChip(ISD::CCDChip::PRIMARY_CCD);
918         }
919 
920         // Make sure we have a valid chip and valid base device.
921         // Make sure we are not in capture process.
922         if (!targetChip || !targetChip->getCCD() || !targetChip->getCCD()->getBaseDevice() ||
923                 targetChip->isCapturing())
924             return;
925 
926         for (auto &ccd : CCDs)
927         {
928             disconnect(ccd, &ISD::CCD::numberUpdated, this, &Ekos::Capture::processCCDNumber);
929             disconnect(ccd, &ISD::CCD::newTemperatureValue, this, &Ekos::Capture::updateCCDTemperature);
930             disconnect(ccd, &ISD::CCD::coolerToggled, this, &Ekos::Capture::setCoolerToggled);
931             disconnect(ccd, &ISD::CCD::newRemoteFile, this, &Ekos::Capture::setNewRemoteFile);
932             disconnect(ccd, &ISD::CCD::videoStreamToggled, this, &Ekos::Capture::setVideoStreamEnabled);
933             disconnect(ccd, &ISD::CCD::ready, this, &Ekos::Capture::ready);
934             disconnect(ccd, &ISD::CCD::error, this, &Ekos::Capture::processCaptureError);
935         }
936 
937         if (currentCCD->hasCoolerControl())
938         {
939             coolerOnB->setEnabled(true);
940             coolerOffB->setEnabled(true);
941             coolerOnB->setChecked(currentCCD->isCoolerOn());
942             coolerOffB->setChecked(!currentCCD->isCoolerOn());
943         }
944         else
945         {
946             coolerOnB->setEnabled(false);
947             coolerOnB->setChecked(false);
948             coolerOffB->setEnabled(false);
949             coolerOffB->setChecked(false);
950         }
951 
952 
953         if (currentCCD->hasCooler())
954         {
955             cameraTemperatureS->setEnabled(true);
956             cameraTemperatureN->setEnabled(true);
957 
958             if (currentCCD->getBaseDevice()->getPropertyPermission("CCD_TEMPERATURE") != IP_RO)
959             {
960                 double min, max, step;
961                 setTemperatureB->setEnabled(true);
962                 cameraTemperatureN->setReadOnly(false);
963                 cameraTemperatureS->setEnabled(true);
964                 currentCCD->getMinMaxStep("CCD_TEMPERATURE", "CCD_TEMPERATURE_VALUE", &min, &max, &step);
965                 cameraTemperatureN->setMinimum(min);
966                 cameraTemperatureN->setMaximum(max);
967                 cameraTemperatureN->setSingleStep(1);
968                 bool isChecked = currentCCD->getDriverInfo()->getAuxInfo().value(QString("%1_TC").arg(currentCCD->getDeviceName()),
969                                  false).toBool();
970                 cameraTemperatureS->setChecked(isChecked);
971             }
972             else
973             {
974                 setTemperatureB->setEnabled(false);
975                 cameraTemperatureN->setReadOnly(true);
976                 cameraTemperatureS->setEnabled(false);
977                 cameraTemperatureS->setChecked(false);
978             }
979 
980             double temperature = 0;
981             if (currentCCD->getTemperature(&temperature))
982             {
983                 temperatureOUT->setText(QString("%L1").arg(temperature, 0, 'f', 2));
984                 if (cameraTemperatureN->cleanText().isEmpty())
985                     cameraTemperatureN->setValue(temperature);
986             }
987         }
988         else
989         {
990             cameraTemperatureS->setEnabled(false);
991             cameraTemperatureN->setEnabled(false);
992             cameraTemperatureN->clear();
993             temperatureOUT->clear();
994             setTemperatureB->setEnabled(false);
995         }
996 
997         updateFrameProperties();
998 
999         QStringList frameTypes = targetChip->getFrameTypes();
1000 
1001         captureTypeS->clear();
1002 
1003         if (frameTypes.isEmpty())
1004             captureTypeS->setEnabled(false);
1005         else
1006         {
1007             captureTypeS->setEnabled(true);
1008             captureTypeS->addItems(frameTypes);
1009             captureTypeS->setCurrentIndex(targetChip->getFrameType());
1010         }
1011 
1012         QStringList isoList = targetChip->getISOList();
1013         captureISOS->clear();
1014 
1015         captureFormatS->blockSignals(true);
1016         captureFormatS->clear();
1017 
1018         if (isoList.isEmpty())
1019         {
1020             // Only one transfer format
1021             captureFormatS->addItem(i18n("FITS"));
1022             captureISOS->setEnabled(false);
1023         }
1024         else
1025         {
1026             captureISOS->setEnabled(true);
1027             captureISOS->addItems(isoList);
1028             captureISOS->setCurrentIndex(targetChip->getISOIndex());
1029 
1030             // DSLRs have two transfer formats
1031             captureFormatS->addItem(i18n("FITS"));
1032             captureFormatS->addItem(i18n("Native"));
1033 
1034             //captureFormatS->setCurrentIndex(currentCCD->getTargetTransferFormat());
1035             // 2018-05-07 JM: Set value to the value in options
1036             captureFormatS->setCurrentIndex(static_cast<int>(Options::captureFormatIndex()));
1037 
1038             uint16_t w, h;
1039             uint8_t bbp {8};
1040             double pixelX = 0, pixelY = 0;
1041             bool rc = targetChip->getImageInfo(w, h, pixelX, pixelY, bbp);
1042             bool isModelInDB = isModelinDSLRInfo(QString(currentCCD->getDeviceName()));
1043             // If rc == true, then the property has been defined by the driver already
1044             // Only then we check if the pixels are zero
1045             if (rc == true && (pixelX == 0.0 || pixelY == 0.0 || isModelInDB == false))
1046             {
1047                 // If model is already in database, no need to show dialog
1048                 // The zeros above are the initial packets so we can safely ignore them
1049                 if (isModelInDB == false)
1050                 {
1051                     createDSLRDialog();
1052                 }
1053                 else
1054                 {
1055                     QString model = QString(currentCCD->getDeviceName());
1056                     syncDSLRToTargetChip(model);
1057                 }
1058             }
1059         }
1060 
1061         captureFormatS->blockSignals(false);
1062 
1063         // Gain Check
1064         if (currentCCD->hasGain())
1065         {
1066             double min, max, step, value, targetCustomGain;
1067             currentCCD->getGainMinMaxStep(&min, &max, &step);
1068 
1069             // Allow the possibility of no gain value at all.
1070             GainSpinSpecialValue = min - step;
1071             captureGainN->setRange(GainSpinSpecialValue, max);
1072             captureGainN->setSpecialValueText(i18n("--"));
1073             captureGainN->setEnabled(true);
1074             captureGainN->setSingleStep(step);
1075             currentCCD->getGain(&value);
1076 
1077             targetCustomGain = getGain();
1078 
1079             // Set the custom gain if we have one
1080             // otherwise it will not have an effect.
1081             if (targetCustomGain > 0)
1082                 captureGainN->setValue(targetCustomGain);
1083             else
1084                 captureGainN->setValue(GainSpinSpecialValue);
1085 
1086             captureGainN->setReadOnly(currentCCD->getGainPermission() == IP_RO);
1087 
1088             connect(captureGainN, &QDoubleSpinBox::editingFinished, [this]()
1089             {
1090                 if (captureGainN->value() != GainSpinSpecialValue)
1091                     setGain(captureGainN->value());
1092             });
1093         }
1094         else
1095             captureGainN->setEnabled(false);
1096 
1097         // Offset checks
1098         if (currentCCD->hasOffset())
1099         {
1100             double min, max, step, value, targetCustomOffset;
1101             currentCCD->getOffsetMinMaxStep(&min, &max, &step);
1102 
1103             // Allow the possibility of no Offset value at all.
1104             OffsetSpinSpecialValue = min - step;
1105             captureOffsetN->setRange(OffsetSpinSpecialValue, max);
1106             captureOffsetN->setSpecialValueText(i18n("--"));
1107             captureOffsetN->setEnabled(true);
1108             captureOffsetN->setSingleStep(step);
1109             currentCCD->getOffset(&value);
1110 
1111             targetCustomOffset = getOffset();
1112 
1113             // Set the custom Offset if we have one
1114             // otherwise it will not have an effect.
1115             if (targetCustomOffset > 0)
1116                 captureOffsetN->setValue(targetCustomOffset);
1117             else
1118                 captureOffsetN->setValue(OffsetSpinSpecialValue);
1119 
1120             captureOffsetN->setReadOnly(currentCCD->getOffsetPermission() == IP_RO);
1121 
1122             connect(captureOffsetN, &QDoubleSpinBox::editingFinished, [this]()
1123             {
1124                 if (captureOffsetN->value() != OffsetSpinSpecialValue)
1125                     setOffset(captureOffsetN->value());
1126             });
1127         }
1128         else
1129             captureOffsetN->setEnabled(false);
1130 
1131         customPropertiesDialog->setCCD(currentCCD);
1132 
1133         liveVideoB->setEnabled(currentCCD->hasVideoStream());
1134         if (currentCCD->hasVideoStream())
1135             setVideoStreamEnabled(currentCCD->isStreamingEnabled());
1136         else
1137             liveVideoB->setIcon(QIcon::fromTheme("camera-off"));
1138 
1139         connect(currentCCD, &ISD::CCD::numberUpdated, this, &Ekos::Capture::processCCDNumber, Qt::UniqueConnection);
1140         connect(currentCCD, &ISD::CCD::newTemperatureValue, this, &Ekos::Capture::updateCCDTemperature, Qt::UniqueConnection);
1141         connect(currentCCD, &ISD::CCD::coolerToggled, this, &Ekos::Capture::setCoolerToggled, Qt::UniqueConnection);
1142         connect(currentCCD, &ISD::CCD::newRemoteFile, this, &Ekos::Capture::setNewRemoteFile);
1143         connect(currentCCD, &ISD::CCD::videoStreamToggled, this, &Ekos::Capture::setVideoStreamEnabled);
1144         connect(currentCCD, &ISD::CCD::ready, this, &Ekos::Capture::ready);
1145         connect(currentCCD, &ISD::CCD::error, this, &Ekos::Capture::processCaptureError);
1146 
1147         DarkLibrary::Instance()->checkCamera();
1148     }
1149 }
1150 
setGuideChip(ISD::CCDChip * chip)1151 void Capture::setGuideChip(ISD::CCDChip * chip)
1152 {
1153     guideChip = chip;
1154     // We should suspend guide in two scenarios:
1155     // 1. If guide chip is within the primary CCD, then we cannot download any data from guide chip while primary CCD is downloading.
1156     // 2. If we have two CCDs running from ONE driver (Multiple-Devices-Per-Driver mpdp is true). Same issue as above, only one download
1157     // at a time.
1158     // After primary CCD download is complete, we resume guiding.
1159     if (!currentCCD)
1160         return;
1161 
1162     suspendGuideOnDownload =
1163         (currentCCD->getChip(ISD::CCDChip::GUIDE_CCD) == guideChip) ||
1164         (guideChip->getCCD() == currentCCD && currentCCD->getDriverInfo()->getAuxInfo().value("mdpd", false).toBool());
1165 }
1166 
resetFrameToZero()1167 void Capture::resetFrameToZero()
1168 {
1169     captureFrameXN->setMinimum(0);
1170     captureFrameXN->setMaximum(0);
1171     captureFrameXN->setValue(0);
1172 
1173     captureFrameYN->setMinimum(0);
1174     captureFrameYN->setMaximum(0);
1175     captureFrameYN->setValue(0);
1176 
1177     captureFrameWN->setMinimum(0);
1178     captureFrameWN->setMaximum(0);
1179     captureFrameWN->setValue(0);
1180 
1181     captureFrameHN->setMinimum(0);
1182     captureFrameHN->setMaximum(0);
1183     captureFrameHN->setValue(0);
1184 }
1185 
updateFrameProperties(int reset)1186 void Capture::updateFrameProperties(int reset)
1187 {
1188     int binx = 1, biny = 1;
1189     double min, max, step;
1190     int xstep = 0, ystep = 0;
1191 
1192     QString frameProp    = useGuideHead ? QString("GUIDER_FRAME") : QString("CCD_FRAME");
1193     QString exposureProp = useGuideHead ? QString("GUIDER_EXPOSURE") : QString("CCD_EXPOSURE");
1194     QString exposureElem = useGuideHead ? QString("GUIDER_EXPOSURE_VALUE") : QString("CCD_EXPOSURE_VALUE");
1195     targetChip =
1196         useGuideHead ? currentCCD->getChip(ISD::CCDChip::GUIDE_CCD) : currentCCD->getChip(ISD::CCDChip::PRIMARY_CCD);
1197 
1198     captureFrameWN->setEnabled(targetChip->canSubframe());
1199     captureFrameHN->setEnabled(targetChip->canSubframe());
1200     captureFrameXN->setEnabled(targetChip->canSubframe());
1201     captureFrameYN->setEnabled(targetChip->canSubframe());
1202 
1203     captureBinHN->setEnabled(targetChip->canBin());
1204     captureBinVN->setEnabled(targetChip->canBin());
1205 
1206     QList<double> exposureValues;
1207     exposureValues << 0.01 << 0.02 << 0.05 << 0.1 << 0.2 << 0.25 << 0.5 << 1 << 1.5 << 2 << 2.5 << 3 << 5 << 6 << 7 << 8 << 9 <<
1208                    10 << 20 << 30 << 40 << 50 << 60 << 120 << 180 << 300 << 600 << 900 << 1200 << 1800;
1209 
1210     if (currentCCD->getMinMaxStep(exposureProp, exposureElem, &min, &max, &step))
1211     {
1212         if (min < 0.001)
1213             captureExposureN->setDecimals(6);
1214         else
1215             captureExposureN->setDecimals(3);
1216         for(int i = 0; i < exposureValues.count(); i++)
1217         {
1218             double value = exposureValues.at(i);
1219             if(value < min || value > max)
1220             {
1221                 exposureValues.removeAt(i);
1222                 i--; //So we don't skip one
1223             }
1224         }
1225 
1226         exposureValues.prepend(min);
1227         exposureValues.append(max);
1228     }
1229 
1230     captureExposureN->setRecommendedValues(exposureValues);
1231 
1232     if (currentCCD->getMinMaxStep(frameProp, "WIDTH", &min, &max, &step))
1233     {
1234         if (min >= max)
1235         {
1236             resetFrameToZero();
1237             return;
1238         }
1239 
1240         if (step == 0.0)
1241             xstep = static_cast<int>(max * 0.05);
1242         else
1243             xstep = static_cast<int>(step);
1244 
1245         if (min >= 0 && max > 0)
1246         {
1247             captureFrameWN->setMinimum(static_cast<int>(min));
1248             captureFrameWN->setMaximum(static_cast<int>(max));
1249             captureFrameWN->setSingleStep(xstep);
1250         }
1251     }
1252     else
1253         return;
1254 
1255     if (currentCCD->getMinMaxStep(frameProp, "HEIGHT", &min, &max, &step))
1256     {
1257         if (min >= max)
1258         {
1259             resetFrameToZero();
1260             return;
1261         }
1262 
1263         if (step == 0.0)
1264             ystep = static_cast<int>(max * 0.05);
1265         else
1266             ystep = static_cast<int>(step);
1267 
1268         if (min >= 0 && max > 0)
1269         {
1270             captureFrameHN->setMinimum(static_cast<int>(min));
1271             captureFrameHN->setMaximum(static_cast<int>(max));
1272             captureFrameHN->setSingleStep(ystep);
1273         }
1274     }
1275     else
1276         return;
1277 
1278     if (currentCCD->getMinMaxStep(frameProp, "X", &min, &max, &step))
1279     {
1280         if (min >= max)
1281         {
1282             resetFrameToZero();
1283             return;
1284         }
1285 
1286         if (step == 0.0)
1287             step = xstep;
1288 
1289         if (min >= 0 && max > 0)
1290         {
1291             captureFrameXN->setMinimum(static_cast<int>(min));
1292             captureFrameXN->setMaximum(static_cast<int>(max));
1293             captureFrameXN->setSingleStep(static_cast<int>(step));
1294         }
1295     }
1296     else
1297         return;
1298 
1299     if (currentCCD->getMinMaxStep(frameProp, "Y", &min, &max, &step))
1300     {
1301         if (min >= max)
1302         {
1303             resetFrameToZero();
1304             return;
1305         }
1306 
1307         if (step == 0.0)
1308             step = ystep;
1309 
1310         if (min >= 0 && max > 0)
1311         {
1312             captureFrameYN->setMinimum(static_cast<int>(min));
1313             captureFrameYN->setMaximum(static_cast<int>(max));
1314             captureFrameYN->setSingleStep(static_cast<int>(step));
1315         }
1316     }
1317     else
1318         return;
1319 
1320     // cull to camera limits, if there are any
1321     if (useGuideHead == false)
1322         cullToDSLRLimits();
1323 
1324     if (reset == 1 || frameSettings.contains(targetChip) == false)
1325     {
1326         QVariantMap settings;
1327 
1328         settings["x"]    = 0;
1329         settings["y"]    = 0;
1330         settings["w"]    = captureFrameWN->maximum();
1331         settings["h"]    = captureFrameHN->maximum();
1332         settings["binx"] = 1;
1333         settings["biny"] = 1;
1334 
1335         frameSettings[targetChip] = settings;
1336     }
1337     else if (reset == 2 && frameSettings.contains(targetChip))
1338     {
1339         QVariantMap settings = frameSettings[targetChip];
1340         int x, y, w, h;
1341 
1342         x = settings["x"].toInt();
1343         y = settings["y"].toInt();
1344         w = settings["w"].toInt();
1345         h = settings["h"].toInt();
1346 
1347         // Bound them
1348         x = qBound(captureFrameXN->minimum(), x, captureFrameXN->maximum() - 1);
1349         y = qBound(captureFrameYN->minimum(), y, captureFrameYN->maximum() - 1);
1350         w = qBound(captureFrameWN->minimum(), w, captureFrameWN->maximum());
1351         h = qBound(captureFrameHN->minimum(), h, captureFrameHN->maximum());
1352 
1353         settings["x"] = x;
1354         settings["y"] = y;
1355         settings["w"] = w;
1356         settings["h"] = h;
1357 
1358         frameSettings[targetChip] = settings;
1359     }
1360 
1361     if (frameSettings.contains(targetChip))
1362     {
1363         QVariantMap settings = frameSettings[targetChip];
1364         int x = settings["x"].toInt();
1365         int y = settings["y"].toInt();
1366         int w = settings["w"].toInt();
1367         int h = settings["h"].toInt();
1368 
1369         if (targetChip->canBin())
1370         {
1371             targetChip->getMaxBin(&binx, &biny);
1372             captureBinHN->setMaximum(binx);
1373             captureBinVN->setMaximum(biny);
1374 
1375             captureBinHN->setValue(settings["binx"].toInt());
1376             captureBinVN->setValue(settings["biny"].toInt());
1377         }
1378         else
1379         {
1380             captureBinHN->setValue(1);
1381             captureBinVN->setValue(1);
1382         }
1383 
1384         if (x >= 0)
1385             captureFrameXN->setValue(x);
1386         if (y >= 0)
1387             captureFrameYN->setValue(y);
1388         if (w > 0)
1389             captureFrameWN->setValue(w);
1390         if (h > 0)
1391             captureFrameHN->setValue(h);
1392     }
1393 }
1394 
processCCDNumber(INumberVectorProperty * nvp)1395 void Capture::processCCDNumber(INumberVectorProperty * nvp)
1396 {
1397     if (currentCCD == nullptr)
1398         return;
1399 
1400     if ((!strcmp(nvp->name, "CCD_FRAME") && useGuideHead == false) ||
1401             (!strcmp(nvp->name, "GUIDER_FRAME") && useGuideHead))
1402         updateFrameProperties();
1403     else if ((!strcmp(nvp->name, "CCD_INFO") && useGuideHead == false) ||
1404              (!strcmp(nvp->name, "GUIDER_INFO") && useGuideHead))
1405         updateFrameProperties(2);
1406 }
1407 
resetFrame()1408 void Capture::resetFrame()
1409 {
1410     targetChip =
1411         useGuideHead ? currentCCD->getChip(ISD::CCDChip::GUIDE_CCD) : currentCCD->getChip(ISD::CCDChip::PRIMARY_CCD);
1412     targetChip->resetFrame();
1413     updateFrameProperties(1);
1414 }
1415 
syncFrameType(ISD::GDInterface * ccd)1416 void Capture::syncFrameType(ISD::GDInterface * ccd)
1417 {
1418     if (ccd->getDeviceName() != cameraS->currentText().toLatin1())
1419         return;
1420 
1421     ISD::CCDChip * tChip = (static_cast<ISD::CCD *>(ccd))->getChip(ISD::CCDChip::PRIMARY_CCD);
1422 
1423     QStringList frameTypes = tChip->getFrameTypes();
1424 
1425     captureTypeS->clear();
1426 
1427     if (frameTypes.isEmpty())
1428         captureTypeS->setEnabled(false);
1429     else
1430     {
1431         captureTypeS->setEnabled(true);
1432         captureTypeS->addItems(frameTypes);
1433         captureTypeS->setCurrentIndex(tChip->getFrameType());
1434     }
1435 }
1436 
setFilterWheel(const QString & device)1437 bool Capture::setFilterWheel(const QString &device)
1438 {
1439     bool deviceFound = false;
1440 
1441     for (int i = 0; i < filterWheelS->count(); i++)
1442         if (device == filterWheelS->itemText(i))
1443         {
1444             // Check Combo if it was set to something else.
1445             if (filterWheelS->currentIndex() != i)
1446             {
1447                 filterWheelS->blockSignals(true);
1448                 filterWheelS->setCurrentIndex(i);
1449                 filterWheelS->blockSignals(false);
1450             }
1451 
1452             checkFilter(i);
1453             deviceFound = true;
1454             break;
1455         }
1456 
1457     if (deviceFound == false)
1458         return false;
1459 
1460     return true;
1461 }
1462 
filterWheel()1463 QString Capture::filterWheel()
1464 {
1465     if (filterWheelS->currentIndex() >= 1)
1466         return filterWheelS->currentText();
1467 
1468     return QString();
1469 }
1470 
setFilter(const QString & filter)1471 bool Capture::setFilter(const QString &filter)
1472 {
1473     if (filterWheelS->currentIndex() >= 1)
1474     {
1475         captureFilterS->setCurrentText(filter);
1476         return true;
1477     }
1478 
1479     return false;
1480 }
1481 
filter()1482 QString Capture::filter()
1483 {
1484     return captureFilterS->currentText();
1485 }
1486 
checkFilter(int filterNum)1487 void Capture::checkFilter(int filterNum)
1488 {
1489     if (filterNum == -1)
1490     {
1491         filterNum = filterWheelS->currentIndex();
1492         if (filterNum == -1)
1493             return;
1494     }
1495 
1496     // "--" is no filter
1497     if (filterNum == 0)
1498     {
1499         currentFilter = nullptr;
1500         m_CurrentFilterPosition = -1;
1501         filterEditB->setEnabled(false);
1502         captureFilterS->clear();
1503         syncFilterInfo();
1504         return;
1505     }
1506 
1507     if (filterNum <= Filters.count())
1508         currentFilter = Filters.at(filterNum - 1);
1509 
1510     filterManager->setCurrentFilterWheel(currentFilter);
1511 
1512     syncFilterInfo();
1513 
1514     captureFilterS->clear();
1515 
1516     captureFilterS->addItems(filterManager->getFilterLabels());
1517 
1518     m_CurrentFilterPosition = filterManager->getFilterPosition();
1519 
1520     filterEditB->setEnabled(m_CurrentFilterPosition > 0);
1521 
1522     captureFilterS->setCurrentIndex(m_CurrentFilterPosition - 1);
1523 
1524 
1525     /*if (activeJob &&
1526         (activeJob->getStatus() == SequenceJob::JOB_ABORTED || activeJob->getStatus() == SequenceJob::JOB_IDLE))
1527         activeJob->setCurrentFilter(currentFilterPosition);*/
1528 }
1529 
syncFilterInfo()1530 void Capture::syncFilterInfo()
1531 {
1532     if (currentCCD)
1533     {
1534         auto activeDevices = currentCCD->getBaseDevice()->getText("ACTIVE_DEVICES");
1535         if (activeDevices)
1536         {
1537             auto activeFilter = activeDevices->findWidgetByName("ACTIVE_FILTER");
1538             if (activeFilter)
1539             {
1540                 if (currentFilter && (activeFilter->getText() != currentFilter->getDeviceName()))
1541                 {
1542                     Options::setDefaultFocusFilterWheel(currentFilter->getDeviceName());
1543                     activeFilter->setText(currentFilter->getDeviceName().toLatin1().constData());
1544                     currentCCD->getDriverInfo()->getClientManager()->sendNewText(activeDevices);
1545                 }
1546                 // Reset filter name in CCD driver
1547                 else if (!currentFilter && strlen(activeFilter->getText()) > 0)
1548                 {
1549                     activeFilter->setText("");
1550                     currentCCD->getDriverInfo()->getClientManager()->sendNewText(activeDevices);
1551                 }
1552             }
1553         }
1554     }
1555 }
1556 
1557 /**
1558  * @brief Ensure that all pending preparation tasks are be completed (focusing, dithering, etc.)
1559  *        and start the next exposure.
1560  *
1561  * Checks of pending preparations depends upon the frame type:
1562  *
1563  * - For light frames, pending preparations like focusing, dithering etc. needs
1564  *   to be checked before each single frame capture. efore starting to capture the next light frame,
1565  *   checkLightFramePendingTasks() is called to check if all pending preparation tasks have
1566  *   been completed successfully. As soon as this is the case, the sequence timer
1567  *   #seqTimer is started to wait the configured delay and starts capturing the next image.
1568  *
1569  * - For bias, dark and flat frames, preparation jobs are only executed when starting a sequence.
1570  *   Hence, for these frames we directly start the sequence timer #seqTimer.
1571  *
1572  * @return IPS_OK, iff all pending preparation jobs are completed (@see checkLightFramePendingTasks()).
1573  *         In that case, the #seqTimer is started to wait for the configured settling delay and then
1574  *         capture the next image (@see Capture::captureImage). In case that a pending task aborted,
1575  *         IPS_IDLE is returned.
1576  */
startNextExposure()1577 IPState Capture::startNextExposure()
1578 {
1579     // Since this function is looping while pending tasks are running in parallel
1580     // it might happen that one of them leads to abort() which sets the #activeJob to nullptr.
1581     // In this case we terminate the loop by returning #IPS_IDLE without starting a new capture.
1582     if (activeJob == nullptr)
1583         return IPS_IDLE;
1584 
1585     // check pending jobs for light frames. All other frame types do not contain mid-sequence checks.
1586     if (activeJob->getFrameType() == FRAME_LIGHT)
1587     {
1588         IPState pending = checkLightFramePendingTasks();
1589         if (pending != IPS_OK)
1590             // there are still some jobs pending
1591             return pending;
1592     }
1593 
1594     // nothing pending, let's start the next exposure
1595     if (seqDelay > 0)
1596     {
1597         secondsLabel->setText(i18n("Waiting..."));
1598         m_State = CAPTURE_WAITING;
1599         emit newStatus(Ekos::CAPTURE_WAITING);
1600     }
1601     seqTimer->start(seqDelay);
1602 
1603     return IPS_OK;
1604 }
1605 
1606 /**
1607  * @brief Try to start capturing the next exposure (@see startNextExposure()).
1608  *        If startNextExposure() returns, that there are still some jobs pending,
1609  *        we wait for 1 second and retry to start it again.
1610  *        If one of the pending preparation jobs has problems, the looping stops.
1611  */
checkNextExposure()1612 void Capture::checkNextExposure()
1613 {
1614     IPState started = startNextExposure();
1615     // if starting the next exposure did not succeed due to pending jobs running,
1616     // we retry after 1 second
1617     if (started == IPS_BUSY)
1618         QTimer::singleShot(1000, this, &Ekos::Capture::checkNextExposure);
1619 }
1620 
1621 
processData(const QSharedPointer<FITSData> & data)1622 void Capture::processData(const QSharedPointer<FITSData> &data)
1623 {
1624     ISD::CCDChip * tChip = nullptr;
1625 
1626     QString blobInfo;
1627     if (data)
1628     {
1629         m_ImageData = data;
1630         blobInfo = QString("{Device: %1 Property: %2 Element: %3 Chip: %4}").arg(data->property("device").toString())
1631                    .arg(data->property("blobVector").toString())
1632                    .arg(data->property("blobElement").toString())
1633                    .arg(data->property("chip").toInt());
1634     }
1635     else
1636         m_ImageData.reset();
1637 
1638     // If there is no active job, ignore
1639     if (activeJob == nullptr)
1640     {
1641         if (data)
1642             qCWarning(KSTARS_EKOS_CAPTURE) << blobInfo << "Ignoring received FITS as active job is null.";
1643         return;
1644     }
1645 
1646     if (meridianFlipStage >= MF_ALIGNING)
1647     {
1648         if (data)
1649             qCWarning(KSTARS_EKOS_CAPTURE) << blobInfo << "Ignoring Received FITS as meridian flip stage is" << meridianFlipStage;
1650         return;
1651     }
1652 
1653     // If image is client or both, let's process it.
1654     if (currentCCD->getUploadMode() != ISD::CCD::UPLOAD_LOCAL)
1655     {
1656         //        if (data.isNull())
1657         //        {
1658         //            appendLogText(i18n("Failed to save file to %1", activeJob->getSignature()));
1659         //            abort();
1660         //            return;
1661         //        }
1662 
1663         if (m_State == CAPTURE_IDLE || m_State == CAPTURE_ABORTED)
1664         {
1665             qCWarning(KSTARS_EKOS_CAPTURE) << blobInfo << "Ignoring Received FITS as current capture state is not active" << m_State;
1666             return;
1667         }
1668 
1669         //if (!strcmp(data->name, "CCD2"))
1670         if (data)
1671         {
1672             tChip = currentCCD->getChip(static_cast<ISD::CCDChip::ChipType>(data->property("chip").toInt()));
1673             if (tChip != targetChip)
1674             {
1675                 if (m_GuideState == GUIDE_IDLE)
1676                     qCWarning(KSTARS_EKOS_CAPTURE) << blobInfo << "Ignoring Received FITS as it does not correspond to the target chip"
1677                                                    << targetChip->getType();
1678                 return;
1679             }
1680         }
1681 
1682         if (targetChip->getCaptureMode() == FITS_FOCUS || targetChip->getCaptureMode() == FITS_GUIDE)
1683         {
1684             qCWarning(KSTARS_EKOS_CAPTURE) << blobInfo << "Ignoring Received FITS as it has the wrong capture mode" <<
1685                                            targetChip->getCaptureMode();
1686             return;
1687         }
1688 
1689         // If the FITS is not for our device, simply ignore
1690 
1691         if (data && data->property("device").toString() != currentCCD->getDeviceName())
1692         {
1693             qCWarning(KSTARS_EKOS_CAPTURE) << blobInfo << "Ignoring Received FITS as the blob device name does not equal active camera"
1694                                            << currentCCD->getDeviceName();
1695             return;
1696         }
1697 
1698         // If this is a preview job, make sure to enable preview button after
1699         // we receive the FITS
1700         if (activeJob->isPreview() && previewB->isEnabled() == false)
1701             previewB->setEnabled(true);
1702 
1703         // If dark is selected, perform dark substraction.
1704         if (data && darkB->isChecked() && activeJob->isPreview() && useGuideHead == false)
1705         {
1706             m_DarkProcessor->denoise(targetChip, m_ImageData, activeJob->getExposure(), activeJob->getSubX(), activeJob->getSubY());
1707             return;
1708         }
1709     }
1710 
1711     setCaptureComplete();
1712 }
1713 
1714 /**
1715  * @brief Manage the capture process after a captured image has been successfully downloaded from the camera.
1716  *
1717  * When a image frame has been captured and downloaded successfully, send the image to the client (if configured)
1718  * and execute the book keeping for the captured frame. After this, either processJobCompletion() is executed
1719  * in case that the job is completed, and resumeSequence() otherwise.
1720  *
1721  * Book keeping means:
1722  * - increase / decrease the counters for focusing and dithering
1723  * - increase the frame counter
1724  * - update the average download time
1725  *
1726  * @return IPS_BUSY iff pausing is requested, IPS_OK otherwise.
1727  */
setCaptureComplete()1728 IPState Capture::setCaptureComplete()
1729 {
1730     captureTimeout.stop();
1731     m_CaptureTimeoutCounter = 0;
1732 
1733     downloadProgressTimer.stop();
1734 
1735     if (!activeJob)
1736         return IPS_BUSY;
1737 
1738     // In case we're framing, let's return quick to continue the process.
1739     if (m_isFraming)
1740     {
1741         emit newImage(activeJob, m_ImageData);
1742         // If fast exposure is on, do not capture again, it will be captured by the driver.
1743         if (currentCCD->isFastExposureEnabled() == false)
1744         {
1745             secondsLabel->setText(i18n("Framing..."));
1746             activeJob->capture(m_AutoFocusReady);
1747         }
1748         return IPS_OK;
1749     }
1750 
1751     // If fast exposure is off, disconnect exposure progress
1752     // otherwise, keep it going since it fires off from driver continuous capture process.
1753     if (currentCCD->isFastExposureEnabled() == false)
1754     {
1755         disconnect(currentCCD, &ISD::CCD::newExposureValue, this, &Ekos::Capture::setExposureProgress);
1756         DarkLibrary::Instance()->disconnect(this);
1757     }
1758 
1759     // Do not calculate download time for images stored on server.
1760     // Only calculate for longer exposures.
1761     if (currentCCD->getUploadMode() != ISD::CCD::UPLOAD_LOCAL)
1762     {
1763         //This determines the time since the image started downloading
1764         //Then it gets the estimated time left and displays it in the log.
1765         double currentDownloadTime = downloadTimer.elapsed() / 1000.0;
1766         downloadTimes << currentDownloadTime;
1767         QString dLTimeString = QString::number(currentDownloadTime, 'd', 2);
1768         QString estimatedTimeString = QString::number(getEstimatedDownloadTime(), 'd', 2);
1769         appendLogText(i18n("Download Time: %1 s, New Download Time Estimate: %2 s.", dLTimeString, estimatedTimeString));
1770     }
1771 
1772 
1773     secondsLabel->setText(i18n("Complete."));
1774     // Do not display notifications for very short captures
1775     if (activeJob->getExposure() >= 1)
1776         KSNotification::event(QLatin1String("EkosCaptureImageReceived"), i18n("Captured image received"),
1777                               KSNotification::EVENT_INFO);
1778 
1779     // If it was initially set as pure preview job and NOT as preview for calibration
1780     if (activeJob->isPreview() && calibrationStage != CAL_CALIBRATION)
1781     {
1782         //sendNewImage(blobFilename, blobChip);
1783         emit newImage(activeJob, m_ImageData);
1784         jobs.removeOne(activeJob);
1785         // Reset upload mode if it was changed by preview
1786         currentCCD->setUploadMode(activeJob->getUploadMode());
1787         //delete (activeJob);
1788         activeJob->deleteLater();
1789         // Reset active job pointer
1790         activeJob = nullptr;
1791         abort();
1792         if (m_GuideState == GUIDE_SUSPENDED && suspendGuideOnDownload)
1793             emit resumeGuiding();
1794 
1795         m_State = CAPTURE_IDLE;
1796         emit newStatus(Ekos::CAPTURE_IDLE);
1797         return IPS_OK;
1798     }
1799 
1800     // check if pausing has been requested
1801     if (checkPausing() == true)
1802     {
1803         pauseFunction = &Capture::setCaptureComplete;
1804         return IPS_BUSY;
1805     }
1806 
1807     if (! activeJob->isPreview())
1808     {
1809         /* Increase the sequence's current capture count */
1810         activeJob->setCompleted(activeJob->getCompleted() + 1);
1811         /* Decrease the counter for in-sequence focusing */
1812         inSequenceFocusCounter--;
1813     }
1814 
1815     /* Decrease the dithering counter except for directly after meridian flip                                           */
1816     /* Hint: this happens only when a meridian flip happened during a paused sequence when pressing "Start" afterwards. */
1817     if (meridianFlipStage < MF_FLIPPING)
1818         ditherCounter--;
1819 
1820     // JM 2020-06-17: Emit newImage for LOCAL images (stored on remote host)
1821     //if (currentCCD->getUploadMode() == ISD::CCD::UPLOAD_LOCAL)
1822     emit newImage(activeJob, m_ImageData);
1823     // For Client/Both images, send file name.
1824     //else
1825     //    sendNewImage(blobFilename, blobChip);
1826 
1827 
1828     /* If we were assigned a captured frame map, also increase the relevant counter for prepareJob */
1829     SchedulerJob::CapturedFramesMap::iterator frame_item = capturedFramesMap.find(activeJob->getSignature());
1830     if (capturedFramesMap.end() != frame_item)
1831         frame_item.value()++;
1832 
1833     if (activeJob->getFrameType() != FRAME_LIGHT)
1834     {
1835         if (processPostCaptureCalibrationStage() == false)
1836             return IPS_OK;
1837 
1838         if (calibrationStage == CAL_CALIBRATION_COMPLETE)
1839             calibrationStage = CAL_CAPTURING;
1840     }
1841 
1842     /* The image progress has now one more capture */
1843     imgProgress->setValue(activeJob->getCompleted());
1844 
1845     appendLogText(i18n("Received image %1 out of %2.", activeJob->getCompleted(), activeJob->getCount()));
1846 
1847     double hfr = -1, eccentricity = -1;
1848     int numStars = -1, median = -1;
1849     QString filename;
1850     if (m_ImageData)
1851     {
1852         QVariant frameType;
1853         if (Options::autoHFR() && m_ImageData && !m_ImageData->areStarsSearched() && m_ImageData->getRecordValue("FRAME", frameType)
1854                 && frameType.toString() == "Light")
1855         {
1856 
1857 #ifdef HAVE_STELLARSOLVER
1858             QVariantMap extractionSettings;
1859             extractionSettings["optionsProfileIndex"] = Options::hFROptionsProfile();
1860             extractionSettings["optionsProfileGroup"] = static_cast<int>(Ekos::HFRProfiles);
1861             m_ImageData->setSourceExtractorSettings(extractionSettings);
1862 #endif
1863 
1864             QFuture<bool> result = m_ImageData->findStars(ALGORITHM_SEP);
1865             result.waitForFinished();
1866         }
1867         hfr = m_ImageData->getHFR(HFR_AVERAGE);
1868         numStars = m_ImageData->getSkyBackground().starsDetected;
1869         median = m_ImageData->getMedian();
1870         eccentricity = m_ImageData->getEccentricity();
1871         filename = m_ImageData->filename();
1872         appendLogText(i18n("Captured %1", filename));
1873         auto remainingPlaceholders = PlaceholderPath::remainingPlaceholders(filename);
1874         if (remainingPlaceholders.size() > 0)
1875         {
1876             appendLogText(
1877                 i18n("WARNING: remaining and potentially unknown placeholders %1 in %2",
1878                      remainingPlaceholders.join(", "), filename));
1879         }
1880     }
1881 
1882     m_State = CAPTURE_IMAGE_RECEIVED;
1883     emit newStatus(Ekos::CAPTURE_IMAGE_RECEIVED);
1884 
1885     if (activeJob)
1886     {
1887         emit captureComplete(filename, activeJob->getExposure(), activeJob->getFilterName(), hfr,
1888                              numStars, median, eccentricity);
1889 
1890         currentImgCountOUT->setText(QString("%L1").arg(activeJob->getCompleted()));
1891 
1892         // Check if we need to execute post capture script first
1893 
1894         const QString postCaptureScript = activeJob->getScript(SCRIPT_POST_CAPTURE);
1895         if (postCaptureScript.isEmpty() == false)
1896         {
1897             m_CaptureScriptType = SCRIPT_POST_CAPTURE;
1898             m_CaptureScript.start(postCaptureScript, generateScriptArguments());
1899             appendLogText(i18n("Executing post capture script %1", postCaptureScript));
1900             return IPS_OK;
1901         }
1902 
1903         // if we're done
1904         if (activeJob->getCount() <= activeJob->getCompleted())
1905         {
1906             processJobCompletionStage1();
1907             return IPS_OK;
1908         }
1909     }
1910 
1911     return resumeSequence();
1912 }
1913 
1914 
processJobCompletionStage1()1915 void Capture::processJobCompletionStage1()
1916 {
1917     if (activeJob == nullptr)
1918     {
1919         qWarning(KSTARS_EKOS_CAPTURE) << "procesJobCompletionStage1 with null activeJob.";
1920     }
1921     else
1922     {
1923         // JM 2020-12-06: Check if we need to execute post-job script first.
1924         const QString postJobScript = activeJob->getScript(SCRIPT_POST_JOB);
1925         if (!postJobScript.isEmpty())
1926         {
1927             m_CaptureScriptType = SCRIPT_POST_JOB;
1928             m_CaptureScript.start(postJobScript, generateScriptArguments());
1929             appendLogText(i18n("Executing post job script %1", postJobScript));
1930             return;
1931         }
1932     }
1933 
1934     processJobCompletionStage2();
1935 }
1936 
1937 /**
1938  * @brief Stop execution of the current sequence and check whether there exists a next sequence
1939  *        and start it, if there is a next one to be started (@see resumeSequence()).
1940  */
processJobCompletionStage2()1941 void Capture::processJobCompletionStage2()
1942 {
1943     if (activeJob == nullptr)
1944     {
1945         qWarning(KSTARS_EKOS_CAPTURE) << "procesJobCompletionStage2 with null activeJob.";
1946     }
1947     else
1948     {
1949         activeJob->done();
1950 
1951         if (activeJob->isPreview() == false)
1952         {
1953             int index = jobs.indexOf(activeJob);
1954             QJsonObject oneSequence = m_SequenceArray[index].toObject();
1955             oneSequence["Status"] = "Complete";
1956             m_SequenceArray.replace(index, oneSequence);
1957             emit sequenceChanged(m_SequenceArray);
1958         }
1959     }
1960     stop();
1961 
1962     // Check if there are more pending jobs and execute them
1963     if (resumeSequence() == IPS_OK)
1964         return;
1965     // Otherwise, we're done. We park if required and resume guiding if no parking is done and autoguiding was engaged before.
1966     else
1967     {
1968         //KNotification::event(QLatin1String("CaptureSuccessful"), i18n("CCD capture sequence completed"));
1969         KSNotification::event(QLatin1String("CaptureSuccessful"), i18n("CCD capture sequence completed"),
1970                               KSNotification::EVENT_INFO);
1971 
1972         abort();
1973 
1974         m_State = CAPTURE_COMPLETE;
1975         emit newStatus(Ekos::CAPTURE_COMPLETE);
1976 
1977         //Resume guiding if it was suspended before
1978         //if (isAutoGuiding && currentCCD->getChip(ISD::CCDChip::GUIDE_CCD) == guideChip)
1979         if (m_GuideState == GUIDE_SUSPENDED && suspendGuideOnDownload)
1980             emit resumeGuiding();
1981     }
1982 }
1983 
1984 /**
1985  * @brief Check, whether dithering is necessary and, in that case initiate it.
1986  *
1987  *  Dithering is only required for batch images and does not apply for PREVIEW.
1988  *
1989  * There are several situations that determine, if dithering is necessary:
1990  * 1. the current job captures light frames AND the dither counter has reached 0 AND
1991  * 2. guiding is running OR the manual dithering option is selected AND
1992  * 3. there is a guiding camera active AND
1993  * 4. there hasn't just a meridian flip been finised.
1994  *
1995  * @return true iff dithering is necessary.
1996  */
checkDithering()1997 bool Capture::checkDithering()
1998 {
1999     // No need if preview only
2000     if (activeJob && activeJob->isPreview())
2001         return false;
2002 
2003     if ( (Options::ditherEnabled() || Options::ditherNoGuiding())
2004             // 2017-09-20 Jasem: No need to dither after post meridian flip guiding
2005             && meridianFlipStage != MF_GUIDING
2006             // If CCD is looping, we cannot dither UNLESS a different camera and NOT a guide chip is doing the guiding for us.
2007             && (currentCCD->isFastExposureEnabled() == false || guideChip == nullptr)
2008             // We must be either in guide mode or if non-guide dither (via pulsing) is enabled
2009             && (m_GuideState == GUIDE_GUIDING || Options::ditherNoGuiding())
2010             // Must be only done for light frames
2011             && (activeJob != nullptr && activeJob->getFrameType() == FRAME_LIGHT)
2012             // Check dither counter
2013             && ditherCounter == 0)
2014     {
2015         ditherCounter = Options::ditherFrames();
2016 
2017         secondsLabel->setText(i18n("Dithering..."));
2018 
2019         qCInfo(KSTARS_EKOS_CAPTURE) << "Dithering...";
2020         appendLogText(i18n("Dithering..."));
2021 
2022         if (currentCCD->isFastExposureEnabled())
2023             targetChip->abortExposure();
2024 
2025         m_State = CAPTURE_DITHERING;
2026         m_DitheringState = IPS_BUSY;
2027         emit newStatus(Ekos::CAPTURE_DITHERING);
2028 
2029         return true;
2030     }
2031     // no dithering required
2032     return false;
2033 }
2034 
2035 /**
2036  * @brief Try to continue capturing.
2037  *
2038  * Take the active job, if there is one, or search for the next one that is either
2039  * idle or aborted. If a new job is selected, call prepareJob(*SequenceJob) to prepare it and
2040  * resume guiding (TODO: is this not part of the preparation?). If the current job is still active,
2041  * initiate checkNextExposure().
2042  *
2043  * @return IPS_OK if there is a job that may be continued, IPS_BUSY otherwise.
2044  */
resumeSequence()2045 IPState Capture::resumeSequence()
2046 {
2047     // If no job is active, we have to find if there are more pending jobs in the queue
2048     if (!activeJob)
2049     {
2050         SequenceJob * next_job = nullptr;
2051 
2052         foreach (SequenceJob * job, jobs)
2053         {
2054             if (job->getStatus() == SequenceJob::JOB_IDLE || job->getStatus() == SequenceJob::JOB_ABORTED)
2055             {
2056                 next_job = job;
2057                 break;
2058             }
2059         }
2060 
2061         if (next_job)
2062         {
2063             //check delta also when starting a new job!
2064             isTemperatureDeltaCheckActive = (m_AutoFocusReady && limitFocusDeltaTS->isChecked());
2065 
2066             prepareJob(next_job);
2067 
2068             //Resume guiding if it was suspended before
2069             //if (isAutoGuiding && currentCCD->getChip(ISD::CCDChip::GUIDE_CCD) == guideChip)
2070             if (m_GuideState == GUIDE_SUSPENDED && suspendGuideOnDownload)
2071             {
2072                 qCDebug(KSTARS_EKOS_CAPTURE) << "Resuming guiding...";
2073                 emit resumeGuiding();
2074             }
2075 
2076             return IPS_OK;
2077         }
2078         else
2079         {
2080             qCDebug(KSTARS_EKOS_CAPTURE) << "All capture jobs complete.";
2081             return IPS_BUSY;
2082         }
2083     }
2084     // Otherwise, let's prepare for next exposure.
2085     else
2086     {
2087         isTemperatureDeltaCheckActive = (m_AutoFocusReady && limitFocusDeltaTS->isChecked());
2088 
2089         // If we suspended guiding due to primary chip download, resume guide chip guiding now
2090         if (m_GuideState == GUIDE_SUSPENDED && suspendGuideOnDownload)
2091         {
2092             qCInfo(KSTARS_EKOS_CAPTURE) << "Resuming guiding...";
2093             emit resumeGuiding();
2094         }
2095 
2096         // If looping, we just increment the file system image count
2097         if (currentCCD->isFastExposureEnabled())
2098         {
2099             if (currentCCD->getUploadMode() != ISD::CCD::UPLOAD_LOCAL)
2100             {
2101                 checkSeqBoundary(activeJob->getSignature());
2102                 currentCCD->setNextSequenceID(nextSequenceID);
2103             }
2104         }
2105         // otherwise we loop starting the next exposure until all pending
2106         // jobs are completed
2107         else
2108         {
2109             const QString preCaptureScript = activeJob->getScript(SCRIPT_PRE_CAPTURE);
2110             // JM 2020-12-06: Check if we need to execute pre-capture script first.
2111             if (!preCaptureScript.isEmpty())
2112             {
2113                 m_CaptureScriptType = SCRIPT_PRE_CAPTURE;
2114                 m_CaptureScript.start(preCaptureScript, generateScriptArguments());
2115                 appendLogText(i18n("Executing pre capture script %1", preCaptureScript));
2116                 return IPS_BUSY;
2117             }
2118             else
2119                 checkNextExposure();
2120         }
2121     }
2122 
2123     return IPS_OK;
2124 }
2125 
2126 /**
2127  * @brief Check, if re-focusing is required and initiate it in that case.
2128  * @return true iff re-focusing is necessary.
2129  */
startFocusIfRequired()2130 bool Capture::startFocusIfRequired()
2131 {
2132     // Do not start focus if:
2133     // 1. There is no active job, or
2134     // 2. Target frame is not LIGHT
2135     // 3. Capture is preview only
2136     if (activeJob == nullptr || activeJob->getFrameType() != FRAME_LIGHT || activeJob->isPreview())
2137         return false;
2138 
2139     isRefocus = false;
2140     isInSequenceFocus = (m_AutoFocusReady && limitFocusHFRS->isChecked());
2141 
2142     // check if time for forced refocus
2143     if (limitRefocusS->isChecked())
2144     {
2145         qCDebug(KSTARS_EKOS_CAPTURE) << "Focus elapsed time (secs): " << getRefocusEveryNTimerElapsedSec() <<
2146                                      ". Requested Interval (secs): " << limitRefocusN->value() * 60;
2147 
2148         if (getRefocusEveryNTimerElapsedSec() >= limitRefocusN->value() * 60)
2149         {
2150             isRefocus = true;
2151             appendLogText(i18n("Scheduled refocus starting after %1 seconds...", getRefocusEveryNTimerElapsedSec()));
2152         }
2153     }
2154 
2155     if (!isRefocus && isTemperatureDeltaCheckActive)
2156     {
2157         qCDebug(KSTARS_EKOS_CAPTURE) << "Focus temperature delta (°C): " << focusTemperatureDelta <<
2158                                      ". Requested maximum delta (°C): " << limitFocusDeltaTN->value();
2159 
2160         if (focusTemperatureDelta > limitFocusDeltaTN->value())
2161         {
2162             isRefocus = true;
2163             appendLogText(i18n("Refocus starting because of temperature change of %1 °C...", focusTemperatureDelta));
2164         }
2165     }
2166 
2167     // Either it is time to force autofocus or temperature has changed
2168     if (isRefocus)
2169     {
2170         secondsLabel->setText(i18n("Focusing..."));
2171 
2172         if (currentCCD->isFastExposureEnabled())
2173             targetChip->abortExposure();
2174 
2175         // If we are over 30 mins since last autofocus, we'll reset frame.
2176         if (limitRefocusN->value() >= 30)
2177             emit resetFocus();
2178 
2179         // force refocus
2180         qCDebug(KSTARS_EKOS_CAPTURE) << "Capture is triggering autofocus on line " << __LINE__;
2181         setFocusStatus(FOCUS_PROGRESS);
2182         emit checkFocus(0.1);
2183 
2184         m_State = CAPTURE_FOCUSING;
2185         emit newStatus(Ekos::CAPTURE_FOCUSING);
2186         return true;
2187     }
2188     else if (isInSequenceFocus && inSequenceFocusCounter == 0)
2189     {
2190         inSequenceFocusCounter = Options::inSequenceCheckFrames();
2191 
2192         // Post meridian flip we need to reset filter _before_ running in-sequence focusing
2193         // as it could have changed for whatever reason (e.g. alignment used a different filter).
2194         // Then when focus process begins with the _target_ filter in place, it should take all the necessary actions to make it
2195         // work for the next set of captures. This is direct reset to the filter device, not via Filter Manager.
2196         if (meridianFlipStage != MF_NONE && currentFilter)
2197         {
2198             int targetFilterPosition = activeJob->getTargetFilter();
2199             int currentFilterPosition = filterManager->getFilterPosition();
2200             if (targetFilterPosition > 0 && targetFilterPosition != currentFilterPosition)
2201                 currentFilter->runCommand(INDI_SET_FILTER, &targetFilterPosition);
2202         }
2203 
2204         secondsLabel->setText(i18n("Focusing..."));
2205 
2206         if (currentCCD->isFastExposureEnabled())
2207             targetChip->abortExposure();
2208 
2209         setFocusStatus(FOCUS_PROGRESS);
2210         emit checkFocus(limitFocusHFRN->value() == 0.0 ? 0.1 : limitFocusHFRN->value());
2211 
2212         qCDebug(KSTARS_EKOS_CAPTURE) << "In-sequence focusing started...";
2213         m_State = CAPTURE_FOCUSING;
2214         emit newStatus(Ekos::CAPTURE_FOCUSING);
2215         return true;
2216     }
2217 
2218     return false;
2219 }
2220 
captureOne()2221 void Capture::captureOne()
2222 {
2223     if (m_FocusState >= FOCUS_PROGRESS)
2224     {
2225         appendLogText(i18n("Cannot capture while focus module is busy."));
2226     }
2227     //    else if (captureFormatS->currentIndex() == ISD::CCD::FORMAT_NATIVE && darkSubCheck->isChecked())
2228     //    {
2229     //        appendLogText(i18n("Cannot perform auto dark subtraction of native DSLR formats."));
2230     //    }
2231     else if (addJob(true))
2232     {
2233         m_State = CAPTURE_PROGRESS;
2234         prepareJob(jobs.last());
2235     }
2236 }
2237 
startFraming()2238 void Capture::startFraming()
2239 {
2240     if (m_FocusState >= FOCUS_PROGRESS)
2241     {
2242         appendLogText(i18n("Cannot start framing while focus module is busy."));
2243     }
2244     else if (!m_isFraming)
2245     {
2246         m_isFraming = true;
2247         appendLogText(i18n("Starting framing..."));
2248         captureOne();
2249     }
2250 }
2251 
captureImage()2252 void Capture::captureImage()
2253 {
2254     if (activeJob == nullptr)
2255         return;
2256 
2257     // This test must be placed before the FOCUS_PROGRESS test,
2258     // as sometimes the FilterManager can cause an auto-focus.
2259     // If the filterManager is not IDLE, then try again in 1 second.
2260     switch (m_FilterManagerState)
2261     {
2262         case FILTER_IDLE:
2263             secondsLabel->clear();
2264             break;
2265 
2266         case FILTER_AUTOFOCUS:
2267             secondsLabel->setText(i18n("Focusing..."));
2268             QTimer::singleShot(1000, this, &Ekos::Capture::captureImage);
2269             return;
2270 
2271         case FILTER_CHANGE:
2272             secondsLabel->setText(i18n("Changing Filters..."));
2273             QTimer::singleShot(1000, this, &Ekos::Capture::captureImage);
2274             return;
2275 
2276         case FILTER_OFFSET:
2277             secondsLabel->setText(i18n("Adjusting Filter Offset..."));
2278             QTimer::singleShot(1000, this, &Ekos::Capture::captureImage);
2279             return;
2280     }
2281 
2282     // Do not start nor abort if Focus is busy
2283     if (m_FocusState >= FOCUS_PROGRESS)
2284     {
2285         appendLogText(i18n("Delaying capture while focus module is busy."));
2286         QTimer::singleShot(1000, this, &Ekos::Capture::captureImage);
2287         return;
2288     }
2289 
2290     // Bail out if we have no CCD anymore
2291     if (currentCCD->isConnected() == false)
2292     {
2293         appendLogText(i18n("Error: Lost connection to CCD."));
2294         abort();
2295         return;
2296     }
2297 
2298     captureTimeout.stop();
2299     seqTimer->stop();
2300 
2301     SequenceJob::CAPTUREResult rc = SequenceJob::CAPTURE_OK;
2302 
2303     if (currentFilter != nullptr)
2304     {
2305         // JM 2021.08.23 Call filter info to set the active filter wheel in the camera driver
2306         // so that it may snoop on the active filter
2307         syncFilterInfo();
2308         m_CurrentFilterPosition = filterManager->getFilterPosition();
2309         activeJob->setCurrentFilter(m_CurrentFilterPosition);
2310     }
2311 
2312     if (currentCCD->hasCooler())
2313     {
2314         double temperature = 0;
2315         currentCCD->getTemperature(&temperature);
2316         activeJob->setCurrentTemperature(temperature);
2317     }
2318 
2319     if (currentCCD->isFastExposureEnabled())
2320     {
2321         int remaining = m_isFraming ? 100000 : (activeJob->getCount() - activeJob->getCompleted());
2322         if (remaining > 1)
2323             currentCCD->setFastCount(static_cast<uint>(remaining));
2324     }
2325 
2326     connect(currentCCD, &ISD::CCD::newImage, this, &Ekos::Capture::processData, Qt::UniqueConnection);
2327     //connect(currentCCD, &ISD::CCD::previewFITSGenerated, this, &Ekos::Capture::setGeneratedPreviewFITS, Qt::UniqueConnection);
2328 
2329     if (activeJob->getFrameType() == FRAME_FLAT)
2330     {
2331         // If we have to calibrate ADU levels, first capture must be preview and not in batch mode
2332         if (activeJob->isPreview() == false && activeJob->getFlatFieldDuration() == DURATION_ADU &&
2333                 calibrationStage == CAL_PRECAPTURE_COMPLETE)
2334         {
2335             if (currentCCD->getTransferFormat() == ISD::CCD::FORMAT_NATIVE)
2336             {
2337                 appendLogText(i18n("Cannot calculate ADU levels in non-FITS images."));
2338                 abort();
2339                 return;
2340             }
2341 
2342             calibrationStage = CAL_CALIBRATION;
2343             // We need to be in preview mode and in client mode for this to work
2344             activeJob->setPreview(true);
2345         }
2346     }
2347 
2348     // If preview, always set to UPLOAD_CLIENT if not already set.
2349     if (activeJob->isPreview())
2350     {
2351         if (currentCCD->getUploadMode() != ISD::CCD::UPLOAD_CLIENT)
2352             currentCCD->setUploadMode(ISD::CCD::UPLOAD_CLIENT);
2353     }
2354     // If batch mode, ensure upload mode mathces the active job target.
2355     else
2356     {
2357         if (currentCCD->getUploadMode() != activeJob->getUploadMode())
2358             currentCCD->setUploadMode(activeJob->getUploadMode());
2359     }
2360 
2361     if (currentCCD->getUploadMode() != ISD::CCD::UPLOAD_LOCAL)
2362     {
2363         checkSeqBoundary(activeJob->getSignature());
2364         currentCCD->setNextSequenceID(nextSequenceID);
2365     }
2366 
2367     m_State = CAPTURE_CAPTURING;
2368 
2369     //if (activeJob->isPreview() == false)
2370     // NOTE: Why we didn't emit this before for preview?
2371     emit newStatus(Ekos::CAPTURE_CAPTURING);
2372 
2373     if (frameSettings.contains(activeJob->getActiveChip()))
2374     {
2375         QVariantMap settings;
2376         settings["x"]    = activeJob->getSubX();
2377         settings["y"]    = activeJob->getSubY();
2378         settings["w"]    = activeJob->getSubW();
2379         settings["h"]    = activeJob->getSubH();
2380         settings["binx"] = activeJob->getXBin();
2381         settings["biny"] = activeJob->getYBin();
2382 
2383         frameSettings[activeJob->getActiveChip()] = settings;
2384     }
2385 
2386     // If using DSLR, make sure it is set to correct transfer format
2387     currentCCD->setTransformFormat(activeJob->getTransforFormat());
2388 
2389     connect(currentCCD, &ISD::CCD::newExposureValue, this, &Ekos::Capture::setExposureProgress, Qt::UniqueConnection);
2390 
2391     rc = activeJob->capture(m_AutoFocusReady);
2392 
2393     if (rc != SequenceJob::CAPTURE_OK)
2394     {
2395         disconnect(currentCCD, &ISD::CCD::newExposureValue, this, &Ekos::Capture::setExposureProgress);
2396     }
2397     switch (rc)
2398     {
2399         case SequenceJob::CAPTURE_OK:
2400         {
2401             emit captureStarting(activeJob->getExposure(), activeJob->getFilterName());
2402             appendLogText(i18n("Capturing %1-second %2 image...", QString("%L1").arg(activeJob->getExposure(), 0, 'f', 3),
2403                                activeJob->getFilterName()));
2404             captureTimeout.start(static_cast<int>(activeJob->getExposure()) * 1000 + CAPTURE_TIMEOUT_THRESHOLD);
2405             if (activeJob->isPreview() == false)
2406             {
2407                 int index = jobs.indexOf(activeJob);
2408                 QJsonObject oneSequence = m_SequenceArray[index].toObject();
2409                 oneSequence["Status"] = "In Progress";
2410                 m_SequenceArray.replace(index, oneSequence);
2411                 emit sequenceChanged(m_SequenceArray);
2412             }
2413         }
2414         break;
2415 
2416         case SequenceJob::CAPTURE_FRAME_ERROR:
2417             appendLogText(i18n("Failed to set sub frame."));
2418             abort();
2419             break;
2420 
2421         case SequenceJob::CAPTURE_BIN_ERROR:
2422             appendLogText(i18n("Failed to set binning."));
2423             abort();
2424             break;
2425 
2426         case SequenceJob::CAPTURE_FILTER_BUSY:
2427             // Try again in 1 second if filter is busy
2428             secondsLabel->setText(i18n("Changing filter..."));
2429             QTimer::singleShot(1000, this, &Ekos::Capture::captureImage);
2430             break;
2431 
2432         case SequenceJob::CAPTURE_GUIDER_DRIFT_WAIT:
2433             // Try again in 1 second if filter is busy
2434             secondsLabel->setText(i18n("Guider settling..."));
2435             qCDebug(KSTARS_EKOS_CAPTURE) << "Waiting for the guider to settle.";
2436             QTimer::singleShot(1000, this, &Ekos::Capture::captureImage);
2437             break;
2438 
2439         case SequenceJob::CAPTURE_FOCUS_ERROR:
2440             appendLogText(i18n("Cannot capture while focus module is busy."));
2441             abort();
2442             break;
2443     }
2444 }
2445 
2446 /*******************************************************************************/
2447 /* Update the prefix for the sequence of images to be captured                 */
2448 /*******************************************************************************/
updateSequencePrefix(const QString & newPrefix,const QString & dir)2449 void Capture::updateSequencePrefix(const QString &newPrefix, const QString &dir)
2450 {
2451     seqPrefix = newPrefix;
2452 
2453     // If it doesn't exist, create it
2454     QDir().mkpath(dir);
2455 
2456     nextSequenceID = 1;
2457 }
2458 
2459 /*******************************************************************************/
2460 /* Determine the next file number sequence. That is, if we have file1.png      */
2461 /* and file2.png, then the next sequence should be file3.png		           */
2462 /*******************************************************************************/
checkSeqBoundary(const QString & path)2463 void Capture::checkSeqBoundary(const QString &path)
2464 {
2465     int newFileIndex = -1;
2466     QFileInfo const path_info(path);
2467     QString const sig_dir(path_info.dir().path());
2468     QString const sig_file(path_info.completeBaseName());
2469     QString tempName;
2470     // seqFileCount = 0;
2471 
2472     // No updates during meridian flip
2473     if (meridianFlipStage >= MF_ALIGNING)
2474         return;
2475 
2476     QDirIterator it(sig_dir, QDir::Files);
2477 
2478     while (it.hasNext())
2479     {
2480         tempName = it.next();
2481         QFileInfo info(tempName);
2482 
2483         // This returns the filename without the extension
2484         tempName = info.completeBaseName();
2485 
2486         // This remove any additional extension (e.g. m42_001.fits.fz)
2487         // the completeBaseName() would return m42_001.fits
2488         // and this remove .fits so we end up with m42_001
2489         tempName = tempName.remove(".fits");
2490 
2491         QString finalSeqPrefix = seqPrefix;
2492         finalSeqPrefix.remove(SequenceJob::ISOMarker);
2493         // find the prefix first
2494         if (tempName.startsWith(finalSeqPrefix, Qt::CaseInsensitive) == false)
2495             continue;
2496 
2497         /* Do not change the number of captures.
2498          * - If the sequence is required by the end-user, unconditionally run what each sequence item is requiring.
2499          * - If the sequence is required by the scheduler, use capturedFramesMap to determine when to stop capturing.
2500          */
2501         //seqFileCount++;
2502 
2503         int lastUnderScoreIndex = tempName.lastIndexOf("_");
2504         if (lastUnderScoreIndex > 0)
2505         {
2506             bool indexOK = false;
2507 
2508             newFileIndex = tempName.midRef(lastUnderScoreIndex + 1).toInt(&indexOK);
2509             if (indexOK && newFileIndex >= nextSequenceID)
2510                 nextSequenceID = newFileIndex + 1;
2511         }
2512     }
2513 }
2514 
appendLogText(const QString & text)2515 void Capture::appendLogText(const QString &text)
2516 {
2517     m_LogText.insert(0, i18nc("log entry; %1 is the date, %2 is the text", "%1 %2",
2518                               KStarsData::Instance()->lt().toString("yyyy-MM-ddThh:mm:ss"), text));
2519 
2520     qCInfo(KSTARS_EKOS_CAPTURE) << text;
2521 
2522     emit newLog(text);
2523 }
2524 
clearLog()2525 void Capture::clearLog()
2526 {
2527     m_LogText.clear();
2528     emit newLog(QString());
2529 }
2530 
2531 //This method will update the Capture Module and Summary Screen's estimate of how much time is left in the download
setDownloadProgress()2532 void Capture::setDownloadProgress()
2533 {
2534     if (activeJob)
2535     {
2536         double downloadTimeLeft = getEstimatedDownloadTime() - downloadTimer.elapsed() / 1000.0;
2537         if(downloadTimeLeft > 0)
2538         {
2539             exposeOUT->setText(QString("%L1").arg(downloadTimeLeft, 0, 'd', 2));
2540             emit newDownloadProgress(downloadTimeLeft);
2541         }
2542     }
2543 }
2544 
setExposureProgress(ISD::CCDChip * tChip,double value,IPState state)2545 void Capture::setExposureProgress(ISD::CCDChip * tChip, double value, IPState state)
2546 {
2547     if (targetChip != tChip || targetChip->getCaptureMode() != FITS_NORMAL || meridianFlipStage >= MF_ALIGNING)
2548         return;
2549 
2550     exposeOUT->setText(QString("%L1").arg(value, 0, 'd', 2));
2551 
2552     if (activeJob)
2553     {
2554         activeJob->setExposeLeft(value);
2555 
2556         emit newExposureProgress(activeJob);
2557     }
2558 
2559     if (activeJob && state == IPS_ALERT)
2560     {
2561         int retries = activeJob->getCaptureRetires() + 1;
2562 
2563         activeJob->setCaptureRetires(retries);
2564 
2565         appendLogText(i18n("Capture failed. Check INDI Control Panel for details."));
2566 
2567         if (retries == 3)
2568         {
2569             abort();
2570             return;
2571         }
2572 
2573         appendLogText(i18n("Restarting capture attempt #%1", retries));
2574 
2575         nextSequenceID = 1;
2576 
2577         captureImage();
2578         return;
2579     }
2580 
2581     if (activeJob != nullptr && state == IPS_OK)
2582     {
2583         activeJob->setCaptureRetires(0);
2584         activeJob->setExposeLeft(0);
2585 
2586         if (currentCCD && currentCCD->getUploadMode() == ISD::CCD::UPLOAD_LOCAL)
2587         {
2588             if (activeJob && activeJob->getStatus() == SequenceJob::JOB_BUSY)
2589             {
2590                 processData(nullptr);
2591                 return;
2592             }
2593         }
2594 
2595         //if (isAutoGuiding && Options::useEkosGuider() && currentCCD->getChip(ISD::CCDChip::GUIDE_CCD) == guideChip)
2596         if (m_GuideState == GUIDE_GUIDING && Options::guiderType() == 0 && suspendGuideOnDownload)
2597         {
2598             qCDebug(KSTARS_EKOS_CAPTURE) << "Autoguiding suspended until primary CCD chip completes downloading...";
2599             emit suspendGuiding();
2600         }
2601 
2602         secondsLabel->setText(i18n("Downloading..."));
2603 
2604         //This will start the clock to see how long the download takes.
2605         downloadTimer.start();
2606         downloadProgressTimer.start();
2607 
2608 
2609         //disconnect(currentCCD, &ISD::CCD::newExposureValue(ISD::CCDChip*,double,IPState)), this, &Ekos::Capture::updateCaptureProgress(ISD::CCDChip*,double,IPState)));
2610     }
2611     // JM: Don't change to i18np, value is DOUBLE, not Integer.
2612     else if (value <= 1)
2613         secondsLabel->setText(i18n("second left"));
2614     else
2615         secondsLabel->setText(i18n("seconds left"));
2616 }
2617 
processCaptureError(ISD::CCD::ErrorType type)2618 void Capture::processCaptureError(ISD::CCD::ErrorType type)
2619 {
2620     if (!activeJob)
2621         return;
2622 
2623     if (type == ISD::CCD::ERROR_CAPTURE)
2624     {
2625         int retries = activeJob->getCaptureRetires() + 1;
2626 
2627         activeJob->setCaptureRetires(retries);
2628 
2629         appendLogText(i18n("Capture failed. Check INDI Control Panel for details."));
2630 
2631         if (retries == 3)
2632         {
2633             abort();
2634             return;
2635         }
2636 
2637         appendLogText(i18n("Restarting capture attempt #%1", retries));
2638 
2639         nextSequenceID = 1;
2640 
2641         captureImage();
2642         return;
2643     }
2644     else
2645     {
2646         abort();
2647     }
2648 }
2649 
updateCCDTemperature(double value)2650 void Capture::updateCCDTemperature(double value)
2651 {
2652     if (cameraTemperatureS->isEnabled() == false)
2653     {
2654         if (currentCCD->getBaseDevice()->getPropertyPermission("CCD_TEMPERATURE") != IP_RO)
2655             checkCCD();
2656     }
2657 
2658     temperatureOUT->setText(QString("%L1").arg(value, 0, 'f', 2));
2659 
2660     if (cameraTemperatureN->cleanText().isEmpty())
2661         cameraTemperatureN->setValue(value);
2662 
2663     //if (activeJob && (activeJob->getStatus() == SequenceJob::JOB_ABORTED || activeJob->getStatus() == SequenceJob::JOB_IDLE))
2664     if (activeJob)
2665         activeJob->setCurrentTemperature(value);
2666 }
2667 
updateRotatorNumber(INumberVectorProperty * nvp)2668 void Capture::updateRotatorNumber(INumberVectorProperty * nvp)
2669 {
2670     if (!strcmp(nvp->name, "ABS_ROTATOR_ANGLE"))
2671     {
2672         // Update widget rotator position
2673         rotatorSettings->setCurrentAngle(nvp->np[0].value);
2674 
2675         //if (activeJob && (activeJob->getStatus() == SequenceJob::JOB_ABORTED || activeJob->getStatus() == SequenceJob::JOB_IDLE))
2676         if (activeJob)
2677             activeJob->setCurrentRotation(rotatorSettings->getCurrentRotationPA());
2678     }
2679 }
2680 
addJob(bool preview)2681 bool Capture::addJob(bool preview)
2682 {
2683     if (m_State != CAPTURE_IDLE && m_State != CAPTURE_ABORTED && m_State != CAPTURE_COMPLETE)
2684         return false;
2685 
2686     SequenceJob * job = nullptr;
2687 
2688     //    if (preview == false && darkSubCheck->isChecked())
2689     //    {
2690     //        KSNotification::error(i18n("Auto dark subtract is not supported in batch mode."));
2691     //        return false;
2692     //    }
2693 
2694     if (fileUploadModeS->currentIndex() != ISD::CCD::UPLOAD_CLIENT && fileRemoteDirT->text().isEmpty())
2695     {
2696         KSNotification::error(i18n("You must set remote directory for Local & Both modes."));
2697         return false;
2698     }
2699 
2700     if (fileUploadModeS->currentIndex() != ISD::CCD::UPLOAD_LOCAL && fileDirectoryT->text().isEmpty())
2701     {
2702         KSNotification::error(i18n("You must set local directory for Client & Both modes."));
2703         return false;
2704     }
2705 
2706     if (m_JobUnderEdit)
2707         job = jobs.at(queueTable->currentRow());
2708     else
2709     {
2710         job = new SequenceJob();
2711         job->setFilterManager(filterManager);
2712     }
2713 
2714     Q_ASSERT_X(job, __FUNCTION__, "Capture Job is invalid.");
2715 
2716     if (captureISOS)
2717         job->setISOIndex(captureISOS->currentIndex());
2718 
2719     if (getGain() >= 0)
2720         job->setGain(getGain());
2721 
2722     if (getOffset() >= 0)
2723         job->setOffset(getOffset());
2724 
2725     job->setTransforFormat(static_cast<ISD::CCD::TransferFormat>(captureFormatS->currentIndex()));
2726 
2727     job->setPreview(preview);
2728 
2729     if (cameraTemperatureN->isEnabled())
2730     {
2731         double currentTemperature;
2732         currentCCD->getTemperature(&currentTemperature);
2733         job->setEnforceTemperature(cameraTemperatureS->isChecked());
2734         job->setTargetTemperature(cameraTemperatureN->value());
2735         job->setCurrentTemperature(currentTemperature);
2736     }
2737 
2738     //job->setCaptureFilter(static_cast<FITSScale>(filterCombo->currentIndex()));
2739 
2740     job->setUploadMode(static_cast<ISD::CCD::UploadMode>(fileUploadModeS->currentIndex()));
2741     job->setScripts(m_Scripts);
2742     job->setFlatFieldDuration(flatFieldDuration);
2743     job->setFlatFieldSource(flatFieldSource);
2744     job->setPreMountPark(preMountPark);
2745     job->setPreDomePark(preDomePark);
2746     job->setWallCoord(wallCoord);
2747     job->setTargetADU(targetADU);
2748     job->setTargetADUTolerance(targetADUTolerance);
2749 
2750     // JM 2019-11-26: In case there is no raw prefix set
2751     // BUT target name is set, we update the prefix to include
2752     // the target name, which is usually set by the scheduler.
2753     if (filePrefixT->text().isEmpty() && !m_TargetName.isEmpty())
2754     {
2755         filePrefixT->setText(m_TargetName);
2756     }
2757 
2758     job->setPrefixSettings(filePrefixT->text(), fileFilterS->isChecked(), fileDurationS->isChecked(),
2759                            fileTimestampS->isChecked());
2760     job->setFrameType(static_cast<CCDFrameType>(captureTypeS->currentIndex()));
2761 
2762     job->setEnforceStartGuiderDrift(job->getFrameType() == FRAME_LIGHT &&
2763                                     startGuiderDriftS->isChecked());
2764     job->setTargetStartGuiderDrift(startGuiderDriftN->value());
2765 
2766     //if (filterSlot != nullptr && currentFilter != nullptr)
2767     if (captureFilterS->currentIndex() != -1 && currentFilter != nullptr)
2768         job->setTargetFilter(captureFilterS->currentIndex() + 1, captureFilterS->currentText());
2769 
2770     job->setExposure(captureExposureN->value());
2771 
2772     job->setCount(captureCountN->value());
2773 
2774     job->setBin(captureBinHN->value(), captureBinVN->value());
2775 
2776     job->setDelay(captureDelayN->value() * 1000); /* in ms */
2777 
2778     job->setActiveChip(targetChip);
2779     job->setActiveCCD(currentCCD);
2780     job->setActiveFilter(currentFilter);
2781 
2782     // Custom Properties
2783     job->setCustomProperties(customPropertiesDialog->getCustomProperties());
2784 
2785     if (currentRotator && rotatorSettings->isRotationEnforced())
2786     {
2787         job->setActiveRotator(currentRotator);
2788         job->setTargetRotation(rotatorSettings->getTargetRotationPA());
2789         job->setCurrentRotation(rotatorSettings->getCurrentRotationPA());
2790     }
2791 
2792     job->setFrame(captureFrameXN->value(), captureFrameYN->value(), captureFrameWN->value(), captureFrameHN->value());
2793     job->setRemoteDir(fileRemoteDirT->text());
2794 
2795     // Remove trailing slash, if any.
2796     QString fileDirectory = fileDirectoryT->text();
2797     while (fileDirectory.endsWith("/"))
2798         fileDirectory.chop(1);
2799     job->setLocalDir(fileDirectory);
2800 
2801     if (m_JobUnderEdit == false)
2802     {
2803         // JM 2018-09-24: If this is the first job added
2804         // We always ignore job progress by default.
2805         if (jobs.isEmpty() && preview == false)
2806             ignoreJobProgress = true;
2807 
2808         jobs.append(job);
2809 
2810         // Nothing more to do if preview
2811         if (preview)
2812             return true;
2813     }
2814 
2815     QJsonObject jsonJob = {{"Status", "Idle"}};
2816 
2817     auto placeholderPath = Ekos::PlaceholderPath();
2818     placeholderPath.addJob(job, m_TargetName);
2819 
2820     int currentRow = 0;
2821     if (m_JobUnderEdit == false)
2822     {
2823         currentRow = queueTable->rowCount();
2824         queueTable->insertRow(currentRow);
2825     }
2826     else
2827         currentRow = queueTable->currentRow();
2828 
2829     QTableWidgetItem * status = m_JobUnderEdit ? queueTable->item(currentRow, 0) : new QTableWidgetItem();
2830     job->setStatusCell(status);
2831 
2832     QTableWidgetItem * filter = m_JobUnderEdit ? queueTable->item(currentRow, 1) : new QTableWidgetItem();
2833     filter->setText("--");
2834     jsonJob.insert("Filter", "--");
2835     /*if (captureTypeS->currentText().compare("Bias", Qt::CaseInsensitive) &&
2836             captureTypeS->currentText().compare("Dark", Qt::CaseInsensitive) &&
2837             captureFilterS->count() > 0)*/
2838     if (captureFilterS->count() > 0 &&
2839             (captureTypeS->currentIndex() == FRAME_LIGHT || captureTypeS->currentIndex() == FRAME_FLAT))
2840     {
2841         filter->setText(captureFilterS->currentText());
2842         jsonJob.insert("Filter", captureFilterS->currentText());
2843     }
2844 
2845     filter->setTextAlignment(Qt::AlignHCenter);
2846     filter->setFlags(Qt::ItemIsSelectable | Qt::ItemIsEnabled);
2847 
2848     QTableWidgetItem * count = m_JobUnderEdit ? queueTable->item(currentRow, 2) : new QTableWidgetItem();
2849     job->setCountCell(count);
2850     jsonJob.insert("Count", count->text());
2851 
2852     QTableWidgetItem * exp = m_JobUnderEdit ? queueTable->item(currentRow, 3) : new QTableWidgetItem();
2853     exp->setText(QString("%L1").arg(captureExposureN->value(), 0, 'f', captureExposureN->decimals()));
2854     exp->setTextAlignment(Qt::AlignHCenter);
2855     exp->setFlags(Qt::ItemIsSelectable | Qt::ItemIsEnabled);
2856     jsonJob.insert("Exp", exp->text());
2857 
2858     QTableWidgetItem * type = m_JobUnderEdit ? queueTable->item(currentRow, 4) : new QTableWidgetItem();
2859     type->setText(captureTypeS->currentText());
2860     type->setTextAlignment(Qt::AlignHCenter);
2861     type->setFlags(Qt::ItemIsSelectable | Qt::ItemIsEnabled);
2862     jsonJob.insert("Type", type->text());
2863 
2864     QTableWidgetItem * bin = m_JobUnderEdit ? queueTable->item(currentRow, 5) : new QTableWidgetItem();
2865     bin->setText(QString("%1x%2").arg(captureBinHN->value()).arg(captureBinVN->value()));
2866     bin->setTextAlignment(Qt::AlignHCenter);
2867     bin->setFlags(Qt::ItemIsSelectable | Qt::ItemIsEnabled);
2868     jsonJob.insert("Bin", bin->text());
2869 
2870     QTableWidgetItem * iso = m_JobUnderEdit ? queueTable->item(currentRow, 6) : new QTableWidgetItem();
2871     if (captureISOS && captureISOS->currentIndex() != -1)
2872     {
2873         iso->setText(captureISOS->currentText());
2874         jsonJob.insert("ISO/Gain", iso->text());
2875     }
2876     else if (job->getGain() >= 0)
2877     {
2878         iso->setText(QString::number(job->getGain(), 'f', 1));
2879         jsonJob.insert("ISO/Gain", iso->text());
2880     }
2881     else
2882     {
2883         iso->setText("--");
2884         jsonJob.insert("ISO/Gain", "--");
2885     }
2886     iso->setTextAlignment(Qt::AlignHCenter);
2887     iso->setFlags(Qt::ItemIsSelectable | Qt::ItemIsEnabled);
2888 
2889     QTableWidgetItem * offset = m_JobUnderEdit ? queueTable->item(currentRow, 7) : new QTableWidgetItem();
2890     if (job->getOffset() >= 0)
2891     {
2892         offset->setText(QString::number(job->getOffset(), 'f', 1));
2893         jsonJob.insert("Offset", offset->text());
2894     }
2895     else
2896     {
2897         offset->setText("--");
2898         jsonJob.insert("Offset", "--");
2899     }
2900     offset->setTextAlignment(Qt::AlignHCenter);
2901     offset->setFlags(Qt::ItemIsSelectable | Qt::ItemIsEnabled);
2902 
2903     if (m_JobUnderEdit == false)
2904     {
2905         queueTable->setItem(currentRow, 0, status);
2906         queueTable->setItem(currentRow, 1, filter);
2907         queueTable->setItem(currentRow, 2, count);
2908         queueTable->setItem(currentRow, 3, exp);
2909         queueTable->setItem(currentRow, 4, type);
2910         queueTable->setItem(currentRow, 5, bin);
2911         queueTable->setItem(currentRow, 6, iso);
2912         queueTable->setItem(currentRow, 7, offset);
2913 
2914         m_SequenceArray.append(jsonJob);
2915         emit sequenceChanged(m_SequenceArray);
2916     }
2917 
2918     removeFromQueueB->setEnabled(true);
2919 
2920     if (queueTable->rowCount() > 0)
2921     {
2922         queueSaveAsB->setEnabled(true);
2923         queueSaveB->setEnabled(true);
2924         resetB->setEnabled(true);
2925         m_Dirty = true;
2926     }
2927 
2928     if (queueTable->rowCount() > 1)
2929     {
2930         queueUpB->setEnabled(true);
2931         queueDownB->setEnabled(true);
2932     }
2933 
2934     if (m_JobUnderEdit)
2935     {
2936         m_JobUnderEdit = false;
2937         resetJobEdit();
2938         appendLogText(i18n("Job #%1 changes applied.", currentRow + 1));
2939 
2940         m_SequenceArray.replace(currentRow, jsonJob);
2941         emit sequenceChanged(m_SequenceArray);
2942     }
2943 
2944     return true;
2945 }
2946 
removeJobFromQueue()2947 void Capture::removeJobFromQueue()
2948 {
2949     int currentRow = queueTable->currentRow();
2950 
2951     if (currentRow < 0)
2952         currentRow = queueTable->rowCount() - 1;
2953 
2954     removeJob(currentRow);
2955 
2956     // update selection
2957     if (queueTable->rowCount() == 0)
2958         return;
2959 
2960     if (currentRow > queueTable->rowCount())
2961         queueTable->selectRow(queueTable->rowCount() - 1);
2962     else
2963         queueTable->selectRow(currentRow);
2964 }
2965 
removeJob(int index)2966 void Capture::removeJob(int index)
2967 {
2968     if (m_State != CAPTURE_IDLE && m_State != CAPTURE_ABORTED && m_State != CAPTURE_COMPLETE)
2969         return;
2970 
2971     if (m_JobUnderEdit)
2972     {
2973         resetJobEdit();
2974         return;
2975     }
2976 
2977     if (index < 0 || index >= jobs.count())
2978         return;
2979 
2980 
2981     queueTable->removeRow(index);
2982 
2983     m_SequenceArray.removeAt(index);
2984     emit sequenceChanged(m_SequenceArray);
2985 
2986     if (jobs.empty())
2987         return;
2988 
2989     SequenceJob * job = jobs.at(index);
2990     jobs.removeOne(job);
2991     if (job == activeJob)
2992         activeJob = nullptr;
2993 
2994     delete job;
2995 
2996     if (queueTable->rowCount() == 0)
2997         removeFromQueueB->setEnabled(false);
2998 
2999     if (queueTable->rowCount() == 1)
3000     {
3001         queueUpB->setEnabled(false);
3002         queueDownB->setEnabled(false);
3003     }
3004 
3005     for (int i = 0; i < jobs.count(); i++)
3006         jobs.at(i)->setStatusCell(queueTable->item(i, 0));
3007 
3008     if (index < queueTable->rowCount())
3009         queueTable->selectRow(index);
3010     else if (queueTable->rowCount() > 0)
3011         queueTable->selectRow(queueTable->rowCount() - 1);
3012 
3013     if (queueTable->rowCount() == 0)
3014     {
3015         queueSaveAsB->setEnabled(false);
3016         queueSaveB->setEnabled(false);
3017         resetB->setEnabled(false);
3018     }
3019 
3020     m_Dirty = true;
3021 }
3022 
moveJobUp()3023 void Capture::moveJobUp()
3024 {
3025     int currentRow = queueTable->currentRow();
3026 
3027     int columnCount = queueTable->columnCount();
3028 
3029     if (currentRow <= 0 || queueTable->rowCount() == 1)
3030         return;
3031 
3032     int destinationRow = currentRow - 1;
3033 
3034     for (int i = 0; i < columnCount; i++)
3035     {
3036         QTableWidgetItem * downItem = queueTable->takeItem(currentRow, i);
3037         QTableWidgetItem * upItem   = queueTable->takeItem(destinationRow, i);
3038 
3039         queueTable->setItem(destinationRow, i, downItem);
3040         queueTable->setItem(currentRow, i, upItem);
3041     }
3042 
3043     SequenceJob * job = jobs.takeAt(currentRow);
3044 
3045     jobs.removeOne(job);
3046     jobs.insert(destinationRow, job);
3047 
3048     QJsonObject currentJob = m_SequenceArray[currentRow].toObject();
3049     m_SequenceArray.replace(currentRow, m_SequenceArray[destinationRow]);
3050     m_SequenceArray.replace(destinationRow, currentJob);
3051     emit sequenceChanged(m_SequenceArray);
3052 
3053     queueTable->selectRow(destinationRow);
3054 
3055     for (int i = 0; i < jobs.count(); i++)
3056         jobs.at(i)->setStatusCell(queueTable->item(i, 0));
3057 
3058     m_Dirty = true;
3059 }
3060 
moveJobDown()3061 void Capture::moveJobDown()
3062 {
3063     int currentRow = queueTable->currentRow();
3064 
3065     int columnCount = queueTable->columnCount();
3066 
3067     if (currentRow < 0 || queueTable->rowCount() == 1 || (currentRow + 1) == queueTable->rowCount())
3068         return;
3069 
3070     int destinationRow = currentRow + 1;
3071 
3072     for (int i = 0; i < columnCount; i++)
3073     {
3074         QTableWidgetItem * downItem = queueTable->takeItem(currentRow, i);
3075         QTableWidgetItem * upItem   = queueTable->takeItem(destinationRow, i);
3076 
3077         queueTable->setItem(destinationRow, i, downItem);
3078         queueTable->setItem(currentRow, i, upItem);
3079     }
3080 
3081     SequenceJob * job = jobs.takeAt(currentRow);
3082 
3083     jobs.removeOne(job);
3084     jobs.insert(destinationRow, job);
3085 
3086     QJsonObject currentJob = m_SequenceArray[currentRow].toObject();
3087     m_SequenceArray.replace(currentRow, m_SequenceArray[destinationRow]);
3088     m_SequenceArray.replace(destinationRow, currentJob);
3089     emit sequenceChanged(m_SequenceArray);
3090 
3091     queueTable->selectRow(destinationRow);
3092 
3093     for (int i = 0; i < jobs.count(); i++)
3094         jobs.at(i)->setStatusCell(queueTable->item(i, 0));
3095 
3096     m_Dirty = true;
3097 }
3098 
setBusy(bool enable)3099 void Capture::setBusy(bool enable)
3100 {
3101     isBusy = enable;
3102 
3103     enable ? pi->startAnimation() : pi->stopAnimation();
3104     previewB->setEnabled(!enable);
3105     loopB->setEnabled(!enable);
3106 
3107     foreach (QAbstractButton * button, queueEditButtonGroup->buttons())
3108         button->setEnabled(!enable);
3109 }
3110 
3111 /**
3112  * @brief Update the counters of existing frames and continue with prepareActiveJob(), if there exist less
3113  *        images than targeted. If enough images exist, continue with processJobCompletion().
3114  */
prepareJob(SequenceJob * job)3115 void Capture::prepareJob(SequenceJob * job)
3116 {
3117     activeJob = job;
3118 
3119     if (m_isFraming == false)
3120         qCDebug(KSTARS_EKOS_CAPTURE) << "Preparing capture job" << job->getSignature() << "for execution.";
3121 
3122     int index = jobs.indexOf(job);
3123     if (index >= 0)
3124         queueTable->selectRow(index);
3125 
3126     if (activeJob->getActiveCCD() != currentCCD)
3127     {
3128         setCamera(activeJob->getActiveCCD()->getDeviceName());
3129     }
3130 
3131     /*if (activeJob->isPreview())
3132         seqTotalCount = -1;
3133     else
3134         seqTotalCount = activeJob->getCount();*/
3135 
3136     seqDelay = activeJob->getDelay();
3137 
3138     // seqCurrentCount = activeJob->getCompleted();
3139 
3140     if (activeJob->isPreview() == false)
3141     {
3142         fullImgCountOUT->setText(QString("%L1").arg(activeJob->getCount()));
3143         currentImgCountOUT->setText(QString("%L1").arg(activeJob->getCompleted()));
3144 
3145         // set the progress info
3146         imgProgress->setEnabled(true);
3147         imgProgress->setMaximum(activeJob->getCount());
3148         imgProgress->setValue(activeJob->getCompleted());
3149 
3150         if (currentCCD->getUploadMode() != ISD::CCD::UPLOAD_LOCAL)
3151             updateSequencePrefix(activeJob->getFullPrefix(), QFileInfo(activeJob->getSignature()).path());
3152     }
3153 
3154     // We check if the job is already fully or partially complete by checking how many files of its type exist on the file system
3155     if (activeJob->isPreview() == false)
3156     {
3157         // The signature is the unique identification path in the system for a particular job. Format is "<storage path>/<target>/<frame type>/<filter name>".
3158         // If the Scheduler is requesting the Capture tab to process a sequence job, a target name will be inserted after the sequence file storage field (e.g. /path/to/storage/target/Light/...)
3159         // If the end-user is requesting the Capture tab to process a sequence job, the sequence file storage will be used as is (e.g. /path/to/storage/Light/...)
3160         QString signature = activeJob->getSignature();
3161 
3162         // Now check on the file system ALL the files that exist with the above signature
3163         // If 29 files exist for example, then nextSequenceID would be the NEXT file number (30)
3164         // Therefore, we know how to number the next file.
3165         // However, we do not deduce the number of captures to process from this function.
3166         checkSeqBoundary(signature);
3167 
3168         // Captured Frames Map contains a list of signatures:count of _already_ captured files in the file system.
3169         // This map is set by the Scheduler in order to complete efficiently the required captures.
3170         // When the end-user requests a sequence to be processed, that map is empty.
3171         //
3172         // Example with a 5xL-5xR-5xG-5xB sequence
3173         //
3174         // When the end-user loads and runs this sequence, each filter gets to capture 5 frames, then the procedure stops.
3175         // When the Scheduler executes a job with this sequence, the procedure depends on what is in the storage.
3176         //
3177         // Let's consider the Scheduler has 3 instances of this job to run.
3178         //
3179         // When the first job completes the sequence, there are 20 images in the file system (5 for each filter).
3180         // When the second job starts, Scheduler finds those 20 images but requires 20 more images, thus sets the frames map counters to 0 for all LRGB frames.
3181         // When the third job starts, Scheduler now has 40 images, but still requires 20 more, thus again sets the frames map counters to 0 for all LRGB frames.
3182         //
3183         // Now let's consider something went wrong, and the third job was aborted before getting to 60 images, say we have full LRG, but only 1xB.
3184         // When Scheduler attempts to run the aborted job again, it will count captures in storage, subtract previous job requirements, and set the frames map counters to 0 for LRG, and 4 for B.
3185         // When the sequence runs, the procedure will bypass LRG and proceed to capture 4xB.
3186         if (capturedFramesMap.contains(signature))
3187         {
3188             // Get the current capture count from the map
3189             int count = capturedFramesMap[signature];
3190 
3191             // Count how many captures this job has to process, given that previous jobs may have done some work already
3192             for (auto &a_job : jobs)
3193                 if (a_job == activeJob)
3194                     break;
3195                 else if (a_job->getSignature() == activeJob->getSignature())
3196                     count -= a_job->getCompleted();
3197 
3198             // This is the current completion count of the current job
3199             activeJob->setCompleted(count);
3200         }
3201         // JM 2018-09-24: Only set completed jobs to 0 IF the scheduler set captured frames map to begin with
3202         // If the map is empty, then no scheduler is used and it should proceed as normal.
3203         else if (capturedFramesMap.count() > 0)
3204         {
3205             // No preliminary information, we reset the job count and run the job unconditionally to clarify the behavior
3206             activeJob->setCompleted(0);
3207         }
3208         // JM 2018-09-24: In case ignoreJobProgress is enabled
3209         // We check if this particular job progress ignore flag is set. If not,
3210         // then we set it and reset completed to zero. Next time it is evaluated here again
3211         // It will maintain its count regardless
3212         else if (ignoreJobProgress && activeJob->getJobProgressIgnored() == false)
3213         {
3214             activeJob->setJobProgressIgnored(true);
3215             activeJob->setCompleted(0);
3216         }
3217         // We cannot rely on sequenceID to give us a count - if we don't ignore job progress, we leave the count as it was originally
3218 #if 0
3219         // If we cannot ignore job progress, then we set completed job number according to what
3220         // was found on the file system.
3221         else if (ignoreJobProgress == false)
3222         {
3223             int count = nextSequenceID - 1;
3224             if (count < activeJob->getCount())
3225                 activeJob->setCompleted(count);
3226             else
3227                 activeJob->setCompleted(activeJob->getCount());
3228         }
3229 #endif
3230 
3231         // Check whether active job is complete by comparing required captures to what is already available
3232         if (activeJob->getCount() <= activeJob->getCompleted())
3233         {
3234             activeJob->setCompleted(activeJob->getCount());
3235             appendLogText(i18n("Job requires %1-second %2 images, has already %3/%4 captures and does not need to run.",
3236                                QString("%L1").arg(job->getExposure(), 0, 'f', 3), job->getFilterName(),
3237                                activeJob->getCompleted(), activeJob->getCount()));
3238             processJobCompletionStage2();
3239 
3240             /* FIXME: find a clearer way to exit here */
3241             return;
3242         }
3243         else
3244         {
3245             // There are captures to process
3246             currentImgCountOUT->setText(QString("%L1").arg(activeJob->getCompleted()));
3247             appendLogText(i18n("Job requires %1-second %2 images, has %3/%4 frames captured and will be processed.",
3248                                QString("%L1").arg(job->getExposure(), 0, 'f', 3), job->getFilterName(),
3249                                activeJob->getCompleted(), activeJob->getCount()));
3250 
3251             // Emit progress update - done a few lines below
3252             // emit newImage(nullptr, activeJob);
3253 
3254             currentCCD->setNextSequenceID(nextSequenceID);
3255         }
3256     }
3257 
3258     if (currentCCD->isBLOBEnabled() == false)
3259     {
3260         // FIXME: Move this warning pop-up elsewhere, it will interfere with automation.
3261         //        if (Options::guiderType() != Ekos::Guide::GUIDE_INTERNAL || KMessageBox::questionYesNo(nullptr, i18n("Image transfer is disabled for this camera. Would you like to enable it?")) ==
3262         //                KMessageBox::Yes)
3263         if (Options::guiderType() != Ekos::Guide::GUIDE_INTERNAL)
3264         {
3265             currentCCD->setBLOBEnabled(true);
3266         }
3267         else
3268         {
3269             connect(KSMessageBox::Instance(), &KSMessageBox::accepted, this, [this]()
3270             {
3271                 //QObject::disconnect(KSMessageBox::Instance(), &KSMessageBox::accepted, this, nullptr);
3272                 KSMessageBox::Instance()->disconnect(this);
3273                 currentCCD->setBLOBEnabled(true);
3274                 prepareActiveJobStage1();
3275 
3276             });
3277             connect(KSMessageBox::Instance(), &KSMessageBox::rejected, this, [this]()
3278             {
3279                 //QObject::disconnect(KSMessageBox::Instance(), &KSMessageBox::rejected, this, nullptr);
3280                 KSMessageBox::Instance()->disconnect(this);
3281                 currentCCD->setBLOBEnabled(true);
3282                 setBusy(false);
3283             });
3284 
3285             KSMessageBox::Instance()->questionYesNo(i18n("Image transfer is disabled for this camera. Would you like to enable it?"),
3286                                                     i18n("Image Transfer"), 15);
3287 
3288             return;
3289         }
3290     }
3291 
3292     prepareActiveJobStage1();
3293 
3294 }
3295 
prepareActiveJobStage1()3296 void Capture::prepareActiveJobStage1()
3297 {
3298     if (activeJob == nullptr)
3299     {
3300         qWarning(KSTARS_EKOS_CAPTURE) << "prepareActiveJobStage1 with null activeJob.";
3301     }
3302     else
3303     {
3304         // JM 2020-12-06: Check if we need to execute pre-job script first.
3305         const QString preJobScript = activeJob->getScript(SCRIPT_PRE_JOB);
3306         // Only run pre-job script for the first time and not after some images were captured but then stopped due to abort.
3307         if (!preJobScript.isEmpty() && activeJob->getCompleted() == 0)
3308         {
3309             m_CaptureScriptType = SCRIPT_PRE_JOB;
3310             m_CaptureScript.start(preJobScript, generateScriptArguments());
3311             appendLogText(i18n("Executing pre job script %1", preJobScript));
3312             return;
3313         }
3314     }
3315     prepareActiveJobStage2();
3316 }
3317 /**
3318  * @brief Reset #calibrationStage and continue with preparePreCaptureActions().
3319  */
prepareActiveJobStage2()3320 void Capture::prepareActiveJobStage2()
3321 {
3322     // Just notification of active job stating up
3323     if (activeJob == nullptr)
3324     {
3325         qWarning(KSTARS_EKOS_CAPTURE) << "prepareActiveJobStage2 with null activeJob.";
3326     }
3327     else
3328         emit newImage(activeJob, m_ImageData);
3329 
3330     //connect(job, SIGNAL(checkFocus()), this, &Ekos::Capture::startPostFilterAutoFocus()));
3331 
3332     // Reset calibration stage
3333     if (calibrationStage == CAL_CAPTURING)
3334     {
3335         if (activeJob != nullptr && activeJob->getFrameType() != FRAME_LIGHT)
3336             calibrationStage = CAL_PRECAPTURE_COMPLETE;
3337         else
3338             calibrationStage = CAL_NONE;
3339     }
3340 
3341     /* Disable this restriction, let the sequence run even if focus did not run prior to the capture.
3342      * Besides, this locks up the Scheduler when the Capture module starts a sequence without any prior focus procedure done.
3343      * This is quite an old code block. The message "Manual scheduled" seems to even refer to some manual intervention?
3344      * With the new HFR threshold, it might be interesting to prevent the execution because we actually need an HFR value to
3345      * begin capturing, but even there, on one hand it makes sense for the end-user to know what HFR to put in the edit box,
3346      * and on the other hand the focus procedure will deduce the next HFR automatically.
3347      * But in the end, it's not entirely clear what the intent was. Note there is still a warning that a preliminary autofocus
3348      * procedure is important to avoid any surprise that could make the whole schedule ineffective.
3349      */
3350     if (activeJob != nullptr)
3351     {
3352         const QString preCaptureScript = activeJob->getScript(SCRIPT_PRE_CAPTURE);
3353         // JM 2020-12-06: Check if we need to execute pre-capture script first.
3354         if (!preCaptureScript.isEmpty())
3355         {
3356             m_CaptureScriptType = SCRIPT_PRE_CAPTURE;
3357             m_CaptureScript.start(preCaptureScript, generateScriptArguments());
3358             appendLogText(i18n("Executing pre capture script %1", preCaptureScript));
3359             return;
3360         }
3361     }
3362 
3363     preparePreCaptureActions();
3364 }
3365 
3366 /**
3367  * @brief Trigger setting the filter, temperature, (if existing) the rotator angle and
3368  *        let the #activeJob execute the preparation actions before a capture may
3369  *        take place (@see SequenceJob::prepareCapture()).
3370  *
3371  * After triggering the settings, this method returns. This mechanism is slightly tricky, since it
3372  * asynchronous and event based and works as collaboration between Capture and SequenceJob. Capture has
3373  * the connection to devices and SequenceJob knows the target values.
3374  *
3375  * Each time Capture receives an updated value - e.g. the current CCD temperature
3376  * (@see updateCCDTemperature()) - it informs the #activeJob about the current CCD temperature.
3377  * SequenceJob checks, if it has reached the target value and if yes, sets this action as as completed.
3378  *
3379  * As soon as all actions are completed, SequenceJob emits a prepareComplete() event, which triggers
3380  * executeJob() from the Capture module.
3381  */
preparePreCaptureActions()3382 void Capture::preparePreCaptureActions()
3383 {
3384     if (activeJob == nullptr)
3385     {
3386         qWarning(KSTARS_EKOS_CAPTURE) << "preparePreCaptureActions with null activeJob.";
3387         // Everything below depends on activeJob. Just return.
3388         return;
3389     }
3390 
3391     // Update position
3392     if (m_CurrentFilterPosition > 0)
3393         activeJob->setCurrentFilter(m_CurrentFilterPosition);
3394 
3395     // update temperature
3396     if (currentCCD->hasCooler() && activeJob->getEnforceTemperature())
3397     {
3398         double temperature = 0;
3399         currentCCD->getTemperature(&temperature);
3400         activeJob->setCurrentTemperature(temperature);
3401     }
3402 
3403     activeJob->resetCurrentGuiderDrift();
3404 
3405     // update rotator angle
3406     if (currentRotator != nullptr && activeJob->getTargetRotation() != Ekos::INVALID_VALUE)
3407         activeJob->setCurrentRotation(rotatorSettings->getCurrentRotationPA());
3408 
3409     setBusy(true);
3410 
3411     if (activeJob->isPreview())
3412     {
3413         startB->setIcon(
3414             QIcon::fromTheme("media-playback-stop"));
3415         startB->setToolTip(i18n("Stop"));
3416     }
3417 
3418     connect(activeJob, &SequenceJob::prepareState, this, &Ekos::Capture::updatePrepareState);
3419     connect(activeJob, &SequenceJob::prepareComplete, this, &Ekos::Capture::executeJob);
3420 
3421     activeJob->prepareCapture();
3422 }
3423 
3424 /**
3425  * @brief Listen to device property changes (temperature, rotator) that are triggered by
3426  *        SequenceJob.
3427  */
updatePrepareState(Ekos::CaptureState prepareState)3428 void Capture::updatePrepareState(Ekos::CaptureState prepareState)
3429 {
3430     m_State = prepareState;
3431     emit newStatus(prepareState);
3432 
3433     if (activeJob == nullptr)
3434     {
3435         qWarning(KSTARS_EKOS_CAPTURE) << "updatePrepareState with null activeJob.";
3436         // Everything below depends on activeJob. Just return.
3437         return;
3438     }
3439 
3440     switch (prepareState)
3441     {
3442         case CAPTURE_SETTING_TEMPERATURE:
3443             appendLogText(i18n("Setting temperature to %1 C...", activeJob->getTargetTemperature()));
3444             secondsLabel->setText(i18n("Set %1 C...", activeJob->getTargetTemperature()));
3445             break;
3446         case CAPTURE_GUIDER_DRIFT:
3447             appendLogText(i18n("Waiting for guide drift below %1 a-s...", activeJob->getTargetStartGuiderDrift()));
3448             secondsLabel->setText(i18n("Wait for Guider < %1 a-s...", activeJob->getTargetStartGuiderDrift()));
3449             break;
3450 
3451         case CAPTURE_SETTING_ROTATOR:
3452             appendLogText(i18n("Setting rotation to %1 degrees E of N...", activeJob->getTargetRotation()));
3453             secondsLabel->setText(i18n("Set Rotator %1...", activeJob->getTargetRotation()));
3454             break;
3455 
3456         default:
3457             break;
3458 
3459     }
3460 }
3461 
3462 /**
3463  * @brief Start the execution of #activeJob by initiating updatePreCaptureCalibrationStatus().
3464  */
executeJob()3465 void Capture::executeJob()
3466 {
3467     if (activeJob == nullptr)
3468         qWarning(KSTARS_EKOS_CAPTURE) << "executeJob with null activeJob.";
3469     else
3470         activeJob->disconnect(this);
3471 
3472     QMap<QString, QString> FITSHeader;
3473     QString rawPrefix = activeJob->property("rawPrefix").toString();
3474     if (m_ObserverName.isEmpty() == false)
3475         FITSHeader["FITS_OBSERVER"] = m_ObserverName;
3476     if (m_TargetName.isEmpty() == false)
3477         FITSHeader["FITS_OBJECT"] = m_TargetName;
3478     else if (rawPrefix.isEmpty() == false)
3479     {
3480         // JM 2021-07-08: Remove "_" from target name.
3481         FITSHeader["FITS_OBJECT"] = rawPrefix.remove("_");
3482     }
3483 
3484     if (FITSHeader.count() > 0)
3485         currentCCD->setFITSHeader(FITSHeader);
3486 
3487     // Update button status
3488     setBusy(true);
3489 
3490     useGuideHead = (activeJob != nullptr &&
3491                     activeJob->getActiveChip()->getType() == ISD::CCDChip::PRIMARY_CCD) ? false : true;
3492 
3493     syncGUIToJob(activeJob);
3494 
3495     calibrationCheckType = CAL_CHECK_TASK;
3496 
3497     updatePreCaptureCalibrationStatus();
3498 
3499     // Check calibration frame requirements
3500 #if 0
3501     if (activeJob->getFrameType() != FRAME_LIGHT && activeJob->isPreview() == false)
3502     {
3503         updatePreCaptureCalibrationStatus();
3504         return;
3505     }
3506 
3507     captureImage();
3508 #endif
3509 }
3510 
3511 /**
3512  * @brief This is a wrapping loop for processPreCaptureCalibrationStage(), which contains
3513  *        all checks before captureImage() may be called.
3514  *
3515  * If processPreCaptureCalibrationStage() returns IPS_OK (i.e. everything is ready so that
3516  * capturing may be started), captureImage() is called. Otherwise, it waits for a second and
3517  * calls itself again.
3518  */
updatePreCaptureCalibrationStatus()3519 void Capture::updatePreCaptureCalibrationStatus()
3520 {
3521     // If process was aborted or stopped by the user
3522     if (isBusy == false)
3523     {
3524         appendLogText(i18n("Warning: Calibration process was prematurely terminated."));
3525         return;
3526     }
3527 
3528     IPState rc = processPreCaptureCalibrationStage();
3529 
3530     if (rc == IPS_ALERT)
3531         return;
3532     else if (rc == IPS_BUSY)
3533     {
3534         // Clear the label if we are neither executing a meridian flip nor re-focusing
3535         if ((meridianFlipStage == MF_NONE || meridianFlipStage == MF_READY) && m_State != CAPTURE_FOCUSING)
3536             secondsLabel->clear();
3537         QTimer::singleShot(1000, this, &Ekos::Capture::updatePreCaptureCalibrationStatus);
3538         return;
3539     }
3540 
3541     captureImage();
3542 }
3543 
setFocusTemperatureDelta(double focusTemperatureDelta,double absTemperture)3544 void Capture::setFocusTemperatureDelta(double focusTemperatureDelta, double absTemperture)
3545 {
3546     Q_UNUSED(absTemperture);
3547     // This produces too much log spam
3548     // Maybe add a threshold to report later?
3549     //qCDebug(KSTARS_EKOS_CAPTURE) << "setFocusTemperatureDelta: " << focusTemperatureDelta;
3550     this->focusTemperatureDelta = focusTemperatureDelta;
3551 }
3552 
3553 /**
3554  * @brief Slot that listens to guiding deviations reported by the Guide module.
3555  *
3556  * Depending on the current status, it triggers several actions:
3557  * - If there is no active job, it calls checkMeridianFlipReady(), which may initiate a meridian flip.
3558  * - If guiding has been started after a meridian flip and the deviation is within the expected limits,
3559  *   the meridian flip is regarded as completed by setMeridianFlipStage(MF_NONE) (@see setMeridianFlipStage()).
3560  * - If the deviation is beyond the defined limit, capturing is suspended (@see suspend()) and the
3561  *   #guideDeviationTimer is started.
3562  * - Otherwise, it checks if there has been a job suspended and restarts it, since guiding is within the limits.
3563  */
setGuideDeviation(double delta_ra,double delta_dec)3564 void Capture::setGuideDeviation(double delta_ra, double delta_dec)
3565 {
3566     //    if (activeJob == nullptr)
3567     //    {
3568     //        if (deviationDetected == false)
3569     //            return;
3570 
3571     //        // Try to find first job that was aborted due to deviation
3572     //        for(SequenceJob *job : jobs)
3573     //        {
3574     //            if (job->getStatus() == SequenceJob::JOB_ABORTED)
3575     //            {
3576     //                activeJob = job;
3577     //                break;
3578     //            }
3579     //        }
3580 
3581     //        if (activeJob == nullptr)
3582     //            return;
3583     //    }
3584     const double deviation_rms = std::hypot(delta_ra, delta_dec);
3585     if (activeJob)
3586         activeJob->setCurrentGuiderDrift(deviation_rms);
3587 
3588     // if guiding deviations occur and no job is active, check if a meridian flip is ready to be executed
3589     if (activeJob == nullptr && checkMeridianFlipReady())
3590         return;
3591 
3592     // If guiding is started after a meridian flip we will start getting guide deviations again
3593     // if the guide deviations are within our limits, we resume the sequence
3594     if (meridianFlipStage == MF_GUIDING)
3595     {
3596         // If the user didn't select any guiding deviation, we fall through
3597         // otherwise we can for deviation RMS
3598         if (limitGuideDeviationS->isChecked() == false || deviation_rms < limitGuideDeviationN->value())
3599         {
3600             appendLogText(i18n("Post meridian flip calibration completed successfully."));
3601             // N.B. Set meridian flip stage AFTER resumeSequence() always
3602             setMeridianFlipStage(MF_NONE);
3603             return;
3604         }
3605     }
3606 
3607     // We don't enforce limit on previews
3608     if (limitGuideDeviationS->isChecked() == false || (activeJob && (activeJob->isPreview()
3609             || activeJob->getExposeLeft() == 0.0)))
3610         return;
3611 
3612     QString deviationText = QString("%1").arg(deviation_rms, 0, 'f', 3);
3613 
3614     // If we have an active busy job, let's abort it if guiding deviation is exceeded.
3615     // And we accounted for the spike
3616     if (activeJob && activeJob->getStatus() == SequenceJob::JOB_BUSY && activeJob->getFrameType() == FRAME_LIGHT)
3617     {
3618         if (deviation_rms <= limitGuideDeviationN->value())
3619             m_SpikesDetected = 0;
3620         else
3621         {
3622             // Require several consecutive spikes to fail.
3623             constexpr int CONSECUTIVE_SPIKES_TO_FAIL = 3;
3624             if (++m_SpikesDetected < CONSECUTIVE_SPIKES_TO_FAIL)
3625                 return;
3626 
3627             appendLogText(i18n("Guiding deviation %1 exceeded limit value of %2 arcsecs for %4 consecutive samples, "
3628                                "suspending exposure and waiting for guider up to %3 seconds.",
3629                                deviationText, limitGuideDeviationN->value(),
3630                                QString("%L1").arg(guideDeviationTimer.interval() / 1000.0, 0, 'f', 3),
3631                                CONSECUTIVE_SPIKES_TO_FAIL));
3632 
3633             suspend();
3634 
3635             m_SpikesDetected    = 0;
3636             m_DeviationDetected = true;
3637 
3638             // Check if we need to start meridian flip. If yes, we need to start capturing
3639             // to ensure that capturing is recovered after the flip
3640             if (checkMeridianFlipReady())
3641                 start();
3642             else
3643                 guideDeviationTimer.start();
3644         }
3645         return;
3646     }
3647 
3648     // Find the first aborted job
3649     SequenceJob * abortedJob = nullptr;
3650     for(SequenceJob * job : jobs)
3651     {
3652         if (job->getStatus() == SequenceJob::JOB_ABORTED)
3653         {
3654             abortedJob = job;
3655             break;
3656         }
3657     }
3658 
3659     if (abortedJob && m_DeviationDetected)
3660     {
3661         if (deviation_rms <= limitGuideDeviationN->value())
3662         {
3663             guideDeviationTimer.stop();
3664 
3665             if (seqDelay == 0)
3666                 appendLogText(i18n("Guiding deviation %1 is now lower than limit value of %2 arcsecs, "
3667                                    "resuming exposure.",
3668                                    deviationText, limitGuideDeviationN->value()));
3669             else
3670                 appendLogText(i18n("Guiding deviation %1 is now lower than limit value of %2 arcsecs, "
3671                                    "resuming exposure in %3 seconds.",
3672                                    deviationText, limitGuideDeviationN->value(), seqDelay / 1000.0));
3673 
3674             QTimer::singleShot(seqDelay, this, &Ekos::Capture::start);
3675             return;
3676         }
3677         else appendLogText(i18n("Guiding deviation %1 is still higher than limit value of %2 arcsecs.",
3678                                     deviationText, limitGuideDeviationN->value()));
3679     }
3680 }
3681 
setFocusStatus(FocusState state)3682 void Capture::setFocusStatus(FocusState state)
3683 {
3684     if (state != m_FocusState)
3685         qCDebug(KSTARS_EKOS_CAPTURE) << "Focus State changed from" << Ekos::getFocusStatusString(
3686                                          m_FocusState) << "to" << Ekos::getFocusStatusString(state);
3687     m_FocusState = state;
3688 
3689     // Do not process above aborted or when meridian flip in progress
3690     if (m_FocusState > FOCUS_ABORTED || meridianFlipStage == MF_FLIPPING ||  meridianFlipStage == MF_SLEWING)
3691         return;
3692 
3693     if (m_FocusState == FOCUS_COMPLETE)
3694     {
3695         // enable option to have a refocus event occur if HFR goes over threshold
3696         m_AutoFocusReady = true;
3697 
3698         //if (limitFocusHFRN->value() == 0.0 && fileHFR == 0.0)
3699         if (fileHFR == 0.0)
3700         {
3701             QList<double> filterHFRList;
3702             if (m_CurrentFilterPosition > 0)
3703             {
3704                 // If we are using filters, then we retrieve which filter is currently active.
3705                 // We check if filter lock is used, and store that instead of the current filter.
3706                 // e.g. If current filter HA, but lock filter is L, then the HFR value is stored for L filter.
3707                 // If no lock filter exists, then we store as is (HA)
3708                 QString currentFilterText = captureFilterS->itemText(m_CurrentFilterPosition - 1);
3709                 //QString filterLock = filterManager.data()->getFilterLock(currentFilterText);
3710                 //QString finalFilter = (filterLock == "--" ? currentFilterText : filterLock);
3711 
3712                 //filterHFRList = HFRMap[finalFilter];
3713                 filterHFRList = HFRMap[currentFilterText];
3714                 filterHFRList.append(focusHFR);
3715                 //HFRMap[finalFilter] = filterHFRList;
3716                 HFRMap[currentFilterText] = filterHFRList;
3717             }
3718             // No filters
3719             else
3720             {
3721                 filterHFRList = HFRMap["--"];
3722                 filterHFRList.append(focusHFR);
3723                 HFRMap["--"] = filterHFRList;
3724             }
3725 
3726             double median = focusHFR;
3727             int count = filterHFRList.size();
3728             if (Options::useMedianFocus() && count > 1)
3729                 median = (count % 2) ? filterHFRList[count / 2] : (filterHFRList[count / 2 - 1] + filterHFRList[count / 2]) / 2.0;
3730 
3731             // Add 2.5% (default) to the automatic initial HFR value to allow for minute changes in HFR without need to refocus
3732             // in case in-sequence-focusing is used.
3733             limitFocusHFRN->setValue(median + (median * (Options::hFRThresholdPercentage() / 100.0)));
3734         }
3735 
3736         // successful focus so reset elapsed time
3737         restartRefocusEveryNTimer();
3738     }
3739 
3740     if ((isRefocus || isInSequenceFocus) && activeJob && activeJob->getStatus() == SequenceJob::JOB_BUSY)
3741     {
3742         // if the focusing has been started during the post-calibration, return to the calibration
3743         if (calibrationStage < CAL_PRECAPTURE_COMPLETE && m_State == CAPTURE_FOCUSING)
3744         {
3745             if (m_FocusState == FOCUS_COMPLETE)
3746             {
3747                 appendLogText(i18n("Focus complete."));
3748                 secondsLabel->setText(i18n("Focus complete."));
3749                 m_State = CAPTURE_PROGRESS;
3750             }
3751             else if (m_FocusState == FOCUS_FAILED || m_FocusState == FOCUS_ABORTED)
3752             {
3753                 appendLogText(i18n("Autofocus failed."));
3754                 secondsLabel->setText(i18n("Autofocus failed."));
3755                 abort();
3756             }
3757         }
3758         else if (m_FocusState == FOCUS_COMPLETE)
3759         {
3760             appendLogText(i18n("Focus complete."));
3761             secondsLabel->setText(i18n("Focus complete."));
3762         }
3763         // Meridian flip will abort focusing. In this case, after the meridian flip has completed capture
3764         // will restart the re-focus attempt. Therefore we only abort capture if meridian flip is not running.
3765         else if ((m_FocusState == FOCUS_FAILED || m_FocusState == FOCUS_ABORTED) &&
3766                  !(meridianFlipStage == MF_INITIATED || meridianFlipStage == MF_SLEWING)
3767                 )
3768         {
3769             appendLogText(i18n("Autofocus failed. Aborting exposure..."));
3770             secondsLabel->setText(i18n("Autofocus failed."));
3771             abort();
3772         }
3773     }
3774 }
3775 
updateHFRThreshold()3776 void Capture::updateHFRThreshold()
3777 {
3778     if (fileHFR != 0.0)
3779         return;
3780 
3781     QList<double> filterHFRList;
3782     if (captureFilterS->currentIndex() != -1)
3783     {
3784         // If we are using filters, then we retrieve which filter is currently active.
3785         // We check if filter lock is used, and store that instead of the current filter.
3786         // e.g. If current filter HA, but lock filter is L, then the HFR value is stored for L filter.
3787         // If no lock filter exists, then we store as is (HA)
3788         QString currentFilterText = captureFilterS->currentText();
3789         QString filterLock = filterManager.data()->getFilterLock(currentFilterText);
3790         QString finalFilter = (filterLock == "--" ? currentFilterText : filterLock);
3791 
3792         filterHFRList = HFRMap[finalFilter];
3793     }
3794     // No filters
3795     else
3796     {
3797         filterHFRList = HFRMap["--"];
3798     }
3799 
3800     if (filterHFRList.empty())
3801     {
3802         limitFocusHFRN->setValue(Options::hFRDeviation());
3803         return;
3804     }
3805 
3806     double median = 0;
3807     int count = filterHFRList.size();
3808     if (count > 1)
3809         median = (count % 2) ? filterHFRList[count / 2] : (filterHFRList[count / 2 - 1] + filterHFRList[count / 2]) / 2.0;
3810     else if (count == 1)
3811         median = filterHFRList[0];
3812 
3813     // Add 2.5% (default) to the automatic initial HFR value to allow for minute changes in HFR without need to refocus
3814     // in case in-sequence-focusing is used.
3815     limitFocusHFRN->setValue(median + (median * (Options::hFRThresholdPercentage() / 100.0)));
3816 }
3817 
setMeridianFlipStage(MFStage stage)3818 void Capture::setMeridianFlipStage(MFStage stage)
3819 {
3820     qCDebug(KSTARS_EKOS_CAPTURE) << "setMeridianFlipStage: " << MFStageString(stage);
3821     if (meridianFlipStage != stage)
3822     {
3823         switch (stage)
3824         {
3825             case MF_NONE:
3826                 if (m_State == CAPTURE_PAUSED)
3827                     secondsLabel->setText(i18n("Paused..."));
3828                 meridianFlipStage = stage;
3829                 emit newMeridianFlipStatus(Mount::FLIP_NONE);
3830                 break;
3831 
3832             case MF_READY:
3833                 if (meridianFlipStage == MF_REQUESTED)
3834                 {
3835                     // we keep the stage on requested until the mount starts the meridian flip
3836                     emit newMeridianFlipStatus(Mount::FLIP_ACCEPTED);
3837                 }
3838                 else if (m_State == CAPTURE_PAUSED)
3839                 {
3840                     // paused after meridian flip requested
3841                     secondsLabel->setText(i18n("Paused..."));
3842                     meridianFlipStage = stage;
3843                     emit newMeridianFlipStatus(Mount::FLIP_ACCEPTED);
3844                 }
3845                 else if (!(checkMeridianFlipRunning() || meridianFlipStage == MF_COMPLETED))
3846                 {
3847                     // if neither a MF has been requested (checked above) or is in a post
3848                     // MF calibration phase, no MF needs to take place.
3849                     // Hence we set to the stage to NONE
3850                     meridianFlipStage = MF_NONE;
3851                     break;
3852                 }
3853                 // in any other case, ignore it
3854                 break;
3855 
3856             case MF_INITIATED:
3857                 meridianFlipStage = MF_INITIATED;
3858                 emit meridianFlipStarted();
3859                 secondsLabel->setText(i18n("Meridian Flip..."));
3860                 KSNotification::event(QLatin1String("MeridianFlipStarted"), i18n("Meridian flip started"), KSNotification::EVENT_INFO);
3861                 break;
3862 
3863             case MF_REQUESTED:
3864                 if (m_State == CAPTURE_PAUSED)
3865                     // paused before meridian flip requested
3866                     emit newMeridianFlipStatus(Mount::FLIP_ACCEPTED);
3867                 else
3868                     emit newMeridianFlipStatus(Mount::FLIP_WAITING);
3869                 meridianFlipStage = stage;
3870                 break;
3871 
3872             case MF_COMPLETED:
3873                 secondsLabel->setText(i18n("Flip complete."));
3874                 meridianFlipStage = MF_COMPLETED;
3875 
3876                 // Reset HFR pixels to file value after meridian flip
3877                 if (isInSequenceFocus)
3878                 {
3879                     qCDebug(KSTARS_EKOS_CAPTURE) << "Resetting HFR value to file value of" << fileHFR << "pixels after meridian flip.";
3880                     //firstAutoFocus = true;
3881                     inSequenceFocusCounter = 0;
3882                     limitFocusHFRN->setValue(fileHFR);
3883                 }
3884 
3885                 // after a meridian flip we do not need to dither
3886                 if ( Options::ditherEnabled() || Options::ditherNoGuiding())
3887                     ditherCounter = Options::ditherFrames();
3888 
3889                 break;
3890 
3891             default:
3892                 meridianFlipStage = stage;
3893                 break;
3894         }
3895     }
3896 }
3897 
3898 
meridianFlipStatusChanged(Mount::MeridianFlipStatus status)3899 void Capture::meridianFlipStatusChanged(Mount::MeridianFlipStatus status)
3900 {
3901     qCDebug(KSTARS_EKOS_CAPTURE) << "meridianFlipStatusChanged: " << Mount::meridianFlipStatusString(status);
3902     switch (status)
3903     {
3904         case Mount::FLIP_NONE:
3905             // MF_NONE as external signal ignored so that re-alignment and guiding are processed first
3906             if (meridianFlipStage < MF_COMPLETED)
3907                 setMeridianFlipStage(MF_NONE);
3908             break;
3909 
3910         case Mount::FLIP_PLANNED:
3911             if (meridianFlipStage > MF_NONE)
3912             {
3913                 // This should never happen, since a meridian flip seems to be ongoing
3914                 qCritical(KSTARS_EKOS_CAPTURE) << "Accepting meridian flip request while being in stage " << meridianFlipStage;
3915             }
3916 
3917             // If we are autoguiding, we should resume autoguiding after flip
3918             resumeGuidingAfterFlip = isGuidingOn();
3919 
3920             if (m_State == CAPTURE_IDLE || m_State == CAPTURE_ABORTED || m_State == CAPTURE_COMPLETE || m_State == CAPTURE_PAUSED)
3921             {
3922                 setMeridianFlipStage(MF_INITIATED);
3923                 emit newMeridianFlipStatus(Mount::FLIP_ACCEPTED);
3924             }
3925             else
3926                 setMeridianFlipStage(MF_REQUESTED);
3927 
3928             break;
3929 
3930         case Mount::FLIP_RUNNING:
3931             setMeridianFlipStage(MF_INITIATED);
3932             emit newStatus(Ekos::CAPTURE_MERIDIAN_FLIP);
3933             break;
3934 
3935         case Mount::FLIP_COMPLETED:
3936             setMeridianFlipStage(MF_COMPLETED);
3937             emit newStatus(Ekos::CAPTURE_IDLE);
3938             processFlipCompleted();
3939             break;
3940 
3941         default:
3942             break;
3943     }
3944 }
3945 
getTotalFramesCount(QString signature)3946 int Capture::getTotalFramesCount(QString signature)
3947 {
3948     int  result = 0;
3949     bool found  = false;
3950 
3951     foreach (SequenceJob * job, jobs)
3952     {
3953         // FIXME: this should be part of SequenceJob
3954         QString sig = job->getSignature();
3955         if (sig == signature)
3956         {
3957             result += job->getCount();
3958             found = true;
3959         }
3960     }
3961 
3962     if (found)
3963         return result;
3964     else
3965         return -1;
3966 }
3967 
3968 
setRotator(ISD::GDInterface * newRotator)3969 void Capture::setRotator(ISD::GDInterface * newRotator)
3970 {
3971     currentRotator = newRotator;
3972     connect(currentRotator, &ISD::GDInterface::numberUpdated, this, &Ekos::Capture::updateRotatorNumber, Qt::UniqueConnection);
3973     rotatorB->setEnabled(true);
3974 
3975     rotatorSettings->setRotator(newRotator);
3976 
3977     auto nvp = newRotator->getBaseDevice()->getNumber("ABS_ROTATOR_ANGLE");
3978     rotatorSettings->setCurrentAngle(nvp->at(0)->getValue());
3979 }
3980 
setTelescope(ISD::GDInterface * newTelescope)3981 void Capture::setTelescope(ISD::GDInterface * newTelescope)
3982 {
3983     currentTelescope = static_cast<ISD::Telescope *>(newTelescope);
3984 
3985     currentTelescope->disconnect(this);
3986     connect(currentTelescope, &ISD::GDInterface::numberUpdated, this, &Ekos::Capture::processTelescopeNumber);
3987     connect(currentTelescope, &ISD::Telescope::newTarget, [&](SkyObject currentObject)
3988     {
3989         if (m_State == CAPTURE_IDLE || m_State == CAPTURE_COMPLETE)
3990         {
3991             QString sanitized = currentObject.name();
3992             // Remove illegal characters that can be problematic
3993             sanitized = sanitized.replace( QRegularExpression("\\s|/|\\(|\\)|:|\\*|~|\"" ), "_" )
3994                         // Remove any two or more __
3995                         .replace( QRegularExpression("_{2,}"), "_")
3996                         // Remove any _ at the end
3997                         .replace( QRegularExpression("_$"), "");
3998             filePrefixT->setText(sanitized);
3999         }
4000     });
4001 
4002     syncTelescopeInfo();
4003 }
4004 
syncTelescopeInfo()4005 void Capture::syncTelescopeInfo()
4006 {
4007     if (currentTelescope && currentTelescope->isConnected())
4008     {
4009         // Sync ALL CCDs to current telescope
4010         for (ISD::CCD * oneCCD : CCDs)
4011         {
4012             auto activeDevices = oneCCD->getBaseDevice()->getText("ACTIVE_DEVICES");
4013             if (activeDevices)
4014             {
4015                 auto activeTelescope = activeDevices->findWidgetByName("ACTIVE_TELESCOPE");
4016                 if (activeTelescope)
4017                 {
4018                     activeTelescope->setText(currentTelescope->getDeviceName().toLatin1().constData());
4019                     oneCCD->getDriverInfo()->getClientManager()->sendNewText(activeDevices);
4020                 }
4021             }
4022         }
4023     }
4024 }
4025 
saveFITSDirectory()4026 void Capture::saveFITSDirectory()
4027 {
4028     QString dir =
4029         QFileDialog::getExistingDirectory(Ekos::Manager::Instance(), i18nc("@title:window", "FITS Save Directory"),
4030                                           dirPath.toLocalFile());
4031 
4032     if (dir.isEmpty())
4033         return;
4034 
4035     fileDirectoryT->setText(dir);
4036 }
4037 
loadSequenceQueue()4038 void Capture::loadSequenceQueue()
4039 {
4040     QUrl fileURL = QFileDialog::getOpenFileUrl(Ekos::Manager::Instance(), i18nc("@title:window", "Open Ekos Sequence Queue"),
4041                    dirPath,
4042                    "Ekos Sequence Queue (*.esq)");
4043     if (fileURL.isEmpty())
4044         return;
4045 
4046     if (fileURL.isValid() == false)
4047     {
4048         QString message = i18n("Invalid URL: %1", fileURL.toLocalFile());
4049         KSNotification::sorry(message, i18n("Invalid URL"));
4050         return;
4051     }
4052 
4053     dirPath = QUrl(fileURL.url(QUrl::RemoveFilename));
4054 
4055     loadSequenceQueue(fileURL.toLocalFile());
4056 }
4057 
loadSequenceQueue(const QString & fileURL)4058 bool Capture::loadSequenceQueue(const QString &fileURL)
4059 {
4060     QFile sFile(fileURL);
4061     if (!sFile.open(QIODevice::ReadOnly))
4062     {
4063         QString message = i18n("Unable to open file %1", fileURL);
4064         KSNotification::sorry(message, i18n("Could Not Open File"));
4065         return false;
4066     }
4067 
4068     capturedFramesMap.clear();
4069     clearSequenceQueue();
4070 
4071     LilXML * xmlParser = newLilXML();
4072 
4073     char errmsg[MAXRBUF];
4074     XMLEle * root = nullptr;
4075     XMLEle * ep   = nullptr;
4076     char c;
4077 
4078     // We expect all data read from the XML to be in the C locale - QLocale::c().
4079     QLocale cLocale = QLocale::c();
4080 
4081     while (sFile.getChar(&c))
4082     {
4083         root = readXMLEle(xmlParser, c, errmsg);
4084 
4085         if (root)
4086         {
4087             double sqVersion = cLocale.toDouble(findXMLAttValu(root, "version"));
4088             if (sqVersion < SQ_COMPAT_VERSION)
4089             {
4090                 appendLogText(i18n("Deprecated sequence file format version %1. Please construct a new sequence file.",
4091                                    sqVersion));
4092                 return false;
4093             }
4094 
4095             for (ep = nextXMLEle(root, 1); ep != nullptr; ep = nextXMLEle(root, 0))
4096             {
4097                 if (!strcmp(tagXMLEle(ep), "Observer"))
4098                 {
4099                     m_ObserverName = QString(pcdataXMLEle(ep));
4100                 }
4101                 else if (!strcmp(tagXMLEle(ep), "GuideDeviation"))
4102                 {
4103                     limitGuideDeviationS->setChecked(!strcmp(findXMLAttValu(ep, "enabled"), "true"));
4104                     limitGuideDeviationN->setValue(cLocale.toDouble(pcdataXMLEle(ep)));
4105                 }
4106                 else if (!strcmp(tagXMLEle(ep), "GuideStartDeviation"))
4107                 {
4108                     startGuiderDriftS->setChecked(!strcmp(findXMLAttValu(ep, "enabled"), "true"));
4109                     startGuiderDriftN->setValue(cLocale.toDouble(pcdataXMLEle(ep)));
4110                 }
4111                 else if (!strcmp(tagXMLEle(ep), "Autofocus"))
4112                 {
4113                     limitFocusHFRS->setChecked(!strcmp(findXMLAttValu(ep, "enabled"), "true"));
4114                     double const HFRValue = cLocale.toDouble(pcdataXMLEle(ep));
4115                     // Set the HFR value from XML, or reset it to zero, don't let another unrelated older HFR be used
4116                     // Note that HFR value will only be serialized to XML when option "Save Sequence HFR to File" is enabled
4117                     fileHFR = HFRValue > 0.0 ? HFRValue : 0.0;
4118                     limitFocusHFRN->setValue(fileHFR);
4119                 }
4120                 else if (!strcmp(tagXMLEle(ep), "RefocusOnTemperatureDelta"))
4121                 {
4122                     limitFocusDeltaTS->setChecked(!strcmp(findXMLAttValu(ep, "enabled"), "true"));
4123                     double const deltaValue = cLocale.toDouble(pcdataXMLEle(ep));
4124                     limitFocusDeltaTN->setValue(deltaValue);
4125                 }
4126                 else if (!strcmp(tagXMLEle(ep), "RefocusEveryN"))
4127                 {
4128                     limitRefocusS->setChecked(!strcmp(findXMLAttValu(ep, "enabled"), "true"));
4129                     int const minutesValue = cLocale.toInt(pcdataXMLEle(ep));
4130                     // Set the refocus period from XML, or reset it to zero, don't let another unrelated older refocus period be used.
4131                     refocusEveryNMinutesValue = minutesValue > 0 ? minutesValue : 0;
4132                     limitRefocusN->setValue(refocusEveryNMinutesValue);
4133                 }
4134                 else if (!strcmp(tagXMLEle(ep), "MeridianFlip"))
4135                 {
4136                     // meridian flip is managed by the mount only
4137                     // older files might nevertheless contain MF settings
4138                     if (! strcmp(findXMLAttValu(ep, "enabled"), "true"))
4139                         appendLogText(
4140                             i18n("Meridian flip configuration has been shifted to the mount module. Please configure the meridian flip there."));
4141                 }
4142                 else if (!strcmp(tagXMLEle(ep), "CCD"))
4143                 {
4144                     cameraS->setCurrentText(pcdataXMLEle(ep));
4145                     // Signal "activated" of QComboBox does not fire when changing the text programmatically
4146                     setCamera(pcdataXMLEle(ep));
4147                 }
4148                 else if (!strcmp(tagXMLEle(ep), "FilterWheel"))
4149                 {
4150                     filterWheelS->setCurrentText(pcdataXMLEle(ep));
4151                     checkFilter();
4152                 }
4153                 else
4154                 {
4155                     processJobInfo(ep);
4156                 }
4157             }
4158             delXMLEle(root);
4159         }
4160         else if (errmsg[0])
4161         {
4162             appendLogText(QString(errmsg));
4163             delLilXML(xmlParser);
4164             return false;
4165         }
4166     }
4167 
4168     m_SequenceURL = QUrl::fromLocalFile(fileURL);
4169     m_Dirty      = false;
4170     delLilXML(xmlParser);
4171     // update save button tool tip
4172     queueSaveB->setToolTip("Save to " + sFile.fileName());
4173 
4174     return true;
4175 }
4176 
processJobInfo(XMLEle * root)4177 bool Capture::processJobInfo(XMLEle * root)
4178 {
4179     XMLEle * ep;
4180     XMLEle * subEP;
4181     rotatorSettings->setRotationEnforced(false);
4182 
4183     m_Scripts.clear();
4184     QLocale cLocale = QLocale::c();
4185 
4186     for (ep = nextXMLEle(root, 1); ep != nullptr; ep = nextXMLEle(root, 0))
4187     {
4188         if (!strcmp(tagXMLEle(ep), "Exposure"))
4189             captureExposureN->setValue(cLocale.toDouble(pcdataXMLEle(ep)));
4190         else if (!strcmp(tagXMLEle(ep), "Binning"))
4191         {
4192             subEP = findXMLEle(ep, "X");
4193             if (subEP)
4194                 captureBinHN->setValue(cLocale.toInt(pcdataXMLEle(subEP)));
4195             subEP = findXMLEle(ep, "Y");
4196             if (subEP)
4197                 captureBinVN->setValue(cLocale.toInt(pcdataXMLEle(subEP)));
4198         }
4199         else if (!strcmp(tagXMLEle(ep), "Frame"))
4200         {
4201             subEP = findXMLEle(ep, "X");
4202             if (subEP)
4203                 captureFrameXN->setValue(cLocale.toInt(pcdataXMLEle(subEP)));
4204             subEP = findXMLEle(ep, "Y");
4205             if (subEP)
4206                 captureFrameYN->setValue(cLocale.toInt(pcdataXMLEle(subEP)));
4207             subEP = findXMLEle(ep, "W");
4208             if (subEP)
4209                 captureFrameWN->setValue(cLocale.toInt(pcdataXMLEle(subEP)));
4210             subEP = findXMLEle(ep, "H");
4211             if (subEP)
4212                 captureFrameHN->setValue(cLocale.toInt(pcdataXMLEle(subEP)));
4213         }
4214         else if (!strcmp(tagXMLEle(ep), "Temperature"))
4215         {
4216             if (cameraTemperatureN->isEnabled())
4217                 cameraTemperatureN->setValue(cLocale.toDouble(pcdataXMLEle(ep)));
4218 
4219             // If force attribute exist, we change cameraTemperatureS, otherwise do nothing.
4220             if (!strcmp(findXMLAttValu(ep, "force"), "true"))
4221                 cameraTemperatureS->setChecked(true);
4222             else if (!strcmp(findXMLAttValu(ep, "force"), "false"))
4223                 cameraTemperatureS->setChecked(false);
4224         }
4225         else if (!strcmp(tagXMLEle(ep), "Filter"))
4226         {
4227             //captureFilterS->setCurrentIndex(atoi(pcdataXMLEle(ep))-1);
4228             captureFilterS->setCurrentText(pcdataXMLEle(ep));
4229         }
4230         else if (!strcmp(tagXMLEle(ep), "Type"))
4231         {
4232             captureTypeS->setCurrentText(pcdataXMLEle(ep));
4233         }
4234         else if (!strcmp(tagXMLEle(ep), "Prefix"))
4235         {
4236             subEP = findXMLEle(ep, "RawPrefix");
4237             if (subEP)
4238                 filePrefixT->setText(pcdataXMLEle(subEP));
4239             subEP = findXMLEle(ep, "FilterEnabled");
4240             if (subEP)
4241                 fileFilterS->setChecked(!strcmp("1", pcdataXMLEle(subEP)));
4242             subEP = findXMLEle(ep, "ExpEnabled");
4243             if (subEP)
4244                 fileDurationS->setChecked(!strcmp("1", pcdataXMLEle(subEP)));
4245             subEP = findXMLEle(ep, "TimeStampEnabled");
4246             if (subEP)
4247                 fileTimestampS->setChecked(!strcmp("1", pcdataXMLEle(subEP)));
4248         }
4249         else if (!strcmp(tagXMLEle(ep), "Count"))
4250         {
4251             captureCountN->setValue(cLocale.toInt(pcdataXMLEle(ep)));
4252         }
4253         else if (!strcmp(tagXMLEle(ep), "Delay"))
4254         {
4255             captureDelayN->setValue(cLocale.toInt(pcdataXMLEle(ep)));
4256         }
4257         else if (!strcmp(tagXMLEle(ep), "PostCaptureScript"))
4258         {
4259             m_Scripts[SCRIPT_POST_CAPTURE] = pcdataXMLEle(ep);
4260         }
4261         else if (!strcmp(tagXMLEle(ep), "PreCaptureScript"))
4262         {
4263             m_Scripts[SCRIPT_PRE_CAPTURE] = pcdataXMLEle(ep);
4264         }
4265         else if (!strcmp(tagXMLEle(ep), "PostJobScript"))
4266         {
4267             m_Scripts[SCRIPT_POST_JOB] = pcdataXMLEle(ep);
4268         }
4269         else if (!strcmp(tagXMLEle(ep), "PreJobScript"))
4270         {
4271             m_Scripts[SCRIPT_PRE_JOB] = pcdataXMLEle(ep);
4272         }
4273         else if (!strcmp(tagXMLEle(ep), "FITSDirectory"))
4274         {
4275             fileDirectoryT->setText(pcdataXMLEle(ep));
4276         }
4277         else if (!strcmp(tagXMLEle(ep), "RemoteDirectory"))
4278         {
4279             fileRemoteDirT->setText(pcdataXMLEle(ep));
4280         }
4281         else if (!strcmp(tagXMLEle(ep), "UploadMode"))
4282         {
4283             fileUploadModeS->setCurrentIndex(cLocale.toInt(pcdataXMLEle(ep)));
4284         }
4285         else if (!strcmp(tagXMLEle(ep), "ISOIndex"))
4286         {
4287             if (captureISOS)
4288                 captureISOS->setCurrentIndex(cLocale.toInt(pcdataXMLEle(ep)));
4289         }
4290         else if (!strcmp(tagXMLEle(ep), "FormatIndex"))
4291         {
4292             captureFormatS->setCurrentIndex(cLocale.toInt(pcdataXMLEle(ep)));
4293         }
4294         else if (!strcmp(tagXMLEle(ep), "Rotation"))
4295         {
4296             rotatorSettings->setRotationEnforced(true);
4297             rotatorSettings->setTargetRotationPA(cLocale.toDouble(pcdataXMLEle(ep)));
4298         }
4299         else if (!strcmp(tagXMLEle(ep), "Properties"))
4300         {
4301             QMap<QString, QMap<QString, double>> propertyMap;
4302 
4303             for (subEP = nextXMLEle(ep, 1); subEP != nullptr; subEP = nextXMLEle(ep, 0))
4304             {
4305                 QMap<QString, double> numbers;
4306                 XMLEle * oneNumber = nullptr;
4307                 for (oneNumber = nextXMLEle(subEP, 1); oneNumber != nullptr; oneNumber = nextXMLEle(subEP, 0))
4308                 {
4309                     const char * name = findXMLAttValu(oneNumber, "name");
4310                     numbers[name] = cLocale.toDouble(pcdataXMLEle(oneNumber));
4311                 }
4312 
4313                 const char * name = findXMLAttValu(subEP, "name");
4314                 propertyMap[name] = numbers;
4315             }
4316 
4317             customPropertiesDialog->setCustomProperties(propertyMap);
4318             const double gain = getGain();
4319             if (gain >= 0)
4320                 captureGainN->setValue(gain);
4321             const double offset = getOffset();
4322             if (offset >= 0)
4323                 captureOffsetN->setValue(offset);
4324         }
4325         else if (!strcmp(tagXMLEle(ep), "Calibration"))
4326         {
4327             subEP = findXMLEle(ep, "FlatSource");
4328             if (subEP)
4329             {
4330                 XMLEle * typeEP = findXMLEle(subEP, "Type");
4331                 if (typeEP)
4332                 {
4333                     if (!strcmp(pcdataXMLEle(typeEP), "Manual"))
4334                         flatFieldSource = SOURCE_MANUAL;
4335                     else if (!strcmp(pcdataXMLEle(typeEP), "FlatCap"))
4336                         flatFieldSource = SOURCE_FLATCAP;
4337                     else if (!strcmp(pcdataXMLEle(typeEP), "DarkCap"))
4338                         flatFieldSource = SOURCE_DARKCAP;
4339                     else if (!strcmp(pcdataXMLEle(typeEP), "Wall"))
4340                     {
4341                         XMLEle * azEP  = findXMLEle(subEP, "Az");
4342                         XMLEle * altEP = findXMLEle(subEP, "Alt");
4343 
4344                         if (azEP && altEP)
4345                         {
4346                             flatFieldSource = SOURCE_WALL;
4347                             wallCoord.setAz(cLocale.toDouble(pcdataXMLEle(azEP)));
4348                             wallCoord.setAlt(cLocale.toDouble(pcdataXMLEle(altEP)));
4349                         }
4350                     }
4351                     else
4352                         flatFieldSource = SOURCE_DAWN_DUSK;
4353                 }
4354             }
4355 
4356             subEP = findXMLEle(ep, "FlatDuration");
4357             if (subEP)
4358             {
4359                 XMLEle * typeEP = findXMLEle(subEP, "Type");
4360                 if (typeEP)
4361                 {
4362                     if (!strcmp(pcdataXMLEle(typeEP), "Manual"))
4363                         flatFieldDuration = DURATION_MANUAL;
4364                 }
4365 
4366                 XMLEle * aduEP = findXMLEle(subEP, "Value");
4367                 if (aduEP)
4368                 {
4369                     flatFieldDuration = DURATION_ADU;
4370                     targetADU         = cLocale.toDouble(pcdataXMLEle(aduEP));
4371                 }
4372 
4373                 aduEP = findXMLEle(subEP, "Tolerance");
4374                 if (aduEP)
4375                 {
4376                     targetADUTolerance = cLocale.toDouble(pcdataXMLEle(aduEP));
4377                 }
4378             }
4379 
4380             subEP = findXMLEle(ep, "PreMountPark");
4381             if (subEP)
4382             {
4383                 if (!strcmp(pcdataXMLEle(subEP), "True"))
4384                     preMountPark = true;
4385                 else
4386                     preMountPark = false;
4387             }
4388 
4389             subEP = findXMLEle(ep, "PreDomePark");
4390             if (subEP)
4391             {
4392                 if (!strcmp(pcdataXMLEle(subEP), "True"))
4393                     preDomePark = true;
4394                 else
4395                     preDomePark = false;
4396             }
4397         }
4398     }
4399 
4400     addJob(false);
4401 
4402     return true;
4403 }
4404 
saveSequenceQueue()4405 void Capture::saveSequenceQueue()
4406 {
4407     QUrl backupCurrent = m_SequenceURL;
4408 
4409     if (m_SequenceURL.toLocalFile().startsWith(QLatin1String("/tmp/")) || m_SequenceURL.toLocalFile().contains("/Temp"))
4410         m_SequenceURL.clear();
4411 
4412     // If no changes made, return.
4413     if (m_Dirty == false && !m_SequenceURL.isEmpty())
4414         return;
4415 
4416     if (m_SequenceURL.isEmpty())
4417     {
4418         m_SequenceURL = QFileDialog::getSaveFileUrl(Ekos::Manager::Instance(), i18nc("@title:window", "Save Ekos Sequence Queue"),
4419                         dirPath,
4420                         "Ekos Sequence Queue (*.esq)");
4421         // if user presses cancel
4422         if (m_SequenceURL.isEmpty())
4423         {
4424             m_SequenceURL = backupCurrent;
4425             return;
4426         }
4427 
4428         dirPath = QUrl(m_SequenceURL.url(QUrl::RemoveFilename));
4429 
4430         if (m_SequenceURL.toLocalFile().endsWith(QLatin1String(".esq")) == false)
4431             m_SequenceURL.setPath(m_SequenceURL.toLocalFile() + ".esq");
4432 
4433         /*if (QFile::exists(sequenceURL.toLocalFile()))
4434         {
4435             int r = KMessageBox::warningContinueCancel(0,
4436                                                        i18n("A file named \"%1\" already exists. "
4437                                                             "Overwrite it?",
4438                                                             sequenceURL.fileName()),
4439                                                        i18n("Overwrite File?"), KStandardGuiItem::overwrite());
4440             if (r == KMessageBox::Cancel)
4441                 return;
4442         }*/
4443     }
4444 
4445     if (m_SequenceURL.isValid())
4446     {
4447         if ((saveSequenceQueue(m_SequenceURL.toLocalFile())) == false)
4448         {
4449             KSNotification::error(i18n("Failed to save sequence queue"), i18n("Save"));
4450             return;
4451         }
4452 
4453         m_Dirty = false;
4454     }
4455     else
4456     {
4457         QString message = i18n("Invalid URL: %1", m_SequenceURL.url());
4458         KSNotification::sorry(message, i18n("Invalid URL"));
4459     }
4460 }
4461 
saveSequenceQueueAs()4462 void Capture::saveSequenceQueueAs()
4463 {
4464     m_SequenceURL.clear();
4465     saveSequenceQueue();
4466 }
4467 
saveSequenceQueue(const QString & path)4468 bool Capture::saveSequenceQueue(const QString &path)
4469 {
4470     QFile file;
4471     QString rawPrefix;
4472     bool filterEnabled, expEnabled, tsEnabled;
4473     const QMap<QString, CCDFrameType> frameTypes =
4474     {
4475         { "Light", FRAME_LIGHT }, { "Dark", FRAME_DARK }, { "Bias", FRAME_BIAS }, { "Flat", FRAME_FLAT }
4476     };
4477 
4478     file.setFileName(path);
4479 
4480     if (!file.open(QIODevice::WriteOnly))
4481     {
4482         QString message = i18n("Unable to write to file %1", path);
4483         KSNotification::sorry(message, i18n("Could not open file"));
4484         return false;
4485     }
4486 
4487     QTextStream outstream(&file);
4488 
4489     // We serialize sequence data to XML using the C locale
4490     QLocale cLocale = QLocale::c();
4491 
4492     outstream << "<?xml version=\"1.0\" encoding=\"UTF-8\"?>" << endl;
4493     outstream << "<SequenceQueue version='" << SQ_FORMAT_VERSION << "'>" << endl;
4494     if (m_ObserverName.isEmpty() == false)
4495         outstream << "<Observer>" << m_ObserverName << "</Observer>" << endl;
4496     outstream << "<CCD>" << cameraS->currentText() << "</CCD>" << endl;
4497     outstream << "<FilterWheel>" << filterWheelS->currentText() << "</FilterWheel>" << endl;
4498     outstream << "<GuideDeviation enabled='" << (limitGuideDeviationS->isChecked() ? "true" : "false") << "'>"
4499               << cLocale.toString(limitGuideDeviationN->value()) << "</GuideDeviation>" << endl;
4500     outstream << "<GuideStartDeviation enabled='" << (startGuiderDriftS->isChecked() ? "true" : "false") << "'>"
4501               << cLocale.toString(startGuiderDriftN->value()) << "</GuideStartDeviation>" << endl;
4502     // Issue a warning when autofocus is enabled but Ekos options prevent HFR value from being written
4503     if (limitFocusHFRS->isChecked() && !Options::saveHFRToFile())
4504         appendLogText(i18n(
4505                           "Warning: HFR-based autofocus is set but option \"Save Sequence HFR Value to File\" is not enabled. "
4506                           "Current HFR value will not be written to sequence file."));
4507     outstream << "<Autofocus enabled='" << (limitFocusHFRS->isChecked() ? "true" : "false") << "'>"
4508               << cLocale.toString(Options::saveHFRToFile() ? limitFocusHFRN->value() : 0) << "</Autofocus>" << endl;
4509     outstream << "<RefocusOnTemperatureDelta enabled='" << (limitFocusDeltaTS->isChecked() ? "true" : "false") << "'>"
4510               << cLocale.toString(limitFocusDeltaTN->value()) << "</RefocusOnTemperatureDelta>" << endl;
4511     outstream << "<RefocusEveryN enabled='" << (limitRefocusS->isChecked() ? "true" : "false") << "'>"
4512               << cLocale.toString(limitRefocusN->value()) << "</RefocusEveryN>" << endl;
4513     for (auto &job : jobs)
4514     {
4515         job->getPrefixSettings(rawPrefix, filterEnabled, expEnabled, tsEnabled);
4516 
4517         outstream << "<Job>" << endl;
4518 
4519         outstream << "<Exposure>" << cLocale.toString(job->getExposure()) << "</Exposure>" << endl;
4520         outstream << "<Binning>" << endl;
4521         outstream << "<X>" << cLocale.toString(job->getXBin()) << "</X>" << endl;
4522         outstream << "<Y>" << cLocale.toString(job->getXBin()) << "</Y>" << endl;
4523         outstream << "</Binning>" << endl;
4524         outstream << "<Frame>" << endl;
4525         outstream << "<X>" << cLocale.toString(job->getSubX()) << "</X>" << endl;
4526         outstream << "<Y>" << cLocale.toString(job->getSubY()) << "</Y>" << endl;
4527         outstream << "<W>" << cLocale.toString(job->getSubW()) << "</W>" << endl;
4528         outstream << "<H>" << cLocale.toString(job->getSubH()) << "</H>" << endl;
4529         outstream << "</Frame>" << endl;
4530         if (job->getTargetTemperature() != Ekos::INVALID_VALUE)
4531             outstream << "<Temperature force='" << (job->getEnforceTemperature() ? "true" : "false") << "'>"
4532                       << cLocale.toString(job->getTargetTemperature()) << "</Temperature>" << endl;
4533         if (job->getTargetFilter() >= 0)
4534             //outstream << "<Filter>" << job->getTargetFilter() << "</Filter>" << endl;
4535             outstream << "<Filter>" << job->getFilterName() << "</Filter>" << endl;
4536         outstream << "<Type>" << frameTypes.key(job->getFrameType()) << "</Type>" << endl;
4537         outstream << "<Prefix>" << endl;
4538         //outstream << "<CompletePrefix>" << job->getPrefix() << "</CompletePrefix>" << endl;
4539         outstream << "<RawPrefix>" << rawPrefix << "</RawPrefix>" << endl;
4540         outstream << "<FilterEnabled>" << (filterEnabled ? 1 : 0) << "</FilterEnabled>" << endl;
4541         outstream << "<ExpEnabled>" << (expEnabled ? 1 : 0) << "</ExpEnabled>" << endl;
4542         outstream << "<TimeStampEnabled>" << (tsEnabled ? 1 : 0) << "</TimeStampEnabled>" << endl;
4543         outstream << "</Prefix>" << endl;
4544         outstream << "<Count>" << cLocale.toString(job->getCount()) << "</Count>" << endl;
4545         // ms to seconds
4546         outstream << "<Delay>" << cLocale.toString(job->getDelay() / 1000.0) << "</Delay>" << endl;
4547         if (job->getScript(SCRIPT_PRE_CAPTURE).isEmpty() == false)
4548             outstream << "<PreCaptureScript>" << job->getScript(SCRIPT_PRE_CAPTURE) << "</PreCaptureScript>" << endl;
4549         if (job->getScript(SCRIPT_POST_CAPTURE).isEmpty() == false)
4550             outstream << "<PostCaptureScript>" << job->getScript(SCRIPT_POST_CAPTURE) << "</PostCaptureScript>" << endl;
4551         if (job->getScript(SCRIPT_PRE_JOB).isEmpty() == false)
4552             outstream << "<PreJobScript>" << job->getScript(SCRIPT_PRE_JOB) << "</PreJobScript>" << endl;
4553         if (job->getScript(SCRIPT_POST_JOB).isEmpty() == false)
4554             outstream << "<PostJobScript>" << job->getScript(SCRIPT_POST_JOB) << "</PostJobScript>" << endl;
4555         outstream << "<FITSDirectory>" << job->getLocalDir() << "</FITSDirectory>" << endl;
4556         outstream << "<UploadMode>" << job->getUploadMode() << "</UploadMode>" << endl;
4557         if (job->getRemoteDir().isEmpty() == false)
4558             outstream << "<RemoteDirectory>" << job->getRemoteDir() << "</RemoteDirectory>" << endl;
4559         if (job->getISOIndex() != -1)
4560             outstream << "<ISOIndex>" << (job->getISOIndex()) << "</ISOIndex>" << endl;
4561         outstream << "<FormatIndex>" << (job->getTransforFormat()) << "</FormatIndex>" << endl;
4562         if (job->getTargetRotation() != Ekos::INVALID_VALUE)
4563             outstream << "<Rotation>" << (job->getTargetRotation()) << "</Rotation>" << endl;
4564         QMapIterator<QString, QMap<QString, double>> customIter(job->getCustomProperties());
4565         outstream << "<Properties>" << endl;
4566         while (customIter.hasNext())
4567         {
4568             customIter.next();
4569             outstream << "<NumberVector name='" << customIter.key() << "'>" << endl;
4570             QMap<QString, double> numbers = customIter.value();
4571             QMapIterator<QString, double> numberIter(numbers);
4572             while (numberIter.hasNext())
4573             {
4574                 numberIter.next();
4575                 outstream << "<OneNumber name='" << numberIter.key()
4576                           << "'>" << cLocale.toString(numberIter.value()) << "</OneNumber>" << endl;
4577             }
4578             outstream << "</NumberVector>" << endl;
4579         }
4580         outstream << "</Properties>" << endl;
4581 
4582         outstream << "<Calibration>" << endl;
4583         outstream << "<FlatSource>" << endl;
4584         if (job->getFlatFieldSource() == SOURCE_MANUAL)
4585             outstream << "<Type>Manual</Type>" << endl;
4586         else if (job->getFlatFieldSource() == SOURCE_FLATCAP)
4587             outstream << "<Type>FlatCap</Type>" << endl;
4588         else if (job->getFlatFieldSource() == SOURCE_DARKCAP)
4589             outstream << "<Type>DarkCap</Type>" << endl;
4590         else if (job->getFlatFieldSource() == SOURCE_WALL)
4591         {
4592             outstream << "<Type>Wall</Type>" << endl;
4593             outstream << "<Az>" << cLocale.toString(job->getWallCoord().az().Degrees()) << "</Az>" << endl;
4594             outstream << "<Alt>" << cLocale.toString(job->getWallCoord().alt().Degrees()) << "</Alt>" << endl;
4595         }
4596         else
4597             outstream << "<Type>DawnDust</Type>" << endl;
4598         outstream << "</FlatSource>" << endl;
4599 
4600         outstream << "<FlatDuration>" << endl;
4601         if (job->getFlatFieldDuration() == DURATION_MANUAL)
4602             outstream << "<Type>Manual</Type>" << endl;
4603         else
4604         {
4605             outstream << "<Type>ADU</Type>" << endl;
4606             outstream << "<Value>" << cLocale.toString(job->getTargetADU()) << "</Value>" << endl;
4607             outstream << "<Tolerance>" << cLocale.toString(job->getTargetADUTolerance()) << "</Tolerance>" << endl;
4608         }
4609         outstream << "</FlatDuration>" << endl;
4610 
4611         outstream << "<PreMountPark>" << (job->isPreMountPark() ? "True" : "False") << "</PreMountPark>" << endl;
4612         outstream << "<PreDomePark>" << (job->isPreDomePark() ? "True" : "False") << "</PreDomePark>" << endl;
4613         outstream << "</Calibration>" << endl;
4614 
4615         outstream << "</Job>" << endl;
4616     }
4617 
4618     outstream << "</SequenceQueue>" << endl;
4619 
4620     appendLogText(i18n("Sequence queue saved to %1", path));
4621     file.flush();
4622     file.close();
4623     // update save button tool tip
4624     queueSaveB->setToolTip("Save to " + file.fileName());
4625 
4626     return true;
4627 }
4628 
resetJobs()4629 void Capture::resetJobs()
4630 {
4631     // Stop any running capture
4632     stop();
4633 
4634     // If a job is selected for edit, reset only that job
4635     if (m_JobUnderEdit == true)
4636     {
4637         SequenceJob * job = jobs.at(queueTable->currentRow());
4638         if (nullptr != job)
4639             job->resetStatus();
4640     }
4641     else
4642     {
4643         if (KMessageBox::warningContinueCancel(
4644                     nullptr, i18n("Are you sure you want to reset status of all jobs?"), i18n("Reset job status"),
4645                     KStandardGuiItem::cont(), KStandardGuiItem::cancel(), "reset_job_status_warning") != KMessageBox::Continue)
4646         {
4647             return;
4648         }
4649 
4650         foreach (SequenceJob * job, jobs)
4651             job->resetStatus();
4652     }
4653 
4654     // Also reset the storage count for all jobs
4655     capturedFramesMap.clear();
4656 
4657     // We're not controlled by the Scheduler, restore progress option
4658     ignoreJobProgress = Options::alwaysResetSequenceWhenStarting();
4659 }
4660 
ignoreSequenceHistory()4661 void Capture::ignoreSequenceHistory()
4662 {
4663     // This function is called independently from the Scheduler or the UI, so honor the change
4664     ignoreJobProgress = true;
4665 }
4666 
syncGUIToJob(SequenceJob * job)4667 void Capture::syncGUIToJob(SequenceJob * job)
4668 {
4669     if (job == nullptr)
4670     {
4671         qWarning(KSTARS_EKOS_CAPTURE) << "syncGuiToJob with null job.";
4672         // Everything below depends on job. Just return.
4673         return;
4674     }
4675 
4676     QString rawPrefix;
4677     bool filterEnabled, expEnabled, tsEnabled;
4678 
4679     job->getPrefixSettings(rawPrefix, filterEnabled, expEnabled, tsEnabled);
4680 
4681     captureExposureN->setValue(job->getExposure());
4682     captureBinHN->setValue(job->getXBin());
4683     captureBinVN->setValue(job->getYBin());
4684     captureFrameXN->setValue(job->getSubX());
4685     captureFrameYN->setValue(job->getSubY());
4686     captureFrameWN->setValue(job->getSubW());
4687     captureFrameHN->setValue(job->getSubH());
4688     captureFilterS->setCurrentIndex(job->getTargetFilter() - 1);
4689     captureTypeS->setCurrentIndex(job->getFrameType());
4690     filePrefixT->setText(rawPrefix);
4691     fileFilterS->setChecked(filterEnabled);
4692     fileDurationS->setChecked(expEnabled);
4693     fileTimestampS->setChecked(tsEnabled);
4694     captureCountN->setValue(job->getCount());
4695     captureDelayN->setValue(job->getDelay() / 1000);
4696     fileUploadModeS->setCurrentIndex(job->getUploadMode());
4697     fileRemoteDirT->setEnabled(fileUploadModeS->currentIndex() != 0);
4698     fileRemoteDirT->setText(job->getRemoteDir());
4699     fileDirectoryT->setText(job->getLocalDir());
4700 
4701     // Temperature Options
4702     cameraTemperatureS->setChecked(job->getEnforceTemperature());
4703     if (job->getEnforceTemperature())
4704         cameraTemperatureN->setValue(job->getTargetTemperature());
4705 
4706     // Start guider drift options
4707     startGuiderDriftS->setChecked(job->getEnforceStartGuiderDrift());
4708     if (job->getEnforceStartGuiderDrift())
4709         startGuiderDriftN->setValue(job->getTargetStartGuiderDrift());
4710 
4711     // Flat field options
4712     calibrationB->setEnabled(job->getFrameType() != FRAME_LIGHT);
4713     flatFieldDuration  = job->getFlatFieldDuration();
4714     flatFieldSource    = job->getFlatFieldSource();
4715     targetADU          = job->getTargetADU();
4716     targetADUTolerance = job->getTargetADUTolerance();
4717     wallCoord          = job->getWallCoord();
4718     preMountPark       = job->isPreMountPark();
4719     preDomePark        = job->isPreDomePark();
4720 
4721     // Script options
4722     m_Scripts          = job->getScripts();
4723 
4724     // Custom Properties
4725     customPropertiesDialog->setCustomProperties(job->getCustomProperties());
4726 
4727     if (captureISOS)
4728         captureISOS->setCurrentIndex(job->getISOIndex());
4729 
4730     double gain = getGain();
4731     if (gain >= 0)
4732         captureGainN->setValue(gain);
4733     else
4734         captureGainN->setValue(GainSpinSpecialValue);
4735 
4736     double offset = getOffset();
4737     if (offset >= 0)
4738         captureOffsetN->setValue(offset);
4739     else
4740         captureOffsetN->setValue(OffsetSpinSpecialValue);
4741 
4742     captureFormatS->setCurrentIndex(job->getTransforFormat());
4743 
4744     if (job->getTargetRotation() != Ekos::INVALID_VALUE)
4745     {
4746         rotatorSettings->setRotationEnforced(true);
4747         rotatorSettings->setTargetRotationPA(job->getTargetRotation());
4748     }
4749     else
4750         rotatorSettings->setRotationEnforced(false);
4751 
4752     emit settingsUpdated(getPresetSettings());
4753 }
4754 
getPresetSettings()4755 QJsonObject Capture::getPresetSettings()
4756 {
4757     QJsonObject settings;
4758 
4759     // Try to get settings value
4760     // if not found, fallback to camera value
4761     double gain = -1;
4762     if (GainSpinSpecialValue > INVALID_VALUE && captureGainN->value() > GainSpinSpecialValue)
4763         gain = captureGainN->value();
4764     else if (currentCCD && currentCCD->hasGain())
4765         currentCCD->getGain(&gain);
4766 
4767     double offset = -1;
4768     if (OffsetSpinSpecialValue > INVALID_VALUE && captureOffsetN->value() > OffsetSpinSpecialValue)
4769         offset = captureOffsetN->value();
4770     else if (currentCCD && currentCCD->hasOffset())
4771         currentCCD->getOffset(&offset);
4772 
4773     int iso = -1;
4774     if (captureISOS)
4775         iso = captureISOS->currentIndex();
4776     else if (currentCCD)
4777         iso = currentCCD->getChip(ISD::CCDChip::PRIMARY_CCD)->getISOIndex();
4778 
4779     settings.insert("camera", cameraS->currentText());
4780     settings.insert("fw", filterWheelS->currentText());
4781     settings.insert("filter", captureFilterS->currentText());
4782     settings.insert("dark", darkB->isChecked());
4783     settings.insert("exp", captureExposureN->value());
4784     settings.insert("bin", captureBinHN->value());
4785     settings.insert("iso", iso);
4786     settings.insert("frameType", captureTypeS->currentIndex());
4787     settings.insert("format", captureFormatS->currentIndex());
4788     settings.insert("gain", gain);
4789     settings.insert("offset", offset);
4790     settings.insert("temperature", cameraTemperatureN->value());
4791 
4792     return settings;
4793 }
4794 
selectedJobChanged(QModelIndex current,QModelIndex previous)4795 void Capture::selectedJobChanged(QModelIndex current, QModelIndex previous)
4796 {
4797     Q_UNUSED(previous)
4798     selectJob(current);
4799 }
4800 
selectJob(QModelIndex i)4801 void Capture::selectJob(QModelIndex i)
4802 {
4803     if (i.row() < 0 || (i.row() + 1) > jobs.size())
4804         return;
4805 
4806     SequenceJob * job = jobs.at(i.row());
4807 
4808     if (job == nullptr)
4809         return;
4810 
4811     syncGUIToJob(job);
4812 
4813     if (isBusy || jobs.size() < 2)
4814         return;
4815 
4816     queueUpB->setEnabled(i.row() > 0);
4817     queueDownB->setEnabled(i.row() + 1 < jobs.size());
4818 }
4819 
editJob(QModelIndex i)4820 void Capture::editJob(QModelIndex i)
4821 {
4822     selectJob(i);
4823     appendLogText(i18n("Editing job #%1...", i.row() + 1));
4824 
4825     addToQueueB->setIcon(QIcon::fromTheme("dialog-ok-apply"));
4826     addToQueueB->setToolTip(i18n("Apply job changes."));
4827     removeFromQueueB->setToolTip(i18n("Cancel job changes."));
4828 
4829     m_JobUnderEdit = true;
4830 }
4831 
resetJobEdit()4832 void Capture::resetJobEdit()
4833 {
4834     if (m_JobUnderEdit)
4835         appendLogText(i18n("Editing job canceled."));
4836 
4837     m_JobUnderEdit = false;
4838     addToQueueB->setIcon(QIcon::fromTheme("list-add"));
4839 
4840     addToQueueB->setToolTip(i18n("Add job to sequence queue"));
4841     removeFromQueueB->setToolTip(i18n("Remove job from sequence queue"));
4842 }
4843 
getProgressPercentage()4844 double Capture::getProgressPercentage()
4845 {
4846     int totalImageCount     = 0;
4847     int totalImageCompleted = 0;
4848 
4849     foreach (SequenceJob * job, jobs)
4850     {
4851         totalImageCount += job->getCount();
4852         totalImageCompleted += job->getCompleted();
4853     }
4854 
4855     if (totalImageCount != 0)
4856         return ((static_cast<double>(totalImageCompleted) / totalImageCount) * 100.0);
4857     else
4858         return -1;
4859 }
4860 
getActiveJobID()4861 int Capture::getActiveJobID()
4862 {
4863     if (activeJob == nullptr)
4864         return -1;
4865 
4866     for (int i = 0; i < jobs.count(); i++)
4867     {
4868         if (activeJob == jobs[i])
4869             return i;
4870     }
4871 
4872     return -1;
4873 }
4874 
getPendingJobCount()4875 int Capture::getPendingJobCount()
4876 {
4877     int completedJobs = 0;
4878 
4879     foreach (SequenceJob * job, jobs)
4880     {
4881         if (job->getStatus() == SequenceJob::JOB_DONE)
4882             completedJobs++;
4883     }
4884 
4885     return (jobs.count() - completedJobs);
4886 }
4887 
getJobState(int id)4888 QString Capture::getJobState(int id)
4889 {
4890     if (id < jobs.count())
4891     {
4892         SequenceJob * job = jobs.at(id);
4893         return job->getStatusString();
4894     }
4895 
4896     return QString();
4897 }
4898 
getJobFilterName(int id)4899 QString Capture::getJobFilterName(int id)
4900 {
4901     if (id < jobs.count())
4902     {
4903         SequenceJob * job = jobs.at(id);
4904         return job->getFilterName();
4905     }
4906 
4907     return QString();
4908 }
4909 
getJobImageProgress(int id)4910 int Capture::getJobImageProgress(int id)
4911 {
4912     if (id < jobs.count())
4913     {
4914         SequenceJob * job = jobs.at(id);
4915         return job->getCompleted();
4916     }
4917 
4918     return -1;
4919 }
4920 
getJobImageCount(int id)4921 int Capture::getJobImageCount(int id)
4922 {
4923     if (id < jobs.count())
4924     {
4925         SequenceJob * job = jobs.at(id);
4926         return job->getCount();
4927     }
4928 
4929     return -1;
4930 }
4931 
getJobExposureProgress(int id)4932 double Capture::getJobExposureProgress(int id)
4933 {
4934     if (id < jobs.count())
4935     {
4936         SequenceJob * job = jobs.at(id);
4937         return job->getExposeLeft();
4938     }
4939 
4940     return -1;
4941 }
4942 
getJobExposureDuration(int id)4943 double Capture::getJobExposureDuration(int id)
4944 {
4945     if (id < jobs.count())
4946     {
4947         SequenceJob * job = jobs.at(id);
4948         return job->getExposure();
4949     }
4950 
4951     return -1;
4952 }
4953 
getJobFrameType(int id)4954 CCDFrameType Capture::getJobFrameType(int id)
4955 {
4956     if (id < jobs.count())
4957     {
4958         SequenceJob * job = jobs.at(id);
4959         return job->getFrameType();
4960     }
4961 
4962     return FRAME_NONE;
4963 }
4964 
getJobRemainingTime(SequenceJob * job)4965 int Capture::getJobRemainingTime(SequenceJob * job)
4966 {
4967     int remaining = static_cast<int>((job->getExposure() + getEstimatedDownloadTime() + job->getDelay() / 1000) *
4968                                      (job->getCount() - job->getCompleted()));
4969 
4970     if (job->getStatus() == SequenceJob::JOB_BUSY)
4971     {
4972         if (job->getExposeLeft())
4973             remaining -= job->getExposure() - job->getExposeLeft();
4974         else
4975             remaining += job->getExposeLeft() + getEstimatedDownloadTime();
4976     }
4977 
4978     return remaining;
4979 }
4980 
getOverallRemainingTime()4981 int Capture::getOverallRemainingTime()
4982 {
4983     int remaining = 0;
4984 
4985     foreach (SequenceJob * job, jobs)
4986         remaining += getJobRemainingTime(job);
4987 
4988     return remaining;
4989 }
4990 
getActiveJobRemainingTime()4991 int Capture::getActiveJobRemainingTime()
4992 {
4993     if (activeJob == nullptr)
4994         return -1;
4995 
4996     return getJobRemainingTime(activeJob);
4997 }
4998 
setMaximumGuidingDeviation(bool enable,double value)4999 void Capture::setMaximumGuidingDeviation(bool enable, double value)
5000 {
5001     limitGuideDeviationS->setChecked(enable);
5002     if (enable)
5003         limitGuideDeviationN->setValue(value);
5004 }
5005 
setInSequenceFocus(bool enable,double HFR)5006 void Capture::setInSequenceFocus(bool enable, double HFR)
5007 {
5008     limitFocusHFRS->setChecked(enable);
5009     if (enable)
5010         limitFocusHFRN->setValue(HFR);
5011 }
5012 
setTargetTemperature(double temperature)5013 void Capture::setTargetTemperature(double temperature)
5014 {
5015     cameraTemperatureN->setValue(temperature);
5016 }
5017 
clearSequenceQueue()5018 void Capture::clearSequenceQueue()
5019 {
5020     activeJob = nullptr;
5021     //m_TargetName.clear();
5022     //stop();
5023     while (queueTable->rowCount() > 0)
5024         queueTable->removeRow(0);
5025     qDeleteAll(jobs);
5026     jobs.clear();
5027 }
5028 
getSequenceQueueStatus()5029 QString Capture::getSequenceQueueStatus()
5030 {
5031     if (jobs.count() == 0)
5032         return "Invalid";
5033 
5034     if (isBusy)
5035         return "Running";
5036 
5037     int idle = 0, error = 0, complete = 0, aborted = 0, running = 0;
5038 
5039     foreach (SequenceJob * job, jobs)
5040     {
5041         switch (job->getStatus())
5042         {
5043             case SequenceJob::JOB_ABORTED:
5044                 aborted++;
5045                 break;
5046             case SequenceJob::JOB_BUSY:
5047                 running++;
5048                 break;
5049             case SequenceJob::JOB_DONE:
5050                 complete++;
5051                 break;
5052             case SequenceJob::JOB_ERROR:
5053                 error++;
5054                 break;
5055             case SequenceJob::JOB_IDLE:
5056                 idle++;
5057                 break;
5058         }
5059     }
5060 
5061     if (error > 0)
5062         return "Error";
5063 
5064     if (aborted > 0)
5065     {
5066         if (m_State == CAPTURE_SUSPENDED)
5067             return "Suspended";
5068         else
5069             return "Aborted";
5070     }
5071 
5072     if (running > 0)
5073         return "Running";
5074 
5075     if (idle == jobs.count())
5076         return "Idle";
5077 
5078     if (complete == jobs.count())
5079         return "Complete";
5080 
5081     return "Invalid";
5082 }
5083 
processTelescopeNumber(INumberVectorProperty * nvp)5084 void Capture::processTelescopeNumber(INumberVectorProperty * nvp)
5085 {
5086     // If it is not ours, return.
5087     if (nvp->device != currentTelescope->getDeviceName() || strstr(nvp->name, "EQUATORIAL_") == nullptr)
5088         return;
5089 
5090     switch (meridianFlipStage)
5091     {
5092         case MF_NONE:
5093             break;
5094         case MF_INITIATED:
5095         {
5096             if (nvp->s == IPS_BUSY)
5097                 setMeridianFlipStage(MF_FLIPPING);
5098         }
5099         break;
5100 
5101         case MF_FLIPPING:
5102         {
5103             if (currentTelescope != nullptr && currentTelescope->isSlewing())
5104                 setMeridianFlipStage(MF_SLEWING);
5105         }
5106         break;
5107 
5108         default:
5109             break;
5110     }
5111 }
5112 
processFlipCompleted()5113 void Capture::processFlipCompleted()
5114 {
5115     // If dome is syncing, wait until it stops
5116     if (currentDome && currentDome->isMoving())
5117         return;
5118 
5119     appendLogText(i18n("Telescope completed the meridian flip."));
5120 
5121     //KNotification::event(QLatin1String("MeridianFlipCompleted"), i18n("Meridian flip is successfully completed"));
5122     KSNotification::event(QLatin1String("MeridianFlipCompleted"), i18n("Meridian flip is successfully completed"),
5123                           KSNotification::EVENT_INFO);
5124 
5125 
5126     if (m_State == CAPTURE_IDLE || m_State == CAPTURE_ABORTED || m_State == CAPTURE_COMPLETE)
5127     {
5128         // reset the meridian flip stage and jump directly MF_NONE, since no
5129         // restart of guiding etc. necessary
5130         setMeridianFlipStage(MF_NONE);
5131         return;
5132     }
5133 }
5134 
checkGuidingAfterFlip()5135 bool Capture::checkGuidingAfterFlip()
5136 {
5137     // if no meridian flip has completed, we do not touch guiding
5138     if (meridianFlipStage < MF_COMPLETED)
5139         return false;
5140     // If we're not autoguiding then we're done
5141     if (resumeGuidingAfterFlip == false)
5142     {
5143         setMeridianFlipStage(MF_NONE);
5144         return false;
5145     }
5146 
5147     // if we are waiting for a calibration, start it
5148     if (m_State < CAPTURE_CALIBRATING)
5149     {
5150         appendLogText(i18n("Performing post flip re-calibration and guiding..."));
5151         secondsLabel->setText(i18n("Calibrating..."));
5152 
5153         m_State = CAPTURE_CALIBRATING;
5154         emit newStatus(Ekos::CAPTURE_CALIBRATING);
5155 
5156         setMeridianFlipStage(MF_GUIDING);
5157         emit meridianFlipCompleted();
5158         return true;
5159     }
5160     else if (m_State == CAPTURE_CALIBRATING && (m_GuideState == GUIDE_CALIBRATION_ERROR || m_GuideState == GUIDE_ABORTED))
5161     {
5162         // restart guiding after failure
5163         appendLogText(i18n("Post meridian flip calibration error. Restarting..."));
5164         emit meridianFlipCompleted();
5165         return true;
5166     }
5167     else
5168         // in all other cases, do not touch
5169         return false;
5170 }
5171 
checkAlignmentAfterFlip()5172 bool Capture::checkAlignmentAfterFlip()
5173 {
5174     // if no meridian flip has completed, we do not touch guiding
5175     if (meridianFlipStage < MF_COMPLETED)
5176         return false;
5177     // If we do not need to align then we're done
5178     if (resumeAlignmentAfterFlip == false)
5179         return false;
5180 
5181     // if we are waiting for a calibration, start it
5182     if (m_State < CAPTURE_ALIGNING)
5183     {
5184         appendLogText(i18n("Performing post flip re-alignment..."));
5185         secondsLabel->setText(i18n("Aligning..."));
5186 
5187         retries = 0;
5188         m_State   = CAPTURE_ALIGNING;
5189         emit newStatus(Ekos::CAPTURE_ALIGNING);
5190 
5191         setMeridianFlipStage(MF_ALIGNING);
5192         //QTimer::singleShot(Options::settlingTime(), [this]() {emit meridialFlipTracked();});
5193         //emit meridialFlipTracked();
5194         return true;
5195     }
5196     else
5197         // in all other cases, do not touch
5198         return false;
5199 }
5200 
5201 
checkPausing()5202 bool Capture::checkPausing()
5203 {
5204     if (m_State == CAPTURE_PAUSE_PLANNED)
5205     {
5206         appendLogText(i18n("Sequence paused."));
5207         secondsLabel->setText(i18n("Paused..."));
5208         m_State = CAPTURE_PAUSED;
5209         emit newStatus(m_State);
5210         // handle a requested meridian flip
5211         if (meridianFlipStage != MF_NONE)
5212             setMeridianFlipStage(MF_READY);
5213         // pause
5214         return true;
5215     }
5216     // no pause
5217     return false;
5218 }
5219 
5220 
checkMeridianFlipReady()5221 bool Capture::checkMeridianFlipReady()
5222 {
5223     if (currentTelescope == nullptr)
5224         return false;
5225 
5226     // If active job is taking flat field image at a wall source
5227     // then do not flip.
5228     if (activeJob && activeJob->getFrameType() == FRAME_FLAT && activeJob->getFlatFieldSource() == SOURCE_WALL)
5229         return false;
5230 
5231     if (meridianFlipStage != MF_REQUESTED)
5232         // if no flip has been requested or is already ongoing
5233         return false;
5234 
5235     // meridian flip requested or already in action
5236 
5237     // Reset frame if we need to do focusing later on
5238     if (isInSequenceFocus || (limitRefocusS->isChecked() && getRefocusEveryNTimerElapsedSec() > 0))
5239         emit resetFocus();
5240 
5241     // signal that meridian flip may take place
5242     if (meridianFlipStage == MF_REQUESTED)
5243         setMeridianFlipStage(MF_READY);
5244 
5245 
5246     return true;
5247 }
5248 
checkGuideDeviationTimeout()5249 void Capture::checkGuideDeviationTimeout()
5250 {
5251     if (activeJob && activeJob->getStatus() == SequenceJob::JOB_ABORTED && m_DeviationDetected)
5252     {
5253         appendLogText(i18n("Guide module timed out."));
5254         m_DeviationDetected = false;
5255 
5256         // If capture was suspended, it should be aborted (failed) now.
5257         if (m_State == CAPTURE_SUSPENDED)
5258         {
5259             m_State = CAPTURE_ABORTED;
5260             emit newStatus(m_State);
5261         }
5262     }
5263 }
5264 
setAlignStatus(AlignState state)5265 void Capture::setAlignStatus(AlignState state)
5266 {
5267     if (state != m_AlignState)
5268         qCDebug(KSTARS_EKOS_CAPTURE) << "Align State changed from" << Ekos::getAlignStatusString(
5269                                          m_AlignState) << "to" << Ekos::getAlignStatusString(state);
5270     m_AlignState = state;
5271 
5272     resumeAlignmentAfterFlip = true;
5273 
5274     switch (state)
5275     {
5276         case ALIGN_COMPLETE:
5277             if (meridianFlipStage == MF_ALIGNING)
5278             {
5279                 appendLogText(i18n("Post flip re-alignment completed successfully."));
5280                 retries = 0;
5281                 // Trigger guiding if necessary.
5282                 if (checkGuidingAfterFlip() == false)
5283                 {
5284                     // If no guiding is required, the meridian flip is complete
5285                     setMeridianFlipStage(MF_NONE);
5286                     m_State = CAPTURE_WAITING;
5287                 }
5288             }
5289             break;
5290 
5291         case ALIGN_ABORTED:
5292         case ALIGN_FAILED:
5293             // TODO run it 3 times before giving up
5294             if (meridianFlipStage == MF_ALIGNING)
5295             {
5296                 if (++retries == 3)
5297                 {
5298                     appendLogText(i18n("Post-flip alignment failed."));
5299                     abort();
5300                 }
5301                 else
5302                 {
5303                     appendLogText(i18n("Post-flip alignment failed. Retrying..."));
5304                     secondsLabel->setText(i18n("Aligning..."));
5305 
5306                     this->m_State = CAPTURE_ALIGNING;
5307                     emit newStatus(Ekos::CAPTURE_ALIGNING);
5308 
5309                     setMeridianFlipStage(MF_ALIGNING);
5310                 }
5311             }
5312             break;
5313 
5314         default:
5315             break;
5316     }
5317 }
5318 
setGuideStatus(GuideState state)5319 void Capture::setGuideStatus(GuideState state)
5320 {
5321     if (state != m_GuideState)
5322         qCDebug(KSTARS_EKOS_CAPTURE) << "Guiding state changed from" << Ekos::getGuideStatusString(m_GuideState)
5323                                      << "to" << Ekos::getGuideStatusString(state);
5324 
5325     switch (state)
5326     {
5327         case GUIDE_IDLE:
5328             break;
5329 
5330         case GUIDE_GUIDING:
5331         case GUIDE_CALIBRATION_SUCESS:
5332             autoGuideReady = true;
5333             break;
5334 
5335         case GUIDE_ABORTED:
5336         case GUIDE_CALIBRATION_ERROR:
5337             processGuidingFailed();
5338             m_GuideState = state;
5339             break;
5340 
5341         case GUIDE_DITHERING_SUCCESS:
5342             qCInfo(KSTARS_EKOS_CAPTURE) << "Dithering succeeded, capture state" << getCaptureStatusString(m_State);
5343             // do nothing if something happened during dithering
5344             appendLogText(i18n("Dithering succeeded."));
5345             if (m_State != CAPTURE_DITHERING)
5346                 break;
5347 
5348             if (Options::guidingSettle() > 0)
5349             {
5350                 // N.B. Do NOT convert to i18np since guidingRate is DOUBLE value (e.g. 1.36) so we always use plural with that.
5351                 appendLogText(i18n("Dither complete. Resuming in %1 seconds...", Options::guidingSettle()));
5352                 QTimer::singleShot(Options::guidingSettle() * 1000, this, [this]()
5353                 {
5354                     m_DitheringState = IPS_OK;
5355                 });
5356             }
5357             else
5358             {
5359                 appendLogText(i18n("Dither complete."));
5360                 m_DitheringState = IPS_OK;
5361             }
5362             break;
5363 
5364         case GUIDE_DITHERING_ERROR:
5365             qCInfo(KSTARS_EKOS_CAPTURE) << "Dithering failed, capture state" << getCaptureStatusString(m_State);
5366             if (m_State != CAPTURE_DITHERING)
5367                 break;
5368 
5369             if (Options::guidingSettle() > 0)
5370             {
5371                 // N.B. Do NOT convert to i18np since guidingRate is DOUBLE value (e.g. 1.36) so we always use plural with that.
5372                 appendLogText(i18n("Warning: Dithering failed. Resuming in %1 seconds...", Options::guidingSettle()));
5373                 // set dithering state to OK after settling time and signal to proceed
5374                 QTimer::singleShot(Options::guidingSettle() * 1000, this, [this]()
5375                 {
5376                     m_DitheringState = IPS_OK;
5377                 });
5378             }
5379             else
5380             {
5381                 appendLogText(i18n("Warning: Dithering failed."));
5382                 // signal OK so that capturing may continue although dithering failed
5383                 m_DitheringState = IPS_OK;
5384             }
5385 
5386             break;
5387 
5388         default:
5389             break;
5390     }
5391 
5392     m_GuideState = state;
5393 
5394     if (activeJob)
5395     {
5396         activeJob->setGuiderActive(isActivelyGuiding());
5397         activeJob->resetCurrentGuiderDrift();
5398     }
5399 }
5400 
5401 
processGuidingFailed()5402 void Capture::processGuidingFailed()
5403 {
5404     if (m_FocusState > FOCUS_PROGRESS)
5405     {
5406         appendLogText(i18n("Autoguiding stopped. Waiting for autofocus to finish..."));
5407     }
5408     // If Autoguiding was started before and now stopped, let's abort (unless we're doing a meridian flip)
5409     else if (isGuidingOn() && meridianFlipStage == MF_NONE &&
5410              ((activeJob && activeJob->getStatus() == SequenceJob::JOB_BUSY) ||
5411               this->m_State == CAPTURE_SUSPENDED || this->m_State == CAPTURE_PAUSED))
5412     {
5413         appendLogText(i18n("Autoguiding stopped. Aborting..."));
5414         abort();
5415     }
5416     else if (meridianFlipStage == MF_GUIDING)
5417     {
5418         if (++retries >= 3)
5419         {
5420             appendLogText(i18n("Post meridian flip calibration error. Aborting..."));
5421             abort();
5422         }
5423     }
5424     autoGuideReady = false;
5425 }
5426 
checkFrameType(int index)5427 void Capture::checkFrameType(int index)
5428 {
5429     if (index == FRAME_LIGHT)
5430         calibrationB->setEnabled(false);
5431     else
5432         calibrationB->setEnabled(true);
5433 }
5434 
setCurrentADU(double value)5435 double Capture::setCurrentADU(double value)
5436 {
5437     if (activeJob == nullptr)
5438     {
5439         qWarning(KSTARS_EKOS_CAPTURE) << "setCurrentADU with null activeJob.";
5440         // Nothing good to do here. Just don't crash.
5441         return value;
5442     }
5443 
5444     double nextExposure = 0;
5445     double targetADU    = activeJob->getTargetADU();
5446     std::vector<double> coeff;
5447 
5448     // Check if saturated, then take shorter capture and discard value
5449     ExpRaw.append(activeJob->getExposure());
5450     ADURaw.append(value);
5451 
5452     qCDebug(KSTARS_EKOS_CAPTURE) << "Capture: Current ADU = " << value << " targetADU = " << targetADU
5453                                  << " Exposure Count: " << ExpRaw.count();
5454 
5455     // Most CCDs are quite linear so 1st degree polynomial is quite sufficient
5456     // But DSLRs can exhibit non-linear response curve and so a 2nd degree polynomial is more appropriate
5457     if (ExpRaw.count() >= 2)
5458     {
5459         if (ExpRaw.count() >= 5)
5460         {
5461             double chisq = 0;
5462 
5463             coeff = gsl_polynomial_fit(ADURaw.data(), ExpRaw.data(), ExpRaw.count(), 2, chisq);
5464             qCDebug(KSTARS_EKOS_CAPTURE) << "Running polynomial fitting. Found " << coeff.size() << " coefficients.";
5465             if (std::isnan(coeff[0]) || std::isinf(coeff[0]))
5466             {
5467                 qCDebug(KSTARS_EKOS_CAPTURE) << "Coefficients are invalid.";
5468                 targetADUAlgorithm = ADU_LEAST_SQUARES;
5469             }
5470             else
5471             {
5472                 nextExposure = coeff[0] + (coeff[1] * targetADU) + (coeff[2] * pow(targetADU, 2));
5473                 // If exposure is not valid or does not make sense, then we fall back to least squares
5474                 if (nextExposure < 0 || (nextExposure > ExpRaw.last() || targetADU < ADURaw.last())
5475                         || (nextExposure < ExpRaw.last() || targetADU > ADURaw.last()))
5476                 {
5477                     nextExposure = 0;
5478                     targetADUAlgorithm = ADU_LEAST_SQUARES;
5479                 }
5480                 else
5481                 {
5482                     targetADUAlgorithm = ADU_POLYNOMIAL;
5483                     for (size_t i = 0; i < coeff.size(); i++)
5484                         qCDebug(KSTARS_EKOS_CAPTURE) << "Coeff #" << i << "=" << coeff[i];
5485                 }
5486             }
5487         }
5488 
5489         bool looping = false;
5490         if (ExpRaw.count() >= 10)
5491         {
5492             int size = ExpRaw.count();
5493             looping  = (std::fabs(ExpRaw[size - 1] - ExpRaw[size - 2] < 0.01)) &&
5494                        (std::fabs(ExpRaw[size - 2] - ExpRaw[size - 3] < 0.01));
5495             if (looping && targetADUAlgorithm == ADU_POLYNOMIAL)
5496             {
5497                 qWarning(KSTARS_EKOS_CAPTURE) << "Detected looping in polynomial results. Falling back to llsqr.";
5498                 targetADUAlgorithm = ADU_LEAST_SQUARES;
5499             }
5500         }
5501 
5502         // If we get invalid data, let's fall back to llsq
5503         // Since polyfit can be unreliable at low counts, let's only use it at the 5th exposure
5504         // if we don't have results already.
5505         if (targetADUAlgorithm == ADU_LEAST_SQUARES)
5506         {
5507             double a = 0, b = 0;
5508             llsq(ExpRaw, ADURaw, a, b);
5509 
5510             // If we have valid results, let's calculate next exposure
5511             if (a != 0.0)
5512             {
5513                 nextExposure = (targetADU - b) / a;
5514                 // If we get invalid value, let's just proceed iteratively
5515                 if (nextExposure < 0)
5516                     nextExposure = 0;
5517             }
5518         }
5519     }
5520 
5521     if (nextExposure == 0.0)
5522     {
5523         if (value < targetADU)
5524             nextExposure = activeJob->getExposure() * 1.25;
5525         else
5526             nextExposure = activeJob->getExposure() * .75;
5527     }
5528 
5529     qCDebug(KSTARS_EKOS_CAPTURE) << "next flat exposure is" << nextExposure;
5530 
5531     return nextExposure;
5532 }
5533 
5534 //  Based on  John Burkardt LLSQ (LGPL)
llsq(QVector<double> x,QVector<double> y,double & a,double & b)5535 void Capture::llsq(QVector<double> x, QVector<double> y, double &a, double &b)
5536 {
5537     double bot;
5538     int i;
5539     double top;
5540     double xbar;
5541     double ybar;
5542     int n = x.count();
5543     //
5544     //  Special case.
5545     //
5546     if (n == 1)
5547     {
5548         a = 0.0;
5549         b = y[0];
5550         return;
5551     }
5552     //
5553     //  Average X and Y.
5554     //
5555     xbar = 0.0;
5556     ybar = 0.0;
5557     for (i = 0; i < n; i++)
5558     {
5559         xbar = xbar + x[i];
5560         ybar = ybar + y[i];
5561     }
5562     xbar = xbar / static_cast<double>(n);
5563     ybar = ybar / static_cast<double>(n);
5564     //
5565     //  Compute Beta.
5566     //
5567     top = 0.0;
5568     bot = 0.0;
5569     for (i = 0; i < n; i++)
5570     {
5571         top = top + (x[i] - xbar) * (y[i] - ybar);
5572         bot = bot + (x[i] - xbar) * (x[i] - xbar);
5573     }
5574 
5575     a = top / bot;
5576 
5577     b = ybar - a * xbar;
5578 }
5579 
setDirty()5580 void Capture::setDirty()
5581 {
5582     m_Dirty = true;
5583 }
5584 
5585 
hasCoolerControl()5586 bool Capture::hasCoolerControl()
5587 {
5588     if (currentCCD && currentCCD->hasCoolerControl())
5589         return true;
5590 
5591     return false;
5592 }
5593 
setCoolerControl(bool enable)5594 bool Capture::setCoolerControl(bool enable)
5595 {
5596     if (currentCCD && currentCCD->hasCoolerControl())
5597         return currentCCD->setCoolerControl(enable);
5598 
5599     return false;
5600 }
5601 
clearAutoFocusHFR()5602 void Capture::clearAutoFocusHFR()
5603 {
5604     // If HFR limit was set from file, we cannot override it.
5605     if (fileHFR > 0)
5606         return;
5607 
5608     limitFocusHFRN->setValue(0);
5609     //firstAutoFocus = true;
5610 }
5611 
openCalibrationDialog()5612 void Capture::openCalibrationDialog()
5613 {
5614     QDialog calibrationDialog;
5615 
5616     Ui_calibrationOptions calibrationOptions;
5617     calibrationOptions.setupUi(&calibrationDialog);
5618 
5619     if (currentTelescope)
5620     {
5621         calibrationOptions.parkMountC->setEnabled(currentTelescope->canPark());
5622         calibrationOptions.parkMountC->setChecked(preMountPark);
5623     }
5624     else
5625         calibrationOptions.parkMountC->setEnabled(false);
5626 
5627     if (currentDome)
5628     {
5629         calibrationOptions.parkDomeC->setEnabled(currentDome->canPark());
5630         calibrationOptions.parkDomeC->setChecked(preDomePark);
5631     }
5632     else
5633         calibrationOptions.parkDomeC->setEnabled(false);
5634 
5635     //connect(calibrationOptions.wallSourceC, SIGNAL(toggled(bool)), calibrationOptions.parkC, &Ekos::Capture::setDisabled(bool)));
5636 
5637     switch (flatFieldSource)
5638     {
5639         case SOURCE_MANUAL:
5640             calibrationOptions.manualSourceC->setChecked(true);
5641             break;
5642 
5643         case SOURCE_FLATCAP:
5644             calibrationOptions.flatDeviceSourceC->setChecked(true);
5645             break;
5646 
5647         case SOURCE_DARKCAP:
5648             calibrationOptions.darkDeviceSourceC->setChecked(true);
5649             break;
5650 
5651         case SOURCE_WALL:
5652             calibrationOptions.wallSourceC->setChecked(true);
5653             calibrationOptions.azBox->setText(wallCoord.az().toDMSString());
5654             calibrationOptions.altBox->setText(wallCoord.alt().toDMSString());
5655             break;
5656 
5657         case SOURCE_DAWN_DUSK:
5658             calibrationOptions.dawnDuskFlatsC->setChecked(true);
5659             break;
5660     }
5661 
5662     switch (flatFieldDuration)
5663     {
5664         case DURATION_MANUAL:
5665             calibrationOptions.manualDurationC->setChecked(true);
5666             break;
5667 
5668         case DURATION_ADU:
5669             calibrationOptions.ADUC->setChecked(true);
5670             calibrationOptions.ADUValue->setValue(static_cast<int>(std::round(targetADU)));
5671             calibrationOptions.ADUTolerance->setValue(static_cast<int>(std::round(targetADUTolerance)));
5672             break;
5673     }
5674 
5675     if (calibrationDialog.exec() == QDialog::Accepted)
5676     {
5677         if (calibrationOptions.manualSourceC->isChecked())
5678             flatFieldSource = SOURCE_MANUAL;
5679         else if (calibrationOptions.flatDeviceSourceC->isChecked())
5680             flatFieldSource = SOURCE_FLATCAP;
5681         else if (calibrationOptions.darkDeviceSourceC->isChecked())
5682             flatFieldSource = SOURCE_DARKCAP;
5683         else if (calibrationOptions.wallSourceC->isChecked())
5684         {
5685             dms wallAz, wallAlt;
5686             bool azOk = false, altOk = false;
5687 
5688             wallAz  = calibrationOptions.azBox->createDms(true, &azOk);
5689             wallAlt = calibrationOptions.altBox->createDms(true, &altOk);
5690 
5691             if (azOk && altOk)
5692             {
5693                 flatFieldSource = SOURCE_WALL;
5694                 wallCoord.setAz(wallAz);
5695                 wallCoord.setAlt(wallAlt);
5696             }
5697             else
5698             {
5699                 calibrationOptions.manualSourceC->setChecked(true);
5700                 KSNotification::error(i18n("Wall coordinates are invalid."));
5701             }
5702         }
5703         else
5704             flatFieldSource = SOURCE_DAWN_DUSK;
5705 
5706         if (calibrationOptions.manualDurationC->isChecked())
5707             flatFieldDuration = DURATION_MANUAL;
5708         else
5709         {
5710             flatFieldDuration  = DURATION_ADU;
5711             targetADU          = calibrationOptions.ADUValue->value();
5712             targetADUTolerance = calibrationOptions.ADUTolerance->value();
5713         }
5714 
5715         preMountPark = calibrationOptions.parkMountC->isChecked();
5716         preDomePark  = calibrationOptions.parkDomeC->isChecked();
5717 
5718         setDirty();
5719 
5720         Options::setCalibrationFlatSourceIndex(flatFieldSource);
5721         Options::setCalibrationFlatDurationIndex(flatFieldDuration);
5722         Options::setCalibrationWallAz(wallCoord.az().Degrees());
5723         Options::setCalibrationWallAlt(wallCoord.alt().Degrees());
5724         Options::setCalibrationADUValue(static_cast<uint>(std::round(targetADU)));
5725         Options::setCalibrationADUValueTolerance(static_cast<uint>(std::round(targetADUTolerance)));
5726     }
5727 }
5728 
5729 /**
5730  * @brief Check all tasks that might be pending before capturing may start.
5731  *
5732  * The following checks are executed:
5733  *  1. Are there any pending jobs that failed? If yes, return with IPS_ALERT.
5734  *  2. Is the scope cover open (@see checkLightFrameScopeCoverOpen()).
5735  *  3. Has pausing been initiated (@see checkPausing()).
5736  *  4. Is a meridian flip already running (@see checkMeridianFlipRunning()) or ready
5737  *     for execution (@see checkMeridianFlipReady()).
5738  *  5. Is a post meridian flip alignment running (@see checkAlignmentAfterFlip()).
5739  *  6. Is post flip guiding required or running (@see checkGuidingAfterFlip().
5740  *  7. Is the guiding deviation below the expected limit (@see setGuideDeviation(double,double)).
5741  *  8. Is dithering required or ongoing (@see checkDithering()).
5742  *  9. Is re-focusing required or ongoing (@see startFocusIfRequired()).
5743  * 10. Has guiding been resumed and needs to be restarted (@see resumeGuiding())
5744  *
5745  * If none of this is true, everything is ready and capturing may be started.
5746  *
5747  * @return IPS_OK iff no task is pending, IPS_BUSY otherwise (or IPS_ALERT if a problem occured)
5748  */
checkLightFramePendingTasks()5749 IPState Capture::checkLightFramePendingTasks()
5750 {
5751     // step 1: did one of the pending jobs fail or has the user aborted the capture?
5752     if (m_State == CAPTURE_ABORTED)
5753         return IPS_ALERT;
5754 
5755     // step 2: ensure that the scope cover is open and wait until it's open
5756     IPState coverState = checkLightFrameScopeCoverOpen();
5757     if (coverState != IPS_OK)
5758         return coverState;
5759 
5760     // step 3: check if pausing has been requested
5761     if (checkPausing() == true)
5762     {
5763         pauseFunction = &Capture::startNextExposure;
5764         return IPS_BUSY;
5765     }
5766 
5767     // step 4: check if meridian flip is already running or ready for execution
5768     if (checkMeridianFlipRunning() || checkMeridianFlipReady())
5769         return IPS_BUSY;
5770 
5771     // step 5: check if post flip alignment is running
5772     if (m_State == CAPTURE_ALIGNING || checkAlignmentAfterFlip())
5773         return IPS_BUSY;
5774 
5775     // step 6: check if post flip guiding is running
5776     // MF_NONE is set as soon as guiding is running and the guide deviation is below the limit
5777     if ((m_State == CAPTURE_CALIBRATING && m_GuideState != GUIDE_GUIDING) || checkGuidingAfterFlip())
5778         return IPS_BUSY;
5779 
5780     // step 7: in case that a meridian flip has been completed and a guide deviation limit is set, we wait
5781     //         until the guide deviation is reported to be below the limit (@see setGuideDeviation(double, double)).
5782     //         Otherwise the meridian flip is complete
5783     if (m_State == CAPTURE_CALIBRATING && meridianFlipStage == MF_GUIDING)
5784     {
5785         if (limitGuideDeviationS->isChecked() == true)
5786             return IPS_BUSY;
5787         else
5788             setMeridianFlipStage(MF_NONE);
5789     }
5790 
5791     // step 8: check if dithering is required or running
5792     if ((m_State == CAPTURE_DITHERING && m_DitheringState != IPS_OK) || checkDithering())
5793         return IPS_BUSY;
5794 
5795     // step 9: check if re-focusing is required
5796     //         Needs to be checked after dithering checks to avoid dithering in parallel
5797     //         to focusing, since @startFocusIfRequired() might change its value over time
5798     if ((m_State == CAPTURE_FOCUSING  && m_FocusState != FOCUS_COMPLETE && m_FocusState != FOCUS_ABORTED)
5799             || startFocusIfRequired())
5800         return IPS_BUSY;
5801 
5802     // step 10: resume guiding if it was suspended
5803     if (m_GuideState == GUIDE_SUSPENDED)
5804     {
5805         appendLogText(i18n("Autoguiding resumed."));
5806         emit resumeGuiding();
5807         // No need to return IPS_BUSY here, we can continue immediately.
5808         // In the case that the capturing sequence has a guiding limit,
5809         // capturing will be interrupted by setGuideDeviation().
5810     }
5811 
5812     // everything is ready for capturing light frames
5813     calibrationStage = CAL_PRECAPTURE_COMPLETE;
5814 
5815     return IPS_OK;
5816 
5817 }
5818 
checkLightFrameScopeCoverOpen()5819 IPState Capture::checkLightFrameScopeCoverOpen()
5820 {
5821     if (activeJob == nullptr)
5822     {
5823         qWarning(KSTARS_EKOS_CAPTURE) << "checkLightFrameScopeCoverOpen with null activeJob.";
5824         // Can't do anything. Don't worry about scope cover. Bigger problems.
5825         abort();
5826         return IPS_ALERT;
5827     }
5828 
5829     switch (activeJob->getFlatFieldSource())
5830     {
5831         // All these are considered MANUAL when it comes to light frames
5832         case SOURCE_MANUAL:
5833         case SOURCE_DAWN_DUSK:
5834         case SOURCE_WALL:
5835             // If telescopes were MANUALLY covered before
5836             // we need to manually uncover them.
5837             if (m_TelescopeCoveredDarkExposure || m_TelescopeCoveredFlatExposure)
5838             {
5839                 // If we already asked for confirmation and waiting for it
5840                 // let us see if the confirmation is fulfilled
5841                 // otherwise we return.
5842                 if (calibrationCheckType == CAL_CHECK_CONFIRMATION)
5843                     return IPS_BUSY;
5844 
5845                 // Otherwise, we ask user to confirm manually
5846                 calibrationCheckType = CAL_CHECK_CONFIRMATION;
5847 
5848                 // Continue
5849                 connect(KSMessageBox::Instance(), &KSMessageBox::accepted, this, [this]()
5850                 {
5851                     KSMessageBox::Instance()->disconnect(this);
5852                     m_TelescopeCoveredDarkExposure = false;
5853                     m_TelescopeCoveredFlatExposure = false;
5854                     calibrationCheckType = CAL_CHECK_TASK;
5855                 });
5856 
5857                 // Cancel
5858                 connect(KSMessageBox::Instance(), &KSMessageBox::rejected, this, [this]()
5859                 {
5860                     KSMessageBox::Instance()->disconnect(this);
5861                     calibrationCheckType = CAL_CHECK_TASK;
5862                     abort();
5863                 });
5864 
5865                 KSMessageBox::Instance()->warningContinueCancel(i18n("Remove cover from the telescope in order to continue."),
5866                         i18n("Telescope Covered"),
5867                         Options::manualCoverTimeout());
5868 
5869                 return IPS_BUSY;
5870             }
5871             break;
5872         case SOURCE_FLATCAP:
5873         case SOURCE_DARKCAP:
5874             // Account for light box only (no dust cap)
5875             if (currentLightBox && currentLightBox->isLightOn())
5876             {
5877                 lightBoxLightEnabled = false;
5878                 currentLightBox->SetLightEnabled(false);
5879                 break;
5880             }
5881 
5882             if (currentDustCap == nullptr)
5883             {
5884                 qCWarning(KSTARS_EKOS_CAPTURE) << "Skipping flat/dark cap since it is not connected.";
5885                 break;
5886             }
5887 
5888             // If dust cap HAS light and light is ON, then turn it off.
5889             if (currentDustCap->hasLight() && currentDustCap->isLightOn() == true)
5890             {
5891                 dustCapLightEnabled = false;
5892                 currentDustCap->SetLightEnabled(false);
5893             }
5894 
5895             // If cap is parked, we need to unpark it
5896             if (calibrationStage < CAL_DUSTCAP_UNPARKING && currentDustCap->isParked())
5897             {
5898                 if (currentDustCap->UnPark())
5899                 {
5900                     calibrationStage = CAL_DUSTCAP_UNPARKING;
5901                     appendLogText(i18n("Unparking dust cap..."));
5902                     return IPS_BUSY;
5903                 }
5904                 else
5905                 {
5906                     appendLogText(i18n("Unparking dust cap failed, aborting..."));
5907                     abort();
5908                     return IPS_ALERT;
5909                 }
5910             }
5911 
5912             // Wait until cap is unparked
5913             if (calibrationStage == CAL_DUSTCAP_UNPARKING)
5914             {
5915                 if (currentDustCap->isUnParked() == false)
5916                     return IPS_BUSY;
5917                 else
5918                 {
5919                     calibrationStage = CAL_DUSTCAP_UNPARKED;
5920                     appendLogText(i18n("Dust cap unparked."));
5921                 }
5922             }
5923             break;
5924     }
5925     // scope cover open (or no scope cover)
5926     return IPS_OK;
5927 }
5928 
5929 
5930 
checkDarkFramePendingTasks()5931 IPState Capture::checkDarkFramePendingTasks()
5932 {
5933     if (activeJob == nullptr)
5934     {
5935         qWarning(KSTARS_EKOS_CAPTURE) << "checkDarkFramePendingTasks with null activeJob.";
5936         abort();
5937         return IPS_ALERT;
5938     }
5939 
5940     QStringList shutterfulCCDs  = Options::shutterfulCCDs();
5941     QStringList shutterlessCCDs = Options::shutterlessCCDs();
5942     QString deviceName = currentCCD->getDeviceName();
5943 
5944     bool hasShutter   = shutterfulCCDs.contains(deviceName);
5945     bool hasNoShutter = shutterlessCCDs.contains(deviceName) || (captureISOS && captureISOS->count() > 0);
5946 
5947     // If we have no information, we ask before we proceed.
5948     if (hasShutter == false && hasNoShutter == false)
5949     {
5950         // Awaiting user input
5951         if (calibrationCheckType == CAL_CHECK_CONFIRMATION)
5952             return IPS_BUSY;
5953 
5954         connect(KSMessageBox::Instance(), &KSMessageBox::accepted, this, [this]()
5955         {
5956             KSMessageBox::Instance()->disconnect(this);
5957             QStringList shutterfulCCDs  = Options::shutterfulCCDs();
5958             QString deviceName = currentCCD->getDeviceName();
5959             shutterfulCCDs.append(deviceName);
5960             Options::setShutterfulCCDs(shutterfulCCDs);
5961             calibrationCheckType = CAL_CHECK_TASK;
5962         });
5963         connect(KSMessageBox::Instance(), &KSMessageBox::rejected, this, [this]()
5964         {
5965             KSMessageBox::Instance()->disconnect(this);
5966             QStringList shutterlessCCDs = Options::shutterlessCCDs();
5967             QString deviceName = currentCCD->getDeviceName();
5968             shutterlessCCDs.append(deviceName);
5969             Options::setShutterlessCCDs(shutterlessCCDs);
5970             calibrationCheckType = CAL_CHECK_TASK;
5971         });
5972 
5973         calibrationCheckType = CAL_CHECK_CONFIRMATION;
5974 
5975         KSMessageBox::Instance()->questionYesNo(i18n("Does %1 have a shutter?", deviceName),
5976                                                 i18n("Dark Exposure"));
5977 
5978         return IPS_BUSY;
5979     }
5980 
5981     switch (activeJob->getFlatFieldSource())
5982     {
5983         // All these are manual when it comes to dark frames
5984         case SOURCE_MANUAL:
5985         case SOURCE_DAWN_DUSK:
5986             // For cameras without a shutter, we need to ask the user to cover the telescope
5987             // if the telescope is not already covered.
5988             if (hasNoShutter && !m_TelescopeCoveredDarkExposure)
5989             {
5990                 if (calibrationCheckType == CAL_CHECK_CONFIRMATION)
5991                     return IPS_BUSY;
5992 
5993                 // Continue
5994                 connect(KSMessageBox::Instance(), &KSMessageBox::accepted, this, [this]()
5995                 {
5996                     KSMessageBox::Instance()->disconnect(this);
5997                     m_TelescopeCoveredDarkExposure = true;
5998                     m_TelescopeCoveredFlatExposure = false;
5999                     calibrationCheckType = CAL_CHECK_TASK;
6000                 });
6001 
6002                 // Cancel
6003                 connect(KSMessageBox::Instance(), &KSMessageBox::rejected, this, [this]()
6004                 {
6005                     KSMessageBox::Instance()->disconnect(this);
6006                     calibrationCheckType = CAL_CHECK_TASK;
6007                     abort();
6008                 });
6009 
6010                 calibrationCheckType = CAL_CHECK_CONFIRMATION;
6011 
6012                 KSMessageBox::Instance()->warningContinueCancel(i18n("Cover the telescope in order to take a dark exposure.")
6013                         , i18n("Dark Exposure"),
6014                         Options::manualCoverTimeout());
6015 
6016                 return IPS_BUSY;
6017             }
6018             break;
6019         case SOURCE_FLATCAP:
6020         case SOURCE_DARKCAP:
6021             // Account for light box only (no dust cap)
6022             if (currentLightBox && currentLightBox->isLightOn())
6023             {
6024                 lightBoxLightEnabled = false;
6025                 currentLightBox->SetLightEnabled(false);
6026                 break;
6027             }
6028 
6029             if (currentDustCap == nullptr)
6030             {
6031                 qCWarning(KSTARS_EKOS_CAPTURE) << "Skipping flat/dark cap since it is not connected.";
6032                 break;
6033             }
6034 
6035             // If cap is not park, park it
6036             if (calibrationStage < CAL_DUSTCAP_PARKING && currentDustCap->isParked() == false)
6037             {
6038                 if (currentDustCap->Park())
6039                 {
6040                     calibrationStage = CAL_DUSTCAP_PARKING;
6041                     appendLogText(i18n("Parking dust cap..."));
6042                     return IPS_BUSY;
6043                 }
6044                 else
6045                 {
6046                     appendLogText(i18n("Parking dust cap failed, aborting..."));
6047                     abort();
6048                     return IPS_ALERT;
6049                 }
6050             }
6051 
6052             // Wait until cap is parked
6053             if (calibrationStage == CAL_DUSTCAP_PARKING)
6054             {
6055                 if (currentDustCap->isParked() == false)
6056                     return IPS_BUSY;
6057                 else
6058                 {
6059                     calibrationStage = CAL_DUSTCAP_PARKED;
6060                     appendLogText(i18n("Dust cap parked."));
6061                 }
6062             }
6063 
6064             // Turn off light if it exists and was on.
6065             if (currentDustCap->hasLight() && currentDustCap->isLightOn() == true)
6066             {
6067                 dustCapLightEnabled = false;
6068                 currentDustCap->SetLightEnabled(false);
6069             }
6070             break;
6071 
6072         case SOURCE_WALL:
6073             if (currentTelescope)
6074             {
6075                 if (calibrationStage < CAL_SLEWING)
6076                 {
6077                     wallCoord = activeJob->getWallCoord();
6078                     wallCoord.HorizontalToEquatorial(KStarsData::Instance()->lst(),
6079                                                      KStarsData::Instance()->geo()->lat());
6080                     currentTelescope->Slew(&wallCoord);
6081                     appendLogText(i18n("Mount slewing to wall position..."));
6082                     calibrationStage = CAL_SLEWING;
6083                     return IPS_BUSY;
6084                 }
6085 
6086                 // Check if slewing is complete
6087                 if (calibrationStage == CAL_SLEWING)
6088                 {
6089                     if (currentTelescope->isSlewing() == false)
6090                     {
6091                         // Disable mount tracking if supported by the driver.
6092                         currentTelescope->setTrackEnabled(false);
6093                         calibrationStage = CAL_SLEWING_COMPLETE;
6094                         appendLogText(i18n("Slew to wall position complete."));
6095                     }
6096                     else
6097                         return IPS_BUSY;
6098                 }
6099 
6100                 if (currentLightBox && currentLightBox->isLightOn() == true)
6101                 {
6102                     lightBoxLightEnabled = false;
6103                     currentLightBox->SetLightEnabled(false);
6104                 }
6105             }
6106             break;
6107     }
6108 
6109     calibrationStage = CAL_PRECAPTURE_COMPLETE;
6110 
6111     return IPS_OK;
6112 }
6113 
checkFlatFramePendingTasks()6114 IPState Capture::checkFlatFramePendingTasks()
6115 {
6116     if (activeJob == nullptr)
6117     {
6118         qWarning(KSTARS_EKOS_CAPTURE) << "checkFlatFramePendingTasks with null activeJob.";
6119         abort();
6120         return IPS_ALERT;
6121     }
6122     switch (activeJob->getFlatFieldSource())
6123     {
6124         case SOURCE_MANUAL:
6125             // Manual mode we need to cover mount with evenly illuminated field.
6126             if (m_TelescopeCoveredFlatExposure == false)
6127             {
6128                 if (calibrationCheckType == CAL_CHECK_CONFIRMATION)
6129                     return IPS_BUSY;
6130 
6131                 calibrationCheckType = CAL_CHECK_CONFIRMATION;
6132 
6133                 // Continue
6134                 connect(KSMessageBox::Instance(), &KSMessageBox::accepted, this, [this]()
6135                 {
6136                     KSMessageBox::Instance()->disconnect(this);
6137                     m_TelescopeCoveredFlatExposure = true;
6138                     m_TelescopeCoveredDarkExposure = false;
6139                     calibrationCheckType = CAL_CHECK_TASK;
6140                 });
6141 
6142                 // Cancel
6143                 connect(KSMessageBox::Instance(), &KSMessageBox::rejected, this, [this]()
6144                 {
6145                     KSMessageBox::Instance()->disconnect(this);
6146                     calibrationCheckType = CAL_CHECK_TASK;
6147                     abort();
6148                 });
6149 
6150                 KSMessageBox::Instance()->warningContinueCancel(i18n("Cover telescope with an evenly illuminated light source."),
6151                         i18n("Flat Frame"),
6152                         Options::manualCoverTimeout());
6153 
6154                 return IPS_BUSY;
6155             }
6156             break;
6157         // Not implemented.
6158         case SOURCE_DAWN_DUSK:
6159             break;
6160         case SOURCE_FLATCAP:
6161             if (currentLightBox && currentLightBox->isLightOn() == false)
6162             {
6163                 lightBoxLightEnabled = true;
6164                 currentLightBox->SetLightEnabled(true);
6165                 break;
6166             }
6167 
6168             if (currentDustCap == nullptr)
6169             {
6170                 qCWarning(KSTARS_EKOS_CAPTURE) << "Skipping flat/dark cap since it is not connected.";
6171                 break;
6172             }
6173 
6174             // If cap is not park, park it
6175             if (calibrationStage < CAL_DUSTCAP_PARKING && currentDustCap->isParked() == false)
6176             {
6177                 if (currentDustCap->Park())
6178                 {
6179                     calibrationStage = CAL_DUSTCAP_PARKING;
6180                     appendLogText(i18n("Parking dust cap..."));
6181                     return IPS_BUSY;
6182                 }
6183                 else
6184                 {
6185                     appendLogText(i18n("Parking dust cap failed, aborting..."));
6186                     abort();
6187                     return IPS_ALERT;
6188                 }
6189             }
6190 
6191             // Wait until cap is parked
6192             if (calibrationStage == CAL_DUSTCAP_PARKING)
6193             {
6194                 if (currentDustCap->isParked() == false)
6195                     return IPS_BUSY;
6196                 else
6197                 {
6198                     calibrationStage = CAL_DUSTCAP_PARKED;
6199                     appendLogText(i18n("Dust cap parked."));
6200                 }
6201             }
6202 
6203             // If light is not on, turn it on.
6204             if (currentDustCap->hasLight() && currentDustCap->isLightOn() == false)
6205             {
6206                 dustCapLightEnabled = true;
6207                 currentDustCap->SetLightEnabled(true);
6208             }
6209             break;
6210         case SOURCE_WALL:
6211             if (currentTelescope)
6212             {
6213                 if (calibrationStage < CAL_SLEWING)
6214                 {
6215                     wallCoord = activeJob->getWallCoord();
6216                     wallCoord.HorizontalToEquatorial(KStarsData::Instance()->lst(),
6217                                                      KStarsData::Instance()->geo()->lat());
6218                     currentTelescope->Slew(&wallCoord);
6219                     appendLogText(i18n("Mount slewing to wall position..."));
6220                     calibrationStage = CAL_SLEWING;
6221                     return IPS_BUSY;
6222                 }
6223 
6224                 // Check if slewing is complete
6225                 if (calibrationStage == CAL_SLEWING)
6226                 {
6227                     if (currentTelescope->isSlewing() == false)
6228                     {
6229                         // Disable mount tracking if supported by the driver.
6230                         currentTelescope->setTrackEnabled(false);
6231                         calibrationStage = CAL_SLEWING_COMPLETE;
6232                         appendLogText(i18n("Slew to wall position complete."));
6233                     }
6234                     else
6235                         return IPS_BUSY;
6236                 }
6237 
6238                 if (currentLightBox)
6239                 {
6240                     // Check if we have a light box to turn on
6241                     if (activeJob->getFrameType() == FRAME_FLAT && currentLightBox->isLightOn() == false)
6242                     {
6243                         lightBoxLightEnabled = true;
6244                         currentLightBox->SetLightEnabled(true);
6245                     }
6246                     else if (activeJob->getFrameType() != FRAME_FLAT && currentLightBox->isLightOn() == true)
6247                     {
6248                         lightBoxLightEnabled = false;
6249                         currentLightBox->SetLightEnabled(false);
6250                     }
6251                 }
6252             }
6253             break;
6254 
6255 
6256         case SOURCE_DARKCAP:
6257             if (currentLightBox && currentLightBox->isLightOn() == false)
6258             {
6259                 lightBoxLightEnabled = true;
6260                 currentLightBox->SetLightEnabled(true);
6261                 break;
6262             }
6263 
6264             if (currentDustCap == nullptr)
6265             {
6266                 qCWarning(KSTARS_EKOS_CAPTURE) << "Skipping flat/dark cap since it is not connected.";
6267                 break;
6268             }
6269 
6270             // If cap is parked, unpark it since dark cap uses external light source.
6271             if (calibrationStage < CAL_DUSTCAP_UNPARKING && currentDustCap->isParked() == true)
6272             {
6273                 if (currentDustCap->UnPark())
6274                 {
6275                     calibrationStage = CAL_DUSTCAP_UNPARKING;
6276                     appendLogText(i18n("UnParking dust cap..."));
6277                     return IPS_BUSY;
6278                 }
6279                 else
6280                 {
6281                     appendLogText(i18n("UnParking dust cap failed, aborting..."));
6282                     abort();
6283                     return IPS_ALERT;
6284                 }
6285             }
6286 
6287             // Wait until cap is unparked
6288             if (calibrationStage == CAL_DUSTCAP_UNPARKING)
6289             {
6290                 if (currentDustCap->isUnParked() == false)
6291                     return IPS_BUSY;
6292                 else
6293                 {
6294                     calibrationStage = CAL_DUSTCAP_UNPARKED;
6295                     appendLogText(i18n("Dust cap unparked."));
6296                 }
6297             }
6298 
6299             // If light is off, turn it on.
6300             if (currentDustCap->hasLight() && currentDustCap->isLightOn() == false)
6301             {
6302                 dustCapLightEnabled = true;
6303                 currentDustCap->SetLightEnabled(true);
6304             }
6305             break;
6306     }
6307 
6308     // Check if we need to perform mount prepark
6309     if (preMountPark && currentTelescope && activeJob->getFlatFieldSource() != SOURCE_WALL)
6310     {
6311         if (calibrationStage < CAL_MOUNT_PARKING && currentTelescope->isParked() == false)
6312         {
6313             if (currentTelescope->Park())
6314             {
6315                 calibrationStage = CAL_MOUNT_PARKING;
6316                 //emit mountParking();
6317                 appendLogText(i18n("Parking mount prior to calibration frames capture..."));
6318                 return IPS_BUSY;
6319             }
6320             else
6321             {
6322                 appendLogText(i18n("Parking mount failed, aborting..."));
6323                 abort();
6324                 return IPS_ALERT;
6325             }
6326         }
6327 
6328         if (calibrationStage == CAL_MOUNT_PARKING)
6329         {
6330             // If not parked yet, check again in 1 second
6331             // Otherwise proceed to the rest of the algorithm
6332             if (currentTelescope->isParked() == false)
6333                 return IPS_BUSY;
6334             else
6335             {
6336                 calibrationStage = CAL_MOUNT_PARKED;
6337                 appendLogText(i18n("Mount parked."));
6338             }
6339         }
6340     }
6341 
6342     // Check if we need to perform dome prepark
6343     if (preDomePark && currentDome)
6344     {
6345         if (calibrationStage < CAL_DOME_PARKING && currentDome->isParked() == false)
6346         {
6347             if (currentDome->Park())
6348             {
6349                 calibrationStage = CAL_DOME_PARKING;
6350                 //emit mountParking();
6351                 appendLogText(i18n("Parking dome..."));
6352                 return IPS_BUSY;
6353             }
6354             else
6355             {
6356                 appendLogText(i18n("Parking dome failed, aborting..."));
6357                 abort();
6358                 return IPS_ALERT;
6359             }
6360         }
6361 
6362         if (calibrationStage == CAL_DOME_PARKING)
6363         {
6364             // If not parked yet, check again in 1 second
6365             // Otherwise proceed to the rest of the algorithm
6366             if (currentDome->isParked() == false)
6367                 return IPS_BUSY;
6368             else
6369             {
6370                 calibrationStage = CAL_DOME_PARKED;
6371                 appendLogText(i18n("Dome parked."));
6372             }
6373         }
6374     }
6375 
6376     // If we used AUTOFOCUS before for a specific frame (e.g. Lum)
6377     // then the absolute focus position for Lum is recorded in the filter manager
6378     // when we take flats again, we always go back to the same focus position as the light frames to ensure
6379     // near identical focus for both frames.
6380     if (activeJob->getFrameType() == FRAME_FLAT &&
6381             m_AutoFocusReady &&
6382             currentFilter != nullptr &&
6383             Options::flatSyncFocus())
6384     {
6385         if (filterManager->syncAbsoluteFocusPosition(activeJob->getTargetFilter() - 1) == false)
6386             return IPS_BUSY;
6387     }
6388 
6389     calibrationStage = CAL_PRECAPTURE_COMPLETE;
6390 
6391     return IPS_OK;
6392 
6393 }
6394 
6395 /**
6396  * @brief Execute the tasks that need to be completed before capturing may start.
6397  *
6398  * For light frames, checkLightFramePendingTasks() is called, for bias and dark frames
6399  * checkDarkFramePendingTasks() and for flat frames checkFlatFramePendingTasks().
6400  *
6401  * @return IPS_OK if all necessary tasks have been completed
6402  */
processPreCaptureCalibrationStage()6403 IPState Capture::processPreCaptureCalibrationStage()
6404 {
6405     // in some rare cases it might happen that activeJob has been cleared by a concurrent thread
6406     if (activeJob == nullptr)
6407     {
6408         qCWarning(KSTARS_EKOS_CAPTURE) << "Processing pre capture calibration without active job, state = " <<
6409                                        getCaptureStatusString(m_State);
6410         return IPS_ALERT;
6411     }
6412 
6413     // If we are currently guide and the frame is NOT a light frame, then we shopld suspend.
6414     // N.B. The guide camera could be on its own scope unaffected but it doesn't hurt to stop
6415     // guiding since it is no longer used anyway.
6416     if (activeJob->getFrameType() != FRAME_LIGHT && m_GuideState == GUIDE_GUIDING)
6417     {
6418         appendLogText(i18n("Autoguiding suspended."));
6419         emit suspendGuiding();
6420     }
6421 
6422     // Run necessary tasks for each frame type
6423     switch (activeJob->getFrameType())
6424     {
6425         case FRAME_LIGHT:
6426             return checkLightFramePendingTasks();
6427 
6428         case FRAME_BIAS:
6429         case FRAME_DARK:
6430             return checkDarkFramePendingTasks();
6431 
6432         case FRAME_FLAT:
6433             return checkFlatFramePendingTasks();
6434 
6435         case FRAME_NONE:
6436             // to appease the compiler.
6437             break;
6438     }
6439 
6440     return IPS_OK;
6441 }
6442 
processPostCaptureCalibrationStage()6443 bool Capture::processPostCaptureCalibrationStage()
6444 {
6445     if (activeJob == nullptr)
6446     {
6447         qWarning(KSTARS_EKOS_CAPTURE) << "processPostCaptureCalibrationStage with null activeJob.";
6448         abort();
6449         return false;
6450     }
6451 
6452     // If there are no more images to capture, do not bother calculating next exposure
6453     if (calibrationStage == CAL_CALIBRATION_COMPLETE)
6454         if (activeJob && activeJob->getCount() <= activeJob->getCompleted())
6455             return true;
6456 
6457     // Check if we need to do flat field slope calculation if the user specified a desired ADU value
6458     if (activeJob->getFrameType() == FRAME_FLAT && activeJob->getFlatFieldDuration() == DURATION_ADU &&
6459             activeJob->getTargetADU() > 0)
6460     {
6461         if (Options::useFITSViewer() == false)
6462         {
6463             Options::setUseFITSViewer(true);
6464             qCInfo(KSTARS_EKOS_CAPTURE) << "Enabling FITS Viewer...";
6465         }
6466 
6467         //        QSharedPointer<FITSData> image_data;
6468         //        FITSView * currentImage = targetChip->getImageView(FITS_NORMAL);
6469 
6470         if (!m_ImageData.isNull())
6471         {
6472             //image_data        = currentImage->imageData();
6473             double currentADU = m_ImageData->getADU();
6474             bool outOfRange = false, saturated = false;
6475 
6476             switch (m_ImageData->bpp())
6477             {
6478                 case 8:
6479                     if (activeJob->getTargetADU() > UINT8_MAX)
6480                         outOfRange = true;
6481                     else if (currentADU / UINT8_MAX > 0.95)
6482                         saturated = true;
6483                     break;
6484 
6485                 case 16:
6486                     if (activeJob->getTargetADU() > UINT16_MAX)
6487                         outOfRange = true;
6488                     else if (currentADU / UINT16_MAX > 0.95)
6489                         saturated = true;
6490                     break;
6491 
6492                 case 32:
6493                     if (activeJob->getTargetADU() > UINT32_MAX)
6494                         outOfRange = true;
6495                     else if (currentADU / UINT32_MAX > 0.95)
6496                         saturated = true;
6497                     break;
6498 
6499                 default:
6500                     break;
6501             }
6502 
6503             if (outOfRange)
6504             {
6505                 appendLogText(i18n("Flat calibration failed. Captured image is only %1-bit while requested ADU is %2.",
6506                                    QString::number(m_ImageData->bpp())
6507                                    , QString::number(activeJob->getTargetADU(), 'f', 2)));
6508                 abort();
6509                 return false;
6510             }
6511             else if (saturated)
6512             {
6513                 double nextExposure = activeJob->getExposure() * 0.1;
6514                 nextExposure = qBound(captureExposureN->minimum(), nextExposure, captureExposureN->maximum());
6515 
6516                 appendLogText(i18n("Current image is saturated (%1). Next exposure is %2 seconds.",
6517                                    QString::number(currentADU, 'f', 0), QString("%L1").arg(nextExposure, 0, 'f', 6)));
6518 
6519                 calibrationStage = CAL_CALIBRATION;
6520                 activeJob->setExposure(nextExposure);
6521                 activeJob->setPreview(true);
6522                 if (currentCCD->getUploadMode() != ISD::CCD::UPLOAD_CLIENT)
6523                 {
6524                     currentCCD->setUploadMode(ISD::CCD::UPLOAD_CLIENT);
6525                 }
6526                 startNextExposure();
6527                 return false;
6528             }
6529 
6530             double ADUDiff = fabs(currentADU - activeJob->getTargetADU());
6531 
6532             // If it is within tolerance range of target ADU
6533             if (ADUDiff <= targetADUTolerance)
6534             {
6535                 if (calibrationStage == CAL_CALIBRATION)
6536                 {
6537                     appendLogText(
6538                         i18n("Current ADU %1 within target ADU tolerance range.", QString::number(currentADU, 'f', 0)));
6539                     activeJob->setPreview(false);
6540                     currentCCD->setUploadMode(activeJob->getUploadMode());
6541 
6542                     // Get raw prefix
6543                     captureExposureN->setValue(activeJob->getExposure());
6544                     QString imagePrefix = activeJob->property("rawPrefix").toString();
6545                     auto placeholderPath = Ekos::PlaceholderPath();
6546                     placeholderPath.setGenerateFilenameSettings(*activeJob);
6547                     placeholderPath.constructPrefix(activeJob, imagePrefix);
6548                     activeJob->setFullPrefix(imagePrefix);
6549                     seqPrefix = imagePrefix;
6550                     currentCCD->setSeqPrefix(imagePrefix);
6551 
6552                     currentCCD->setISOMode(activeJob->isTimestampPrefixEnabled());
6553                     currentCCD->setPlaceholderPath(placeholderPath);
6554 
6555                     currentCCD->updateUploadSettings(activeJob->getRemoteDir() + activeJob->getDirectoryPostfix());
6556 
6557                     calibrationStage = CAL_CALIBRATION_COMPLETE;
6558                     startNextExposure();
6559                     return false;
6560                 }
6561 
6562                 return true;
6563             }
6564 
6565             double nextExposure = -1;
6566 
6567             // If value is saturated, try to reduce it to valid range first
6568             if (std::fabs(m_ImageData->getMax(0) - m_ImageData->getMin(0)) < 10)
6569                 nextExposure = activeJob->getExposure() * 0.5;
6570             else
6571                 nextExposure = setCurrentADU(currentADU);
6572 
6573             if (nextExposure <= 0 || std::isnan(nextExposure))
6574             {
6575                 appendLogText(
6576                     i18n("Unable to calculate optimal exposure settings, please capture the flats manually."));
6577                 //activeJob->setTargetADU(0);
6578                 //targetADU = 0;
6579                 abort();
6580                 return false;
6581             }
6582 
6583             // Limit to minimum and maximum values
6584             nextExposure = qBound(captureExposureN->minimum(), nextExposure, captureExposureN->maximum());
6585 
6586             appendLogText(i18n("Current ADU is %1 Next exposure is %2 seconds.", QString::number(currentADU, 'f', 0),
6587                                QString("%L1").arg(nextExposure, 0, 'f', 6)));
6588 
6589             calibrationStage = CAL_CALIBRATION;
6590             activeJob->setExposure(nextExposure);
6591             activeJob->setPreview(true);
6592             if (currentCCD->getUploadMode() != ISD::CCD::UPLOAD_CLIENT)
6593             {
6594                 currentCCD->setUploadMode(ISD::CCD::UPLOAD_CLIENT);
6595             }
6596 
6597             startNextExposure();
6598             return false;
6599 
6600             // Start next exposure in case ADU Slope is not calculated yet
6601             /*if (currentSlope == 0)
6602             {
6603                 startNextExposure();
6604                 return;
6605             }*/
6606         }
6607         else if (currentCCD->getUploadMode() == ISD::CCD::UPLOAD_CLIENT)
6608         {
6609             appendLogText(i18n("An empty image is received, aborting..."));
6610             abort();
6611             return false;
6612         }
6613     }
6614 
6615     calibrationStage = CAL_CALIBRATION_COMPLETE;
6616     return true;
6617 }
6618 
setNewRemoteFile(QString file)6619 void Capture::setNewRemoteFile(QString file)
6620 {
6621     appendLogText(i18n("Remote image saved to %1", file));
6622     emit newSequenceImage(file, QString());
6623 }
6624 
6625 /*
6626 void Capture::startPostFilterAutoFocus()
6627 {
6628     if (focusState >= FOCUS_PROGRESS || state == CAPTURE_FOCUSING)
6629         return;
6630 
6631     secondsLabel->setText(i18n("Focusing..."));
6632 
6633     state = CAPTURE_FOCUSING;
6634     emit newStatus(Ekos::CAPTURE_FOCUSING);
6635 
6636     appendLogText(i18n("Post filter change Autofocus..."));
6637 
6638     // Force it to always run autofocus routine
6639     emit checkFocus(0.1);
6640 }
6641 */
6642 
scriptFinished(int exitCode,QProcess::ExitStatus status)6643 void Capture::scriptFinished(int exitCode, QProcess::ExitStatus status)
6644 {
6645     Q_UNUSED(status)
6646 
6647     switch (m_CaptureScriptType)
6648     {
6649         case SCRIPT_PRE_CAPTURE:
6650             appendLogText(i18n("Pre capture script finished with code %1.", exitCode));
6651             if (activeJob && activeJob->getStatus() == SequenceJob::JOB_IDLE)
6652                 preparePreCaptureActions();
6653             else
6654                 checkNextExposure();
6655             break;
6656 
6657         case SCRIPT_POST_CAPTURE:
6658             appendLogText(i18n("Post capture script finished with code %1.", exitCode));
6659 
6660             // If we're done, proceed to completion.
6661             if (activeJob == nullptr || activeJob->getCount() <= activeJob->getCompleted())
6662             {
6663                 processJobCompletionStage1();
6664             }
6665             // Else check if meridian condition is met.
6666             else if (checkMeridianFlipReady())
6667             {
6668                 appendLogText(i18n("Processing meridian flip..."));
6669             }
6670             // Then if nothing else, just resume sequence.
6671             else
6672             {
6673                 appendLogText(i18n("Resuming sequence..."));
6674                 resumeSequence();
6675             }
6676             break;
6677 
6678         case SCRIPT_PRE_JOB:
6679             appendLogText(i18n("Pre job script finished with code %1.", exitCode));
6680             prepareActiveJobStage2();
6681             break;
6682 
6683         case SCRIPT_POST_JOB:
6684             appendLogText(i18n("Post job script finished with code %1.", exitCode));
6685             processJobCompletionStage2();
6686             break;
6687     }
6688 }
6689 
6690 
toggleVideo(bool enabled)6691 void Capture::toggleVideo(bool enabled)
6692 {
6693     if (currentCCD == nullptr)
6694         return;
6695 
6696     if (currentCCD->isBLOBEnabled() == false)
6697     {
6698         if (Options::guiderType() != Ekos::Guide::GUIDE_INTERNAL)
6699             currentCCD->setBLOBEnabled(true);
6700         else
6701         {
6702             connect(KSMessageBox::Instance(), &KSMessageBox::accepted, this, [this, enabled]()
6703             {
6704                 //QObject::disconnect(KSMessageBox::Instance(), &KSMessageBox::accepted, this, nullptr);
6705                 KSMessageBox::Instance()->disconnect(this);
6706                 currentCCD->setBLOBEnabled(true);
6707                 currentCCD->setVideoStreamEnabled(enabled);
6708             });
6709 
6710             KSMessageBox::Instance()->questionYesNo(i18n("Image transfer is disabled for this camera. Would you like to enable it?"),
6711                                                     i18n("Image Transfer"), 15);
6712 
6713             return;
6714         }
6715     }
6716 
6717     currentCCD->setVideoStreamEnabled(enabled);
6718 }
6719 
setVideoLimits(uint16_t maxBufferSize,uint16_t maxPreviewFPS)6720 bool Capture::setVideoLimits(uint16_t maxBufferSize, uint16_t maxPreviewFPS)
6721 {
6722     if (currentCCD == nullptr)
6723         return false;
6724 
6725     return currentCCD->setStreamLimits(maxBufferSize, maxPreviewFPS);
6726 }
6727 
setVideoStreamEnabled(bool enabled)6728 void Capture::setVideoStreamEnabled(bool enabled)
6729 {
6730     if (enabled)
6731     {
6732         liveVideoB->setChecked(true);
6733         liveVideoB->setIcon(QIcon::fromTheme("camera-on"));
6734         //liveVideoB->setStyleSheet("color:red;");
6735     }
6736     else
6737     {
6738         liveVideoB->setChecked(false);
6739         liveVideoB->setIcon(QIcon::fromTheme("camera-ready"));
6740         //liveVideoB->setStyleSheet(QString());
6741     }
6742 }
6743 
setMountStatus(ISD::Telescope::Status newState)6744 void Capture::setMountStatus(ISD::Telescope::Status newState)
6745 {
6746     switch (newState)
6747     {
6748         case ISD::Telescope::MOUNT_PARKING:
6749         case ISD::Telescope::MOUNT_SLEWING:
6750         case ISD::Telescope::MOUNT_MOVING:
6751             previewB->setEnabled(false);
6752             liveVideoB->setEnabled(false);
6753             // Only disable when button is "Start", and not "Stopped"
6754             // If mount is in motion, Stopped button should always be enabled to terminate
6755             // the sequence
6756             if (pi->isAnimated() == false)
6757                 startB->setEnabled(false);
6758             break;
6759 
6760         default:
6761             if (pi->isAnimated() == false)
6762             {
6763                 previewB->setEnabled(true);
6764                 if (currentCCD)
6765                     liveVideoB->setEnabled(currentCCD->hasVideoStream());
6766                 startB->setEnabled(true);
6767             }
6768 
6769             break;
6770     }
6771 }
6772 
showObserverDialog()6773 void Capture::showObserverDialog()
6774 {
6775     QList<OAL::Observer *> m_observerList;
6776     KStars::Instance()->data()->userdb()->GetAllObservers(m_observerList);
6777     QStringList observers;
6778     for (auto &o : m_observerList)
6779         observers << QString("%1 %2").arg(o->name(), o->surname());
6780 
6781     QDialog observersDialog(this);
6782     observersDialog.setWindowTitle(i18nc("@title:window", "Select Current Observer"));
6783 
6784     QLabel label(i18n("Current Observer:"));
6785 
6786     QComboBox observerCombo(&observersDialog);
6787     observerCombo.addItems(observers);
6788     observerCombo.setCurrentText(m_ObserverName);
6789     observerCombo.setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Fixed);
6790 
6791     QPushButton manageObserver(&observersDialog);
6792     manageObserver.setFixedSize(QSize(32, 32));
6793     manageObserver.setIcon(QIcon::fromTheme("document-edit"));
6794     manageObserver.setAttribute(Qt::WA_LayoutUsesWidgetRect);
6795     manageObserver.setToolTip(i18n("Manage Observers"));
6796     connect(&manageObserver, &QPushButton::clicked, this, [&]()
6797     {
6798         ObserverAdd add;
6799         add.exec();
6800 
6801         QList<OAL::Observer *> m_observerList;
6802         KStars::Instance()->data()->userdb()->GetAllObservers(m_observerList);
6803         QStringList observers;
6804         for (auto &o : m_observerList)
6805             observers << QString("%1 %2").arg(o->name(), o->surname());
6806 
6807         observerCombo.clear();
6808         observerCombo.addItems(observers);
6809         observerCombo.setCurrentText(m_ObserverName);
6810 
6811     });
6812 
6813     QHBoxLayout * layout = new QHBoxLayout;
6814     layout->addWidget(&label);
6815     layout->addWidget(&observerCombo);
6816     layout->addWidget(&manageObserver);
6817 
6818     observersDialog.setLayout(layout);
6819 
6820     observersDialog.exec();
6821 
6822     m_ObserverName = observerCombo.currentText();
6823 
6824     Options::setDefaultObserver(m_ObserverName);
6825 }
6826 
6827 
startRefocusTimer(bool forced)6828 void Capture::startRefocusTimer(bool forced)
6829 {
6830     /* If refocus is requested, only restart timer if not already running in order to keep current elapsed time since last refocus */
6831     if (limitRefocusS->isChecked())
6832     {
6833         // How much time passed since we last started the time
6834         long elapsedSecs = refocusEveryNTimer.elapsed() / 1000;
6835         // How many seconds do we wait for between focusing (60 mins ==> 3600 secs)
6836         int totalSecs   = limitRefocusN->value() * 60;
6837 
6838         if (!refocusEveryNTimer.isValid() || forced)
6839         {
6840             appendLogText(i18n("Ekos will refocus in %1 seconds.", totalSecs));
6841             refocusEveryNTimer.restart();
6842         }
6843         else if (elapsedSecs < totalSecs)
6844         {
6845             //appendLogText(i18n("Ekos will refocus in %1 seconds, last procedure was %2 seconds ago.", refocusEveryNTimer.elapsed()/1000-refocusEveryNTimer.elapsed()*60, refocusEveryNTimer.elapsed()/1000));
6846             appendLogText(i18n("Ekos will refocus in %1 seconds, last procedure was %2 seconds ago.", totalSecs - elapsedSecs,
6847                                elapsedSecs));
6848         }
6849         else
6850         {
6851             appendLogText(i18n("Ekos will refocus as soon as possible, last procedure was %1 seconds ago.", elapsedSecs));
6852         }
6853     }
6854 }
6855 
getRefocusEveryNTimerElapsedSec()6856 int Capture::getRefocusEveryNTimerElapsedSec()
6857 {
6858     /* If timer isn't valid, consider there is no focus to be done, that is, that focus was just done */
6859     return refocusEveryNTimer.isValid() ? static_cast<int>(refocusEveryNTimer.elapsed() / 1000) : 0;
6860 }
6861 
setAlignResults(double orientation,double ra,double de,double pixscale)6862 void Capture::setAlignResults(double orientation, double ra, double de, double pixscale)
6863 {
6864     Q_UNUSED(orientation)
6865     Q_UNUSED(ra)
6866     Q_UNUSED(de)
6867     Q_UNUSED(pixscale)
6868 
6869     if (currentRotator == nullptr)
6870         return;
6871 
6872     rotatorSettings->refresh();
6873 }
6874 
setFilterManager(const QSharedPointer<FilterManager> & manager)6875 void Capture::setFilterManager(const QSharedPointer<FilterManager> &manager)
6876 {
6877     filterManager = manager;
6878     connect(filterManagerB, &QPushButton::clicked, [this]()
6879     {
6880         filterManager->show();
6881         filterManager->raise();
6882     });
6883 
6884     connect(filterManager.data(), &FilterManager::ready, [this]()
6885     {
6886         m_CurrentFilterPosition = filterManager->getFilterPosition();
6887         // Due to race condition,
6888         m_FocusState = FOCUS_IDLE;
6889         if (activeJob)
6890             activeJob->setCurrentFilter(m_CurrentFilterPosition);
6891 
6892     }
6893            );
6894 
6895     connect(filterManager.data(), &FilterManager::failed, [this]()
6896     {
6897         if (activeJob)
6898         {
6899             appendLogText(i18n("Filter operation failed."));
6900             abort();
6901         }
6902     }
6903            );
6904 
6905     connect(filterManager.data(), &FilterManager::newStatus, [this](Ekos::FilterState filterState)
6906     {
6907         if (filterState != m_FilterManagerState)
6908             qCDebug(KSTARS_EKOS_CAPTURE) << "Focus State changed from" << Ekos::getFilterStatusString(
6909                                              m_FilterManagerState) << "to" << Ekos::getFilterStatusString(filterState);
6910         m_FilterManagerState = filterState;
6911         if (m_State == CAPTURE_CHANGING_FILTER)
6912         {
6913             secondsLabel->setText(Ekos::getFilterStatusString(filterState));
6914             switch (filterState)
6915             {
6916                 case FILTER_OFFSET:
6917                     appendLogText(i18n("Changing focus offset by %1 steps...", filterManager->getTargetFilterOffset()));
6918                     break;
6919 
6920                 case FILTER_CHANGE:
6921                     appendLogText(i18n("Changing filter to %1...", captureFilterS->itemText(filterManager->getTargetFilterPosition() - 1)));
6922                     break;
6923 
6924                 case FILTER_AUTOFOCUS:
6925                     appendLogText(i18n("Auto focus on filter change..."));
6926                     clearAutoFocusHFR();
6927                     break;
6928 
6929                 default:
6930                     break;
6931             }
6932         }
6933     });
6934 
6935     connect(filterManager.data(), &FilterManager::labelsChanged, this, [this]()
6936     {
6937         captureFilterS->clear();
6938         captureFilterS->addItems(filterManager->getFilterLabels());
6939         m_CurrentFilterPosition = filterManager->getFilterPosition();
6940         captureFilterS->setCurrentIndex(m_CurrentFilterPosition - 1);
6941     });
6942     connect(filterManager.data(), &FilterManager::positionChanged, this, [this]()
6943     {
6944         m_CurrentFilterPosition = filterManager->getFilterPosition();
6945         captureFilterS->setCurrentIndex(m_CurrentFilterPosition - 1);
6946     });
6947 }
6948 
addDSLRInfo(const QString & model,uint32_t maxW,uint32_t maxH,double pixelW,double pixelH)6949 void Capture::addDSLRInfo(const QString &model, uint32_t maxW, uint32_t maxH, double pixelW, double pixelH)
6950 {
6951     // Check if model already exists
6952     auto pos = std::find_if(DSLRInfos.begin(), DSLRInfos.end(), [model](QMap<QString, QVariant> &oneDSLRInfo)
6953     {
6954         return (oneDSLRInfo["Model"] == model);
6955     });
6956 
6957     if (pos != DSLRInfos.end())
6958     {
6959         KStarsData::Instance()->userdb()->DeleteDSLRInfo(model);
6960         DSLRInfos.removeOne(*pos);
6961     }
6962 
6963     QMap<QString, QVariant> oneDSLRInfo;
6964     oneDSLRInfo["Model"] = model;
6965     oneDSLRInfo["Width"] = maxW;
6966     oneDSLRInfo["Height"] = maxH;
6967     oneDSLRInfo["PixelW"] = pixelW;
6968     oneDSLRInfo["PixelH"] = pixelH;
6969 
6970     KStarsData::Instance()->userdb()->AddDSLRInfo(oneDSLRInfo);
6971     KStarsData::Instance()->userdb()->GetAllDSLRInfos(DSLRInfos);
6972 
6973     updateFrameProperties();
6974     resetFrame();
6975     syncDSLRToTargetChip(model);
6976 
6977     // In case the dialog was opened, let's close it
6978     if (dslrInfoDialog)
6979         dslrInfoDialog.reset();
6980 }
6981 
isModelinDSLRInfo(const QString & model)6982 bool Capture::isModelinDSLRInfo(const QString &model)
6983 {
6984     auto pos = std::find_if(DSLRInfos.begin(), DSLRInfos.end(), [model](QMap<QString, QVariant> &oneDSLRInfo)
6985     {
6986         return (oneDSLRInfo["Model"] == model);
6987     });
6988 
6989     return (pos != DSLRInfos.end());
6990 }
6991 
cullToDSLRLimits()6992 void Capture::cullToDSLRLimits()
6993 {
6994     QString model(currentCCD->getDeviceName());
6995 
6996     // Check if model already exists
6997     auto pos = std::find_if(DSLRInfos.begin(), DSLRInfos.end(), [model](QMap<QString, QVariant> &oneDSLRInfo)
6998     {
6999         return (oneDSLRInfo["Model"] == model);
7000     });
7001 
7002     if (pos != DSLRInfos.end())
7003     {
7004         if (captureFrameWN->maximum() == 0 || captureFrameWN->maximum() > (*pos)["Width"].toInt())
7005         {
7006             captureFrameWN->setValue((*pos)["Width"].toInt());
7007             captureFrameWN->setMaximum((*pos)["Width"].toInt());
7008         }
7009 
7010         if (captureFrameHN->maximum() == 0 || captureFrameHN->maximum() > (*pos)["Height"].toInt())
7011         {
7012             captureFrameHN->setValue((*pos)["Height"].toInt());
7013             captureFrameHN->setMaximum((*pos)["Height"].toInt());
7014         }
7015     }
7016 }
7017 
setCapturedFramesMap(const QString & signature,int count)7018 void Capture::setCapturedFramesMap(const QString &signature, int count)
7019 {
7020     capturedFramesMap[signature] = static_cast<ushort>(count);
7021     qCDebug(KSTARS_EKOS_CAPTURE) <<
7022                                  QString("Client module indicates that storage for '%1' has already %2 captures processed.").arg(signature).arg(count);
7023     // Scheduler's captured frame map overrides the progress option of the Capture module
7024     ignoreJobProgress = false;
7025 }
7026 
setPresetSettings(const QJsonObject & settings)7027 void Capture::setPresetSettings(const QJsonObject &settings)
7028 {
7029     static bool init = false;
7030 
7031     // FIXME: QComboBox signal "activated" does not trigger when setting text programmatically.
7032     const QString targetCamera = settings["camera"].toString(cameraS->currentText());
7033     const QString targetFW = settings["fw"].toString(filterWheelS->currentText());
7034     const QString targetFilter = settings["filter"].toString(captureFilterS->currentText());
7035 
7036     if (cameraS->currentText() != targetCamera || init == false)
7037     {
7038         const int index = cameraS->findText(targetCamera);
7039         cameraS->setCurrentIndex(index);
7040         checkCCD(index);
7041     }
7042 
7043     if ((!targetFW.isEmpty() && filterWheelS->currentText() != targetFW) || init == false)
7044     {
7045         const int index = filterWheelS->findText(targetFW);
7046         filterWheelS->setCurrentIndex(index);
7047         checkFilter(index);
7048     }
7049 
7050     if (!targetFilter.isEmpty() && captureFilterS->currentText() != targetFilter)
7051     {
7052         captureFilterS->setCurrentIndex(captureFilterS->findText(targetFilter));
7053     }
7054 
7055     captureExposureN->setValue(settings["exp"].toDouble(1));
7056 
7057     int bin = settings["bin"].toInt(1);
7058     setBinning(bin, bin);
7059 
7060     double temperature = settings["temperature"].toDouble(INVALID_VALUE);
7061     if (temperature > INVALID_VALUE && currentCCD && currentCCD->hasCoolerControl())
7062     {
7063         setForceTemperature(true);
7064         setTargetTemperature(temperature);
7065     }
7066     else
7067         setForceTemperature(false);
7068 
7069     double gain = settings["gain"].toDouble(GainSpinSpecialValue);
7070     if (currentCCD && currentCCD->hasGain())
7071     {
7072         if (gain == GainSpinSpecialValue)
7073             captureGainN->setValue(GainSpinSpecialValue);
7074         else
7075             setGain(gain);
7076     }
7077 
7078     double offset = settings["offset"].toDouble(OffsetSpinSpecialValue);
7079     if (currentCCD && currentCCD->hasOffset())
7080     {
7081         if (offset == OffsetSpinSpecialValue)
7082             captureOffsetN->setValue(OffsetSpinSpecialValue);
7083         else
7084             setOffset(offset);
7085     }
7086 
7087     int format = settings["format"].toInt(-1);
7088     if (format >= 0)
7089     {
7090         captureFormatS->setCurrentIndex(format);
7091     }
7092 
7093     captureTypeS->setCurrentIndex(qMax(0, settings["frameType"].toInt(0)));
7094 
7095     // ISO
7096     int isoIndex = settings["iso"].toInt(-1);
7097     if (isoIndex >= 0)
7098         setISO(isoIndex);
7099 
7100     bool dark = settings["dark"].toBool(darkB->isChecked());
7101     if (dark != darkB->isChecked())
7102         darkB->setChecked(dark);
7103 
7104     init = true;
7105 }
7106 
setFileSettings(const QJsonObject & settings)7107 void Capture::setFileSettings(const QJsonObject &settings)
7108 {
7109     const QString prefix = settings["prefix"].toString(filePrefixT->text());
7110     //const QString script = settings["script"].toString(fileScriptT->text());
7111     const QString directory = settings["directory"].toString(fileDirectoryT->text());
7112     const bool filter = settings["filter"].toBool(fileFilterS->isChecked());
7113     const bool duration = settings["duration"].toBool(fileDurationS->isChecked());
7114     const bool ts = settings["ts"].toBool(fileTimestampS->isChecked());
7115     const int upload = settings["upload"].toInt(fileUploadModeS->currentIndex());
7116     const QString remote = settings["remote"].toString(fileRemoteDirT->text());
7117 
7118     filePrefixT->setText(prefix);
7119     //    fileScriptT->setText(script);
7120     fileDirectoryT->setText(directory);
7121     fileFilterS->setChecked(filter);
7122     fileDurationS->setChecked(duration);
7123     fileTimestampS->setChecked(ts);
7124     fileUploadModeS->setCurrentIndex(upload);
7125     fileRemoteDirT->setText(remote);
7126 }
7127 
getFileSettings()7128 QJsonObject Capture::getFileSettings()
7129 {
7130     QJsonObject settings =
7131     {
7132         {"prefix", filePrefixT->text()},
7133         //        {"script", fileScriptT->text()},
7134         {"directory", fileDirectoryT->text()},
7135         {"filter", fileFilterS->isChecked()},
7136         {"duration", fileDurationS->isChecked()},
7137         {"ts", fileTimestampS->isChecked()},
7138         {"upload", fileUploadModeS->currentIndex()},
7139         {"remote", fileRemoteDirT->text()}
7140     };
7141 
7142     return settings;
7143 }
7144 
setCalibrationSettings(const QJsonObject & settings)7145 void Capture::setCalibrationSettings(const QJsonObject &settings)
7146 {
7147     const int source = settings["source"].toInt(flatFieldSource);
7148     const int duration = settings["duration"].toInt(flatFieldDuration);
7149     const double az = settings["az"].toDouble(wallCoord.az().Degrees());
7150     const double al = settings["al"].toDouble(wallCoord.alt().Degrees());
7151     const int adu = settings["adu"].toInt(static_cast<int>(std::round(targetADU)));
7152     const int tolerance = settings["tolerance"].toInt(static_cast<int>(std::round(targetADUTolerance)));
7153     const bool parkMount = settings["parkMount"].toBool(preMountPark);
7154     const bool parkDome = settings["parkDome"].toBool(preDomePark);
7155 
7156     flatFieldSource = static_cast<FlatFieldSource>(source);
7157     flatFieldDuration = static_cast<FlatFieldDuration>(duration);
7158     wallCoord.setAz(az);
7159     wallCoord.setAlt(al);
7160     targetADU = adu;
7161     targetADUTolerance = tolerance;
7162     preMountPark = parkMount;
7163     preDomePark = parkDome;
7164 }
7165 
getCalibrationSettings()7166 QJsonObject Capture::getCalibrationSettings()
7167 {
7168     QJsonObject settings =
7169     {
7170         {"source", flatFieldSource},
7171         {"duration", flatFieldDuration},
7172         {"az", wallCoord.az().Degrees()},
7173         {"al", wallCoord.alt().Degrees()},
7174         {"adu", targetADU},
7175         {"tolerance", targetADUTolerance},
7176         {"parkMount", preMountPark},
7177         {"parkDome", preDomePark},
7178     };
7179 
7180     return settings;
7181 }
7182 
setLimitSettings(const QJsonObject & settings)7183 void Capture::setLimitSettings(const QJsonObject &settings)
7184 {
7185     const bool deviationCheck = settings["deviationCheck"].toBool(limitGuideDeviationS->isChecked());
7186     const double deviationValue = settings["deviationValue"].toDouble(limitGuideDeviationN->value());
7187     const bool focusHFRCheck = settings["focusHFRCheck"].toBool(limitFocusHFRS->isChecked());
7188     const double focusHFRValue = settings["focusHFRValue"].toDouble(limitFocusHFRN->value());
7189     const bool focusDeltaTCheck = settings["focusDeltaTCheck"].toBool(limitFocusDeltaTS->isChecked());
7190     const double focusDeltaTValue = settings["focusDeltaTValue"].toDouble(limitFocusDeltaTN->value());
7191     const bool refocusNCheck = settings["refocusNCheck"].toBool(limitRefocusS->isChecked());
7192     const int refocusNValue = settings["refocusNValue"].toInt(limitRefocusN->value());
7193 
7194     if (deviationCheck)
7195     {
7196         limitGuideDeviationS->setChecked(true);
7197         limitGuideDeviationN->setValue(deviationValue);
7198     }
7199     else
7200         limitGuideDeviationS->setChecked(false);
7201 
7202     if (focusHFRCheck)
7203     {
7204         limitFocusHFRS->setChecked(true);
7205         limitFocusHFRN->setValue(focusHFRValue);
7206     }
7207     else
7208         limitFocusHFRS->setChecked(false);
7209 
7210     if (focusDeltaTCheck)
7211     {
7212         limitFocusDeltaTS->setChecked(true);
7213         limitFocusDeltaTN->setValue(focusDeltaTValue);
7214     }
7215     else
7216         limitFocusDeltaTS->setChecked(false);
7217 
7218     if (refocusNCheck)
7219     {
7220         limitRefocusS->setChecked(true);
7221         limitRefocusN->setValue(refocusNValue);
7222     }
7223     else
7224         limitRefocusS->setChecked(false);
7225 }
7226 
getLimitSettings()7227 QJsonObject Capture::getLimitSettings()
7228 {
7229     QJsonObject settings =
7230     {
7231         {"deviationCheck", limitGuideDeviationS->isChecked()},
7232         {"deviationValue", limitGuideDeviationN->value()},
7233         {"focusHFRCheck", limitFocusHFRS->isChecked()},
7234         {"focusHFRValue", limitFocusHFRN->value()},
7235         {"focusDeltaTCheck", limitFocusDeltaTS->isChecked()},
7236         {"focusDeltaTValue", limitFocusDeltaTN->value()},
7237         {"refocusNCheck", limitRefocusS->isChecked()},
7238         {"refocusNValue", limitRefocusN->value()},
7239     };
7240 
7241     return settings;
7242 }
7243 
clearCameraConfiguration()7244 void Capture::clearCameraConfiguration()
7245 {
7246     //if (!Options::autonomousMode() && KMessageBox::questionYesNo(nullptr, i18n("Reset %1 configuration to default?", currentCCD->getDeviceName()), i18n("Confirmation")) == KMessageBox::No)
7247     //    return;
7248 
7249     connect(KSMessageBox::Instance(), &KSMessageBox::accepted, this, [this]()
7250     {
7251         //QObject::disconnect(KSMessageBox::Instance(), &KSMessageBox::accepted, this, nullptr);
7252         KSMessageBox::Instance()->disconnect(this);
7253         currentCCD->setConfig(PURGE_CONFIG);
7254         KStarsData::Instance()->userdb()->DeleteDSLRInfo(currentCCD->getDeviceName());
7255 
7256         QStringList shutterfulCCDs  = Options::shutterfulCCDs();
7257         QStringList shutterlessCCDs = Options::shutterlessCCDs();
7258 
7259         // Remove camera from shutterful and shutterless CCDs
7260         if (shutterfulCCDs.contains(currentCCD->getDeviceName()))
7261         {
7262             shutterfulCCDs.removeOne(currentCCD->getDeviceName());
7263             Options::setShutterfulCCDs(shutterfulCCDs);
7264         }
7265         if (shutterlessCCDs.contains(currentCCD->getDeviceName()))
7266         {
7267             shutterlessCCDs.removeOne(currentCCD->getDeviceName());
7268             Options::setShutterlessCCDs(shutterlessCCDs);
7269         }
7270 
7271         // For DSLRs, immediately ask them to enter the values again.
7272         if (captureISOS && captureISOS->count() > 0)
7273         {
7274             createDSLRDialog();
7275         }
7276     });
7277 
7278     KSMessageBox::Instance()->questionYesNo( i18n("Reset %1 configuration to default?", currentCCD->getDeviceName()),
7279             i18n("Confirmation"), 30);
7280 }
7281 
setCoolerToggled(bool enabled)7282 void Capture::setCoolerToggled(bool enabled)
7283 {
7284     coolerOnB->blockSignals(true);
7285     coolerOnB->setChecked(enabled);
7286     coolerOnB->blockSignals(false);
7287 
7288     coolerOffB->blockSignals(true);
7289     coolerOffB->setChecked(!enabled);
7290     coolerOffB->blockSignals(false);
7291 
7292     appendLogText(enabled ? i18n("Cooler is on") : i18n("Cooler is off"));
7293 }
7294 
processCaptureTimeout()7295 void Capture::processCaptureTimeout()
7296 {
7297     m_CaptureTimeoutCounter++;
7298 
7299     if (m_DeviceRestartCounter >= 3)
7300     {
7301         m_CaptureTimeoutCounter = 0;
7302         m_DeviceRestartCounter = 0;
7303         appendLogText(i18n("Exposure timeout. Aborting..."));
7304         abort();
7305         return;
7306     }
7307 
7308     if (m_CaptureTimeoutCounter > 1)
7309     {
7310         QString camera = currentCCD->getDeviceName();
7311         QString fw = currentFilter ? currentFilter->getDeviceName() : "";
7312         emit driverTimedout(camera);
7313         QTimer::singleShot(5000, this, [ &, camera, fw]()
7314         {
7315             m_DeviceRestartCounter++;
7316             reconnectDriver(camera, fw);
7317         });
7318         return;
7319     }
7320     else
7321     {
7322         // Double check that currentCCD is valid in case it was reset due to driver restart.
7323         if (currentCCD)
7324         {
7325             appendLogText(i18n("Exposure timeout. Restarting exposure..."));
7326             currentCCD->setTransformFormat(ISD::CCD::FORMAT_FITS);
7327             ISD::CCDChip *targetChip = currentCCD->getChip(useGuideHead ? ISD::CCDChip::GUIDE_CCD : ISD::CCDChip::PRIMARY_CCD);
7328             targetChip->abortExposure();
7329             targetChip->capture(captureExposureN->value());
7330             captureTimeout.start(static_cast<int>(captureExposureN->value() * 1000 + CAPTURE_TIMEOUT_THRESHOLD));
7331         }
7332         else
7333         {
7334             qCDebug(KSTARS_EKOS_CAPTURE) << "Unable to restart exposure as camera is missing, trying again in 5 seconds...";
7335             QTimer::singleShot(5000, this, &Capture::processCaptureTimeout);
7336         }
7337     }
7338 }
7339 
7340 //void Capture::setGeneratedPreviewFITS(const QString &previewFITS)
7341 //{
7342 //    m_GeneratedPreviewFITS = previewFITS;
7343 //}
7344 
createDSLRDialog()7345 void Capture::createDSLRDialog()
7346 {
7347     dslrInfoDialog.reset(new DSLRInfo(this, currentCCD));
7348 
7349     connect(dslrInfoDialog.get(), &DSLRInfo::infoChanged, [this]()
7350     {
7351         addDSLRInfo(QString(currentCCD->getDeviceName()),
7352                     dslrInfoDialog->sensorMaxWidth,
7353                     dslrInfoDialog->sensorMaxHeight,
7354                     dslrInfoDialog->sensorPixelW,
7355                     dslrInfoDialog->sensorPixelH);
7356     });
7357 
7358     dslrInfoDialog->show();
7359 
7360     emit dslrInfoRequested(currentCCD->getDeviceName());
7361 }
7362 
removeDevice(ISD::GDInterface * device)7363 void Capture::removeDevice(ISD::GDInterface *device)
7364 {
7365     device->disconnect(this);
7366     if (currentTelescope && currentTelescope->getDeviceName() == device->getDeviceName())
7367     {
7368         currentTelescope = nullptr;
7369     }
7370     else if (currentDome && currentDome->getDeviceName() == device->getDeviceName())
7371     {
7372         currentDome = nullptr;
7373     }
7374     else if (currentRotator && currentRotator->getDeviceName() == device->getDeviceName())
7375     {
7376         currentRotator = nullptr;
7377         rotatorB->setEnabled(false);
7378     }
7379 
7380     if (CCDs.contains(static_cast<ISD::CCD *>(device)))
7381     {
7382         ISD::CCD *oneCCD = static_cast<ISD::CCD *>(device);
7383         CCDs.removeAll(oneCCD);
7384         cameraS->removeItem(cameraS->findText(device->getDeviceName()));
7385         cameraS->removeItem(cameraS->findText(device->getDeviceName() + QString(" Guider")));
7386 
7387         DarkLibrary::Instance()->removeCamera(oneCCD);
7388 
7389         if (CCDs.empty())
7390         {
7391             currentCCD = nullptr;
7392             cameraS->setCurrentIndex(-1);
7393         }
7394         else
7395             cameraS->setCurrentIndex(0);
7396 
7397         //checkCCD();
7398         QTimer::singleShot(1000, this, [this]()
7399         {
7400             checkCCD();
7401         });
7402     }
7403 
7404     if (Filters.contains(static_cast<ISD::Filter *>(device)))
7405     {
7406         Filters.removeOne(static_cast<ISD::Filter *>(device));
7407         filterManager->removeDevice(device);
7408         filterWheelS->removeItem(filterWheelS->findText(device->getDeviceName()));
7409         if (Filters.empty())
7410         {
7411             currentFilter = nullptr;
7412             filterWheelS->setCurrentIndex(-1);
7413         }
7414         else
7415             filterWheelS->setCurrentIndex(0);
7416 
7417         //checkFilter();
7418         QTimer::singleShot(1000, this, [this]()
7419         {
7420             checkFilter();
7421         });
7422     }
7423 }
7424 
setGain(double value)7425 void Capture::setGain(double value)
7426 {
7427     QMap<QString, QMap<QString, double> > customProps = customPropertiesDialog->getCustomProperties();
7428 
7429     // Gain is manifested in two forms
7430     // Property CCD_GAIN and
7431     // Part of CCD_CONTROLS properties.
7432     // Therefore, we have to find what the currently camera supports first.
7433     if (currentCCD->getProperty("CCD_GAIN"))
7434     {
7435         QMap<QString, double> ccdGain;
7436         ccdGain["GAIN"] = value;
7437         customProps["CCD_GAIN"] = ccdGain;
7438     }
7439     else if (currentCCD->getProperty("CCD_CONTROLS"))
7440     {
7441         QMap<QString, double> ccdGain = customProps["CCD_CONTROLS"];
7442         ccdGain["Gain"] = value;
7443         customProps["CCD_CONTROLS"] = ccdGain;
7444     }
7445 
7446     customPropertiesDialog->setCustomProperties(customProps);
7447 }
7448 
getGain()7449 double Capture::getGain()
7450 {
7451     QMap<QString, QMap<QString, double> > customProps = customPropertiesDialog->getCustomProperties();
7452 
7453     // Gain is manifested in two forms
7454     // Property CCD_GAIN and
7455     // Part of CCD_CONTROLS properties.
7456     // Therefore, we have to find what the currently camera supports first.
7457     if (currentCCD->getProperty("CCD_GAIN"))
7458     {
7459         return customProps["CCD_GAIN"].value("GAIN", -1);
7460     }
7461     else if (currentCCD->getProperty("CCD_CONTROLS"))
7462     {
7463         return customProps["CCD_CONTROLS"].value("Gain", -1);
7464     }
7465 
7466     return -1;
7467 }
7468 
setOffset(double value)7469 void Capture::setOffset(double value)
7470 {
7471     QMap<QString, QMap<QString, double> > customProps = customPropertiesDialog->getCustomProperties();
7472 
7473     // Offset is manifested in two forms
7474     // Property CCD_OFFSET and
7475     // Part of CCD_CONTROLS properties.
7476     // Therefore, we have to find what the currently camera supports first.
7477     if (currentCCD->getProperty("CCD_OFFSET"))
7478     {
7479         QMap<QString, double> ccdOffset;
7480         ccdOffset["OFFSET"] = value;
7481         customProps["CCD_OFFSET"] = ccdOffset;
7482     }
7483     else if (currentCCD->getProperty("CCD_CONTROLS"))
7484     {
7485         QMap<QString, double> ccdOffset = customProps["CCD_CONTROLS"];
7486         ccdOffset["Offset"] = value;
7487         customProps["CCD_CONTROLS"] = ccdOffset;
7488     }
7489 
7490     customPropertiesDialog->setCustomProperties(customProps);
7491 }
7492 
getOffset()7493 double Capture::getOffset()
7494 {
7495     QMap<QString, QMap<QString, double> > customProps = customPropertiesDialog->getCustomProperties();
7496 
7497     // Gain is manifested in two forms
7498     // Property CCD_GAIN and
7499     // Part of CCD_CONTROLS properties.
7500     // Therefore, we have to find what the currently camera supports first.
7501     if (currentCCD->getProperty("CCD_OFFSET"))
7502     {
7503         return customProps["CCD_OFFSET"].value("OFFSET", -1);
7504     }
7505     else if (currentCCD->getProperty("CCD_CONTROLS"))
7506     {
7507         return customProps["CCD_CONTROLS"].value("Offset", -1);
7508     }
7509 
7510     return -1;
7511 }
7512 
getEstimatedDownloadTime()7513 double Capture::getEstimatedDownloadTime()
7514 {
7515     double total = 0;
7516     foreach(double dlTime, downloadTimes)
7517         total += dlTime;
7518     if(downloadTimes.count() == 0)
7519         return 0;
7520     else
7521         return total / downloadTimes.count();
7522 }
7523 
reconnectDriver(const QString & camera,const QString & filterWheel)7524 void Capture::reconnectDriver(const QString &camera, const QString &filterWheel)
7525 {
7526     for (auto &oneCamera : CCDs)
7527     {
7528         if (oneCamera->getDeviceName() == camera)
7529         {
7530             // Set camera again to the one we restarted
7531             cameraS->setCurrentIndex(cameraS->findText(camera));
7532             filterWheelS->setCurrentIndex(filterWheelS->findText(filterWheel));
7533             checkCCD();
7534 
7535             // restart capture
7536             m_CaptureTimeoutCounter = 0;
7537 
7538             if (activeJob)
7539             {
7540                 activeJob->setActiveChip(targetChip);
7541                 activeJob->setActiveCCD(currentCCD);
7542                 activeJob->setActiveFilter(currentFilter);
7543                 captureImage();
7544             }
7545 
7546             return;
7547         }
7548     }
7549 
7550     QTimer::singleShot(5000, this, [ &, camera, filterWheel]()
7551     {
7552         reconnectDriver(camera, filterWheel);
7553     });
7554 }
7555 
isGuidingOn()7556 bool Capture::isGuidingOn()
7557 {
7558     // In case we are doing non guiding dither, then we are not performing autoguiding.
7559     if (Options::ditherNoGuiding())
7560         return false;
7561 
7562     return (m_GuideState == GUIDE_GUIDING ||
7563             m_GuideState == GUIDE_CALIBRATING ||
7564             m_GuideState == GUIDE_CALIBRATION_SUCESS ||
7565             m_GuideState == GUIDE_REACQUIRE ||
7566             m_GuideState == GUIDE_DITHERING ||
7567             m_GuideState == GUIDE_DITHERING_SUCCESS ||
7568             m_GuideState == GUIDE_DITHERING_ERROR ||
7569             m_GuideState == GUIDE_DITHERING_SETTLE ||
7570             m_GuideState == GUIDE_SUSPENDED
7571            );
7572 }
7573 
isActivelyGuiding()7574 bool Capture::isActivelyGuiding()
7575 {
7576     return isGuidingOn() && (m_GuideState == GUIDE_GUIDING);
7577 }
7578 
MFStageString(MFStage stage)7579 QString Capture::MFStageString(MFStage stage)
7580 {
7581     switch(stage)
7582     {
7583         case MF_NONE:
7584             return "MF_NONE";
7585         case MF_REQUESTED:
7586             return "MF_REQUESTED";
7587         case MF_READY:
7588             return "MF_READY";
7589         case MF_INITIATED:
7590             return "MF_INITIATED";
7591         case MF_FLIPPING:
7592             return "MF_FLIPPING";
7593         case MF_SLEWING:
7594             return "MF_SLEWING";
7595         case MF_COMPLETED:
7596             return "MF_COMPLETED";
7597         case MF_ALIGNING:
7598             return "MF_ALIGNING";
7599         case MF_GUIDING:
7600             return "MF_GUIDING";
7601     }
7602     return "MFStage unknown.";
7603 }
7604 
syncDSLRToTargetChip(const QString & model)7605 void Capture::syncDSLRToTargetChip(const QString &model)
7606 {
7607     auto pos = std::find_if(DSLRInfos.begin(), DSLRInfos.end(), [model](const QMap<QString, QVariant> &oneDSLRInfo)
7608     {
7609         return (oneDSLRInfo["Model"] == model);
7610     });
7611 
7612     // Sync Pixel Size
7613     if (pos != DSLRInfos.end())
7614     {
7615         auto camera = *pos;
7616         targetChip->setImageInfo(camera["Width"].toInt(),
7617                                  camera["Height"].toInt(),
7618                                  camera["PixelW"].toDouble(),
7619                                  camera["PixelH"].toDouble(),
7620                                  8);
7621     }
7622 }
7623 
editFilterName()7624 void Capture::editFilterName()
7625 {
7626     if (!currentFilter || m_CurrentFilterPosition < 1)
7627         return;
7628 
7629     QStringList labels = filterManager->getFilterLabels();
7630     QDialog filterDialog;
7631 
7632     QFormLayout *formLayout = new QFormLayout(&filterDialog);
7633     QVector<QLineEdit *> newLabelEdits;
7634 
7635     for (uint8_t i = 0; i < labels.count(); i++)
7636     {
7637         QLabel *existingLabel = new QLabel(QString("%1. <b>%2</b>").arg(i + 1).arg(labels[i]), &filterDialog);
7638         QLineEdit *newLabel = new QLineEdit(labels[i], &filterDialog);
7639         newLabelEdits.append(newLabel);
7640         formLayout->addRow(existingLabel, newLabel);
7641     }
7642 
7643     filterDialog.setWindowTitle(currentFilter->getDeviceName());
7644     filterDialog.setLayout(formLayout);
7645     QDialogButtonBox *buttonBox = new QDialogButtonBox(QDialogButtonBox::Ok | QDialogButtonBox::Cancel, &filterDialog);
7646     connect(buttonBox, &QDialogButtonBox::accepted, &filterDialog, &QDialog::accept);
7647     connect(buttonBox, &QDialogButtonBox::rejected, &filterDialog, &QDialog::reject);
7648     filterDialog.layout()->addWidget(buttonBox);
7649 
7650     if (filterDialog.exec() == QDialog::Accepted)
7651     {
7652         QStringList newLabels;
7653         for (uint8_t i = 0; i < labels.count(); i++)
7654             newLabels << newLabelEdits[i]->text();
7655         filterManager->setFilterNames(newLabels);
7656     }
7657 }
7658 
restartCamera(const QString & name)7659 void Capture::restartCamera(const QString &name)
7660 {
7661     connect(KSMessageBox::Instance(), &KSMessageBox::accepted, this, [this, name]()
7662     {
7663         KSMessageBox::Instance()->disconnect(this);
7664         abort();
7665         emit driverTimedout(name);
7666     });
7667     connect(KSMessageBox::Instance(), &KSMessageBox::rejected, this, [this]()
7668     {
7669         KSMessageBox::Instance()->disconnect(this);
7670     });
7671 
7672     KSMessageBox::Instance()->questionYesNo(i18n("Are you sure you want to restart %1 camera driver?", name),
7673                                             i18n("Driver Restart"), 5);
7674 }
7675 
handleScriptsManager()7676 void Capture::handleScriptsManager()
7677 {
7678     QPointer<ScriptsManager> manager = new ScriptsManager(this);
7679 
7680     manager->setScripts(m_Scripts);
7681 
7682     if (manager->exec() == QDialog::Accepted)
7683     {
7684         m_Scripts = manager->getScripts();
7685     }
7686 }
7687 
generateScriptArguments() const7688 QStringList Capture::generateScriptArguments() const
7689 {
7690     // TODO based on user feedback on what paramters are most useful to pass
7691     return QStringList();
7692 }
7693 
showTemperatureRegulation()7694 void Capture::showTemperatureRegulation()
7695 {
7696     if (!currentCCD)
7697         return;
7698 
7699     double currentRamp, currentThreshold;
7700     if (!currentCCD->getTemperatureRegulation(currentRamp, currentThreshold))
7701         return;
7702 
7703 
7704     double rMin, rMax, rStep, tMin, tMax, tStep;
7705 
7706     currentCCD->getMinMaxStep("CCD_TEMP_RAMP", "RAMP_SLOPE", &rMin, &rMax, &rStep);
7707     currentCCD->getMinMaxStep("CCD_TEMP_RAMP", "RAMP_THRESHOLD", &tMin, &tMax, &tStep);
7708 
7709     QLabel rampLabel(i18nc("Temperature ramp celcius per minute", "Ramp (C/min):"));
7710     QDoubleSpinBox rampSpin;
7711     rampSpin.setMinimum(rMin);
7712     rampSpin.setMaximum(rMax);
7713     rampSpin.setSingleStep(rStep);
7714     rampSpin.setValue(currentRamp);
7715     rampSpin.setToolTip(i18n("Maximum temperature change per minute when cooling or warming the camera. Set zero to disable."));
7716 
7717     QLabel thresholdLabel(i18n("Threshold:"));
7718     QDoubleSpinBox thresholdSpin;
7719     thresholdSpin.setMinimum(tMin);
7720     thresholdSpin.setMaximum(tMax);
7721     thresholdSpin.setSingleStep(tStep);
7722     thresholdSpin.setValue(currentThreshold);
7723     thresholdSpin.setToolTip(i18n("Maximum difference between camera and target temperatures"));
7724 
7725     QFormLayout layout;
7726     layout.addRow(&rampLabel, &rampSpin);
7727     layout.addRow(&thresholdLabel, &thresholdSpin);
7728 
7729     QPointer<QDialog> dialog = new QDialog(this);
7730     QDialogButtonBox buttonBox(QDialogButtonBox::Ok | QDialogButtonBox::Cancel, dialog);
7731     connect(&buttonBox, &QDialogButtonBox::accepted, dialog, &QDialog::accept);
7732     connect(&buttonBox, &QDialogButtonBox::rejected, dialog, &QDialog::reject);
7733     dialog->setWindowTitle(i18nc("@title:window", "Set Temperature Regulation"));
7734     layout.addWidget(&buttonBox);
7735     dialog->setLayout(&layout);
7736     dialog->setMinimumWidth(300);
7737 
7738     if (dialog->exec() == QDialog::Accepted)
7739     {
7740         currentCCD->setTemperatureRegulation(rampSpin.value(), thresholdSpin.value());
7741     }
7742 
7743     //delete(dialog);
7744 }
7745 
7746 }
7747