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(¤tTemperature);
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