Kstars

capture.cpp
1 /*
2  SPDX-FileCopyrightText: 2012 Jasem Mutlaq <[email protected]>
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/indifilterwheel.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.3
44 // We accept file formats with version back to:
45 #define SQ_COMPAT_VERSION 2.0
46 
47 // Qt version calming
48 #include <qtendl.h>
49 
50 namespace Ekos
51 {
53 {
54  setupUi(this);
55 
56  qRegisterMetaType<Ekos::CaptureState>("Ekos::CaptureState");
57  qDBusRegisterMetaType<Ekos::CaptureState>();
58 
59  new CaptureAdaptor(this);
60  m_captureDeviceAdaptor.reset(new CaptureDeviceAdaptor());
61  m_captureState.reset(new SequenceJobState::CaptureState());
62 
63  QDBusConnection::sessionBus().registerObject("/KStars/Ekos/Capture", this);
64  QPointer<QDBusInterface> ekosInterface = new QDBusInterface("org.kde.kstars", "/KStars/Ekos", "org.kde.kstars.Ekos",
66 
67  // Connecting DBus signals
68  QDBusConnection::sessionBus().connect("org.kde.kstars", "/KStars/Ekos", "org.kde.kstars.Ekos", "newModule", this,
69  SLOT(registerNewModule(QString)));
70 
71  // ensure that the mount interface is present
72  registerNewModule("Mount");
73 
74  KStarsData::Instance()->userdb()->GetAllDSLRInfos(DSLRInfos);
75 
76  if (DSLRInfos.count() > 0)
77  {
78  qCDebug(KSTARS_EKOS_CAPTURE) << "DSLR Cameras Info:";
79  qCDebug(KSTARS_EKOS_CAPTURE) << DSLRInfos;
80  }
81 
82  m_LimitsDialog = new QDialog(this);
83  m_LimitsUI.reset(new Ui::Limits());
84  m_LimitsUI->setupUi(m_LimitsDialog);
85 
87 
88  //isAutoGuiding = false;
89 
90  rotatorSettings.reset(new RotatorSettings(this));
91 
92  // hide avg. download time and target drift initially
93  targetDriftLabel->setVisible(false);
94  targetDrift->setVisible(false);
95  targetDriftUnit->setVisible(false);
96  avgDownloadTime->setVisible(false);
97  avgDownloadLabel->setVisible(false);
98  secLabel->setVisible(false);
99  connect(rotatorSettings->setAngleB, &QPushButton::clicked, this, [this]()
100  {
101  double angle = rotatorSettings->angleSpin->value();
102  m_captureDeviceAdaptor->setRotatorAngle(&angle);
103  });
104  connect(rotatorSettings->setPAB, &QPushButton::clicked, this, [this]()
105  {
106  // 1. PA = (RawAngle * Multiplier) - Offset
107  // 2. Offset = (RawAngle * Multiplier) - PA
108  // 3. RawAngle = (Offset + PA) / Multiplier
109  double rawAngle = (rotatorSettings->PAOffsetSpin->value() + rotatorSettings->PASpin->value()) /
110  rotatorSettings->PAMulSpin->value();
111  while (rawAngle < 0)
112  rawAngle += 360;
113  while (rawAngle > 360)
114  rawAngle -= 360;
115 
116  rotatorSettings->angleSpin->setValue(rawAngle);
117  m_captureDeviceAdaptor->setRotatorAngle(&rawAngle);
118  });
119  connect(rotatorSettings->ReverseDirectionCheck, &QCheckBox::toggled, this, [this](bool toggled)
120  {
121  m_captureDeviceAdaptor->reverseRotator(toggled);
122  });
123 
124  seqFileCount = 0;
125  seqDelayTimer = new QTimer(this);
126  connect(seqDelayTimer, &QTimer::timeout, this, &Ekos::Capture::captureImage);
127  captureDelayTimer = new QTimer(this);
128  connect(captureDelayTimer, &QTimer::timeout, this, &Ekos::Capture::start);
129 
132  connect(darkLibraryB, &QPushButton::clicked, DarkLibrary::Instance(), &QDialog::show);
133  connect(limitsB, &QPushButton::clicked, m_LimitsDialog, &QDialog::show);
135 
136  startB->setIcon(QIcon::fromTheme("media-playback-start"));
137  startB->setAttribute(Qt::WA_LayoutUsesWidgetRect);
138  pauseB->setIcon(QIcon::fromTheme("media-playback-pause"));
139  pauseB->setAttribute(Qt::WA_LayoutUsesWidgetRect);
140 
141  filterManagerB->setIcon(QIcon::fromTheme("view-filter"));
142  filterManagerB->setAttribute(Qt::WA_LayoutUsesWidgetRect);
143 
144  filterWheelS->addItem("--");
145 
146  connect(captureBinHN, static_cast<void (QSpinBox::*)(int)>(&QSpinBox::valueChanged), captureBinVN, &QSpinBox::setValue);
147 
148 #if QT_VERSION < QT_VERSION_CHECK(5, 14, 0)
149  connect(cameraS, static_cast<void (QComboBox::*)(const QString &)>(&QComboBox::activated), this,
150  &Ekos::Capture::setDefaultCCD);
151 #else
152  connect(cameraS, static_cast<void (QComboBox::*)(const QString &)>(&QComboBox::textActivated), this,
153  &Ekos::Capture::setDefaultCCD);
154 #endif
155  connect(cameraS, static_cast<void (QComboBox::*)(int)>(&QComboBox::activated), this, &Ekos::Capture::checkCamera);
156 
158 
159  guideDeviationTimer.setInterval(GD_TIMER_TIMEOUT);
160  connect(&guideDeviationTimer, &QTimer::timeout, this, &Ekos::Capture::checkGuideDeviationTimeout);
161 
162  connect(clearConfigurationB, &QPushButton::clicked, this, &Ekos::Capture::clearCameraConfiguration);
163 
164  darkB->setChecked(Options::autoDark());
165  connect(darkB, &QAbstractButton::toggled, this, [this]()
166  {
167  Options::setAutoDark(darkB->isChecked());
168  });
169 
170 #if QT_VERSION < QT_VERSION_CHECK(5, 14, 0)
171  connect(filterWheelS, static_cast<void (QComboBox::*)(const QString &)>(&QComboBox::activated), this,
172  &Ekos::Capture::setDefaultFilterWheel);
173 #else
174  connect(filterWheelS, static_cast<void (QComboBox::*)(const QString &)>(&QComboBox::textActivated), this,
175  &Ekos::Capture::setDefaultFilterWheel);
176 #endif
177  connect(filterWheelS, static_cast<void (QComboBox::*)(int)>(&QComboBox::activated), this,
179 
180  connect(restartCameraB, &QPushButton::clicked, [this]()
181  {
182  restartCamera(cameraS->currentText());
183  });
184 
185  connect(cameraTemperatureS, &QCheckBox::toggled, [this](bool toggled)
186  {
187  if (m_captureDeviceAdaptor->getActiveCamera())
188  {
189  QVariantMap auxInfo = m_captureDeviceAdaptor->getActiveCamera()->getDriverInfo()->getAuxInfo();
190  auxInfo[QString("%1_TC").arg(m_captureDeviceAdaptor->getActiveCamera()->getDeviceName())] = toggled;
191  m_captureDeviceAdaptor->getActiveCamera()->getDriverInfo()->setAuxInfo(auxInfo);
192  }
193  });
194 
195  connect(filterEditB, &QPushButton::clicked, this, &Ekos::Capture::editFilterName);
196 
197  connect(captureFilterS, static_cast<void(QComboBox::*)(const QString &)>(&QComboBox::currentTextChanged),
198  [ = ]()
199  {
200  updateHFRThreshold();
201  });
204 
205  //connect( seqWatcher, SIGNAL(dirty(QString)), this, &Ekos::Capture::checkSeqFile(QString)));
206 
208  connect(removeFromQueueB, &QPushButton::clicked, this, &Ekos::Capture::removeJobFromQueue);
211  connect(selectFileDirectoryB, &QPushButton::clicked, this, &Ekos::Capture::saveFITSDirectory);
212  connect(queueSaveB, &QPushButton::clicked, this, static_cast<void(Ekos::Capture::*)()>(&Ekos::Capture::saveSequenceQueue));
213  connect(queueSaveAsB, &QPushButton::clicked, this, &Ekos::Capture::saveSequenceQueueAs);
214  connect(queueLoadB, &QPushButton::clicked, this, static_cast<void(Ekos::Capture::*)()>(&Ekos::Capture::loadSequenceQueue));
215  connect(resetB, &QPushButton::clicked, this, &Ekos::Capture::resetJobs);
216  connect(queueTable->selectionModel(), &QItemSelectionModel::currentRowChanged, this, &Ekos::Capture::selectedJobChanged);
217  connect(queueTable, &QAbstractItemView::doubleClicked, this, &Ekos::Capture::editJob);
218  connect(queueTable, &QTableWidget::itemSelectionChanged, this, &Ekos::Capture::resetJobEdit);
219  connect(setTemperatureB, &QPushButton::clicked, this, [&]()
220  {
221  if (m_captureDeviceAdaptor->getActiveCamera())
222  m_captureDeviceAdaptor->getActiveCamera()->setTemperature(cameraTemperatureN->value());
223  });
224  connect(coolerOnB, &QPushButton::clicked, this, [&]()
225  {
226  if (m_captureDeviceAdaptor->getActiveCamera())
227  m_captureDeviceAdaptor->getActiveCamera()->setCoolerControl(true);
228  });
229  connect(coolerOffB, &QPushButton::clicked, this, [&]()
230  {
231  if (m_captureDeviceAdaptor->getActiveCamera())
232  m_captureDeviceAdaptor->getActiveCamera()->setCoolerControl(false);
233  });
234  connect(cameraTemperatureN, &QDoubleSpinBox::editingFinished, setTemperatureB,
235  static_cast<void (QPushButton::*)()>(&QPushButton::setFocus));
236  connect(captureTypeS, static_cast<void (QComboBox::*)(int)>(&QComboBox::activated), this, &Ekos::Capture::checkFrameType);
237  connect(resetFrameB, &QPushButton::clicked, this, &Ekos::Capture::resetFrame);
238  connect(calibrationB, &QPushButton::clicked, this, &Ekos::Capture::openCalibrationDialog);
239  connect(rotatorB, &QPushButton::clicked, rotatorSettings.get(), &Ekos::Capture::show);
240 
241  connect(generateDarkFlatsB, &QPushButton::clicked, this, &Ekos::Capture::generateDarkFlats);
242  connect(scriptManagerB, &QPushButton::clicked, this, &Ekos::Capture::handleScriptsManager);
243 
244  addToQueueB->setIcon(QIcon::fromTheme("list-add"));
245  addToQueueB->setAttribute(Qt::WA_LayoutUsesWidgetRect);
246  removeFromQueueB->setIcon(QIcon::fromTheme("list-remove"));
247  removeFromQueueB->setAttribute(Qt::WA_LayoutUsesWidgetRect);
248  queueUpB->setIcon(QIcon::fromTheme("go-up"));
249  queueUpB->setAttribute(Qt::WA_LayoutUsesWidgetRect);
250  queueDownB->setIcon(QIcon::fromTheme("go-down"));
251  queueDownB->setAttribute(Qt::WA_LayoutUsesWidgetRect);
252  selectFileDirectoryB->setIcon(QIcon::fromTheme("document-open-folder"));
253  selectFileDirectoryB->setAttribute(Qt::WA_LayoutUsesWidgetRect);
254  queueLoadB->setIcon(QIcon::fromTheme("document-open"));
255  queueLoadB->setAttribute(Qt::WA_LayoutUsesWidgetRect);
256  queueSaveB->setIcon(QIcon::fromTheme("document-save"));
257  queueSaveB->setAttribute(Qt::WA_LayoutUsesWidgetRect);
258  queueSaveAsB->setIcon(QIcon::fromTheme("document-save-as"));
259  queueSaveAsB->setAttribute(Qt::WA_LayoutUsesWidgetRect);
260  resetB->setIcon(QIcon::fromTheme("system-reboot"));
261  resetB->setAttribute(Qt::WA_LayoutUsesWidgetRect);
262  resetFrameB->setIcon(QIcon::fromTheme("view-refresh"));
263  resetFrameB->setAttribute(Qt::WA_LayoutUsesWidgetRect);
264  calibrationB->setIcon(QIcon::fromTheme("run-build"));
265  calibrationB->setAttribute(Qt::WA_LayoutUsesWidgetRect);
266  generateDarkFlatsB->setIcon(QIcon::fromTheme("tools-wizard"));
267  generateDarkFlatsB->setAttribute(Qt::WA_LayoutUsesWidgetRect);
268  rotatorB->setIcon(QIcon::fromTheme("kstars_solarsystem"));
269  rotatorB->setAttribute(Qt::WA_LayoutUsesWidgetRect);
270 
271  addToQueueB->setToolTip(i18n("Add job to sequence queue"));
272  removeFromQueueB->setToolTip(i18n("Remove job from sequence queue"));
273 
274  fileDirectoryT->setText(Options::fitsDir());
275 
276  ////////////////////////////////////////////////////////////////////////
277  /// Device Adaptor
278  ////////////////////////////////////////////////////////////////////////
279  connect(m_captureDeviceAdaptor.data(), &Ekos::CaptureDeviceAdaptor::newCCDTemperatureValue, this,
281  connect(m_captureDeviceAdaptor.data(), &Ekos::CaptureDeviceAdaptor::newRotatorAngle, this,
282  &Ekos::Capture::updateRotatorAngle, Qt::UniqueConnection);
283 
284  ////////////////////////////////////////////////////////////////////////
285  /// Settings
286  ////////////////////////////////////////////////////////////////////////
287  // #0 Start Guide Deviation Check
288  m_LimitsUI->startGuiderDriftS->setChecked(Options::enforceStartGuiderDrift());
289  connect(m_LimitsUI->startGuiderDriftS, &QCheckBox::toggled, [ = ](bool checked)
290  {
291  Options::setEnforceStartGuiderDrift(checked);
292  });
293 
294  // #1 Abort Guide Deviation Check
295  m_LimitsUI->limitGuideDeviationS->setChecked(Options::enforceGuideDeviation());
296  connect(m_LimitsUI->limitGuideDeviationS, &QCheckBox::toggled, [ = ](bool checked)
297  {
298  Options::setEnforceGuideDeviation(checked);
299  });
300 
301  // #2 Guide Deviation Value
302  m_LimitsUI->limitGuideDeviationN->setValue(Options::guideDeviation());
303  connect(m_LimitsUI->limitGuideDeviationN, &QDoubleSpinBox::editingFinished, [ = ]()
304  {
305  Options::setGuideDeviation(m_LimitsUI->limitGuideDeviationN->value());
306  });
307 
308  // 3. Autofocus HFR Check
309  m_LimitsUI->limitFocusHFRS->setChecked(Options::enforceAutofocus());
310  connect(m_LimitsUI->limitFocusHFRS, &QCheckBox::toggled, [ = ](bool checked)
311  {
312  Options::setEnforceAutofocus(checked);
313  if (checked == false)
314  isInSequenceFocus = false;
315  });
316 
317  // 4. Autofocus HFR Deviation
318  m_LimitsUI->limitFocusHFRN->setValue(Options::hFRDeviation());
319  connect(m_LimitsUI->limitFocusHFRN, &QDoubleSpinBox::editingFinished, [ = ]()
320  {
321  Options::setHFRDeviation(m_LimitsUI->limitFocusHFRN->value());
322  });
323 
324  // 5. Autofocus temperature Check
325  m_LimitsUI->limitFocusDeltaTS->setChecked(Options::enforceAutofocusOnTemperature());
326  connect(m_LimitsUI->limitFocusDeltaTS, &QCheckBox::toggled, [ = ](bool checked)
327  {
328  Options::setEnforceAutofocusOnTemperature(checked);
329  if (checked == false)
330  isTemperatureDeltaCheckActive = false;
331  });
332 
333  // 6. Autofocus temperature Delta
334  m_LimitsUI->limitFocusDeltaTN->setValue(Options::maxFocusTemperatureDelta());
335  connect(m_LimitsUI->limitFocusDeltaTN, &QDoubleSpinBox::editingFinished, [ = ]()
336  {
337  Options::setMaxFocusTemperatureDelta(m_LimitsUI->limitFocusDeltaTN->value());
338  });
339 
340  // 7. Refocus Every Check
341  m_LimitsUI->limitRefocusS->setChecked(Options::enforceRefocusEveryN());
342  connect(m_LimitsUI->limitRefocusS, &QCheckBox::toggled, [ = ](bool checked)
343  {
344  Options::setEnforceRefocusEveryN(checked);
345  });
346 
347  // 8. Refocus Every Value
348  m_LimitsUI->limitRefocusN->setValue(static_cast<int>(Options::refocusEveryN()));
349  connect(m_LimitsUI->limitRefocusN, &QDoubleSpinBox::editingFinished, [ = ]()
350  {
351  Options::setRefocusEveryN(static_cast<uint>(m_LimitsUI->limitRefocusN->value()));
352  });
353 
354  // 9. File settings: filter name
355  fileFilterS->setChecked(Options::fileSettingsUseFilter());
356  connect(fileFilterS, &QCheckBox::toggled, [ = ](bool checked)
357  {
358  Options::setFileSettingsUseFilter(checked);
359  });
360 
361  // 10. File settings: duration
362  fileDurationS->setChecked(Options::fileSettingsUseDuration());
363  connect(fileDurationS, &QCheckBox::toggled, [ = ](bool checked)
364  {
365  Options::setFileSettingsUseDuration(checked);
366  });
367 
368  // 11. File settings: timestamp
369  fileTimestampS->setChecked(Options::fileSettingsUseTimestamp());
370  connect(fileTimestampS, &QCheckBox::toggled, [ = ](bool checked)
371  {
372  Options::setFileSettingsUseTimestamp(checked);
373  });
374 
375  // 12. Refocus after meridian flip
376  m_LimitsUI->meridianRefocusS->setChecked(Options::refocusAfterMeridianFlip());
377  connect(m_LimitsUI->meridianRefocusS, &QCheckBox::toggled, [](bool checked)
378  {
379  Options::setRefocusAfterMeridianFlip(checked);
380  });
381 
382  QCheckBox * const checkBoxes[] =
383  {
384  m_LimitsUI->limitGuideDeviationS,
385  m_LimitsUI->limitRefocusS,
386  m_LimitsUI->limitGuideDeviationS,
387  };
388  for (const QCheckBox * control : checkBoxes)
389  connect(control, &QCheckBox::toggled, this, &Ekos::Capture::setDirty);
390 
391  QDoubleSpinBox * const dspinBoxes[]
392  {
393  m_LimitsUI->limitFocusHFRN,
394  m_LimitsUI->limitFocusDeltaTN,
395  m_LimitsUI->limitGuideDeviationN,
396  };
397  for (const QDoubleSpinBox * control : dspinBoxes)
398  connect(control, static_cast<void (QDoubleSpinBox::*)(double)>(&QDoubleSpinBox::valueChanged), this,
399  &Ekos::Capture::setDirty);
400 
401  connect(fileUploadModeS, static_cast<void (QComboBox::*)(int)>(&QComboBox::activated), this, &Ekos::Capture::setDirty);
402  connect(fileRemoteDirT, &QLineEdit::editingFinished, this, &Ekos::Capture::setDirty);
403 
404  m_ObserverName = Options::defaultObserver();
405  observerB->setIcon(QIcon::fromTheme("im-user"));
406  observerB->setAttribute(Qt::WA_LayoutUsesWidgetRect);
407  connect(observerB, &QPushButton::clicked, this, &Ekos::Capture::showObserverDialog);
408 
409  // Exposure Timeout
410  captureTimeout.setSingleShot(true);
411  connect(&captureTimeout, &QTimer::timeout, this, &Ekos::Capture::processCaptureTimeout);
412 
413  // Post capture script
414  connect(&m_CaptureScript, static_cast<void (QProcess::*)(int exitCode, QProcess::ExitStatus status)>(&QProcess::finished),
415  this, &Ekos::Capture::scriptFinished);
416  connect(&m_CaptureScript, &QProcess::errorOccurred,
417  [this](QProcess::ProcessError error)
418  {
419  Q_UNUSED(error)
420  appendLogText(m_CaptureScript.errorString());
421  scriptFinished(-1, QProcess::NormalExit);
422  });
423  connect(&m_CaptureScript, &QProcess::readyReadStandardError,
424  [this]()
425  {
426  appendLogText(m_CaptureScript.readAllStandardError());
427  });
428  connect(&m_CaptureScript, &QProcess::readyReadStandardOutput,
429  [this]()
430  {
431  appendLogText(m_CaptureScript.readAllStandardOutput());
432  });
433 
434  // Remote directory
435  connect(fileUploadModeS, static_cast<void (QComboBox::*)(int)>(&QComboBox::activated), this,
436  [&](int index)
437  {
438  fileRemoteDirT->setEnabled(index != 0);
439  });
440 
441  customPropertiesDialog.reset(new CustomProperties());
442  connect(customValuesB, &QPushButton::clicked, [&]()
443  {
444  customPropertiesDialog.get()->show();
445  customPropertiesDialog.get()->raise();
446  });
447  connect(customPropertiesDialog.get(), &CustomProperties::valueChanged, [&]()
448  {
449  const double newGain = getGain();
450  if (captureGainN && newGain >= 0)
451  captureGainN->setValue(newGain);
452  const int newOffset = getOffset();
453  if (newOffset >= 0)
454  captureOffsetN->setValue(newOffset);
455  });
456 
457  flatFieldSource = static_cast<FlatFieldSource>(Options::calibrationFlatSourceIndex());
458  flatFieldDuration = static_cast<FlatFieldDuration>(Options::calibrationFlatDurationIndex());
459  wallCoord.setAz(Options::calibrationWallAz());
460  wallCoord.setAlt(Options::calibrationWallAlt());
461  targetADU = Options::calibrationADUValue();
462  targetADUTolerance = Options::calibrationADUValueTolerance();
463 
464  fileDirectoryT->setText(Options::captureDirectory());
465  connect(fileDirectoryT, &QLineEdit::textChanged, [&]()
466  {
467  Options::setCaptureDirectory(fileDirectoryT->text());
468  });
469 
470  if (Options::remoteCaptureDirectory().isEmpty() == false)
471  {
472  fileRemoteDirT->setText(Options::remoteCaptureDirectory());
473  }
474  connect(fileRemoteDirT, &QLineEdit::editingFinished, [&]()
475  {
476  Options::setRemoteCaptureDirectory(fileRemoteDirT->text());
477  });
478 
479  //Note: This is to prevent a button from being called the default button
480  //and then executing when the user hits the enter key such as when on a Text Box
481  QList<QPushButton *> qButtons = findChildren<QPushButton *>();
482  for (auto &button : qButtons)
483  button->setAutoDefault(false);
484 
485  //This Timer will update the Exposure time in the capture module to display the estimated download time left
486  //It will also update the Exposure time left in the Summary Screen.
487  //It fires every 100 ms while images are downloading.
488  downloadProgressTimer.setInterval(100);
489  connect(&downloadProgressTimer, &QTimer::timeout, this, &Ekos::Capture::setDownloadProgress);
490 
491  DarkLibrary::Instance()->setCaptureModule(this);
492  m_DarkProcessor = new DarkProcessor(this);
493  connect(m_DarkProcessor, &DarkProcessor::newLog, this, &Ekos::Capture::appendLogText);
494  connect(m_DarkProcessor, &DarkProcessor::darkFrameCompleted, this, &Ekos::Capture::setCaptureComplete);
495 
496  // display the capture status in the UI
497  connect(this, &Ekos::Capture::newStatus, captureStatusWidget, &Ekos::CaptureStatusWidget::setCaptureState);
498 
499 }
500 
501 Capture::~Capture()
502 {
503  qDeleteAll(jobs);
504 }
505 
506 void Capture::setDefaultCCD(QString ccd)
507 {
508  Options::setDefaultCaptureCCD(ccd);
509 }
510 
511 void Capture::setDefaultFilterWheel(QString filterWheel)
512 {
513  Options::setDefaultCaptureFilterWheel(filterWheel);
514 }
515 
517 {
518  for (auto &oneCamera : m_Cameras)
519  {
520  if (oneCamera->getDeviceName() == device->getDeviceName())
521  return false;
522  }
523 
524  m_Cameras.append(device);
525 
526  cameraS->addItem(device->getDeviceName());
527 
528  DarkLibrary::Instance()->addCamera(device);
529 
530  if (m_FilterWheels.count() > 0)
531  syncFilterInfo();
532 
533  checkCamera();
534 
535  emit settingsUpdated(getPresetSettings());
536 
537  if (device->hasGuideHead())
538  addGuideHead(device);
539 
540  return true;
541 }
542 
543 void Capture::addGuideHead(ISD::Camera * device)
544 {
545  QString guiderName = device->getDeviceName() + QString(" Guider");
546 
547  if (cameraS->findText(guiderName) == -1)
548  {
549  cameraS->addItem(guiderName);
550  m_Cameras.append(device);
551  }
552 }
553 
554 bool Capture::addFilterWheel(ISD::FilterWheel * device)
555 {
556  for (auto &oneFilterWheel : m_FilterWheels)
557  {
558  if (oneFilterWheel->getDeviceName() == device->getDeviceName())
559  return false;
560  }
561 
562  filterWheelS->addItem(device->getDeviceName());
563 
564  m_FilterWheels.append(static_cast<ISD::FilterWheel *>(device));
565 
566  filterManagerB->setEnabled(true);
567 
568  int filterWheelIndex = 1;
569  if (Options::defaultCaptureFilterWheel().isEmpty() == false)
570  filterWheelIndex = filterWheelS->findText(Options::defaultCaptureFilterWheel());
571 
572  if (filterWheelIndex < 1)
573  filterWheelIndex = 1;
574 
575  checkFilter(filterWheelIndex);
576  filterWheelS->setCurrentIndex(filterWheelIndex);
577 
578  emit settingsUpdated(getPresetSettings());
579 
580  return true;
581 }
582 
584 {
585  for (auto &oneDome : m_Domes)
586  {
587  if (oneDome->getDeviceName() == device->getDeviceName())
588  return false;
589  }
590 
591  m_Domes.append(device);
592 
593  // forward it to the command processor
594  if (! m_captureDeviceAdaptor.isNull())
595  m_captureDeviceAdaptor->setDome(device);
596 
597  return true;
598 }
599 
601 {
602  for (auto &oneDustCap : m_DustCaps)
603  {
604  if (oneDustCap->getDeviceName() == device->getDeviceName())
605  return false;
606  }
607 
608  m_DustCaps.append(device);
609 
610  // forward it to the command processor
611  if (!m_captureDeviceAdaptor.isNull())
612  m_captureDeviceAdaptor->setDustCap(device);
613 
614  syncFilterInfo();
615  return true;
616 }
617 
619 {
620  for (auto &oneMount : m_Mounts)
621  {
622  if (oneMount->getDeviceName() == device->getDeviceName())
623  return false;
624  }
625 
626  m_Mounts.append(device);
627 
628  // forward it to the command processor
629  m_captureDeviceAdaptor->setMount(static_cast<ISD::Mount *>(device));
630  m_captureDeviceAdaptor->getMount()->disconnect(this);
631  connect(m_captureDeviceAdaptor->getMount(), &ISD::Mount::numberUpdated, this,
633  connect(m_captureDeviceAdaptor->getMount(), &ISD::Mount::newTargetName, this, &Ekos::Capture::processNewTargetName);
634  syncTelescopeInfo();
635  return true;
636 }
637 
639 {
640  for (auto &oneRotator : m_Rotators)
641  {
642  if (oneRotator->getDeviceName() == device->getDeviceName())
643  return false;
644  }
645 
646  m_Rotators.append(device);
647  connect(m_captureDeviceAdaptor.data(), &Ekos::CaptureDeviceAdaptor::rotatorReverseToggled, this,
648  &Capture::setRotatorReversed,
650 
651  m_captureDeviceAdaptor->setRotator(device);
652  rotatorB->setEnabled(true);
653  return true;
654 }
655 
657 {
658  for (auto &oneLightBox : m_LightBoxes)
659  {
660  if (oneLightBox->getDeviceName() == device->getDeviceName())
661  return false;
662  }
663 
664  m_LightBoxes.append(device);
665 
666  // forward it to the command processor
667  if (! m_captureDeviceAdaptor.isNull())
668  m_captureDeviceAdaptor->setLightBox(device);
669 
670  return true;
671 }
672 
674 {
675  if (m_State != CAPTURE_CAPTURING)
676  {
677  // Ensure that the pause function is only called during frame capturing
678  // Handling it this way is by far easier than trying to enable/disable the pause button
679  // Fixme: make pausing possible at all stages. This makes it necessary to separate the pausing states from CaptureState.
680  appendLogText(i18n("Pausing only possible while frame capture is running."));
681  qCInfo(KSTARS_EKOS_CAPTURE) << "Pause button pressed while not capturing.";
682  return;
683  }
684  pauseFunction = nullptr;
685  m_State = CAPTURE_PAUSE_PLANNED;
686  emit newStatus(Ekos::CAPTURE_PAUSE_PLANNED);
687  appendLogText(i18n("Sequence shall be paused after current exposure is complete."));
688  pauseB->setEnabled(false);
689 
690  startB->setIcon(QIcon::fromTheme("media-playback-start"));
691  startB->setToolTip(i18n("Resume Sequence"));
692 }
693 
695 {
696  if (m_State == CAPTURE_PAUSE_PLANNED || m_State == CAPTURE_PAUSED)
697  {
698  startB->setIcon(
699  QIcon::fromTheme("media-playback-stop"));
700  startB->setToolTip(i18n("Stop Sequence"));
701  pauseB->setEnabled(true);
702 
703  m_State = CAPTURE_CAPTURING;
704  emit newStatus(Ekos::CAPTURE_CAPTURING);
705 
706  appendLogText(i18n("Sequence resumed."));
707 
708  // Call from where ever we have left of when we paused
709  if (pauseFunction)
710  (this->*pauseFunction)();
711  }
712  else if (m_State == CAPTURE_IDLE || m_State == CAPTURE_ABORTED || m_State == CAPTURE_COMPLETE)
713  {
714  start();
715  }
716  else
717  {
718  abort();
719  }
720 }
721 
723 {
724  if (name == "Mount" && mountInterface == nullptr)
725  {
726  qCDebug(KSTARS_EKOS_CAPTURE) << "Registering new Module (" << name << ")";
727  mountInterface = new QDBusInterface("org.kde.kstars", "/KStars/Ekos/Mount",
728  "org.kde.kstars.Ekos.Mount", QDBusConnection::sessionBus(), this);
729 
730  }
731 }
732 
733 /**
734  * @brief Start the execution of the Capture::SequenceJob list #jobs.
735  *
736  * Starting the execution of the Capture::SequenceJob list selects the first job
737  * from the ist that may be executed and starts to prepare the job (@see prepareJob()).
738  *
739  * Several factors determine, which of the jobs will be selected:
740  * - First, the list is searched to find the first job that is marked as idle or aborted.
741  * - If none is found, it is checked whether ignoring job progress is set. If yes,
742  * all jobs are are reset (@see reset()) and the first one from the list is selected.
743  * If no, the user is asked whether the jobs should be reset. If the user declines,
744  * starting is aborted.
745  */
747 {
748  // if (darkSubCheck->isChecked())
749  // {
750  // KSNotification::error(i18n("Auto dark subtract is not supported in batch mode."));
751  // return;
752  // }
753 
754  // 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
755  ignoreJobProgress = !capturedFramesMap.count() && Options::alwaysResetSequenceWhenStarting();
756 
757  if (queueTable->rowCount() == 0)
758  {
759  if (addJob() == false)
760  return;
761  }
762 
763  SequenceJob * first_job = nullptr;
764 
765  for (auto &job : jobs)
766  {
767  if (job->getStatus() == JOB_IDLE || job->getStatus() == JOB_ABORTED)
768  {
769  first_job = job;
770  break;
771  }
772  }
773 
774  // If there are no idle nor aborted jobs, question is whether to reset and restart
775  // Scheduler will start a non-empty new job each time and doesn't use this execution path
776  if (first_job == nullptr)
777  {
778  // If we have at least one job that are in error, bail out, even if ignoring job progress
779  for (auto &job : jobs)
780  {
781  if (job->getStatus() != JOB_DONE)
782  {
783  // If we arrived here with a zero-delay timer, raise the interval before returning to avoid a cpu peak
784  if (captureDelayTimer->isActive())
785  {
786  if (captureDelayTimer->interval() <= 0)
787  captureDelayTimer->setInterval(1000);
788  }
789  else appendLogText(i18n("No pending jobs found. Please add a job to the sequence queue."));
790  return;
791  }
792  }
793 
794  // If we only have completed jobs and we don't ignore job progress, ask the end-user what to do
795  if (!ignoreJobProgress)
797  nullptr,
798  i18n("All jobs are complete. Do you want to reset the status of all jobs and restart capturing?"),
799  i18n("Reset job status"), KStandardGuiItem::cont(), KStandardGuiItem::cancel(),
800  "reset_job_complete_status_warning") != KMessageBox::Continue)
801  return;
802 
803  // If the end-user accepted to reset, reset all jobs and restart
804  for (auto &job : jobs)
805  job->resetStatus();
806 
807  first_job = jobs.first();
808  }
809  // If we need to ignore job progress, systematically reset all jobs and restart
810  // Scheduler will never ignore job progress and doesn't use this path
811  else if (ignoreJobProgress)
812  {
813  appendLogText(i18n("Warning: option \"Always Reset Sequence When Starting\" is enabled and resets the sequence counts."));
814  for (auto &job : jobs)
815  job->resetStatus();
816  }
817 
818  // Refocus timer should not be reset on deviation error
819  if (m_DeviationDetected == false && m_State != CAPTURE_SUSPENDED)
820  {
821  // start timer to measure time until next forced refocus
822  startRefocusEveryNTimer();
823  }
824 
825  // Only reset these counters if we are NOT restarting from deviation errors
826  // So when starting a new job or fresh then we reset them.
827  if (m_DeviationDetected == false)
828  {
829  ditherCounter = Options::ditherFrames();
830  inSequenceFocusCounter = Options::inSequenceCheckFrames();
831  }
832 
833  m_DeviationDetected = false;
834  m_SpikesDetected = 0;
835 
836  m_State = CAPTURE_PROGRESS;
837  emit newStatus(m_State);
838 
839  startB->setIcon(QIcon::fromTheme("media-playback-stop"));
840  startB->setToolTip(i18n("Stop Sequence"));
841  pauseB->setEnabled(true);
842 
843  setBusy(true);
844 
845  if (m_LimitsUI->limitGuideDeviationS->isChecked() && autoGuideReady == false)
846  appendLogText(i18n("Warning: Guide deviation is selected but autoguide process was not started."));
847  if (m_LimitsUI->limitFocusHFRS->isChecked() && m_AutoFocusReady == false)
848  appendLogText(i18n("Warning: in-sequence focusing is selected but autofocus process was not started."));
849  if (m_LimitsUI->limitFocusDeltaTS->isChecked() && m_AutoFocusReady == false)
850  appendLogText(i18n("Warning: temperature delta check is selected but autofocus process was not started."));
851 
852  if (m_captureDeviceAdaptor->getActiveCamera()
853  && m_captureDeviceAdaptor->getActiveCamera()->getTelescopeType() != ISD::Camera::TELESCOPE_PRIMARY)
854  {
855  connect(KSMessageBox::Instance(), &KSMessageBox::accepted, this, [ = ]()
856  {
857  KSMessageBox::Instance()->disconnect(this);
858  m_captureDeviceAdaptor->getActiveCamera()->setTelescopeType(ISD::Camera::TELESCOPE_PRIMARY);
859  prepareJob(first_job);
860  });
861  connect(KSMessageBox::Instance(), &KSMessageBox::rejected, this, [ = ]()
862  {
863  KSMessageBox::Instance()->disconnect(this);
864  prepareJob(first_job);
865  });
866 
867  KSMessageBox::Instance()->questionYesNo(i18n("Are you imaging with %1 using your primary telescope?",
868  m_captureDeviceAdaptor->getActiveCamera()->getDeviceName()),
869  i18n("Telescope Type"), 10, true);
870  }
871  else
872  prepareJob(first_job);
873 }
874 
875 /**
876  * @brief Stop, suspend or abort the currently active job.
877  * @param targetState
878  */
879 void Capture::stop(CaptureState targetState)
880 {
881  retries = 0;
882  //seqTotalCount = 0;
883  //seqCurrentCount = 0;
884 
885  captureTimeout.stop();
886  captureDelayTimer->stop();
887 
888  ADURaw.clear();
889  ExpRaw.clear();
890 
891  if (activeJob != nullptr)
892  {
893  if (activeJob->getStatus() == JOB_BUSY)
894  {
895  QString stopText;
896  switch (targetState)
897  {
898  case CAPTURE_SUSPENDED:
899  stopText = i18n("CCD capture suspended");
900  activeJob->resetStatus(JOB_BUSY);
901  break;
902 
903  case CAPTURE_ABORTED:
904  stopText = i18n("CCD capture aborted");
905  activeJob->resetStatus(JOB_ABORTED);
906  break;
907 
908  default:
909  stopText = i18n("CCD capture stopped");
910  activeJob->resetStatus(JOB_IDLE);
911  break;
912  }
913  emit captureAborted(activeJob->getCoreProperty(SequenceJob::SJ_Exposure).toDouble());
914  KSNotification::event(QLatin1String("CaptureFailed"), stopText);
915  appendLogText(stopText);
916  activeJob->abort();
917  if (activeJob->getCoreProperty(SequenceJob::SJ_Preview).toBool() == false)
918  {
919  int index = jobs.indexOf(activeJob);
920  QJsonObject oneSequence = m_SequenceArray[index].toObject();
921  oneSequence["Status"] = "Aborted";
922  m_SequenceArray.replace(index, oneSequence);
923  emit sequenceChanged(m_SequenceArray);
924  }
925  }
926 
927  // In case of batch job
928  if (activeJob->getCoreProperty(SequenceJob::SJ_Preview).toBool() == false)
929  {
930  activeJob->disconnect(this);
931  }
932  // or preview job in calibration stage
933  else if (activeJob->getCalibrationStage() == SequenceJobState::CAL_CALIBRATION)
934  {
935  activeJob->disconnect(this);
936  activeJob->setCoreProperty(SequenceJob::SJ_Preview, false);
937  if (m_captureDeviceAdaptor->getActiveCamera())
938  m_captureDeviceAdaptor->getActiveCamera()->setUploadMode(activeJob->getUploadMode());
939  }
940  // or regular preview job
941  else
942  {
943  if (m_captureDeviceAdaptor->getActiveCamera())
944  m_captureDeviceAdaptor->getActiveCamera()->setUploadMode(activeJob->getUploadMode());
945  jobs.removeOne(activeJob);
946  // Delete preview job
947  activeJob->deleteLater();
948  // Clear active job
949  setActiveJob(nullptr);
950 
951  emit newStatus(targetState);
952  }
953  }
954 
955  // Only emit a new status if there is an active job or if capturing is suspended.
956  // The latter is necessary since suspending clears the active job, but the Capture
957  // module keeps the control.
958  if (activeJob != nullptr || m_State == CAPTURE_SUSPENDED)
959  emit newStatus(targetState);
960 
961  // stop focusing if capture is aborted
962  if (m_State == CAPTURE_FOCUSING && targetState == CAPTURE_ABORTED)
963  emit abortFocus();
964 
965  m_State = targetState;
966 
967  // Turn off any calibration light, IF they were turned on by Capture module
968  if (m_captureDeviceAdaptor->getLightBox() && lightBoxLightEnabled)
969  {
970  lightBoxLightEnabled = false;
971  m_captureDeviceAdaptor->getLightBox()->SetLightEnabled(false);
972  }
973 
974  disconnect(m_captureDeviceAdaptor->getActiveCamera(), &ISD::Camera::newImage, this, &Ekos::Capture::processData);
975  disconnect(m_captureDeviceAdaptor->getActiveCamera(), &ISD::Camera::newExposureValue, this,
976  &Ekos::Capture::setExposureProgress);
977  // disconnect(currentCCD, &ISD::Camera::previewFITSGenerated, this, &Ekos::Capture::setGeneratedPreviewFITS);
978  disconnect(m_captureDeviceAdaptor->getActiveCamera(), &ISD::Camera::ready, this, &Ekos::Capture::ready);
979 
980  // In case of exposure looping, let's abort
981  if (m_captureDeviceAdaptor->getActiveCamera() &&
982  m_captureDeviceAdaptor->getActiveChip() &&
983  m_captureDeviceAdaptor->getActiveCamera()->isFastExposureEnabled())
984  m_captureDeviceAdaptor->getActiveChip()->abortExposure();
985 
986  imgProgress->reset();
987  imgProgress->setEnabled(false);
988 
989  frameRemainingTime->setText("--:--:--");
990  jobRemainingTime->setText("--:--:--");
991  frameInfoLabel->setText(i18n("Expose (-/-):"));
992  m_isFraming = false;
993 
994  setBusy(false);
995 
996  if (m_State == CAPTURE_ABORTED || m_State == CAPTURE_SUSPENDED)
997  {
998  startB->setIcon(
999  QIcon::fromTheme("media-playback-start"));
1000  startB->setToolTip(i18n("Start Sequence"));
1001  pauseB->setEnabled(false);
1002  }
1003 
1004  //foreach (QAbstractButton *button, queueEditButtonGroup->buttons())
1005  //button->setEnabled(true);
1006 
1007  seqDelayTimer->stop();
1008 
1009  setActiveJob(nullptr);
1010  // meridian flip may take place if requested
1011  setMeridianFlipStage(MF_READY);
1012 }
1013 
1014 bool Capture::setCamera(const QString &device)
1015 {
1016  // Do not change camera while in capture
1017  if (m_State == CAPTURE_CAPTURING)
1018  return false;
1019 
1020  for (int i = 0; i < cameraS->count(); i++)
1021  if (device == cameraS->itemText(i))
1022  {
1023  cameraS->setCurrentIndex(i);
1024  checkCamera(i);
1025  return true;
1026  }
1027 
1028  return false;
1029 }
1030 
1031 QString Capture::camera()
1032 {
1033  if (m_captureDeviceAdaptor->getActiveCamera())
1034  return m_captureDeviceAdaptor->getActiveCamera()->getDeviceName();
1035 
1036  return QString();
1037 }
1038 
1039 void Capture::checkCamera(int ccdNum)
1040 {
1041  // Do not update any camera settings while capture is in progress.
1042  if (m_State == CAPTURE_CAPTURING)
1043  return;
1044 
1045  if (ccdNum == -1)
1046  {
1047  ccdNum = cameraS->currentIndex();
1048 
1049  if (ccdNum == -1)
1050  return;
1051  }
1052 
1053  if (ccdNum < m_Cameras.count())
1054  {
1055  // Check whether main camera or guide head only
1056  ISD::Camera *currentCCD = m_Cameras.at(ccdNum);
1057  m_captureDeviceAdaptor->setActiveCamera(currentCCD);
1058 
1059  m_captureDeviceAdaptor->setActiveChip(nullptr);
1060  if (cameraS->itemText(ccdNum).right(6) == QString("Guider"))
1061  {
1062  useGuideHead = true;
1063  m_captureDeviceAdaptor->setActiveChip(currentCCD->getChip(ISD::CameraChip::GUIDE_CCD));
1064  }
1065 
1066  if (m_captureDeviceAdaptor->getActiveChip() == nullptr)
1067  {
1068  useGuideHead = false;
1069  m_captureDeviceAdaptor->setActiveChip(currentCCD->getChip(ISD::CameraChip::PRIMARY_CCD));
1070  }
1071 
1072  // Make sure we have a valid chip and valid base device.
1073  // Make sure we are not in capture process.
1074  ISD::CameraChip *targetChip = m_captureDeviceAdaptor->getActiveChip();
1075  if (!targetChip || !targetChip->getCCD() || targetChip->isCapturing())
1076  return;
1077 
1078  for (auto &ccd : m_Cameras)
1079  {
1080  disconnect(ccd, &ISD::Camera::numberUpdated, this, &Ekos::Capture::processCCDNumber);
1081  disconnect(ccd, &ISD::Camera::newTemperatureValue, this, &Ekos::Capture::updateCCDTemperature);
1082  disconnect(ccd, &ISD::Camera::coolerToggled, this, &Ekos::Capture::setCoolerToggled);
1083  disconnect(ccd, &ISD::Camera::newRemoteFile, this, &Ekos::Capture::setNewRemoteFile);
1084  disconnect(ccd, &ISD::Camera::videoStreamToggled, this, &Ekos::Capture::setVideoStreamEnabled);
1085  disconnect(ccd, &ISD::Camera::ready, this, &Ekos::Capture::ready);
1086  disconnect(ccd, &ISD::Camera::error, this, &Ekos::Capture::processCaptureError);
1087  }
1088 
1089  if (currentCCD->hasCoolerControl())
1090  {
1091  coolerOnB->setEnabled(true);
1092  coolerOffB->setEnabled(true);
1093  coolerOnB->setChecked(currentCCD->isCoolerOn());
1094  coolerOffB->setChecked(!currentCCD->isCoolerOn());
1095  }
1096  else
1097  {
1098  coolerOnB->setEnabled(false);
1099  coolerOnB->setChecked(false);
1100  coolerOffB->setEnabled(false);
1101  coolerOffB->setChecked(false);
1102  }
1103 
1104  updateFrameProperties();
1105 
1106  QStringList frameTypes = m_captureDeviceAdaptor->getActiveChip()->getFrameTypes();
1107 
1108  captureTypeS->clear();
1109 
1110  if (frameTypes.isEmpty())
1111  captureTypeS->setEnabled(false);
1112  else
1113  {
1114  captureTypeS->setEnabled(true);
1115  captureTypeS->addItems(frameTypes);
1116  captureTypeS->setCurrentIndex(m_captureDeviceAdaptor->getActiveChip()->getFrameType());
1117  }
1118 
1119  // Capture Format
1120  captureFormatS->blockSignals(true);
1121  captureFormatS->clear();
1122  captureFormatS->addItems(currentCCD->getCaptureFormats());
1123  captureFormatS->setCurrentText(currentCCD->getCaptureFormat());
1124  captureFormatS->blockSignals(false);
1125 
1126  // Encoding format
1127  captureEncodingS->blockSignals(true);
1128  captureEncodingS->clear();
1129  captureEncodingS->addItems(currentCCD->getEncodingFormats());
1130  captureEncodingS->setCurrentText(currentCCD->getEncodingFormat());
1131  captureEncodingS->blockSignals(false);
1132 
1133  customPropertiesDialog->setCCD(currentCCD);
1134 
1135  liveVideoB->setEnabled(currentCCD->hasVideoStream());
1136  if (currentCCD->hasVideoStream())
1137  setVideoStreamEnabled(currentCCD->isStreamingEnabled());
1138  else
1139  liveVideoB->setIcon(QIcon::fromTheme("camera-off"));
1140 
1141  connect(currentCCD, &ISD::Camera::numberUpdated, this, &Ekos::Capture::processCCDNumber, Qt::UniqueConnection);
1142  connect(currentCCD, &ISD::Camera::coolerToggled, this, &Ekos::Capture::setCoolerToggled, Qt::UniqueConnection);
1143  connect(currentCCD, &ISD::Camera::newRemoteFile, this, &Ekos::Capture::setNewRemoteFile);
1144  connect(currentCCD, &ISD::Camera::videoStreamToggled, this, &Ekos::Capture::setVideoStreamEnabled);
1145  connect(currentCCD, &ISD::Camera::ready, this, &Ekos::Capture::ready);
1146  connect(currentCCD, &ISD::Camera::error, this, &Ekos::Capture::processCaptureError);
1147 
1148  syncCameraInfo();
1149 
1150  // update values received by the device adaptor
1151  // connect(currentCCD, &ISD::Camera::newTemperatureValue, this, &Ekos::Capture::updateCCDTemperature, Qt::UniqueConnection);
1152 
1153  DarkLibrary::Instance()->checkCamera();
1154  }
1155 }
1156 
1157 void Capture::syncCameraInfo()
1158 {
1159  auto currentCCD = m_captureDeviceAdaptor->getActiveCamera();
1160  if (!currentCCD)
1161  return;
1162 
1163  if (currentCCD->hasCooler())
1164  {
1165  cameraTemperatureS->setEnabled(true);
1166  cameraTemperatureN->setEnabled(true);
1167 
1168  if (currentCCD->getPermission("CCD_TEMPERATURE") != IP_RO)
1169  {
1170  double min, max, step;
1171  setTemperatureB->setEnabled(true);
1172  cameraTemperatureN->setReadOnly(false);
1173  cameraTemperatureS->setEnabled(true);
1174  currentCCD->getMinMaxStep("CCD_TEMPERATURE", "CCD_TEMPERATURE_VALUE", &min, &max, &step);
1175  cameraTemperatureN->setMinimum(min);
1176  cameraTemperatureN->setMaximum(max);
1177  cameraTemperatureN->setSingleStep(1);
1178  bool isChecked = currentCCD->getDriverInfo()->getAuxInfo().value(QString("%1_TC").arg(currentCCD->getDeviceName()),
1179  false).toBool();
1180  cameraTemperatureS->setChecked(isChecked);
1181  }
1182  else
1183  {
1184  setTemperatureB->setEnabled(false);
1185  cameraTemperatureN->setReadOnly(true);
1186  cameraTemperatureS->setEnabled(false);
1187  cameraTemperatureS->setChecked(false);
1188  }
1189 
1190  double temperature = 0;
1191  if (currentCCD->getTemperature(&temperature))
1192  {
1193  temperatureOUT->setText(QString("%L1").arg(temperature, 0, 'f', 2));
1194  if (cameraTemperatureN->cleanText().isEmpty())
1195  cameraTemperatureN->setValue(temperature);
1196  }
1197  }
1198  else
1199  {
1200  cameraTemperatureS->setEnabled(false);
1201  cameraTemperatureN->setEnabled(false);
1202  cameraTemperatureN->clear();
1203  temperatureOUT->clear();
1204  setTemperatureB->setEnabled(false);
1205  }
1206 
1207  auto isoList = m_captureDeviceAdaptor->getActiveChip()->getISOList();
1208  captureISOS->blockSignals(true);
1209  captureISOS->clear();
1210 
1211  // No ISO range available
1212  if (isoList.isEmpty())
1213  {
1214  captureISOS->setEnabled(false);
1215  }
1216  else
1217  {
1218  captureISOS->setEnabled(true);
1219  captureISOS->addItems(isoList);
1220  captureISOS->setCurrentIndex(m_captureDeviceAdaptor->getActiveChip()->getISOIndex());
1221 
1222  uint16_t w, h;
1223  uint8_t bbp {8};
1224  double pixelX = 0, pixelY = 0;
1225  bool rc = m_captureDeviceAdaptor->getActiveChip()->getImageInfo(w, h, pixelX, pixelY, bbp);
1226  bool isModelInDB = isModelinDSLRInfo(QString(currentCCD->getDeviceName()));
1227  // If rc == true, then the property has been defined by the driver already
1228  // Only then we check if the pixels are zero
1229  if (rc == true && (pixelX == 0.0 || pixelY == 0.0 || isModelInDB == false))
1230  {
1231  // If model is already in database, no need to show dialog
1232  // The zeros above are the initial packets so we can safely ignore them
1233  if (isModelInDB == false)
1234  {
1235  createDSLRDialog();
1236  }
1237  else
1238  {
1239  QString model = QString(currentCCD->getDeviceName());
1240  syncDSLRToTargetChip(model);
1241  }
1242  }
1243  }
1244  captureISOS->blockSignals(false);
1245 
1246  // Gain Check
1247  if (currentCCD->hasGain())
1248  {
1249  double min, max, step, value, targetCustomGain;
1250  currentCCD->getGainMinMaxStep(&min, &max, &step);
1251 
1252  // Allow the possibility of no gain value at all.
1253  GainSpinSpecialValue = min - step;
1254  captureGainN->setRange(GainSpinSpecialValue, max);
1255  captureGainN->setSpecialValueText(i18n("--"));
1256  captureGainN->setEnabled(true);
1257  captureGainN->setSingleStep(step);
1258  currentCCD->getGain(&value);
1259 
1260  targetCustomGain = getGain();
1261 
1262  // Set the custom gain if we have one
1263  // otherwise it will not have an effect.
1264  if (targetCustomGain > 0)
1265  captureGainN->setValue(targetCustomGain);
1266  else
1267  captureGainN->setValue(GainSpinSpecialValue);
1268 
1269  captureGainN->setReadOnly(currentCCD->getGainPermission() == IP_RO);
1270 
1271  connect(captureGainN, &QDoubleSpinBox::editingFinished, this, [this]()
1272  {
1273  if (captureGainN->value() != GainSpinSpecialValue)
1274  setGain(captureGainN->value());
1275  });
1276  }
1277  else
1278  captureGainN->setEnabled(false);
1279 
1280  // Offset checks
1281  if (currentCCD->hasOffset())
1282  {
1283  double min, max, step, value, targetCustomOffset;
1284  currentCCD->getOffsetMinMaxStep(&min, &max, &step);
1285 
1286  // Allow the possibility of no Offset value at all.
1287  OffsetSpinSpecialValue = min - step;
1288  captureOffsetN->setRange(OffsetSpinSpecialValue, max);
1289  captureOffsetN->setSpecialValueText(i18n("--"));
1290  captureOffsetN->setEnabled(true);
1291  captureOffsetN->setSingleStep(step);
1292  currentCCD->getOffset(&value);
1293 
1294  targetCustomOffset = getOffset();
1295 
1296  // Set the custom Offset if we have one
1297  // otherwise it will not have an effect.
1298  if (targetCustomOffset > 0)
1299  captureOffsetN->setValue(targetCustomOffset);
1300  else
1301  captureOffsetN->setValue(OffsetSpinSpecialValue);
1302 
1303  captureOffsetN->setReadOnly(currentCCD->getOffsetPermission() == IP_RO);
1304 
1305  connect(captureOffsetN, &QDoubleSpinBox::editingFinished, this, [this]()
1306  {
1307  if (captureOffsetN->value() != OffsetSpinSpecialValue)
1308  setOffset(captureOffsetN->value());
1309  });
1310  }
1311  else
1312  captureOffsetN->setEnabled(false);
1313 }
1314 
1315 void Capture::setGuideChip(ISD::CameraChip * guideChip)
1316 {
1317  // We should suspend guide in two scenarios:
1318  // 1. If guide chip is within the primary CCD, then we cannot download any data from guide chip while primary CCD is downloading.
1319  // 2. If we have two CCDs running from ONE driver (Multiple-Devices-Per-Driver mpdp is true). Same issue as above, only one download
1320  // at a time.
1321  // After primary CCD download is complete, we resume guiding.
1322  if (!m_captureDeviceAdaptor->getActiveCamera())
1323  return;
1324 
1325  suspendGuideOnDownload =
1326  (m_captureDeviceAdaptor->getActiveCamera()->getChip(ISD::CameraChip::GUIDE_CCD) == guideChip) ||
1327  (guideChip->getCCD() == m_captureDeviceAdaptor->getActiveCamera() &&
1328  m_captureDeviceAdaptor->getActiveCamera()->getDriverInfo()->getAuxInfo().value("mdpd", false).toBool());
1329 }
1330 
1331 void Capture::resetFrameToZero()
1332 {
1333  captureFrameXN->setMinimum(0);
1334  captureFrameXN->setMaximum(0);
1335  captureFrameXN->setValue(0);
1336 
1337  captureFrameYN->setMinimum(0);
1338  captureFrameYN->setMaximum(0);
1339  captureFrameYN->setValue(0);
1340 
1341  captureFrameWN->setMinimum(0);
1342  captureFrameWN->setMaximum(0);
1343  captureFrameWN->setValue(0);
1344 
1345  captureFrameHN->setMinimum(0);
1346  captureFrameHN->setMaximum(0);
1347  captureFrameHN->setValue(0);
1348 }
1349 
1350 void Capture::updateFrameProperties(int reset)
1351 {
1352  if (!m_captureDeviceAdaptor->getActiveCamera())
1353  return;
1354 
1355  int binx = 1, biny = 1;
1356  double min, max, step;
1357  int xstep = 0, ystep = 0;
1358 
1359  QString frameProp = useGuideHead ? QString("GUIDER_FRAME") : QString("CCD_FRAME");
1360  QString exposureProp = useGuideHead ? QString("GUIDER_EXPOSURE") : QString("CCD_EXPOSURE");
1361  QString exposureElem = useGuideHead ? QString("GUIDER_EXPOSURE_VALUE") : QString("CCD_EXPOSURE_VALUE");
1362  m_captureDeviceAdaptor->setActiveChip(useGuideHead ? m_captureDeviceAdaptor->getActiveCamera()->getChip(
1363  ISD::CameraChip::GUIDE_CCD) :
1364  m_captureDeviceAdaptor->getActiveCamera()->getChip(ISD::CameraChip::PRIMARY_CCD));
1365 
1366  captureFrameWN->setEnabled(m_captureDeviceAdaptor->getActiveChip()->canSubframe());
1367  captureFrameHN->setEnabled(m_captureDeviceAdaptor->getActiveChip()->canSubframe());
1368  captureFrameXN->setEnabled(m_captureDeviceAdaptor->getActiveChip()->canSubframe());
1369  captureFrameYN->setEnabled(m_captureDeviceAdaptor->getActiveChip()->canSubframe());
1370 
1371  captureBinHN->setEnabled(m_captureDeviceAdaptor->getActiveChip()->canBin());
1372  captureBinVN->setEnabled(m_captureDeviceAdaptor->getActiveChip()->canBin());
1373 
1374  QList<double> exposureValues;
1375  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 <<
1376  10 << 20 << 30 << 40 << 50 << 60 << 120 << 180 << 300 << 600 << 900 << 1200 << 1800;
1377 
1378  if (m_captureDeviceAdaptor->getActiveCamera()->getMinMaxStep(exposureProp, exposureElem, &min, &max, &step))
1379  {
1380  if (min < 0.001)
1381  captureExposureN->setDecimals(6);
1382  else
1383  captureExposureN->setDecimals(3);
1384  for(int i = 0; i < exposureValues.count(); i++)
1385  {
1386  double value = exposureValues.at(i);
1387  if(value < min || value > max)
1388  {
1389  exposureValues.removeAt(i);
1390  i--; //So we don't skip one
1391  }
1392  }
1393 
1394  exposureValues.prepend(min);
1395  exposureValues.append(max);
1396  }
1397 
1398  captureExposureN->setRecommendedValues(exposureValues);
1399 
1400  if (m_captureDeviceAdaptor->getActiveCamera()->getMinMaxStep(frameProp, "WIDTH", &min, &max, &step))
1401  {
1402  if (min >= max)
1403  {
1404  resetFrameToZero();
1405  return;
1406  }
1407 
1408  if (step == 0.0)
1409  xstep = static_cast<int>(max * 0.05);
1410  else
1411  xstep = static_cast<int>(step);
1412 
1413  if (min >= 0 && max > 0)
1414  {
1415  captureFrameWN->setMinimum(static_cast<int>(min));
1416  captureFrameWN->setMaximum(static_cast<int>(max));
1417  captureFrameWN->setSingleStep(xstep);
1418  }
1419  }
1420  else
1421  return;
1422 
1423  if (m_captureDeviceAdaptor->getActiveCamera()->getMinMaxStep(frameProp, "HEIGHT", &min, &max, &step))
1424  {
1425  if (min >= max)
1426  {
1427  resetFrameToZero();
1428  return;
1429  }
1430 
1431  if (step == 0.0)
1432  ystep = static_cast<int>(max * 0.05);
1433  else
1434  ystep = static_cast<int>(step);
1435 
1436  if (min >= 0 && max > 0)
1437  {
1438  captureFrameHN->setMinimum(static_cast<int>(min));
1439  captureFrameHN->setMaximum(static_cast<int>(max));
1440  captureFrameHN->setSingleStep(ystep);
1441  }
1442  }
1443  else
1444  return;
1445 
1446  if (m_captureDeviceAdaptor->getActiveCamera()->getMinMaxStep(frameProp, "X", &min, &max, &step))
1447  {
1448  if (min >= max)
1449  {
1450  resetFrameToZero();
1451  return;
1452  }
1453 
1454  if (step == 0.0)
1455  step = xstep;
1456 
1457  if (min >= 0 && max > 0)
1458  {
1459  captureFrameXN->setMinimum(static_cast<int>(min));
1460  captureFrameXN->setMaximum(static_cast<int>(max));
1461  captureFrameXN->setSingleStep(static_cast<int>(step));
1462  }
1463  }
1464  else
1465  return;
1466 
1467  if (m_captureDeviceAdaptor->getActiveCamera()->getMinMaxStep(frameProp, "Y", &min, &max, &step))
1468  {
1469  if (min >= max)
1470  {
1471  resetFrameToZero();
1472  return;
1473  }
1474 
1475  if (step == 0.0)
1476  step = ystep;
1477 
1478  if (min >= 0 && max > 0)
1479  {
1480  captureFrameYN->setMinimum(static_cast<int>(min));
1481  captureFrameYN->setMaximum(static_cast<int>(max));
1482  captureFrameYN->setSingleStep(static_cast<int>(step));
1483  }
1484  }
1485  else
1486  return;
1487 
1488  // cull to camera limits, if there are any
1489  if (useGuideHead == false)
1490  cullToDSLRLimits();
1491 
1492  if (reset == 1 || frameSettings.contains(m_captureDeviceAdaptor->getActiveChip()) == false)
1493  {
1494  QVariantMap settings;
1495 
1496  settings["x"] = 0;
1497  settings["y"] = 0;
1498  settings["w"] = captureFrameWN->maximum();
1499  settings["h"] = captureFrameHN->maximum();
1500  settings["binx"] = 1;
1501  settings["biny"] = 1;
1502 
1503  frameSettings[m_captureDeviceAdaptor->getActiveChip()] = settings;
1504  }
1505  else if (reset == 2 && frameSettings.contains(m_captureDeviceAdaptor->getActiveChip()))
1506  {
1507  QVariantMap settings = frameSettings[m_captureDeviceAdaptor->getActiveChip()];
1508  int x, y, w, h;
1509 
1510  x = settings["x"].toInt();
1511  y = settings["y"].toInt();
1512  w = settings["w"].toInt();
1513  h = settings["h"].toInt();
1514 
1515  // Bound them
1516  x = qBound(captureFrameXN->minimum(), x, captureFrameXN->maximum() - 1);
1517  y = qBound(captureFrameYN->minimum(), y, captureFrameYN->maximum() - 1);
1518  w = qBound(captureFrameWN->minimum(), w, captureFrameWN->maximum());
1519  h = qBound(captureFrameHN->minimum(), h, captureFrameHN->maximum());
1520 
1521  settings["x"] = x;
1522  settings["y"] = y;
1523  settings["w"] = w;
1524  settings["h"] = h;
1525 
1526  frameSettings[m_captureDeviceAdaptor->getActiveChip()] = settings;
1527  }
1528 
1529  if (frameSettings.contains(m_captureDeviceAdaptor->getActiveChip()))
1530  {
1531  QVariantMap settings = frameSettings[m_captureDeviceAdaptor->getActiveChip()];
1532  int x = settings["x"].toInt();
1533  int y = settings["y"].toInt();
1534  int w = settings["w"].toInt();
1535  int h = settings["h"].toInt();
1536 
1537  if (m_captureDeviceAdaptor->getActiveChip()->canBin())
1538  {
1539  m_captureDeviceAdaptor->getActiveChip()->getMaxBin(&binx, &biny);
1540  captureBinHN->setMaximum(binx);
1541  captureBinVN->setMaximum(biny);
1542 
1543  captureBinHN->setValue(settings["binx"].toInt());
1544  captureBinVN->setValue(settings["biny"].toInt());
1545  }
1546  else
1547  {
1548  captureBinHN->setValue(1);
1549  captureBinVN->setValue(1);
1550  }
1551 
1552  if (x >= 0)
1553  captureFrameXN->setValue(x);
1554  if (y >= 0)
1555  captureFrameYN->setValue(y);
1556  if (w > 0)
1557  captureFrameWN->setValue(w);
1558  if (h > 0)
1559  captureFrameHN->setValue(h);
1560  }
1561 }
1562 
1563 void Capture::processCCDNumber(INumberVectorProperty * nvp)
1564 {
1565  if (m_captureDeviceAdaptor->getActiveCamera() == nullptr)
1566  return;
1567 
1568  if ((!strcmp(nvp->name, "CCD_FRAME") && useGuideHead == false) ||
1569  (!strcmp(nvp->name, "GUIDER_FRAME") && useGuideHead))
1570  updateFrameProperties();
1571  else if ((!strcmp(nvp->name, "CCD_INFO") && useGuideHead == false) ||
1572  (!strcmp(nvp->name, "GUIDER_INFO") && useGuideHead))
1573  updateFrameProperties(2);
1574 }
1575 
1576 void Capture::resetFrame()
1577 {
1578  m_captureDeviceAdaptor->setActiveChip(useGuideHead ? m_captureDeviceAdaptor->getActiveCamera()->getChip(
1579  ISD::CameraChip::GUIDE_CCD) :
1580  m_captureDeviceAdaptor->getActiveCamera()->getChip(ISD::CameraChip::PRIMARY_CCD));
1581  m_captureDeviceAdaptor->getActiveChip()->resetFrame();
1582  updateFrameProperties(1);
1583 }
1584 
1585 void Capture::syncFrameType(const QString &name)
1586 {
1587  if (name != cameraS->currentText().toLatin1())
1588  return;
1589 
1590  ISD::CameraChip * tChip = m_captureDeviceAdaptor->getActiveCamera()->getChip(ISD::CameraChip::PRIMARY_CCD);
1591 
1592  QStringList frameTypes = tChip->getFrameTypes();
1593 
1594  captureTypeS->clear();
1595 
1596  if (frameTypes.isEmpty())
1597  captureTypeS->setEnabled(false);
1598  else
1599  {
1600  captureTypeS->setEnabled(true);
1601  captureTypeS->addItems(frameTypes);
1602  captureTypeS->setCurrentIndex(tChip->getFrameType());
1603  }
1604 }
1605 
1607 {
1608  bool deviceFound = false;
1609 
1610  for (int i = 0; i < filterWheelS->count(); i++)
1611  if (device == filterWheelS->itemText(i))
1612  {
1613  // Check Combo if it was set to something else.
1614  if (filterWheelS->currentIndex() != i)
1615  {
1616  filterWheelS->blockSignals(true);
1617  filterWheelS->setCurrentIndex(i);
1618  filterWheelS->blockSignals(false);
1619  }
1620 
1621  checkFilter(i);
1622  deviceFound = true;
1623  break;
1624  }
1625 
1626  if (deviceFound == false)
1627  return false;
1628 
1629  return true;
1630 }
1631 
1632 QString Capture::filterWheel()
1633 {
1634  if (filterWheelS->currentIndex() >= 1)
1635  return filterWheelS->currentText();
1636 
1637  return QString();
1638 }
1639 
1640 bool Capture::setFilter(const QString &filter)
1641 {
1642  if (filterWheelS->currentIndex() >= 1)
1643  {
1644  captureFilterS->setCurrentText(filter);
1645  return true;
1646  }
1647 
1648  return false;
1649 }
1650 
1651 QString Capture::filter()
1652 {
1653  return captureFilterS->currentText();
1654 }
1655 
1656 void Capture::checkFilter(int filterNum)
1657 {
1658  if (filterNum == -1)
1659  {
1660  filterNum = filterWheelS->currentIndex();
1661  if (filterNum == -1)
1662  return;
1663  }
1664 
1665  // "--" is no filter
1666  if (filterNum == 0)
1667  {
1668  m_captureDeviceAdaptor->setFilterWheel(nullptr);
1669  m_CurrentFilterPosition = -1;
1670  filterEditB->setEnabled(false);
1671  captureFilterS->clear();
1672  syncFilterInfo();
1673  return;
1674  }
1675 
1676  if (filterNum <= m_FilterWheels.count())
1677  m_captureDeviceAdaptor->setFilterWheel(m_FilterWheels.at(filterNum - 1));
1678 
1679  m_captureDeviceAdaptor->getFilterManager()->setCurrentFilterWheel(m_captureDeviceAdaptor->getFilterWheel());
1680 
1681  syncFilterInfo();
1682 
1683  captureFilterS->clear();
1684 
1685  captureFilterS->addItems(m_captureDeviceAdaptor->getFilterManager()->getFilterLabels());
1686 
1687  m_CurrentFilterPosition = m_captureDeviceAdaptor->getFilterManager()->getFilterPosition();
1688 
1689  filterEditB->setEnabled(m_CurrentFilterPosition > 0);
1690 
1691  captureFilterS->setCurrentIndex(m_CurrentFilterPosition - 1);
1692 
1693 
1694  /*if (activeJob &&
1695  (activeJob->getStatus() == JOB_ABORTED || activeJob->getStatus() == JOB_IDLE))
1696  activeJob->setCurrentFilter(currentFilterPosition);*/
1697 }
1698 
1699 void Capture::syncFilterInfo()
1700 {
1702  for (auto &oneCamera : m_Cameras)
1703  devices.append(oneCamera);
1704  if (m_captureDeviceAdaptor->getDustCap())
1705  devices.append(m_captureDeviceAdaptor->getDustCap());
1706 
1707  for (auto &oneDevice : devices)
1708  {
1709  auto activeDevices = oneDevice->getText("ACTIVE_DEVICES");
1710  if (activeDevices)
1711  {
1712  auto activeFilter = activeDevices->findWidgetByName("ACTIVE_FILTER");
1713  if (activeFilter)
1714  {
1715  if (m_captureDeviceAdaptor->getFilterWheel() &&
1716  (activeFilter->getText() != m_captureDeviceAdaptor->getFilterWheel()->getDeviceName()))
1717  {
1718  Options::setDefaultFocusFilterWheel(m_captureDeviceAdaptor->getFilterWheel()->getDeviceName());
1719  activeFilter->setText(m_captureDeviceAdaptor->getFilterWheel()->getDeviceName().toLatin1().constData());
1720  oneDevice->sendNewText(activeDevices);
1721  }
1722  // Reset filter name in CCD driver
1723  else if (!m_captureDeviceAdaptor->getFilterWheel() && strlen(activeFilter->getText()) > 0)
1724  {
1725  activeFilter->setText("");
1726  oneDevice->sendNewText(activeDevices);
1727  }
1728  }
1729  }
1730  }
1731 }
1732 
1733 /**
1734  * @brief Ensure that all pending preparation tasks are be completed (focusing, dithering, etc.)
1735  * and start the next exposure.
1736  *
1737  * Checks of pending preparations depends upon the frame type:
1738  *
1739  * - For light frames, pending preparations like focusing, dithering etc. needs
1740  * to be checked before each single frame capture. efore starting to capture the next light frame,
1741  * checkLightFramePendingTasks() is called to check if all pending preparation tasks have
1742  * been completed successfully. As soon as this is the case, the sequence timer
1743  * #seqTimer is started to wait the configured delay and starts capturing the next image.
1744  *
1745  * - For bias, dark and flat frames, preparation jobs are only executed when starting a sequence.
1746  * Hence, for these frames we directly start the sequence timer #seqTimer.
1747  *
1748  * @return IPS_OK, iff all pending preparation jobs are completed (@see checkLightFramePendingTasks()).
1749  * In that case, the #seqTimer is started to wait for the configured settling delay and then
1750  * capture the next image (@see Capture::captureImage). In case that a pending task aborted,
1751  * IPS_IDLE is returned.
1752  */
1753 IPState Capture::startNextExposure()
1754 {
1755  // Since this function is looping while pending tasks are running in parallel
1756  // it might happen that one of them leads to abort() which sets the #activeJob to nullptr.
1757  // In this case we terminate the loop by returning #IPS_IDLE without starting a new capture.
1758  if (activeJob == nullptr)
1759  return IPS_IDLE;
1760 
1761  // check pending jobs for light frames. All other frame types do not contain mid-sequence checks.
1762  if (activeJob->getFrameType() == FRAME_LIGHT)
1763  {
1764  IPState pending = checkLightFramePendingTasks();
1765  if (pending != IPS_OK)
1766  // there are still some jobs pending
1767  return pending;
1768  }
1769 
1770  // nothing pending, let's start the next exposure
1771  if (seqDelay > 0)
1772  {
1773  m_State = CAPTURE_WAITING;
1774  emit newStatus(Ekos::CAPTURE_WAITING);
1775  }
1776  seqDelayTimer->start(seqDelay);
1777 
1778  return IPS_OK;
1779 }
1780 
1781 /**
1782  * @brief Try to start capturing the next exposure (@see startNextExposure()).
1783  * If startNextExposure() returns, that there are still some jobs pending,
1784  * we wait for 1 second and retry to start it again.
1785  * If one of the pending preparation jobs has problems, the looping stops.
1786  */
1787 void Capture::checkNextExposure()
1788 {
1789  IPState started = startNextExposure();
1790  // if starting the next exposure did not succeed due to pending jobs running,
1791  // we retry after 1 second
1792  if (started == IPS_BUSY)
1793  QTimer::singleShot(1000, this, &Ekos::Capture::checkNextExposure);
1794 }
1795 
1796 
1798 {
1799  ISD::CameraChip * tChip = nullptr;
1800 
1801  QString blobInfo;
1802  if (data)
1803  {
1804  m_ImageData = data;
1805  blobInfo = QString("{Device: %1 Property: %2 Element: %3 Chip: %4}").arg(data->property("device").toString())
1806  .arg(data->property("blobVector").toString())
1807  .arg(data->property("blobElement").toString())
1808  .arg(data->property("chip").toInt());
1809  }
1810  else
1811  m_ImageData.reset();
1812 
1813  // If there is no active job, ignore
1814  if (activeJob == nullptr)
1815  {
1816  if (data)
1817  qCWarning(KSTARS_EKOS_CAPTURE) << blobInfo << "Ignoring received FITS as active job is null.";
1818  return;
1819  }
1820 
1821  if (meridianFlipStage >= MF_ALIGNING)
1822  {
1823  if (data)
1824  qCWarning(KSTARS_EKOS_CAPTURE) << blobInfo << "Ignoring Received FITS as meridian flip stage is" << meridianFlipStage;
1825  return;
1826  }
1827 
1828  // If image is client or both, let's process it.
1829  if (m_captureDeviceAdaptor->getActiveCamera()
1830  && m_captureDeviceAdaptor->getActiveCamera()->getUploadMode() != ISD::Camera::UPLOAD_LOCAL)
1831  {
1832  // if (data.isNull())
1833  // {
1834  // appendLogText(i18n("Failed to save file to %1", activeJob->getSignature()));
1835  // abort();
1836  // return;
1837  // }
1838 
1839  if (m_State == CAPTURE_IDLE || m_State == CAPTURE_ABORTED)
1840  {
1841  qCWarning(KSTARS_EKOS_CAPTURE) << blobInfo << "Ignoring Received FITS as current capture state is not active" << m_State;
1842  return;
1843  }
1844 
1845  //if (!strcmp(data->name, "CCD2"))
1846  if (data)
1847  {
1848  tChip = m_captureDeviceAdaptor->getActiveCamera()->getChip(static_cast<ISD::CameraChip::ChipType>
1849  (data->property("chip").toInt()));
1850  if (tChip != m_captureDeviceAdaptor->getActiveChip())
1851  {
1852  if (m_GuideState == GUIDE_IDLE)
1853  qCWarning(KSTARS_EKOS_CAPTURE) << blobInfo << "Ignoring Received FITS as it does not correspond to the target chip"
1854  << m_captureDeviceAdaptor->getActiveChip()->getType();
1855  return;
1856  }
1857  }
1858 
1859  if (m_captureDeviceAdaptor->getActiveChip()->getCaptureMode() == FITS_FOCUS ||
1860  m_captureDeviceAdaptor->getActiveChip()->getCaptureMode() == FITS_GUIDE)
1861  {
1862  qCWarning(KSTARS_EKOS_CAPTURE) << blobInfo << "Ignoring Received FITS as it has the wrong capture mode" <<
1863  m_captureDeviceAdaptor->getActiveChip()->getCaptureMode();
1864  return;
1865  }
1866 
1867  // If the FITS is not for our device, simply ignore
1868 
1869  if (data && data->property("device").toString() != m_captureDeviceAdaptor->getActiveCamera()->getDeviceName())
1870  {
1871  qCWarning(KSTARS_EKOS_CAPTURE) << blobInfo << "Ignoring Received FITS as the blob device name does not equal active camera"
1872  << m_captureDeviceAdaptor->getActiveCamera()->getDeviceName();
1873  return;
1874  }
1875 
1876  // If this is a preview job, make sure to enable preview button after
1877  // we receive the FITS
1878  if (activeJob->getCoreProperty(SequenceJob::SJ_Preview).toBool() && previewB->isEnabled() == false)
1879  previewB->setEnabled(true);
1880 
1881  // If dark is selected, perform dark substraction.
1882  if (data && darkB->isChecked() && activeJob->getCoreProperty(SequenceJob::SJ_Preview).toBool() && useGuideHead == false)
1883  {
1884  m_DarkProcessor->denoise(m_captureDeviceAdaptor->getActiveChip(),
1885  m_ImageData,
1886  activeJob->getCoreProperty(SequenceJob::SJ_Exposure).toDouble(),
1887  activeJob->getCoreProperty(SequenceJob::SJ_ROI).toRect().x(),
1888  activeJob->getCoreProperty(SequenceJob::SJ_ROI).toRect().y());
1889  return;
1890  }
1891  }
1892 
1893  setCaptureComplete();
1894 }
1895 
1896 /**
1897  * @brief Manage the capture process after a captured image has been successfully downloaded from the camera.
1898  *
1899  * When a image frame has been captured and downloaded successfully, send the image to the client (if configured)
1900  * and execute the book keeping for the captured frame. After this, either processJobCompletion() is executed
1901  * in case that the job is completed, and resumeSequence() otherwise.
1902  *
1903  * Book keeping means:
1904  * - increase / decrease the counters for focusing and dithering
1905  * - increase the frame counter
1906  * - update the average download time
1907  *
1908  * @return IPS_BUSY iff pausing is requested, IPS_OK otherwise.
1909  */
1910 IPState Capture::setCaptureComplete()
1911 {
1912  captureTimeout.stop();
1913  m_CaptureTimeoutCounter = 0;
1914 
1915  downloadProgressTimer.stop();
1916 
1917  if (!activeJob)
1918  return IPS_BUSY;
1919 
1920  // In case we're framing, let's return quick to continue the process.
1921  if (m_isFraming)
1922  {
1923  emit newImage(activeJob, m_ImageData);
1924  // If fast exposure is on, do not capture again, it will be captured by the driver.
1925  if (m_captureDeviceAdaptor->getActiveCamera()->isFastExposureEnabled() == false)
1926  {
1927  captureStatusWidget->setStatus(i18n("Framing..."), Qt::darkGreen);
1928  activeJob->capture(m_AutoFocusReady, FITS_NORMAL);
1929  }
1930  return IPS_OK;
1931  }
1932 
1933  // If fast exposure is off, disconnect exposure progress
1934  // otherwise, keep it going since it fires off from driver continuous capture process.
1935  if (m_captureDeviceAdaptor->getActiveCamera()->isFastExposureEnabled() == false)
1936  {
1937  disconnect(m_captureDeviceAdaptor->getActiveCamera(), &ISD::Camera::newExposureValue, this,
1938  &Ekos::Capture::setExposureProgress);
1939  DarkLibrary::Instance()->disconnect(this);
1940  }
1941 
1942  // Do not calculate download time for images stored on server.
1943  // Only calculate for longer exposures.
1944  if (m_captureDeviceAdaptor->getActiveCamera()->getUploadMode() != ISD::Camera::UPLOAD_LOCAL && m_DownloadTimer.isValid())
1945  {
1946  //This determines the time since the image started downloading
1947  //Then it gets the estimated time left and displays it in the log.
1948  double currentDownloadTime = m_DownloadTimer.elapsed() / 1000.0;
1949  downloadTimes << currentDownloadTime;
1950  QString dLTimeString = QString::number(currentDownloadTime, 'd', 2);
1951  QString estimatedTimeString = QString::number(getEstimatedDownloadTime(), 'd', 2);
1952  appendLogText(i18n("Download Time: %1 s, New Download Time Estimate: %2 s.", dLTimeString, estimatedTimeString));
1953 
1954  // Always invalidate timer as it must be explicitly started.
1955  m_DownloadTimer.invalidate();
1956  }
1957 
1958  // Do not display notifications for very short captures
1959  if (activeJob->getCoreProperty(SequenceJob::SJ_Exposure).toDouble() >= 1)
1960  KSNotification::event(QLatin1String("EkosCaptureImageReceived"), i18n("Captured image received"),
1961  KSNotification::EVENT_INFO);
1962 
1963  // If it was initially set as pure preview job and NOT as preview for calibration
1964  if (activeJob->getCoreProperty(SequenceJob::SJ_Preview).toBool())
1965  {
1966  //sendNewImage(blobFilename, blobChip);
1967  emit newImage(activeJob, m_ImageData);
1968  jobs.removeOne(activeJob);
1969  // Reset upload mode if it was changed by preview
1970  m_captureDeviceAdaptor->getActiveCamera()->setUploadMode(activeJob->getUploadMode());
1971  // Reset active job pointer
1972  setActiveJob(nullptr);
1973  abort();
1974  if (m_GuideState == GUIDE_SUSPENDED && suspendGuideOnDownload)
1975  emit resumeGuiding();
1976 
1977  m_State = CAPTURE_IDLE;
1978  emit newStatus(Ekos::CAPTURE_IDLE);
1979  return IPS_OK;
1980  }
1981 
1982  // check if pausing has been requested
1983  if (checkPausing() == true)
1984  {
1985  pauseFunction = &Capture::setCaptureComplete;
1986  return IPS_BUSY;
1987  }
1988 
1989  if (! activeJob->getCoreProperty(SequenceJob::SJ_Preview).toBool()
1990  && activeJob->getCalibrationStage() != SequenceJobState::CAL_CALIBRATION)
1991  {
1992  /* Increase the sequence's current capture count */
1993  activeJob->setCompleted(activeJob->getCompleted() + 1);
1994  /* Decrease the counter for in-sequence focusing */
1995  if (inSequenceFocusCounter > 0)
1996  inSequenceFocusCounter--;
1997  }
1998 
1999  /* Decrease the dithering counter except for directly after meridian flip */
2000  /* Hint: this happens only when a meridian flip happened during a paused sequence when pressing "Start" afterwards. */
2001  if (meridianFlipStage < MF_FLIPPING)
2002  ditherCounter--;
2003 
2004  // JM 2020-06-17: Emit newImage for LOCAL images (stored on remote host)
2005  //if (currentCCD->getUploadMode() == ISD::Camera::UPLOAD_LOCAL)
2006  emit newImage(activeJob, m_ImageData);
2007  // For Client/Both images, send file name.
2008  //else
2009  // sendNewImage(blobFilename, blobChip);
2010 
2011 
2012  /* If we were assigned a captured frame map, also increase the relevant counter for prepareJob */
2013  SchedulerJob::CapturedFramesMap::iterator frame_item = capturedFramesMap.find(activeJob->getSignature());
2014  if (capturedFramesMap.end() != frame_item)
2015  frame_item.value()++;
2016 
2017  if (activeJob->getFrameType() != FRAME_LIGHT)
2018  {
2019  if (processPostCaptureCalibrationStage() == false)
2020  return IPS_OK;
2021 
2022  if (activeJob->getCalibrationStage() == SequenceJobState::CAL_CALIBRATION_COMPLETE)
2023  activeJob->setCalibrationStage(SequenceJobState::CAL_CAPTURING);
2024  }
2025 
2026  /* The image progress has now one more capture */
2027  imgProgress->setValue(activeJob->getCompleted());
2028 
2029  appendLogText(i18n("Received image %1 out of %2.", activeJob->getCompleted(),
2030  activeJob->getCoreProperty(SequenceJob::SJ_Count).toInt()));
2031 
2032  double hfr = -1, eccentricity = -1;
2033  int numStars = -1, median = -1;
2034  QString filename;
2035  if (m_ImageData)
2036  {
2037  QVariant frameType;
2038  if (Options::autoHFR() && m_ImageData && !m_ImageData->areStarsSearched() && m_ImageData->getRecordValue("FRAME", frameType)
2039  && frameType.toString() == "Light")
2040  {
2041  QFuture<bool> result = m_ImageData->findStars(ALGORITHM_SEP);
2042  result.waitForFinished();
2043  }
2044  hfr = m_ImageData->getHFR(HFR_AVERAGE);
2045  numStars = m_ImageData->getSkyBackground().starsDetected;
2046  median = m_ImageData->getMedian();
2047  eccentricity = m_ImageData->getEccentricity();
2048  filename = m_ImageData->filename();
2049  appendLogText(i18n("Captured %1", filename));
2050  auto remainingPlaceholders = PlaceholderPath::remainingPlaceholders(filename);
2051  if (remainingPlaceholders.size() > 0)
2052  {
2053  appendLogText(
2054  i18n("WARNING: remaining and potentially unknown placeholders %1 in %2",
2055  remainingPlaceholders.join(", "), filename));
2056  }
2057  }
2058 
2059  m_State = CAPTURE_IMAGE_RECEIVED;
2060  emit newStatus(Ekos::CAPTURE_IMAGE_RECEIVED);
2061 
2062  if (activeJob)
2063  {
2064  QVariantMap metadata;
2065  metadata["filename"] = filename;
2066  metadata["type"] = activeJob->getFrameType();
2067  metadata["exposure"] = activeJob->getCoreProperty(SequenceJob::SJ_Exposure).toDouble();
2068  metadata["filter"] = activeJob->getCoreProperty(SequenceJob::SJ_Filter).toString();
2069  metadata["hfr"] = hfr;
2070  metadata["starCount"] = numStars;
2071  metadata["median"] = median;
2072  metadata["eccentricity"] = eccentricity;
2073  emit captureComplete(metadata);
2074 
2075  // Check if we need to execute post capture script first
2076 
2077  const QString postCaptureScript = activeJob->getScript(SCRIPT_POST_CAPTURE);
2078  if (postCaptureScript.isEmpty() == false)
2079  {
2080  m_CaptureScriptType = SCRIPT_POST_CAPTURE;
2081  m_CaptureScript.start(postCaptureScript, generateScriptArguments());
2082  appendLogText(i18n("Executing post capture script %1", postCaptureScript));
2083  return IPS_OK;
2084  }
2085 
2086  // if we're done
2087  if (activeJob->getCoreProperty(SequenceJob::SJ_Count).toInt() <= activeJob->getCompleted())
2088  {
2089  processJobCompletionStage1();
2090  return IPS_OK;
2091  }
2092  }
2093 
2094  return resumeSequence();
2095 }
2096 
2097 void Capture::processJobCompletionStage1()
2098 {
2099  if (activeJob == nullptr)
2100  {
2101  qWarning(KSTARS_EKOS_CAPTURE) << "procesJobCompletionStage1 with null activeJob.";
2102  }
2103  else
2104  {
2105  // JM 2020-12-06: Check if we need to execute post-job script first.
2106  const QString postJobScript = activeJob->getScript(SCRIPT_POST_JOB);
2107  if (!postJobScript.isEmpty())
2108  {
2109  m_CaptureScriptType = SCRIPT_POST_JOB;
2110  m_CaptureScript.start(postJobScript, generateScriptArguments());
2111  appendLogText(i18n("Executing post job script %1", postJobScript));
2112  return;
2113  }
2114  }
2115 
2116  processJobCompletionStage2();
2117 }
2118 
2119 /**
2120  * @brief Stop execution of the current sequence and check whether there exists a next sequence
2121  * and start it, if there is a next one to be started (@see resumeSequence()).
2122  */
2123 void Capture::processJobCompletionStage2()
2124 {
2125  if (activeJob == nullptr)
2126  {
2127  qWarning(KSTARS_EKOS_CAPTURE) << "procesJobCompletionStage2 with null activeJob.";
2128  }
2129  else
2130  {
2131  activeJob->done();
2132 
2133  if (activeJob->getCoreProperty(SequenceJob::SJ_Preview).toBool() == false)
2134  {
2135  int index = jobs.indexOf(activeJob);
2136  QJsonObject oneSequence = m_SequenceArray[index].toObject();
2137  oneSequence["Status"] = "Complete";
2138  m_SequenceArray.replace(index, oneSequence);
2139  emit sequenceChanged(m_SequenceArray);
2140  }
2141  }
2142  stop();
2143 
2144  // Check if there are more pending jobs and execute them
2145  if (resumeSequence() == IPS_OK)
2146  return;
2147  // Otherwise, we're done. We park if required and resume guiding if no parking is done and autoguiding was engaged before.
2148  else
2149  {
2150  //KNotification::event(QLatin1String("CaptureSuccessful"), i18n("CCD capture sequence completed"));
2151  KSNotification::event(QLatin1String("CaptureSuccessful"), i18n("CCD capture sequence completed"),
2152  KSNotification::EVENT_INFO);
2153 
2154  abort();
2155 
2156  m_State = CAPTURE_COMPLETE;
2157  emit newStatus(Ekos::CAPTURE_COMPLETE);
2158 
2159  //Resume guiding if it was suspended before
2160  //if (isAutoGuiding && currentCCD->getChip(ISD::CameraChip::GUIDE_CCD) == guideChip)
2161  if (m_GuideState == GUIDE_SUSPENDED && suspendGuideOnDownload)
2162  emit resumeGuiding();
2163  }
2164 }
2165 
2166 /**
2167  * @brief Check, whether dithering is necessary and, in that case initiate it.
2168  *
2169  * Dithering is only required for batch images and does not apply for PREVIEW.
2170  *
2171  * There are several situations that determine, if dithering is necessary:
2172  * 1. the current job captures light frames AND the dither counter has reached 0 AND
2173  * 2. guiding is running OR the manual dithering option is selected AND
2174  * 3. there is a guiding camera active AND
2175  * 4. there hasn't just a meridian flip been finised.
2176  *
2177  * @return true iff dithering is necessary.
2178  */
2179 bool Capture::checkDithering()
2180 {
2181  // No need if preview only
2182  if (activeJob && activeJob->getCoreProperty(SequenceJob::SJ_Preview).toBool())
2183  return false;
2184 
2185  if ( (Options::ditherEnabled() || Options::ditherNoGuiding())
2186  // 2017-09-20 Jasem: No need to dither after post meridian flip guiding
2187  && meridianFlipStage != MF_GUIDING
2188  // We must be either in guide mode or if non-guide dither (via pulsing) is enabled
2189  && (m_GuideState == GUIDE_GUIDING || Options::ditherNoGuiding())
2190  // Must be only done for light frames
2191  && (activeJob != nullptr && activeJob->getFrameType() == FRAME_LIGHT)
2192  // Check dither counter
2193  && ditherCounter == 0)
2194  {
2195  ditherCounter = Options::ditherFrames();
2196 
2197  qCInfo(KSTARS_EKOS_CAPTURE) << "Dithering...";
2198  appendLogText(i18n("Dithering..."));
2199 
2200  m_State = CAPTURE_DITHERING;
2201  m_DitheringState = IPS_BUSY;
2202  emit newStatus(Ekos::CAPTURE_DITHERING);
2203 
2204  return true;
2205  }
2206  // no dithering required
2207  return false;
2208 }
2209 
2210 /**
2211  * @brief Try to continue capturing.
2212  *
2213  * Take the active job, if there is one, or search for the next one that is either
2214  * idle or aborted. If a new job is selected, call prepareJob(*SequenceJob) to prepare it and
2215  * resume guiding (TODO: is this not part of the preparation?). If the current job is still active,
2216  * initiate checkNextExposure().
2217  *
2218  * @return IPS_OK if there is a job that may be continued, IPS_BUSY otherwise.
2219  */
2220 IPState Capture::resumeSequence()
2221 {
2222  // If no job is active, we have to find if there are more pending jobs in the queue
2223  if (!activeJob)
2224  {
2225  SequenceJob * next_job = nullptr;
2226 
2227  for (auto &oneJob : jobs)
2228  {
2229  if (oneJob->getStatus() == JOB_IDLE || oneJob->getStatus() == JOB_ABORTED)
2230  {
2231  next_job = oneJob;
2232  break;
2233  }
2234  }
2235 
2236  if (next_job)
2237  {
2238  //check delta also when starting a new job!
2239  isTemperatureDeltaCheckActive = (m_AutoFocusReady && m_LimitsUI->limitFocusDeltaTS->isChecked());
2240 
2241  prepareJob(next_job);
2242 
2243  //Resume guiding if it was suspended before
2244  //if (isAutoGuiding && currentCCD->getChip(ISD::CameraChip::GUIDE_CCD) == guideChip)
2245  if (m_GuideState == GUIDE_SUSPENDED && suspendGuideOnDownload)
2246  {
2247  qCDebug(KSTARS_EKOS_CAPTURE) << "Resuming guiding...";
2248  emit resumeGuiding();
2249  }
2250 
2251  return IPS_OK;
2252  }
2253  else
2254  {
2255  qCDebug(KSTARS_EKOS_CAPTURE) << "All capture jobs complete.";
2256  return IPS_BUSY;
2257  }
2258  }
2259  // Otherwise, let's prepare for next exposure.
2260  else
2261  {
2262  isTemperatureDeltaCheckActive = (m_AutoFocusReady && m_LimitsUI->limitFocusDeltaTS->isChecked());
2263 
2264  // If we suspended guiding due to primary chip download, resume guide chip guiding now
2265  if (m_GuideState == GUIDE_SUSPENDED && suspendGuideOnDownload)
2266  {
2267  qCInfo(KSTARS_EKOS_CAPTURE) << "Resuming guiding...";
2268  emit resumeGuiding();
2269  }
2270 
2271  // If looping, we just increment the file system image count
2272  if (m_captureDeviceAdaptor->getActiveCamera()->isFastExposureEnabled())
2273  {
2274  if (m_captureDeviceAdaptor->getActiveCamera()->getUploadMode() != ISD::Camera::UPLOAD_LOCAL)
2275  {
2276  checkSeqBoundary(activeJob->getSignature());
2277  m_captureDeviceAdaptor->getActiveCamera()->setNextSequenceID(nextSequenceID);
2278  }
2279  }
2280 
2281  const QString preCaptureScript = activeJob->getScript(SCRIPT_PRE_CAPTURE);
2282  // JM 2020-12-06: Check if we need to execute pre-capture script first.
2283  if (!preCaptureScript.isEmpty())
2284  {
2285  if (m_captureDeviceAdaptor->getActiveCamera()->isFastExposureEnabled())
2286  {
2287  m_RememberFastExposure = true;
2288  m_captureDeviceAdaptor->getActiveCamera()->setFastExposureEnabled(false);
2289  }
2290 
2291  m_CaptureScriptType = SCRIPT_PRE_CAPTURE;
2292  m_CaptureScript.start(preCaptureScript, generateScriptArguments());
2293  appendLogText(i18n("Executing pre capture script %1", preCaptureScript));
2294  return IPS_BUSY;
2295  }
2296  else
2297  {
2298  // Check if we need to stop fast exposure to perform any
2299  // pending tasks. If not continue as is.
2300  if (m_captureDeviceAdaptor->getActiveCamera()->isFastExposureEnabled())
2301  {
2302  if (activeJob &&
2303  activeJob->getFrameType() == FRAME_LIGHT &&
2304  checkLightFramePendingTasks() == IPS_OK)
2305  {
2306  // Continue capturing seamlessly
2307  m_State = CAPTURE_CAPTURING;
2308  emit newStatus(Ekos::CAPTURE_CAPTURING);
2309  return IPS_OK;
2310  }
2311 
2312  // Stop fast exposure now.
2313  m_RememberFastExposure = true;
2314  m_captureDeviceAdaptor->getActiveCamera()->setFastExposureEnabled(false);
2315  }
2316 
2317  checkNextExposure();
2318 
2319  }
2320  }
2321 
2322  return IPS_OK;
2323 }
2324 
2325 /**
2326  * @brief Check, if re-focusing is required and initiate it in that case.
2327  * @return true iff re-focusing is necessary.
2328  */
2330 {
2331  // Do not start focus if:
2332  // 1. There is no active job, or
2333  // 2. Target frame is not LIGHT
2334  // 3. Capture is preview only
2335  if (activeJob == nullptr || activeJob->getFrameType() != FRAME_LIGHT
2336  || activeJob->getCoreProperty(SequenceJob::SJ_Preview).toBool())
2337  return false;
2338 
2339  isRefocus = false;
2340  isInSequenceFocus = (m_AutoFocusReady && m_LimitsUI->limitFocusHFRS->isChecked());
2341 
2342  // check if time for forced refocus
2343  if (m_LimitsUI->limitRefocusS->isChecked())
2344  {
2345  qCDebug(KSTARS_EKOS_CAPTURE) << "Focus elapsed time (secs): " << getRefocusEveryNTimerElapsedSec() <<
2346  ". Requested Interval (secs): " << m_LimitsUI->limitRefocusN->value() * 60;
2347 
2348  if (getRefocusEveryNTimerElapsedSec() >= m_LimitsUI->limitRefocusN->value() * 60)
2349  {
2350  isRefocus = true;
2351  appendLogText(i18n("Scheduled refocus starting after %1 seconds...", getRefocusEveryNTimerElapsedSec()));
2352  }
2353  }
2354 
2355  if (!isRefocus && isTemperatureDeltaCheckActive)
2356  {
2357  qCDebug(KSTARS_EKOS_CAPTURE) << "Focus temperature delta (°C): " << focusTemperatureDelta <<
2358  ". Requested maximum delta (°C): " << m_LimitsUI->limitFocusDeltaTN->value();
2359 
2360  if (focusTemperatureDelta > m_LimitsUI->limitFocusDeltaTN->value())
2361  {
2362  isRefocus = true;
2363  appendLogText(i18n("Refocus starting because of temperature change of %1 °C...", focusTemperatureDelta));
2364  }
2365  }
2366 
2367  if (m_LimitsUI->meridianRefocusS->isChecked() && refocusAfterMeridianFlip)
2368  {
2369  isRefocus = true;
2370  refocusAfterMeridianFlip = false;
2371  appendLogText(i18n("Refocus after meridian flip"));
2372 
2373  // Post meridian flip we need to reset filter _before_ running in-sequence focusing
2374  // as it could have changed for whatever reason (e.g. alignment used a different filter).
2375  if (m_captureDeviceAdaptor->getFilterWheel())
2376  {
2377  int targetFilterPosition = activeJob->getTargetFilter();
2378  int currentFilterPosition = m_captureDeviceAdaptor->getFilterManager()->getFilterPosition();
2379  if (targetFilterPosition > 0 && targetFilterPosition != currentFilterPosition)
2380  m_captureDeviceAdaptor->getFilterWheel()->setPosition(targetFilterPosition);
2381  }
2382  }
2383 
2384  // Either it is time to force autofocus or temperature has changed
2385  if (isRefocus)
2386  {
2387  if (m_captureDeviceAdaptor->getActiveCamera()->isFastExposureEnabled())
2388  m_captureDeviceAdaptor->getActiveChip()->abortExposure();
2389 
2390  // If we are over 30 mins since last autofocus, we'll reset frame.
2391  if (m_LimitsUI->limitRefocusN->value() >= 30)
2392  emit resetFocus();
2393 
2394  // force refocus
2395  qCDebug(KSTARS_EKOS_CAPTURE) << "Capture is triggering autofocus on line " << __LINE__;
2396  setFocusStatus(FOCUS_PROGRESS);
2397  emit checkFocus(0.1);
2398 
2399  m_State = CAPTURE_FOCUSING;
2400  emit newStatus(Ekos::CAPTURE_FOCUSING);
2401  return true;
2402  }
2403  else if (isInSequenceFocus && inSequenceFocusCounter == 0)
2404  {
2405  inSequenceFocusCounter = Options::inSequenceCheckFrames();
2406 
2407  // Post meridian flip we need to reset filter _before_ running in-sequence focusing
2408  // as it could have changed for whatever reason (e.g. alignment used a different filter).
2409  // Then when focus process begins with the _target_ filter in place, it should take all the necessary actions to make it
2410  // work for the next set of captures. This is direct reset to the filter device, not via Filter Manager.
2411  if (meridianFlipStage != MF_NONE && m_captureDeviceAdaptor->getFilterWheel())
2412  {
2413  int targetFilterPosition = activeJob->getTargetFilter();
2414  int currentFilterPosition = m_captureDeviceAdaptor->getFilterManager()->getFilterPosition();
2415  if (targetFilterPosition > 0 && targetFilterPosition != currentFilterPosition)
2416  m_captureDeviceAdaptor->getFilterWheel()->setPosition(targetFilterPosition);
2417  }
2418 
2419  if (m_captureDeviceAdaptor->getActiveCamera()->isFastExposureEnabled())
2420  m_captureDeviceAdaptor->getActiveChip()->abortExposure();
2421 
2422  setFocusStatus(FOCUS_PROGRESS);
2423  emit checkFocus(m_LimitsUI->limitFocusHFRN->value() == 0.0 ? 0.1 : m_LimitsUI->limitFocusHFRN->value());
2424 
2425  qCDebug(KSTARS_EKOS_CAPTURE) << "In-sequence focusing started...";
2426  m_State = CAPTURE_FOCUSING;
2427  emit newStatus(Ekos::CAPTURE_FOCUSING);
2428  return true;
2429  }
2430 
2431  return false;
2432 }
2433 
2435 {
2436  if (m_FocusState >= FOCUS_PROGRESS)
2437  {
2438  appendLogText(i18n("Cannot capture while focus module is busy."));
2439  }
2440  // else if (captureEncodingS->currentIndex() == ISD::Camera::FORMAT_NATIVE && darkSubCheck->isChecked())
2441  // {
2442  // appendLogText(i18n("Cannot perform auto dark subtraction of native DSLR formats."));
2443  // }
2444  else if (addJob(true))
2445  {
2446  m_State = CAPTURE_PROGRESS;
2447  prepareJob(jobs.last());
2448  }
2449 }
2450 
2452 {
2453  if (m_FocusState >= FOCUS_PROGRESS)
2454  {
2455  appendLogText(i18n("Cannot start framing while focus module is busy."));
2456  }
2457  else if (!m_isFraming)
2458  {
2459  m_isFraming = true;
2460  appendLogText(i18n("Starting framing..."));
2461  captureOne();
2462  }
2463 }
2464 
2465 void Capture::updateTargetDistance(double targetDiff)
2466 {
2467  // ensure that the drift is visible
2468  targetDriftLabel->setVisible(true);
2469  targetDrift->setVisible(true);
2470  targetDriftUnit->setVisible(true);
2471  // update the drift value
2472  targetDrift->setText(QString("%L1").arg(targetDiff, 0, 'd', 1));
2473 }
2474 
2476 {
2477  if (activeJob == nullptr)
2478  return;
2479 
2480  // This test must be placed before the FOCUS_PROGRESS test,
2481  // as sometimes the FilterManager can cause an auto-focus.
2482  // If the filterManager is not IDLE, then try again in 1 second.
2483  switch (m_FilterManagerState)
2484  {
2485  case FILTER_IDLE:
2486  // do nothing
2487  break;
2488 
2489  case FILTER_AUTOFOCUS:
2491  return;
2492 
2493  case FILTER_CHANGE:
2495  return;
2496 
2497  case FILTER_OFFSET:
2499  return;
2500  }
2501 
2502  // Do not start nor abort if Focus is busy
2503  if (m_FocusState >= FOCUS_PROGRESS)
2504  {
2505  //appendLogText(i18n("Delaying capture while focus module is busy."));
2507  return;
2508  }
2509 
2510  // Bail out if we have no CCD anymore
2511  if (m_captureDeviceAdaptor->getActiveCamera()->isConnected() == false)
2512  {
2513  appendLogText(i18n("Error: Lost connection to CCD."));
2514  abort();
2515  return;
2516  }
2517 
2518  captureTimeout.stop();
2519  seqDelayTimer->stop();
2520  captureDelayTimer->stop();
2521 
2522  CAPTUREResult rc = CAPTURE_OK;
2523 
2524  if (m_captureDeviceAdaptor->getFilterWheel() != nullptr)
2525  {
2526  // JM 2021.08.23 Call filter info to set the active filter wheel in the camera driver
2527  // so that it may snoop on the active filter
2528  syncFilterInfo();
2529  m_CurrentFilterPosition = m_captureDeviceAdaptor->getFilterManager()->getFilterPosition();
2530  activeJob->setCurrentFilter(m_CurrentFilterPosition);
2531  }
2532 
2533  if (m_captureDeviceAdaptor->getActiveCamera()->isFastExposureEnabled())
2534  {
2535  int remaining = m_isFraming ? 100000 : (activeJob->getCoreProperty(SequenceJob::SJ_Count).toInt() -
2536  activeJob->getCompleted());
2537  if (remaining > 1)
2538  m_captureDeviceAdaptor->getActiveCamera()->setFastCount(static_cast<uint>(remaining));
2539  }
2540 
2541  connect(m_captureDeviceAdaptor->getActiveCamera(), &ISD::Camera::newImage, this, &Ekos::Capture::processData,
2543  //connect(currentCCD, &ISD::Camera::previewFITSGenerated, this, &Ekos::Capture::setGeneratedPreviewFITS, Qt::UniqueConnection);
2544 
2545  if (activeJob->getFrameType() == FRAME_FLAT)
2546  {
2547  // If we have to calibrate ADU levels, first capture must be preview and not in batch mode
2548  if (activeJob->getCoreProperty(SequenceJob::SJ_Preview).toBool() == false
2549  && activeJob->getFlatFieldDuration() == DURATION_ADU &&
2550  activeJob->getCalibrationStage() == SequenceJobState::CAL_NONE)
2551  {
2552  if (m_captureDeviceAdaptor->getActiveCamera()->getEncodingFormat() != "FITS")
2553  {
2554  appendLogText(i18n("Cannot calculate ADU levels in non-FITS images."));
2555  abort();
2556  return;
2557  }
2558 
2559  activeJob->setCalibrationStage(SequenceJobState::CAL_CALIBRATION);
2560  }
2561  }
2562 
2563  // If preview, always set to UPLOAD_CLIENT if not already set.
2564  if (activeJob->getCoreProperty(SequenceJob::SJ_Preview).toBool())
2565  {
2566  if (m_captureDeviceAdaptor->getActiveCamera()->getUploadMode() != ISD::Camera::UPLOAD_CLIENT)
2567  m_captureDeviceAdaptor->getActiveCamera()->setUploadMode(ISD::Camera::UPLOAD_CLIENT);
2568  }
2569  // If batch mode, ensure upload mode mathces the active job target.
2570  else
2571  {
2572  if (m_captureDeviceAdaptor->getActiveCamera()->getUploadMode() != activeJob->getUploadMode())
2573  m_captureDeviceAdaptor->getActiveCamera()->setUploadMode(activeJob->getUploadMode());
2574  }
2575 
2576  if (m_captureDeviceAdaptor->getActiveCamera()->getUploadMode() != ISD::Camera::UPLOAD_LOCAL)
2577  {
2578  checkSeqBoundary(activeJob->getSignature());
2579  m_captureDeviceAdaptor->getActiveCamera()->setNextSequenceID(nextSequenceID);
2580  }
2581 
2582  m_State = CAPTURE_CAPTURING;
2583 
2584  //if (activeJob->getCoreProperty(SequenceJob::SJ_Preview).toBool() == false)
2585  // NOTE: Why we didn't emit this before for preview?
2586  emit newStatus(Ekos::CAPTURE_CAPTURING);
2587 
2588  if (frameSettings.contains(m_captureDeviceAdaptor->getActiveChip()))
2589  {
2590  const auto roi = activeJob->getCoreProperty(SequenceJob::SJ_ROI).toRect();
2591  QVariantMap settings;
2592  settings["x"] = roi.x();
2593  settings["y"] = roi.y();
2594  settings["w"] = roi.width();
2595  settings["h"] = roi.height();
2596  settings["binx"] = activeJob->getCoreProperty(SequenceJob::SJ_Binning).toPoint().x();
2597  settings["biny"] = activeJob->getCoreProperty(SequenceJob::SJ_Binning).toPoint().y();
2598 
2599  frameSettings[m_captureDeviceAdaptor->getActiveChip()] = settings;
2600  }
2601 
2602  // Re-enable fast exposure if it was disabled before due to pending tasks
2603  if (m_RememberFastExposure)
2604  {
2605  m_RememberFastExposure = false;
2606  m_captureDeviceAdaptor->getActiveCamera()->setFastExposureEnabled(true);
2607  }
2608 
2609  // If using DSLR, make sure it is set to correct transfer format
2610  m_captureDeviceAdaptor->getActiveCamera()->setEncodingFormat(activeJob->getCoreProperty(
2611  SequenceJob::SJ_Encoding).toString());
2612 
2613  connect(m_captureDeviceAdaptor->getActiveCamera(), &ISD::Camera::newExposureValue, this,
2614  &Ekos::Capture::setExposureProgress,
2616 
2617  // necessary since the status widget doesn't store the calibration stage
2618  if (activeJob->getCalibrationStage() == SequenceJobState::CAL_CALIBRATION)
2619  captureStatusWidget->setStatus(i18n("Calibrating..."), Qt::yellow);
2620 
2621  rc = activeJob->capture(m_AutoFocusReady,
2622  activeJob->getCalibrationStage() == SequenceJobState::CAL_CALIBRATION ? FITS_CALIBRATE : FITS_NORMAL);
2623 
2624  if (rc != CAPTURE_OK)
2625  {
2626  disconnect(m_captureDeviceAdaptor->getActiveCamera(), &ISD::Camera::newExposureValue, this,
2627  &Ekos::Capture::setExposureProgress);
2628  }
2629  switch (rc)
2630  {
2631  case CAPTURE_OK:
2632  {
2633  emit captureStarting(activeJob->getCoreProperty(SequenceJob::SJ_Exposure).toDouble(),
2634  activeJob->getCoreProperty(SequenceJob::SJ_Filter).toString());
2635  appendLogText(i18n("Capturing %1-second %2 image...",
2636  QString("%L1").arg(activeJob->getCoreProperty(SequenceJob::SJ_Exposure).toDouble(), 0, 'f', 3),
2637  activeJob->getCoreProperty(SequenceJob::SJ_Filter).toString()));
2638  captureTimeout.start(static_cast<int>(activeJob->getCoreProperty(SequenceJob::SJ_Exposure).toDouble()) * 1000 +
2639  CAPTURE_TIMEOUT_THRESHOLD);
2640  // calculate remaining capture time for the current job
2641  imageCountDown.setHMS(0, 0, 0);
2642  double ms_left = std::ceil(activeJob->getExposeLeft() * 1000.0);
2643  imageCountDown = imageCountDown.addMSecs(int(ms_left));
2644  lastRemainingFrameTimeMS = ms_left;
2645  sequenceCountDown.setHMS(0, 0, 0);
2646  sequenceCountDown = sequenceCountDown.addSecs(getActiveJobRemainingTime());
2647  frameInfoLabel->setText(QString("%1 %2 (%L3/%L4):").arg(CCDFrameTypeNames[activeJob->getFrameType()])
2648  .arg(activeJob->getCoreProperty(SequenceJob::SJ_Filter).toString())
2649  .arg(activeJob->getCompleted()).arg(activeJob->getCoreProperty(SequenceJob::SJ_Count).toInt()));
2650  // ensure that the download time label is visible
2651  avgDownloadTime->setVisible(true);
2652  avgDownloadLabel->setVisible(true);
2653  secLabel->setVisible(true);
2654  // show estimated download time
2655  avgDownloadTime->setText(QString("%L1").arg(getEstimatedDownloadTime(), 0, 'd', 2));
2656 
2657  if (activeJob->getCoreProperty(SequenceJob::SJ_Preview).toBool() == false)
2658  {
2659  int index = jobs.indexOf(activeJob);
2660  QJsonObject oneSequence = m_SequenceArray[index].toObject();
2661  oneSequence["Status"] = "In Progress";
2662  m_SequenceArray.replace(index, oneSequence);
2663  emit sequenceChanged(m_SequenceArray);
2664  }
2665  }
2666  break;
2667 
2668  case CAPTURE_FRAME_ERROR:
2669  appendLogText(i18n("Failed to set sub frame."));
2670  abort();
2671  break;
2672 
2673  case CAPTURE_BIN_ERROR:
2674  appendLogText(i18n("Failed to set binning."));
2675  abort();
2676  break;
2677 
2678  case CAPTURE_FILTER_BUSY:
2679  // Try again in 1 second if filter is busy
2681  break;
2682 
2683  case CAPTURE_GUIDER_DRIFT_WAIT:
2684  // Try again in 1 second if filter is busy
2685  qCDebug(KSTARS_EKOS_CAPTURE) << "Waiting for the guider to settle.";
2687  break;
2688 
2689  case CAPTURE_FOCUS_ERROR:
2690  appendLogText(i18n("Cannot capture while focus module is busy."));
2691  abort();
2692  break;
2693  }
2694 }
2695 
2696 /*******************************************************************************/
2697 /* Update the prefix for the sequence of images to be captured */
2698 /*******************************************************************************/
2699 void Capture::updateSequencePrefix(const QString &newPrefix, const QString &dir)
2700 {
2701  seqPrefix = newPrefix;
2702 
2703  // If it doesn't exist, create it
2704  QDir().mkpath(dir);
2705 
2706  nextSequenceID = 1;
2707 }
2708 
2709 /*******************************************************************************/
2710 /* Determine the next file number sequence. That is, if we have file1.png */
2711 /* and file2.png, then the next sequence should be file3.png */
2712 /*******************************************************************************/
2713 void Capture::checkSeqBoundary(const QString &path)
2714 {
2715  int newFileIndex = -1;
2716  QFileInfo const path_info(path);
2717  QString const sig_dir(path_info.dir().path());
2718  QString const sig_file(path_info.completeBaseName());
2719  QString tempName;
2720  // seqFileCount = 0;
2721 
2722  // No updates during meridian flip
2723  if (meridianFlipStage >= MF_ALIGNING)
2724  return;
2725 
2726  QDirIterator it(sig_dir, QDir::Files);
2727 
2728  while (it.hasNext())
2729  {
2730  tempName = it.next();
2731  QFileInfo info(tempName);
2732 
2733  // This returns the filename without the extension
2734  tempName = info.completeBaseName();
2735 
2736  // This remove any additional extension (e.g. m42_001.fits.fz)
2737  // the completeBaseName() would return m42_001.fits
2738  // and this remove .fits so we end up with m42_001
2739  tempName = tempName.remove(".fits");
2740 
2741  QString finalSeqPrefix = seqPrefix;
2742  finalSeqPrefix.remove(SequenceJob::ISOMarker);
2743  // find the prefix first
2744  if (tempName.startsWith(finalSeqPrefix, Qt::CaseInsensitive) == false)
2745  continue;
2746 
2747  /* Do not change the number of captures.
2748  * - If the sequence is required by the end-user, unconditionally run what each sequence item is requiring.
2749  * - If the sequence is required by the scheduler, use capturedFramesMap to determine when to stop capturing.
2750  */
2751  //seqFileCount++;
2752 
2753  int lastUnderScoreIndex = tempName.lastIndexOf("_");
2754  if (lastUnderScoreIndex > 0)
2755  {
2756  bool indexOK = false;
2757 
2758  newFileIndex = tempName.midRef(lastUnderScoreIndex + 1).toInt(&indexOK);
2759  if (indexOK && newFileIndex >= nextSequenceID)
2760  nextSequenceID = newFileIndex + 1;
2761  }
2762  }
2763 }
2764 
2765 void Capture::appendLogText(const QString &text)
2766 {
2767  m_LogText.insert(0, i18nc("log entry; %1 is the date, %2 is the text", "%1 %2",
2768  KStarsData::Instance()->lt().toString("yyyy-MM-ddThh:mm:ss"), text));
2769 
2770  qCInfo(KSTARS_EKOS_CAPTURE) << text;
2771 
2772  emit newLog(text);
2773 }
2774 
2775 void Capture::clearLog()
2776 {
2777  m_LogText.clear();
2778  emit newLog(QString());
2779 }
2780 
2781 //This method will update the Capture Module and Summary Screen's estimate of how much time is left in the download
2782 void Capture::setDownloadProgress()
2783 {
2784  if (activeJob)
2785  {
2786  double downloadTimeLeft = getEstimatedDownloadTime() - m_DownloadTimer.elapsed() / 1000.0;
2787  if(downloadTimeLeft > 0)
2788  {
2789  imageCountDown.setHMS(0, 0, 0);
2790  imageCountDown = imageCountDown.addSecs(int(std::ceil(downloadTimeLeft)));
2791  frameRemainingTime->setText(imageCountDown.toString("hh:mm:ss"));
2792  emit newDownloadProgress(downloadTimeLeft);
2793  }
2794  }
2795 }
2796 
2797 void Capture::setExposureProgress(ISD::CameraChip * tChip, double value, IPState state)
2798 {
2799  if (m_captureDeviceAdaptor->getActiveChip() != tChip ||
2800  m_captureDeviceAdaptor->getActiveChip()->getCaptureMode() != FITS_NORMAL || meridianFlipStage >= MF_ALIGNING)
2801  return;
2802 
2803  double deltaMS = std::ceil(1000.0 * value - lastRemainingFrameTimeMS);
2804  updateCaptureCountDown(int(deltaMS));
2805  lastRemainingFrameTimeMS += deltaMS;
2806 
2807  if (activeJob)
2808  {
2809  activeJob->setExposeLeft(value);
2810 
2811  emit newExposureProgress(activeJob);
2812  }
2813 
2814  if (activeJob && state == IPS_ALERT)
2815  {
2816  int retries = activeJob->getCaptureRetires() + 1;
2817 
2818  activeJob->setCaptureRetires(retries);
2819 
2820  appendLogText(i18n("Capture failed. Check INDI Control Panel for details."));
2821 
2822  if (retries == 3)
2823  {
2824  abort();
2825  return;
2826  }
2827 
2828  appendLogText(i18n("Restarting capture attempt #%1", retries));
2829 
2830  nextSequenceID = 1;
2831 
2832  captureImage();
2833  return;
2834  }
2835 
2836  if (activeJob != nullptr && state == IPS_OK)
2837  {
2838  activeJob->setCaptureRetires(0);
2839  activeJob->setExposeLeft(0);
2840 
2841  if (m_captureDeviceAdaptor->getActiveCamera()
2842  && m_captureDeviceAdaptor->getActiveCamera()->getUploadMode() == ISD::Camera::UPLOAD_LOCAL)
2843  {
2844  if (activeJob && activeJob->getStatus() == JOB_BUSY)
2845  {
2846  processData(nullptr);
2847  return;
2848  }
2849  }
2850 
2851  //if (isAutoGuiding && Options::useEkosGuider() && currentCCD->getChip(ISD::CameraChip::GUIDE_CCD) == guideChip)
2852  if (m_GuideState == GUIDE_GUIDING && Options::guiderType() == 0 && suspendGuideOnDownload)
2853  {
2854  qCDebug(KSTARS_EKOS_CAPTURE) << "Autoguiding suspended until primary CCD chip completes downloading...";
2855  emit suspendGuiding();
2856  }
2857 
2858  captureStatusWidget->setStatus(i18n("Downloading..."), Qt::yellow);
2859 
2860  //This will start the clock to see how long the download takes.
2861  m_DownloadTimer.start();
2862  downloadProgressTimer.start();
2863 
2864 
2865  //disconnect(currentCCD, &ISD::Camera::newExposureValue(ISD::CameraChip*,double,IPState)), this, &Ekos::Capture::updateCaptureProgress(ISD::CameraChip*,double,IPState)));
2866  }
2867 }
2868 
2869 void Capture::updateCaptureCountDown(int deltaMillis)
2870 {
2871  imageCountDown = imageCountDown.addMSecs(deltaMillis);
2872  sequenceCountDown = sequenceCountDown.addMSecs(deltaMillis);
2873  frameRemainingTime->setText(imageCountDown.toString("hh:mm:ss"));
2874  jobRemainingTime->setText(sequenceCountDown.toString("hh:mm:ss"));
2875 }
2876 
2877 void Capture::processCaptureError(ISD::Camera::ErrorType type)
2878 {
2879  if (!activeJob)
2880  return;
2881 
2882  if (type == ISD::Camera::ERROR_CAPTURE)
2883  {
2884  int retries = activeJob->getCaptureRetires() + 1;
2885 
2886  activeJob->setCaptureRetires(retries);
2887 
2888  appendLogText(i18n("Capture failed. Check INDI Control Panel for details."));
2889 
2890  if (retries == 3)
2891  {
2892  abort();
2893  return;
2894  }
2895 
2896  appendLogText(i18n("Restarting capture attempt #%1", retries));
2897 
2898  nextSequenceID = 1;
2899 
2900  captureImage();
2901  return;
2902  }
2903  else
2904  {
2905  abort();
2906  }
2907 }
2908 
2909 void Capture::setActiveJob(SequenceJob *value)
2910 {
2911  // do nothing if active job is not changed
2912  if (activeJob == value)
2913  return;
2914 
2915  // clear existing job connections
2916  if (activeJob != nullptr)
2917  {
2918  disconnect(this, nullptr, activeJob, nullptr);
2919  disconnect(activeJob, nullptr, this, nullptr);
2920  }
2921 
2922  // set the new value
2923  activeJob = value;
2924 
2925  // create job connections
2926  if (activeJob != nullptr)
2927  {
2928  // forward signals to the sequence job
2929  connect(this, &Capture::newGuiderDrift, activeJob, &SequenceJob::updateGuiderDrift);
2930  // react upon sequence job signals
2931  connect(activeJob, &SequenceJob::prepareState, this, &Capture::updatePrepareState);
2932  connect(activeJob, &SequenceJob::prepareComplete, this, &Capture::executeJob);
2933  connect(activeJob, &SequenceJob::abortCapture, this, &Capture::abort);
2934  connect(activeJob, &SequenceJob::newLog, this, &Capture::newLog);
2935  // forward the devices and attributes
2936  activeJob->setLightBox(m_captureDeviceAdaptor->getLightBox());
2937  activeJob->addMount(m_captureDeviceAdaptor->getMount());
2938  activeJob->setDome(m_captureDeviceAdaptor->getDome());
2939  activeJob->setDustCap(m_captureDeviceAdaptor->getDustCap());
2940  activeJob->setFilterManager(m_captureDeviceAdaptor->getFilterManager());
2941  activeJob->setAutoFocusReady(m_AutoFocusReady);
2942  }
2943 }
2944 
2946 {
2947  if (cameraTemperatureS->isEnabled() == false && m_captureDeviceAdaptor->getActiveCamera())
2948  {
2949  if (m_captureDeviceAdaptor->getActiveCamera()->getPermission("CCD_TEMPERATURE") != IP_RO)
2950  checkCamera();
2951  }
2952 
2953  temperatureOUT->setText(QString("%L1").arg(value, 0, 'f', 2));
2954 
2955  if (cameraTemperatureN->cleanText().isEmpty())
2956  cameraTemperatureN->setValue(value);
2957 }
2958 
2959 void Capture::updateRotatorAngle(double value)
2960 {
2961  // Update widget rotator position
2962  rotatorSettings->setCurrentAngle(value);
2963 }
2964 
2966 {
2967  return addJob(false, false);
2968 }
2969 
2970 bool Capture::addJob(bool preview, bool isDarkFlat)
2971 {
2972  if (m_State != CAPTURE_IDLE && m_State != CAPTURE_ABORTED && m_State != CAPTURE_COMPLETE)
2973  return false;
2974 
2975  SequenceJob * job = nullptr;
2976 
2977  // if (preview == false && darkSubCheck->isChecked())
2978  // {
2979  // KSNotification::error(i18n("Auto dark subtract is not supported in batch mode."));
2980  // return false;
2981  // }
2982 
2983  if (fileUploadModeS->currentIndex() != ISD::Camera::UPLOAD_CLIENT && fileRemoteDirT->text().isEmpty())
2984  {
2985  KSNotification::error(i18n("You must set remote directory for Local & Both modes."));
2986  return false;
2987  }
2988 
2989  if (fileUploadModeS->currentIndex() != ISD::Camera::UPLOAD_LOCAL && fileDirectoryT->text().isEmpty())
2990  {
2991  KSNotification::error(i18n("You must set local directory for Client & Both modes."));
2992  return false;
2993  }
2994 
2995  if (m_JobUnderEdit)
2996  job = jobs.at(queueTable->currentRow());
2997  else
2998  {
2999  job = new SequenceJob(m_captureDeviceAdaptor, m_captureState);
3000  job->setFilterManager(m_captureDeviceAdaptor->getFilterManager());
3001  }
3002 
3003  Q_ASSERT_X(job, __FUNCTION__, "Capture Job is invalid.");
3004 
3005  job->setCoreProperty(SequenceJob::SJ_Format, captureFormatS->currentText());
3006  job->setCoreProperty(SequenceJob::SJ_Encoding, captureEncodingS->currentText());
3007  job->setCoreProperty(SequenceJob::SJ_DarkFlat, isDarkFlat);
3008 
3009  if (captureISOS)
3010  job->setCoreProperty(SequenceJob::SJ_ISOIndex, captureISOS->currentIndex());
3011 
3012  if (getGain() >= 0)
3013  job->setCoreProperty(SequenceJob::SJ_Gain, getGain());
3014 
3015  if (getOffset() >= 0)
3016  job->setCoreProperty(SequenceJob::SJ_Offset, getOffset());
3017 
3018  job->setCoreProperty(SequenceJob::SJ_Encoding, captureEncodingS->currentText());
3019  job->setCoreProperty(SequenceJob::SJ_Preview, preview);
3020 
3021  if (cameraTemperatureN->isEnabled())
3022  {
3023  // double currentTemperature;
3024  // currentCCD->getTemperature(&currentTemperature);
3025  job->setCoreProperty(SequenceJob::SJ_EnforceTemperature, cameraTemperatureS->isChecked());
3026  job->setTargetTemperature(cameraTemperatureN->value());
3027  }
3028 
3029  //job->setCaptureFilter(static_cast<FITSScale>(filterCombo->currentIndex()));
3030 
3031  job->setUploadMode(static_cast<ISD::Camera::UploadMode>(fileUploadModeS->currentIndex()));
3032  job->setScripts(m_Scripts);
3033  job->setFlatFieldDuration(flatFieldDuration);
3034  job->setFlatFieldSource(flatFieldSource);
3035  job->setPreMountPark(preMountPark);
3036  job->setPreDomePark(preDomePark);
3037  job->setWallCoord(wallCoord);
3038  job->setCoreProperty(SequenceJob::SJ_TargetADU, targetADU);
3039  job->setCoreProperty(SequenceJob::SJ_TargetADUTolerance, targetADUTolerance);
3040 
3041  // JM 2019-11-26: In case there is no raw prefix set
3042  // BUT target name is set, we update the prefix to include
3043  // the target name, which is usually set by the scheduler.
3044  if (!m_TargetName.isEmpty())
3045  {
3046  // Target name as set externally should override Full Target Name that
3047  // was set by GOTO operation alone.
3048  m_FullTargetName = m_TargetName;
3049  m_FullTargetName.replace("_", " ");
3050  if (filePrefixT->text().isEmpty())
3051  filePrefixT->setText(m_TargetName);
3052  }
3053 
3054  job->setCoreProperty(SequenceJob::SJ_RawPrefix, filePrefixT->text());
3055  job->setCoreProperty(SequenceJob::SJ_FilterPrefixEnabled, fileFilterS->isChecked());
3056  job->setCoreProperty(SequenceJob::SJ_ExpPrefixEnabled, fileDurationS->isChecked());
3057  job->setCoreProperty(SequenceJob::SJ_TimeStampPrefixEnabled, fileTimestampS->isChecked());
3058  job->setFrameType(static_cast<CCDFrameType>(captureTypeS->currentIndex()));
3059 
3060  job->setCoreProperty(SequenceJob::SJ_EnforceStartGuiderDrift, (job->getFrameType() == FRAME_LIGHT
3061  && m_LimitsUI->startGuiderDriftS->isChecked()));
3062  job->setTargetStartGuiderDrift(m_LimitsUI->startGuiderDriftN->value());
3063 
3064  //if (filterSlot != nullptr && currentFilter != nullptr)
3065  if (captureFilterS->currentIndex() != -1 && m_captureDeviceAdaptor->getFilterWheel() != nullptr)
3066  job->setTargetFilter(captureFilterS->currentIndex() + 1, captureFilterS->currentText());
3067 
3068  job->setCoreProperty(SequenceJob::SJ_Exposure, captureExposureN->value());
3069 
3070  job->setCoreProperty(SequenceJob::SJ_Count, captureCountN->value());
3071 
3072  job->setCoreProperty(SequenceJob::SJ_Binning, QPoint(captureBinHN->value(), captureBinVN->value()));
3073 
3074  /* in ms */
3075  job->setCoreProperty(SequenceJob::SJ_Delay, captureDelayN->value() * 1000);
3076 
3077  // Custom Properties
3078  job->setCustomProperties(customPropertiesDialog->getCustomProperties());
3079 
3080  if (m_captureDeviceAdaptor->getRotator() && rotatorSettings->isRotationEnforced())
3081  {
3082  job->setTargetRotation(rotatorSettings->getTargetRotationPA());
3083  }
3084 
3085  job->setCoreProperty(SequenceJob::SJ_ROI, QRect(captureFrameXN->value(), captureFrameYN->value(), captureFrameWN->value(),
3086  captureFrameHN->value()));
3087  job->setCoreProperty(SequenceJob::SJ_RemoteDirectory, fileRemoteDirT->text());
3088 
3089  // Remove trailing slash, if any.
3090  QString fileDirectory = fileDirectoryT->text();
3091  while (fileDirectory.endsWith("/"))
3092  fileDirectory.chop(1);
3093  job->setCoreProperty(SequenceJob::SJ_LocalDirectory, fileDirectory);
3094 
3095  if (m_JobUnderEdit == false)
3096  {
3097  // JM 2018-09-24: If this is the first job added
3098  // We always ignore job progress by default.
3099  if (jobs.isEmpty() && preview == false)
3100  ignoreJobProgress = true;
3101 
3102  jobs.append(job);
3103 
3104  // Nothing more to do if preview
3105  if (preview)
3106  return true;
3107  }
3108 
3109  QJsonObject jsonJob = {{"Status", "Idle"}};
3110 
3111  auto placeholderPath = Ekos::PlaceholderPath();
3112  placeholderPath.addJob(job, m_TargetName);
3113 
3114  int currentRow = 0;
3115  if (m_JobUnderEdit == false)
3116  {
3117  currentRow = queueTable->rowCount();
3118  queueTable->insertRow(currentRow);
3119  }
3120  else
3121  currentRow = queueTable->currentRow();
3122 
3123  QTableWidgetItem * status = m_JobUnderEdit ? queueTable->item(currentRow, 0) : new QTableWidgetItem();
3124  job->setStatusCell(status);
3125 
3126  QTableWidgetItem * filter = m_JobUnderEdit ? queueTable->item(currentRow, 1) : new QTableWidgetItem();
3127  filter->setText("--");
3128  jsonJob.insert("Filter", "--");
3129  if (captureFilterS->count() > 0 && (captureTypeS->currentIndex() == FRAME_LIGHT
3130  || captureTypeS->currentIndex() == FRAME_FLAT || isDarkFlat))
3131  {
3132  filter->setText(captureFilterS->currentText());
3133  jsonJob.insert("Filter", captureFilterS->currentText());
3134  }
3135 
3136  filter->setTextAlignment(Qt::AlignHCenter);
3137  filter->setFlags(Qt::ItemIsSelectable | Qt::ItemIsEnabled);
3138 
3139  QTableWidgetItem * count = m_JobUnderEdit ? queueTable->item(currentRow, 2) : new QTableWidgetItem();
3140  job->setCountCell(count);
3141  jsonJob.insert("Count", count->text());
3142 
3143  QTableWidgetItem * exp = m_JobUnderEdit ? queueTable->item(currentRow, 3) : new QTableWidgetItem();
3144  exp->setText(QString("%L1").arg(captureExposureN->value(), 0, 'f', captureExposureN->decimals()));
3147  jsonJob.insert("Exp", exp->text());
3148 
3149  QTableWidgetItem * type = m_JobUnderEdit ? queueTable->item(currentRow, 4) : new QTableWidgetItem();
3150  type->setText(isDarkFlat ? i18n("Dark Flat") : captureTypeS->currentText());
3151  type->setTextAlignment(Qt::AlignHCenter);
3152  type->setFlags(Qt::ItemIsSelectable | Qt::ItemIsEnabled);
3153  jsonJob.insert("Type", isDarkFlat ? i18n("Dark Flat") : type->text());
3154 
3155  QTableWidgetItem * bin = m_JobUnderEdit ? queueTable->item(currentRow, 5) : new QTableWidgetItem();
3156  bin->setText(QString("%1x%2").arg(captureBinHN->value()).arg(captureBinVN->value()));
3157  bin->setTextAlignment(Qt::AlignHCenter);
3158  bin->setFlags(Qt::ItemIsSelectable | Qt::ItemIsEnabled);
3159  jsonJob.insert("Bin", bin->text());
3160 
3161  QTableWidgetItem * iso = m_JobUnderEdit ? queueTable->item(currentRow, 6) : new QTableWidgetItem();
3162  if (captureISOS && captureISOS->currentIndex() != -1)
3163  {
3164  iso->setText(captureISOS->currentText());
3165  jsonJob.insert("ISO/Gain", iso->text());
3166  }
3167  else if (job->getCoreProperty(SequenceJob::SJ_Gain).toDouble() >= 0)
3168  {
3169  iso->setText(QString::number(job->getCoreProperty(SequenceJob::SJ_Gain).toDouble(), 'f', 1));
3170  jsonJob.insert("ISO/Gain", iso->text());
3171  }
3172  else
3173  {
3174  iso->setText("--");
3175  jsonJob.insert("ISO/Gain", "--");
3176  }
3179 
3180  QTableWidgetItem * offset = m_JobUnderEdit ? queueTable->item(currentRow, 7) : new QTableWidgetItem();
3181  if (job->getCoreProperty(SequenceJob::SJ_Offset).toDouble() >= 0)
3182  {
3183  offset->setText(QString::number(job->getCoreProperty(SequenceJob::SJ_Offset).toDouble(), 'f', 1));
3184  jsonJob.insert("Offset", offset->text());
3185  }
3186  else
3187  {
3188  offset->setText("--");
3189  jsonJob.insert("Offset", "--");
3190  }
3193 
3194  if (m_JobUnderEdit == false)
3195  {
3196  queueTable->setItem(currentRow, 0, status);
3197  queueTable->setItem(currentRow, 1, filter);
3198  queueTable->setItem(currentRow, 2, count);
3199  queueTable->setItem(currentRow, 3, exp);
3200  queueTable->setItem(currentRow, 4, type);
3201  queueTable->setItem(currentRow, 5, bin);
3202  queueTable->setItem(currentRow, 6, iso);
3203  queueTable->setItem(currentRow, 7, offset);
3204 
3205  m_SequenceArray.append(jsonJob);
3206  emit sequenceChanged(m_SequenceArray);
3207  }
3208 
3209  removeFromQueueB->setEnabled(true);
3210 
3211  if (queueTable->rowCount() > 0)
3212  {
3213  queueSaveAsB->setEnabled(true);
3214  queueSaveB->setEnabled(true);
3215  resetB->setEnabled(true);
3216  m_Dirty = true;
3217  }
3218 
3219  if (queueTable->rowCount() > 1)
3220  {
3221  queueUpB->setEnabled(true);
3222  queueDownB->setEnabled(true);
3223  }
3224 
3225  if (m_JobUnderEdit)
3226  {
3227  m_JobUnderEdit = false;
3228  resetJobEdit();
3229  appendLogText(i18n("Job #%1 changes applied.", currentRow + 1));
3230 
3231  m_SequenceArray.replace(currentRow, jsonJob);
3232  emit sequenceChanged(m_SequenceArray);
3233  }
3234 
3235  return true;
3236 }
3237 
3238 void Capture::removeJobFromQueue()
3239 {
3240  int currentRow = queueTable->currentRow();
3241 
3242  if (currentRow < 0)
3243  currentRow = queueTable->rowCount() - 1;
3244 
3245  removeJob(currentRow);
3246 
3247  // update selection
3248  if (queueTable->rowCount() == 0)
3249  return;
3250 
3251  if (currentRow > queueTable->rowCount())
3252  queueTable->selectRow(queueTable->rowCount() - 1);
3253  else
3254  queueTable->selectRow(currentRow);
3255 }
3256 
3257 bool Capture::removeJob(int index)
3258 {
3259  if (m_State != CAPTURE_IDLE && m_State != CAPTURE_ABORTED && m_State != CAPTURE_COMPLETE)
3260  return false;
3261 
3262  if (m_JobUnderEdit)
3263  {
3264  resetJobEdit();
3265  return false;
3266  }
3267 
3268  if (index < 0 || index >= jobs.count())
3269  return false;
3270 
3271 
3272  queueTable->removeRow(index);
3273  m_SequenceArray.removeAt(index);
3274  emit sequenceChanged(m_SequenceArray);
3275 
3276  if (jobs.empty())
3277  return true;
3278 
3279  SequenceJob * job = jobs.at(index);
3280  jobs.removeOne(job);
3281  if (job == activeJob)
3282  setActiveJob(nullptr);
3283 
3284  delete job;
3285 
3286  if (queueTable->rowCount() == 0)
3287  removeFromQueueB->setEnabled(false);
3288 
3289  if (queueTable->rowCount() == 1)
3290  {
3291  queueUpB->setEnabled(false);
3292  queueDownB->setEnabled(false);
3293  }
3294 
3295  for (int i = 0; i < jobs.count(); i++)
3296  jobs.at(i)->setStatusCell(queueTable->item(i, 0));
3297 
3298  if (index < queueTable->rowCount())
3299  queueTable->selectRow(index);
3300  else if (queueTable->rowCount() > 0)
3301  queueTable->selectRow(queueTable->rowCount() - 1);
3302 
3303  if (queueTable->rowCount() == 0)
3304  {
3305  queueSaveAsB->setEnabled(false);
3306  queueSaveB->setEnabled(false);
3307  resetB->setEnabled(false);
3308  }
3309 
3310  m_Dirty = true;
3311 
3312  return true;
3313 }
3314 
3316 {
3317  int currentRow = queueTable->currentRow();
3318 
3319  int columnCount = queueTable->columnCount();
3320 
3321  if (currentRow <= 0 || queueTable->rowCount() == 1)
3322  return;
3323 
3324  int destinationRow = currentRow - 1;
3325 
3326  for (int i = 0; i < columnCount; i++)
3327  {
3328  QTableWidgetItem * downItem = queueTable->takeItem(currentRow, i);
3329  QTableWidgetItem * upItem = queueTable->takeItem(destinationRow, i);
3330 
3331  queueTable->setItem(destinationRow, i, downItem);
3332  queueTable->setItem(currentRow, i, upItem);
3333  }
3334 
3335  SequenceJob * job = jobs.takeAt(currentRow);
3336 
3337  jobs.removeOne(job);
3338  jobs.insert(destinationRow, job);
3339 
3340  QJsonObject currentJob = m_SequenceArray[currentRow].toObject();
3341  m_SequenceArray.replace(currentRow, m_SequenceArray[destinationRow]);
3342  m_SequenceArray.replace(destinationRow, currentJob);
3343  emit sequenceChanged(m_SequenceArray);
3344 
3345  queueTable->selectRow(destinationRow);
3346 
3347  for (int i = 0; i < jobs.count(); i++)
3348  jobs.at(i)->setStatusCell(queueTable->item(i, 0));
3349 
3350  m_Dirty = true;
3351 }
3352 
3354 {
3355  int currentRow = queueTable->currentRow();
3356 
3357  int columnCount = queueTable->columnCount();
3358 
3359  if (currentRow < 0 || queueTable->rowCount() == 1 || (currentRow + 1) == queueTable->rowCount())
3360  return;
3361 
3362  int destinationRow = currentRow + 1;
3363 
3364  for (int i = 0; i < columnCount; i++)
3365  {
3366  QTableWidgetItem * downItem = queueTable->takeItem(currentRow, i);
3367  QTableWidgetItem * upItem = queueTable->takeItem(destinationRow, i);
3368 
3369  queueTable->setItem(destinationRow, i, downItem);
3370  queueTable->setItem(currentRow, i, upItem);
3371  }
3372 
3373  SequenceJob * job = jobs.takeAt(currentRow);
3374 
3375  jobs.removeOne(job);
3376  jobs.insert(destinationRow, job);
3377 
3378  QJsonObject currentJob = m_SequenceArray[currentRow].toObject();
3379  m_SequenceArray.replace(currentRow, m_SequenceArray[destinationRow]);
3380  m_SequenceArray.replace(destinationRow, currentJob);
3381  emit sequenceChanged(m_SequenceArray);
3382 
3383  queueTable->selectRow(destinationRow);
3384 
3385  for (int i = 0; i < jobs.count(); i++)
3386  jobs.at(i)->setStatusCell(queueTable->item(i, 0));
3387 
3388  m_Dirty = true;
3389 }
3390 
3391 void Capture::setBusy(bool enable)
3392 {
3393  isBusy = enable;
3394 
3395  previewB->setEnabled(!enable);
3396  loopB->setEnabled(!enable);
3397 
3398  foreach (QAbstractButton * button, queueEditButtonGroup->buttons())
3399  button->setEnabled(!enable);
3400 }
3401 
3402 /**
3403  * @brief Update the counters of existing frames and continue with prepareActiveJob(), if there exist less
3404  * images than targeted. If enough images exist, continue with processJobCompletion().
3405  */
3406 void Capture::prepareJob(SequenceJob * job)
3407 {
3408  setActiveJob(job);
3409 
3410  // If job is Preview and NO view is available, ask to enable it.
3411  // if job is batch job, then NO VIEW IS REQUIRED at all. It's optional.
3412  if (job->getCoreProperty(SequenceJob::SJ_Preview).toBool() && Options::useFITSViewer() == false
3413  && Options::useSummaryPreview() == false)
3414  {
3415  // ask if FITS viewer usage should be enabled
3416  connect(KSMessageBox::Instance(), &KSMessageBox::accepted, this, [ = ]()
3417  {
3418  KSMessageBox::Instance()->disconnect(this);
3419  Options::setUseFITSViewer(true);
3420  // restart
3421  prepareJob(job);
3422  });
3423  connect(KSMessageBox::Instance(), &KSMessageBox::rejected, this, [&]()
3424  {
3425  KSMessageBox::Instance()->disconnect(this);
3426  abort();
3427  });
3428  KSMessageBox::Instance()->questionYesNo(i18n("No view available for previews. Enable FITS viewer?"),
3429  i18n("Display preview"), 15);
3430  // do nothing because currently none of the previews is active.
3431  return;
3432  }
3433 
3434  if (m_isFraming == false)
3435  qCDebug(KSTARS_EKOS_CAPTURE) << "Preparing capture job" << job->getSignature() << "for execution.";
3436 
3437  int index = jobs.indexOf(job);
3438  if (index >= 0)
3439  queueTable->selectRow(index);
3440 
3441  seqDelay = activeJob->getCoreProperty(SequenceJob::SJ_Delay).toInt();
3442 
3443  if (activeJob->getCoreProperty(SequenceJob::SJ_Preview).toBool() == false)
3444  {
3445  // set the progress info
3446  imgProgress->setEnabled(true);
3447  imgProgress->setMaximum(activeJob->getCoreProperty(SequenceJob::SJ_Count).toInt());
3448  imgProgress->setValue(activeJob->getCompleted());
3449 
3450  if (m_captureDeviceAdaptor->getActiveCamera()->getUploadMode() != ISD::Camera::UPLOAD_LOCAL)
3451  updateSequencePrefix(activeJob->getCoreProperty(SequenceJob::SJ_FullPrefix).toString(),
3452  QFileInfo(activeJob->getSignature()).path());
3453 
3454  // We check if the job is already fully or partially complete by checking how many files of its type exist on the file system
3455  // The signature is the unique identification path in the system for a particular job. Format is "<storage path>/<target>/<frame type>/<filter name>".
3456  // 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/...)
3457  // 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/...)
3458  QString signature = activeJob->getSignature();
3459 
3460  // Now check on the file system ALL the files that exist with the above signature
3461  // If 29 files exist for example, then nextSequenceID would be the NEXT file number (30)
3462  // Therefore, we know how to number the next file.
3463  // However, we do not deduce the number of captures to process from this function.
3464  checkSeqBoundary(signature);
3465 
3466  // Captured Frames Map contains a list of signatures:count of _already_ captured files in the file system.
3467  // This map is set by the Scheduler in order to complete efficiently the required captures.
3468  // When the end-user requests a sequence to be processed, that map is empty.
3469  //
3470  // Example with a 5xL-5xR-5xG-5xB sequence
3471  //
3472  // When the end-user loads and runs this sequence, each filter gets to capture 5 frames, then the procedure stops.
3473  // When the Scheduler executes a job with this sequence, the procedure depends on what is in the storage.
3474  //
3475  // Let's consider the Scheduler has 3 instances of this job to run.
3476  //
3477  // When the first job completes the sequence, there are 20 images in the file system (5 for each filter).
3478  // 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.
3479  // 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.
3480  //
3481  // 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.
3482  // 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.
3483  // When the sequence runs, the procedure will bypass LRG and proceed to capture 4xB.
3484  if (capturedFramesMap.contains(signature))
3485  {
3486  // Get the current capture count from the map
3487  int count = capturedFramesMap[signature];
3488 
3489  // Count how many captures this job has to process, given that previous jobs may have done some work already
3490  for (auto &a_job : jobs)
3491  if (a_job == activeJob)
3492  break;
3493  else if (a_job->getSignature() == activeJob->getSignature())
3494  count -= a_job->getCompleted();
3495 
3496  // This is the current completion count of the current job
3497  activeJob->setCompleted(count);
3498  }
3499  // JM 2018-09-24: Only set completed jobs to 0 IF the scheduler set captured frames map to begin with
3500  // If the map is empty, then no scheduler is used and it should proceed as normal.
3501  else if (capturedFramesMap.count() > 0)
3502  {
3503  // No preliminary information, we reset the job count and run the job unconditionally to clarify the behavior
3504  activeJob->setCompleted(0);
3505  }
3506  // JM 2018-09-24: In case ignoreJobProgress is enabled
3507  // We check if this particular job progress ignore flag is set. If not,
3508  // then we set it and reset completed to zero. Next time it is evaluated here again
3509  // It will maintain its count regardless
3510  else if (ignoreJobProgress && activeJob->getJobProgressIgnored() == false)
3511  {
3512  activeJob->setJobProgressIgnored(true);
3513  activeJob->setCompleted(0);
3514  }
3515  // 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
3516 
3517  // Check whether active job is complete by comparing required captures to what is already available
3518  if (activeJob->getCoreProperty(SequenceJob::SJ_Count).toInt() <= activeJob->getCompleted())
3519  {
3520  activeJob->setCompleted(activeJob->getCoreProperty(SequenceJob::SJ_Count).toInt());
3521  appendLogText(i18n("Job requires %1-second %2 images, has already %3/%4 captures and does not need to run.",
3522  QString("%L1").arg(job->getCoreProperty(SequenceJob::SJ_Exposure).toDouble(), 0, 'f', 3),
3523  job->getCoreProperty(SequenceJob::SJ_Filter).toString(),
3524  activeJob->getCompleted(), activeJob->getCoreProperty(SequenceJob::SJ_Count).toInt()));
3525  processJobCompletionStage2();
3526 
3527  /* FIXME: find a clearer way to exit here */
3528  return;
3529  }
3530  else
3531  {
3532  // There are captures to process
3533  appendLogText(i18n("Job requires %1-second %2 images, has %3/%4 frames captured and will be processed.",
3534  QString("%L1").arg(job->getCoreProperty(SequenceJob::SJ_Exposure).toDouble(), 0, 'f', 3),
3535  job->getCoreProperty(SequenceJob::SJ_Filter).toString(),
3536  activeJob->getCompleted(), activeJob->getCoreProperty(SequenceJob::SJ_Count).toInt()));
3537 
3538  // Emit progress update - done a few lines below
3539  // emit newImage(nullptr, activeJob);
3540 
3541  m_captureDeviceAdaptor->getActiveCamera()->setNextSequenceID(nextSequenceID);
3542  }
3543  }
3544 
3545  if (m_captureDeviceAdaptor->getActiveCamera()->isBLOBEnabled() == false)
3546  {
3547  // FIXME: Move this warning pop-up elsewhere, it will interfere with automation.
3548  // if (Options::guiderType() != Ekos::Guide::GUIDE_INTERNAL || KMessageBox::questionYesNo(nullptr, i18n("Image transfer is disabled for this camera. Would you like to enable it?")) ==
3549  // KMessageBox::Yes)
3550  if (Options::guiderType() != Ekos::Guide::GUIDE_INTERNAL)
3551  {
3552  m_captureDeviceAdaptor->getActiveCamera()->setBLOBEnabled(true);
3553  }
3554  else
3555  {
3556  connect(KSMessageBox::Instance(), &KSMessageBox::accepted, this, [this]()
3557  {
3558  //QObject::disconnect(KSMessageBox::Instance(), &KSMessageBox::accepted, this, nullptr);
3559  KSMessageBox::Instance()->disconnect(this);
3560  m_captureDeviceAdaptor->getActiveCamera()->setBLOBEnabled(true);
3562 
3563  });
3564  connect(KSMessageBox::Instance(), &KSMessageBox::rejected, this, [this]()
3565  {
3566  //QObject::disconnect(KSMessageBox::Instance(), &KSMessageBox::rejected, this, nullptr);
3567  KSMessageBox::Instance()->disconnect(this);
3568  m_captureDeviceAdaptor->getActiveCamera()->setBLOBEnabled(true);
3569  setBusy(false);
3570  });
3571 
3572  KSMessageBox::Instance()->questionYesNo(i18n("Image transfer is disabled for this camera. Would you like to enable it?"),
3573  i18n("Image Transfer"), 15);
3574 
3575  return;
3576  }
3577  }
3578 
3580 
3581 }
3582 
3584 {
3585  if (activeJob == nullptr)
3586  {
3587  qWarning(KSTARS_EKOS_CAPTURE) << "prepareActiveJobStage1 with null activeJob.";
3588  }
3589  else
3590  {
3591  // JM 2020-12-06: Check if we need to execute pre-job script first.
3592  const QString preJobScript = activeJob->getScript(SCRIPT_PRE_JOB);
3593  // Only run pre-job script for the first time and not after some images were captured but then stopped due to abort.
3594  if (!preJobScript.isEmpty() && activeJob->getCompleted() == 0)
3595  {
3596  m_CaptureScriptType = SCRIPT_PRE_JOB;
3597  m_CaptureScript.start(preJobScript, generateScriptArguments());
3598  appendLogText(i18n("Executing pre job script %1", preJobScript));
3599  return;
3600  }
3601  }
3603 }
3604 /**
3605  * @brief Reset #calibrationStage and continue with preparePreCaptureActions().
3606  */
3608 {
3609  // Just notification of active job stating up
3610  if (activeJob == nullptr)
3611  {
3612  qWarning(KSTARS_EKOS_CAPTURE) << "prepareActiveJobStage2 with null activeJob.";
3613  }
3614  else
3615  emit newImage(activeJob, m_ImageData);
3616 
3617 
3618  /* Disable this restriction, let the sequence run even if focus did not run prior to the capture.
3619  * Besides, this locks up the Scheduler when the Capture module starts a sequence without any prior focus procedure done.
3620  * This is quite an old code block. The message "Manual scheduled" seems to even refer to some manual intervention?
3621  * With the new HFR threshold, it might be interesting to prevent the execution because we actually need an HFR value to
3622  * 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,
3623  * and on the other hand the focus procedure will deduce the next HFR automatically.
3624  * But in the end, it's not entirely clear what the intent was. Note there is still a warning that a preliminary autofocus
3625  * procedure is important to avoid any surprise that could make the whole schedule ineffective.
3626  */
3627  if (activeJob != nullptr)
3628  {
3629  const QString preCaptureScript = activeJob->getScript(SCRIPT_PRE_CAPTURE);
3630  // JM 2020-12-06: Check if we need to execute pre-capture script first.
3631  if (!preCaptureScript.isEmpty())
3632  {
3633  m_CaptureScriptType = SCRIPT_PRE_CAPTURE;
3634  m_CaptureScript.start(preCaptureScript, generateScriptArguments());
3635  appendLogText(i18n("Executing pre capture script %1", preCaptureScript));
3636  return;
3637  }
3638  }
3639 
3641 }
3642 
3643 /**
3644  * @brief Trigger setting the filter, temperature, (if existing) the rotator angle and
3645  * let the #activeJob execute the preparation actions before a capture may
3646  * take place (@see SequenceJob::prepareCapture()).
3647  *
3648  * After triggering the settings, this method returns. This mechanism is slightly tricky, since it
3649  * asynchronous and event based and works as collaboration between Capture and SequenceJob. Capture has
3650  * the connection to devices and SequenceJob knows the target values.
3651  *
3652  * Each time Capture receives an updated value - e.g. the current CCD temperature
3653  * (@see updateCCDTemperature()) - it informs the #activeJob about the current CCD temperature.
3654  * SequenceJob checks, if it has reached the target value and if yes, sets this action as as completed.
3655  *
3656  * As soon as all actions are completed, SequenceJob emits a prepareComplete() event, which triggers
3657  * executeJob() from the Capture module.
3658  */
3660 {
3661  if (activeJob == nullptr)
3662  {
3663  qWarning(KSTARS_EKOS_CAPTURE) << "preparePreCaptureActions with null activeJob.";
3664  // Everything below depends on activeJob. Just return.
3665  return;
3666  }
3667 
3668  setBusy(true);
3669 
3670  if (activeJob->getCoreProperty(SequenceJob::SJ_Preview).toBool())
3671  {
3672  startB->setIcon(
3673  QIcon::fromTheme("media-playback-stop"));
3674  startB->setToolTip(i18n("Stop"));
3675  }
3676 
3677  // Update guiderActive before prepareCapture.
3678  activeJob->setCoreProperty(SequenceJob::SJ_GuiderActive, isActivelyGuiding());
3679 
3680  // signal that capture preparation steps should be executed
3681  activeJob->prepareCapture();
3682 }
3683 
3684 /**
3685  * @brief Listen to device property changes (temperature, rotator) that are triggered by
3686  * SequenceJob.
3687  */
3688 void Capture::updatePrepareState(Ekos::CaptureState prepareState)
3689 {
3690  m_State = prepareState;
3691  emit newStatus(prepareState);
3692 
3693  if (activeJob == nullptr)
3694  {
3695  qWarning(KSTARS_EKOS_CAPTURE) << "updatePrepareState with null activeJob.";
3696  // Everything below depends on activeJob. Just return.
3697  return;
3698  }
3699 
3700  switch (prepareState)
3701  {
3703  appendLogText(i18n("Setting temperature to %1 °C...", activeJob->getTargetTemperature()));
3704  captureStatusWidget->setStatus(i18n("Set Temp to %1 °C...", activeJob->getTargetTemperature()), Qt::yellow);
3705  break;
3706  case CAPTURE_GUIDER_DRIFT:
3707  appendLogText(i18n("Waiting for guide drift below %1\"...", activeJob->getTargetStartGuiderDrift()));
3708  captureStatusWidget->setStatus(i18n("Wait for Guider < %1\"...", activeJob->getTargetStartGuiderDrift()), Qt::yellow);
3709  break;
3710 
3712  appendLogText(i18n("Setting rotation to %1 degrees E of N...", activeJob->getTargetRotation()));
3713  captureStatusWidget->setStatus(i18n("Set Rotator to %1 deg...", activeJob->getTargetRotation()), Qt::yellow);
3714  break;
3715 
3716  default:
3717  break;
3718 
3719  }
3720 }
3721 
3722 /**
3723  * @brief Start the execution of #activeJob by initiating updatePreCaptureCalibrationStatus().
3724  */
3725 void Capture::executeJob()
3726 {
3727  if (activeJob == nullptr)
3728  {
3729  qWarning(KSTARS_EKOS_CAPTURE) << "executeJob with null activeJob.";
3730  return;
3731  }
3732 
3733  QMap<QString, QString> FITSHeader;
3734  QString rawPrefix = activeJob->property("rawPrefix").toString();
3735  if (m_ObserverName.isEmpty() == false)
3736  FITSHeader["FITS_OBSERVER"] = m_ObserverName;
3737  if (m_FullTargetName.isEmpty() == false)
3738  FITSHeader["FITS_OBJECT"] = m_FullTargetName;
3739  else if (rawPrefix.isEmpty() == false)
3740  {
3741  // JM 2021-07-08: Remove "_" from target name.
3742  FITSHeader["FITS_OBJECT"] = rawPrefix.remove("_");
3743  }
3744 
3745  if (FITSHeader.count() > 0)
3746  m_captureDeviceAdaptor->getActiveCamera()->setFITSHeader(FITSHeader);
3747 
3748  // Update button status
3749  setBusy(true);
3750 
3751  useGuideHead = (m_captureDeviceAdaptor->getActiveChip()->getType() == ISD::CameraChip::PRIMARY_CCD) ? false : true;
3752 
3753  syncGUIToJob(activeJob);
3754 
3755  // If the job is a dark flat, let's find the optimal exposure from prior
3756  // flat exposures.
3757  if (activeJob->getCoreProperty(SequenceJob::SJ_DarkFlat).toBool())
3758  {
3759  // If we found a prior exposure, and current upload more is not local, then update full prefix
3760  if (setDarkFlatExposure(activeJob)
3761  && m_captureDeviceAdaptor->getActiveCamera()->getUploadMode() != ISD::Camera::UPLOAD_LOCAL)
3762  {
3763  auto placeholderPath = Ekos::PlaceholderPath();
3764  // Make sure to update Full Prefix as exposure value was changed
3765  placeholderPath.processJobInfo(activeJob, activeJob->getCoreProperty(SequenceJob::SJ_TargetName).toString());
3766  updateSequencePrefix(activeJob->getCoreProperty(SequenceJob::SJ_FullPrefix).toString(),
3767  QFileInfo(activeJob->getSignature()).path());
3768  }
3769 
3770  }
3771 
3772  updatePreCaptureCalibrationStatus();
3773 }
3774 
3775 /**
3776  * @brief This is a wrapping loop for processPreCaptureCalibrationStage(), which contains
3777  * all checks before captureImage() may be called.
3778  *
3779  * If processPreCaptureCalibrationStage() returns IPS_OK (i.e. everything is ready so that
3780  * capturing may be started), captureImage() is called. Otherwise, it waits for a second and
3781  * calls itself again.
3782  */
3783 void Capture::updatePreCaptureCalibrationStatus()
3784 {
3785  // If process was aborted or stopped by the user
3786  if (isBusy == false)
3787  {
3788  appendLogText(i18n("Warning: Calibration process was prematurely terminated."));
3789  return;
3790  }
3791 
3792  IPState rc = processPreCaptureCalibrationStage();
3793 
3794  if (rc == IPS_ALERT)
3795  return;
3796  else if (rc == IPS_BUSY)
3797  {
3798  QTimer::singleShot(1000, this, &Ekos::Capture::updatePreCaptureCalibrationStatus);
3799  return;
3800  }
3801 
3802  captureImage();
3803 }
3804 
3805 void Capture::setFocusTemperatureDelta(double focusTemperatureDelta, double absTemperture)
3806 {
3807  Q_UNUSED(absTemperture);
3808  // This produces too much log spam
3809  // Maybe add a threshold to report later?
3810  //qCDebug(KSTARS_EKOS_CAPTURE) << "setFocusTemperatureDelta: " << focusTemperatureDelta;
3811  this->focusTemperatureDelta = focusTemperatureDelta;
3812 }
3813 
3814 /**
3815  * @brief Slot that listens to guiding deviations reported by the Guide module.
3816  *
3817  * Depending on the current status, it triggers several actions:
3818  * - If there is no active job, it calls checkMeridianFlipReady(), which may initiate a meridian flip.
3819  * - If guiding has been started after a meridian flip and the deviation is within the expected limits,
3820  * the meridian flip is regarded as completed by setMeridianFlipStage(MF_NONE) (@see setMeridianFlipStage()).
3821  * - If the deviation is beyond the defined limit, capturing is suspended (@see suspend()) and the
3822  * #guideDeviationTimer is started.
3823  * - Otherwise, it checks if there has been a job suspended and restarts it, since guiding is within the limits.
3824  */
3825 void Capture::setGuideDeviation(double delta_ra, double delta_dec)
3826 {
3827  const double deviation_rms = std::hypot(delta_ra, delta_dec);
3828  QString deviationText = QString("%1").arg(deviation_rms, 0, 'f', 3);
3829 
3830  // communicate the new guiding deviation
3831  emit newGuiderDrift(deviation_rms);
3832 
3833  // if guiding deviations occur and no job is active, check if a meridian flip is ready to be executed
3834  if (activeJob == nullptr && checkMeridianFlipReady())
3835  return;
3836 
3837  // if the job is in the startup phase, check if the deviation is below the initial guiding limit
3838  if (m_State == CAPTURE_PROGRESS)
3839  {
3840  // initial guiding deviation irrelevant or below limit
3841  if (m_LimitsUI->startGuiderDriftS->isChecked() == false || deviation_rms < m_LimitsUI->startGuiderDriftN->value())
3842  {
3843  m_State = CAPTURE_CALIBRATING;
3844  if (m_DeviationDetected == true)
3845  appendLogText(i18n("Initial guiding deviation %1 below limit value of %2 arcsecs",
3846  deviationText, m_LimitsUI->startGuiderDriftN->value()));
3847  m_DeviationDetected = false;
3848  }
3849  else
3850  {
3851  // warn only once
3852  if (m_DeviationDetected == false)
3853  appendLogText(i18n("Initial guiding deviation %1 exceeded limit value of %2 arcsecs",
3854  deviationText, m_LimitsUI->startGuiderDriftN->value()));
3855 
3856  m_DeviationDetected = true;
3857 
3858  // Check if we need to start meridian flip. If yes, we need to start capturing
3859  // to ensure that capturing is recovered after the flip
3860  if (checkMeridianFlipReady())
3861  start();
3862  }
3863 
3864  // in any case, do not proceed
3865  return;
3866  }
3867 
3868  // If guiding is started after a meridian flip we will start getting guide deviations again
3869  // if the guide deviations are within our limits, we resume the sequence
3870  if (meridianFlipStage == MF_GUIDING)
3871  {
3872  // If the user didn't select any guiding deviation, we fall through
3873  // otherwise we can for deviation RMS
3874  if (m_LimitsUI->limitGuideDeviationS->isChecked() == false || deviation_rms < m_LimitsUI->limitGuideDeviationN->value())
3875  {
3876  appendLogText(i18n("Post meridian flip calibration completed successfully."));
3877  // N.B. Set meridian flip stage AFTER resumeSequence() always
3878  setMeridianFlipStage(MF_NONE);
3879  return;
3880  }
3881  }
3882 
3883  // We don't enforce limit on previews or non-LIGHT frames
3884  if (m_LimitsUI->limitGuideDeviationS->isChecked() == false || (activeJob
3885  && (activeJob->getCoreProperty(SequenceJob::SJ_Preview).toBool()
3886  || activeJob->getExposeLeft() == 0.0 || activeJob->getFrameType() != FRAME_LIGHT)))
3887  return;
3888 
3889  // If we have an active busy job, let's abort it if guiding deviation is exceeded.
3890  // And we accounted for the spike
3891  if (activeJob && activeJob->getStatus() == JOB_BUSY && activeJob->getFrameType() == FRAME_LIGHT)
3892  {
3893  if (deviation_rms <= m_LimitsUI->limitGuideDeviationN->value())
3894  m_SpikesDetected = 0;
3895  else
3896  {
3897  // Require several consecutive spikes to fail.
3898  constexpr int CONSECUTIVE_SPIKES_TO_FAIL = 3;
3899  if (++m_SpikesDetected < CONSECUTIVE_SPIKES_TO_FAIL)
3900  return;
3901 
3902  appendLogText(i18n("Guiding deviation %1 exceeded limit value of %2 arcsecs for %4 consecutive samples, "
3903  "suspending exposure and waiting for guider up to %3 seconds.",
3904  deviationText, m_LimitsUI->limitGuideDeviationN->value(),
3905  QString("%L1").arg(guideDeviationTimer.interval() / 1000.0, 0, 'f', 3),
3906  CONSECUTIVE_SPIKES_TO_FAIL));
3907 
3908  suspend();
3909 
3910  m_SpikesDetected = 0;
3911  m_DeviationDetected = true;
3912 
3913  // Check if we need to start meridian flip. If yes, we need to start capturing
3914  // to ensure that capturing is recovered after the flip
3915  if (checkMeridianFlipReady())
3916  start();
3917  else
3918  guideDeviationTimer.start();
3919  }
3920  return;
3921  }
3922 
3923  // Find the first aborted job
3924  SequenceJob * abortedJob = nullptr;
3925  for(auto &job : jobs)
3926  {
3927  if (job->getStatus() == JOB_ABORTED)
3928  {
3929  abortedJob = job;
3930  break;
3931  }
3932  }
3933 
3934  if (abortedJob && m_DeviationDetected)
3935  {
3936  if (deviation_rms <= m_LimitsUI->limitGuideDeviationN->value())
3937  {
3938  guideDeviationTimer.stop();
3939 
3940  // Start with delay if start hasn't been triggered before
3941  if (! captureDelayTimer->isActive())
3942  {
3943  // if capturing has been suspended, restart it
3944  if (m_State == CAPTURE_SUSPENDED)
3945  {
3946  if (seqDelay == 0)
3947  appendLogText(i18n("Guiding deviation %1 is now lower than limit value of %2 arcsecs, "
3948  "resuming exposure.",
3949  deviationText, m_LimitsUI->limitGuideDeviationN->value()));
3950  else
3951  appendLogText(i18n("Guiding deviation %1 is now lower than limit value of %2 arcsecs, "
3952  "resuming exposure in %3 seconds.",
3953  deviationText, m_LimitsUI->limitGuideDeviationN->value(), seqDelay / 1000.0));
3954 
3955  captureDelayTimer->start(seqDelay);
3956  }
3957  }
3958  return;
3959  }
3960  else
3961  {
3962  // stop the delayed capture start if necessary
3963  if (captureDelayTimer->isActive())
3964  captureDelayTimer->stop();
3965 
3966  appendLogText(i18n("Guiding deviation %1 is still higher than limit value of %2 arcsecs.",
3967  deviationText, m_LimitsUI->limitGuideDeviationN->value()));
3968  }
3969  }
3970 }
3971 
3972 void Capture::setFocusStatus(FocusState state)
3973 {
3974  if (state != m_FocusState)
3975  qCDebug(KSTARS_EKOS_CAPTURE) << "Focus State changed from" << Ekos::getFocusStatusString(
3976  m_FocusState) << "to" << Ekos::getFocusStatusString(state);
3977  m_FocusState = state;
3978 
3979  // Do not process above aborted or when meridian flip in progress
3980  if (m_FocusState > FOCUS_ABORTED || meridianFlipStage == MF_FLIPPING || meridianFlipStage == MF_SLEWING)
3981  return;
3982 
3983  if (m_FocusState == FOCUS_COMPLETE)
3984  {
3985  // enable option to have a refocus event occur if HFR goes over threshold
3986  m_AutoFocusReady = true;
3987  // forward to the active job
3988  if (activeJob != nullptr)
3989  activeJob->setAutoFocusReady(m_AutoFocusReady);
3990 
3991  //if (limitFocusHFRN->value() == 0.0 && fileHFR == 0.0)
3992  if (fileHFR == 0.0)
3993  {
3994  QList<double> filterHFRList;
3995  if (m_CurrentFilterPosition > 0)
3996  {
3997  // If we are using filters, then we retrieve which filter is currently active.
3998  // We check if filter lock is used, and store that instead of the current filter.
3999  // e.g. If current filter HA, but lock filter is L, then the HFR value is stored for L filter.
4000  // If no lock filter exists, then we store as is (HA)
4001  QString currentFilterText = captureFilterS->itemText(m_CurrentFilterPosition - 1);
4002  //QString filterLock = filterManager.data()->getFilterLock(currentFilterText);
4003  //QString finalFilter = (filterLock == "--" ? currentFilterText : filterLock);
4004 
4005  //filterHFRList = HFRMap[finalFilter];
4006  filterHFRList = HFRMap[currentFilterText];
4007  filterHFRList.append(focusHFR);
4008  //HFRMap[finalFilter] = filterHFRList;
4009  HFRMap[currentFilterText] = filterHFRList;
4010  }
4011  // No filters
4012  else
4013  {
4014  filterHFRList = HFRMap["--"];
4015  filterHFRList.append(focusHFR);
4016  HFRMap["--"] = filterHFRList;
4017  }
4018 
4019  double median = focusHFR;
4020  int count = filterHFRList.size();
4021  if (Options::useMedianFocus() && count > 1)
4022  median = (count % 2) ? filterHFRList[count / 2] : (filterHFRList[count / 2 - 1] + filterHFRList[count / 2]) / 2.0;
4023 
4024  // Add 2.5% (default) to the automatic initial HFR value to allow for minute changes in HFR without need to refocus
4025  // in case in-sequence-focusing is used.
4026  m_LimitsUI->limitFocusHFRN->setValue(median + (median * (Options::hFRThresholdPercentage() / 100.0)));
4027  }
4028 
4029  // successful focus so reset elapsed time
4030  restartRefocusEveryNTimer();
4031  }
4032 
4033  if ((isRefocus || isInSequenceFocus) && activeJob && activeJob->getStatus() == JOB_BUSY)
4034  {
4035  if (m_FocusState == FOCUS_COMPLETE)
4036  {
4037  appendLogText(i18n("Focus complete."));
4038  captureStatusWidget->setStatus(i18n("Focus complete."), Qt::yellow);
4039  }
4040  // Meridian flip will abort focusing. In this case, after the meridian flip has completed capture
4041  // will restart the re-focus attempt. Therefore we only abort capture if meridian flip is not running.
4042  else if ((m_FocusState == FOCUS_FAILED || m_FocusState == FOCUS_ABORTED) &&
4043  !(meridianFlipStage == MF_INITIATED || meridianFlipStage == MF_SLEWING)
4044  )
4045  {
4046  appendLogText(i18n("Autofocus failed. Aborting exposure..."));
4047  captureStatusWidget->setStatus(i18n("Autofocus failed."), Qt::darkRed);
4048  abort();
4049  }
4050  }
4051 }
4052 
4053 void Capture::updateHFRThreshold()
4054 {
4055  if (fileHFR != 0.0)
4056  return;
4057 
4058  QList<double> filterHFRList;
4059  if (captureFilterS->currentIndex() != -1)
4060  {
4061  // If we are using filters, then we retrieve which filter is currently active.
4062  // We check if filter lock is used, and store that instead of the current filter.
4063  // e.g. If current filter HA, but lock filter is L, then the HFR value is stored for L filter.
4064  // If no lock filter exists, then we store as is (HA)
4065  QString currentFilterText = captureFilterS->currentText();
4066  QString filterLock = m_captureDeviceAdaptor->getFilterManager().data()->getFilterLock(currentFilterText);
4067  QString finalFilter = (filterLock == "--" ? currentFilterText : filterLock);
4068 
4069  filterHFRList = HFRMap[finalFilter];
4070  }
4071  // No filters
4072  else
4073  {
4074  filterHFRList = HFRMap["--"];
4075  }
4076 
4077  if (filterHFRList.empty())
4078  {
4079  m_LimitsUI->limitFocusHFRN->setValue(Options::hFRDeviation());
4080  return;
4081  }
4082 
4083  double median = 0;
4084  int count = filterHFRList.size();
4085  if (count > 1)
4086  median = (count % 2) ? filterHFRList[count / 2] : (filterHFRList[count / 2 - 1] + filterHFRList[count / 2]) / 2.0;
4087  else if (count == 1)
4088  median = filterHFRList[0];
4089 
4090  // Add 2.5% (default) to the automatic initial HFR value to allow for minute changes in HFR without need to refocus
4091  // in case in-sequence-focusing is used.
4092  m_LimitsUI->limitFocusHFRN->setValue(median + (median * (Options::hFRThresholdPercentage() / 100.0)));
4093 }
4094 
4095 void Capture::setMeridianFlipStage(MFStage stage)
4096 {
4097  qCDebug(KSTARS_EKOS_CAPTURE) << "setMeridianFlipStage: " << MFStageString(stage);
4098  if (meridianFlipStage != stage)
4099  {
4100  switch (stage)
4101  {
4102  case MF_NONE:
4103  meridianFlipStage = stage;
4104  emit newMeridianFlipStatus(Mount::FLIP_NONE);
4105  break;
4106 
4107  case MF_READY:
4108  if (meridianFlipStage == MF_REQUESTED)
4109  {
4110  // we keep the stage on requested until the mount starts the meridian flip
4111  emit newMeridianFlipStatus(Mount::FLIP_ACCEPTED);
4112  }
4113  else if (m_State == CAPTURE_PAUSED)
4114  {
4115  // paused after meridian flip requested
4116  captureStatusWidget->setStatus(i18n("Paused..."), Qt::yellow);
4117  meridianFlipStage = stage;
4118  emit newMeridianFlipStatus(Mount::FLIP_ACCEPTED);
4119  }
4120  else if (!(checkMeridianFlipRunning() || meridianFlipStage == MF_COMPLETED))
4121  {
4122  // if neither a MF has been requested (checked above) or is in a post
4123  // MF calibration phase, no MF needs to take place.
4124  // Hence we set to the stage to NONE
4125  meridianFlipStage = MF_NONE;
4126  break;
4127  }
4128  // in any other case, ignore it
4129  break;
4130 
4131  case MF_INITIATED:
4132  meridianFlipStage = MF_INITIATED;
4133  emit meridianFlipStarted();
4134  captureStatusWidget->setStatus(i18n("Meridian Flip..."), Qt::yellow);
4135  KSNotification::event(QLatin1String("MeridianFlipStarted"), i18n("Meridian flip started"), KSNotification::EVENT_INFO);
4136  break;
4137 
4138  case MF_REQUESTED:
4139  if (m_State == CAPTURE_PAUSED)
4140  // paused before meridian flip requested
4141  emit newMeridianFlipStatus(Mount::FLIP_ACCEPTED);
4142  else
4143  emit newMeridianFlipStatus(Mount::FLIP_WAITING);
4144  meridianFlipStage = stage;
4145  break;
4146 
4147  case MF_COMPLETED:
4148  captureStatusWidget->setStatus(i18n("Flip complete."), Qt::yellow);
4149  meridianFlipStage = MF_COMPLETED;
4150 
4151  // Reset HFR pixels to file value after meridian flip
4152  if (isInSequenceFocus)
4153  {
4154  qCDebug(KSTARS_EKOS_CAPTURE) << "Resetting HFR value to file value of" << fileHFR << "pixels after meridian flip.";
4155  //firstAutoFocus = true;
4156  inSequenceFocusCounter = 0;
4157  m_LimitsUI->limitFocusHFRN->setValue(fileHFR);
4158  }
4159 
4160  // after a meridian flip we do not need to dither
4161  if ( Options::ditherEnabled() || Options::ditherNoGuiding())
4162  ditherCounter = Options::ditherFrames();
4163 
4164  // if requested set flag so it perform refocus before next frame
4165  if (m_LimitsUI->meridianRefocusS->isChecked())
4166  refocusAfterMeridianFlip = true;
4167 
4168  break;
4169 
4170  default:
4171  meridianFlipStage = stage;
4172  break;
4173  }
4174  }
4175 }
4176 
4177 
4178 void Capture::meridianFlipStatusChanged(Mount::MeridianFlipStatus status)
4179 {
4180  qCDebug(KSTARS_EKOS_CAPTURE) << "meridianFlipStatusChanged: " << Mount::meridianFlipStatusString(status);
4181  switch (status)
4182  {
4183  case Mount::FLIP_NONE:
4184  // MF_NONE as external signal ignored so that re-alignment and guiding are processed first
4185  if (meridianFlipStage < MF_COMPLETED)
4186  setMeridianFlipStage(MF_NONE);
4187  break;
4188 
4189  case Mount::FLIP_PLANNED:
4190  if (meridianFlipStage > MF_NONE)
4191  {
4192  // This should never happen, since a meridian flip seems to be ongoing
4193  qCritical(KSTARS_EKOS_CAPTURE) << "Accepting meridian flip request while being in stage " << meridianFlipStage;
4194  }
4195 
4196  // If we are autoguiding, we should resume autoguiding after flip
4197  resumeGuidingAfterFlip = isGuidingOn();
4198 
4199  if (m_State == CAPTURE_IDLE || m_State == CAPTURE_ABORTED || m_State == CAPTURE_COMPLETE || m_State == CAPTURE_PAUSED)
4200  {
4201  setMeridianFlipStage(MF_INITIATED);
4202  emit newMeridianFlipStatus(Mount::FLIP_ACCEPTED);
4203  }
4204  else
4205  setMeridianFlipStage(MF_REQUESTED);
4206 
4207  break;
4208 
4209  case Mount::FLIP_RUNNING:
4210  setMeridianFlipStage(MF_INITIATED);
4211  emit newStatus(Ekos::CAPTURE_MERIDIAN_FLIP);
4212  break;
4213 
4214  case Mount::FLIP_COMPLETED:
4215  setMeridianFlipStage(MF_COMPLETED);
4216  emit newStatus(Ekos::CAPTURE_IDLE);
4217  processFlipCompleted();
4218  break;
4219 
4220  default:
4221  break;
4222  }
4223 }
4224 
4225 int Capture::getTotalFramesCount(QString signature)
4226 {
4227  int result = 0;
4228  bool found = false;
4229 
4230  foreach (SequenceJob * job, jobs)
4231  {
4232  // FIXME: this should be part of SequenceJob
4233  QString sig = job->getSignature();
4234  if (sig == signature)
4235  {
4236  result += job->getCoreProperty(SequenceJob::SJ_Count).toInt();
4237  found = true;
4238  }
4239  }
4240 
4241  if (found)
4242  return result;
4243  else
4244  return -1;
4245 }
4246 
4247 void Capture::setRotatorReversed(bool toggled)
4248 {
4249  rotatorSettings->ReverseDirectionCheck->setEnabled(true);
4250 
4251  rotatorSettings->ReverseDirectionCheck->blockSignals(true);
4252  rotatorSettings->ReverseDirectionCheck->setChecked(toggled);
4253  rotatorSettings->ReverseDirectionCheck->blockSignals(false);
4254 
4255 }
4256 
4258 {
4259  if (m_State == CAPTURE_IDLE || m_State == CAPTURE_COMPLETE)
4260  {
4261  QString sanitized = name;
4262  if (sanitized != i18n("unnamed"))
4263  {
4264  m_FullTargetName = name;
4265  // Remove illegal characters that can be problematic
4266  sanitized = sanitized.replace( QRegularExpression("\\s|/|\\(|\\)|:|\\*|~|\"" ), "_" )
4267  // Remove any two or more __
4268  .replace( QRegularExpression("_{2,}"), "_")
4269  // Remove any _ at the end
4270  .replace( QRegularExpression("_$"), "");
4271  filePrefixT->setText(sanitized);
4272  }
4273  }
4274 }
4275 
4276 void Capture::syncTelescopeInfo()
4277 {
4278  if (m_captureDeviceAdaptor->getMount() && m_captureDeviceAdaptor->getMount()->isConnected())
4279  {
4280  // Sync ALL CCDs to current telescope
4281  for (auto &oneCamera : m_Cameras)
4282  {
4283  auto activeDevices = oneCamera->getText("ACTIVE_DEVICES");
4284  if (activeDevices)
4285  {
4286  auto activeTelescope = activeDevices->findWidgetByName("ACTIVE_TELESCOPE");
4287  if (activeTelescope)
4288  {
4289  activeTelescope->setText(m_captureDeviceAdaptor->getMount()->getDeviceName().toLatin1().constData());
4290  oneCamera->getDriverInfo()->getClientManager()->sendNewText(activeDevices);
4291  }
4292  }
4293  }
4294  }
4295 }
4296 
4297 void Capture::saveFITSDirectory()
4298 {
4299  QString dir =
4300  QFileDialog::getExistingDirectory(Ekos::Manager::Instance(), i18nc("@title:window", "FITS Save Directory"),
4301  dirPath.toLocalFile());
4302 
4303  if (dir.isEmpty())
4304  return;
4305 
4306  fileDirectoryT->setText(dir);
4307 }
4308 
4309 void Capture::loadSequenceQueue()
4310 {
4311  QUrl fileURL = QFileDialog::getOpenFileUrl(Ekos::Manager::Instance(), i18nc("@title:window", "Open Ekos Sequence Queue"),
4312  dirPath,
4313  "Ekos Sequence Queue (*.esq)");
4314  if (fileURL.isEmpty())
4315  return;
4316 
4317  if (fileURL.isValid() == false)
4318  {
4319  QString message = i18n("Invalid URL: %1", fileURL.toLocalFile());
4320  KSNotification::sorry(message, i18n("Invalid URL"));
4321  return;
4322  }
4323 
4324  dirPath = QUrl(fileURL.url(QUrl::RemoveFilename));
4325 
4326  loadSequenceQueue(fileURL.toLocalFile());
4327 }
4328 
4329 bool Capture::loadSequenceQueue(const QString &fileURL)
4330 {
4331  QFile sFile(fileURL);
4332  if (!sFile.open(QIODevice::ReadOnly))
4333  {
4334  QString message = i18n("Unable to open file %1", fileURL);
4335  KSNotification::sorry(message, i18n("Could Not Open File"));
4336  return false;
4337  }
4338 
4339  capturedFramesMap.clear();
4341 
4342  LilXML * xmlParser = newLilXML();
4343 
4344  char errmsg[MAXRBUF];
4345  XMLEle * root = nullptr;
4346  XMLEle * ep = nullptr;
4347  char c;
4348 
4349  // We expect all data read from the XML to be in the C locale - QLocale::c().
4350  QLocale cLocale = QLocale::c();
4351 
4352  while (sFile.getChar(&c))
4353  {
4354  root = readXMLEle(xmlParser, c, errmsg);
4355 
4356  if (root)
4357  {
4358  double sqVersion = cLocale.toDouble(findXMLAttValu(root, "version"));
4359  if (sqVersion < SQ_COMPAT_VERSION)
4360  {
4361  appendLogText(i18n("Deprecated sequence file format version %1. Please construct a new sequence file.",
4362  sqVersion));
4363  return false;
4364  }
4365 
4366  for (ep = nextXMLEle(root, 1); ep != nullptr; ep = nextXMLEle(root, 0))
4367  {
4368  if (!strcmp(tagXMLEle(ep), "Observer"))
4369  {
4370  m_ObserverName = QString(pcdataXMLEle(ep));
4371  }
4372  else if (!strcmp(tagXMLEle(ep), "GuideDeviation"))
4373  {
4374  m_LimitsUI->limitGuideDeviationS->setChecked(!strcmp(findXMLAttValu(ep, "enabled"), "true"));
4375  m_LimitsUI->limitGuideDeviationN->setValue(cLocale.toDouble(pcdataXMLEle(ep)));
4376  }
4377  else if (!strcmp(tagXMLEle(ep), "GuideStartDeviation"))
4378  {
4379  m_LimitsUI->startGuiderDriftS->setChecked(!strcmp(findXMLAttValu(ep, "enabled"), "true"));
4380  m_LimitsUI->startGuiderDriftN->setValue(cLocale.toDouble(pcdataXMLEle(ep)));
4381  }
4382  else if (!strcmp(tagXMLEle(ep), "Autofocus"))
4383  {
4384  m_LimitsUI->limitFocusHFRS->setChecked(!strcmp(findXMLAttValu(ep, "enabled"), "true"));
4385  double const HFRValue = cLocale.toDouble(pcdataXMLEle(ep));
4386  // Set the HFR value from XML, or reset it to zero, don't let another unrelated older HFR be used
4387  // Note that HFR value will only be serialized to XML when option "Save Sequence HFR to File" is enabled
4388  fileHFR = HFRValue > 0.0 ? HFRValue : 0.0;
4389  m_LimitsUI->limitFocusHFRN->setValue(fileHFR);
4390  }
4391  else if (!strcmp(tagXMLEle(ep), "RefocusOnTemperatureDelta"))
4392  {
4393  m_LimitsUI->limitFocusDeltaTS->setChecked(!strcmp(findXMLAttValu(ep, "enabled"), "true"));
4394  double const deltaValue = cLocale.toDouble(pcdataXMLEle(ep));
4395  m_LimitsUI->limitFocusDeltaTN->setValue(deltaValue);
4396  }
4397  else if (!strcmp(tagXMLEle(ep), "RefocusEveryN"))
4398  {
4399  m_LimitsUI->limitRefocusS->setChecked(!strcmp(findXMLAttValu(ep, "enabled"), "true"));
4400  int const minutesValue = cLocale.toInt(pcdataXMLEle(ep));
4401  // Set the refocus period from XML, or reset it to zero, don't let another unrelated older refocus period be used.
4402  refocusEveryNMinutesValue = minutesValue > 0 ? minutesValue : 0;
4403  m_LimitsUI->limitRefocusN->setValue(refocusEveryNMinutesValue);
4404  }
4405  else if (!strcmp(tagXMLEle(ep), "RefocusOnMeridianFlip"))
4406  {
4407  m_LimitsUI->meridianRefocusS->setChecked(!strcmp(findXMLAttValu(ep, "enabled"), "true"));
4408  }
4409  else if (!strcmp(tagXMLEle(ep), "MeridianFlip"))
4410  {
4411  // meridian flip is managed by the mount only
4412  // older files might nevertheless contain MF settings
4413  if (! strcmp(findXMLAttValu(ep, "enabled"), "true"))
4414  appendLogText(
4415  i18n("Meridian flip configuration has been shifted to the mount module. Please configure the meridian flip there."));
4416  }
4417  else if (!strcmp(tagXMLEle(ep), "CCD"))
4418  {
4419  cameraS->setCurrentText(pcdataXMLEle(ep));
4420  // Signal "activated" of QComboBox does not fire when changing the text programmatically
4421  setCamera(pcdataXMLEle(ep));
4422  }
4423  else if (!strcmp(tagXMLEle(ep), "FilterWheel"))
4424  {
4425  filterWheelS->setCurrentText(pcdataXMLEle(ep));
4426  checkFilter();
4427  }
4428  else
4429  {
4430  processJobInfo(ep);
4431  }
4432  }
4433  delXMLEle(root);
4434  }
4435  else if (errmsg[0])
4436  {
4437  appendLogText(QString(errmsg));
4438  delLilXML(xmlParser);
4439  return false;
4440  }
4441  }
4442 
4443  m_SequenceURL = QUrl::fromLocalFile(fileURL);
4444  m_Dirty = false;
4445  delLilXML(xmlParser);
4446  // update save button tool tip
4447  queueSaveB->setToolTip("Save to " + sFile.fileName());
4448 
4449  return true;
4450 }
4451 
4452 bool Capture::processJobInfo(XMLEle * root)
4453 {
4454  XMLEle * ep;
4455  XMLEle * subEP;
4456  rotatorSettings->setRotationEnforced(false);
4457 
4458  bool isDarkFlat = false;
4459  m_Scripts.clear();
4460  QLocale cLocale = QLocale::c();
4461 
4462  for (ep = nextXMLEle(root, 1); ep != nullptr; ep = nextXMLEle(root, 0))
4463  {
4464  if (!strcmp(tagXMLEle(ep), "Exposure"))
4465  captureExposureN->setValue(cLocale.toDouble(pcdataXMLEle(ep)));
4466  else if (!strcmp(tagXMLEle(ep), "Format"))
4467  captureFormatS->setCurrentText(pcdataXMLEle(ep));
4468  else if (!strcmp(tagXMLEle(ep), "Encoding"))
4469  {
4470  captureEncodingS->setCurrentText(pcdataXMLEle(ep));
4471  }
4472  else if (!strcmp(tagXMLEle(ep), "Binning"))
4473  {
4474  subEP = findXMLEle(ep, "X");
4475  if (subEP)
4476  captureBinHN->setValue(cLocale.toInt(pcdataXMLEle(subEP)));
4477  subEP = findXMLEle(ep, "Y");
4478  if (subEP)
4479  captureBinVN->setValue(cLocale.toInt(pcdataXMLEle(subEP)));
4480  }
4481  else if (!strcmp(tagXMLEle(ep), "Frame"))
4482  {
4483  subEP = findXMLEle(ep, "X");
4484  if (subEP)
4485  captureFrameXN->setValue(cLocale.toInt(pcdataXMLEle(subEP)));
4486  subEP = findXMLEle(ep, "Y");
4487  if (subEP)
4488  captureFrameYN->setValue(cLocale.toInt(pcdataXMLEle(subEP)));
4489  subEP = findXMLEle(ep, "W");
4490  if (subEP)
4491  captureFrameWN->setValue(cLocale.toInt(pcdataXMLEle(subEP)));
4492  subEP = findXMLEle(ep, "H");
4493  if (subEP)
4494  captureFrameHN->setValue(cLocale.toInt(pcdataXMLEle(subEP)));
4495  }
4496  else if (!strcmp(tagXMLEle(ep), "Temperature"))
4497  {
4498  if (cameraTemperatureN->isEnabled())
4499  cameraTemperatureN->setValue(cLocale.toDouble(pcdataXMLEle(ep)));
4500 
4501  // If force attribute exist, we change cameraTemperatureS, otherwise do nothing.
4502  if (!strcmp(findXMLAttValu(ep, "force"), "true"))
4503  cameraTemperatureS->setChecked(true);
4504  else if (!strcmp(findXMLAttValu(ep, "force"), "false"))
4505  cameraTemperatureS->setChecked(false);
4506  }
4507  else if (!strcmp(tagXMLEle(ep), "Filter"))
4508  {
4509  //captureFilterS->setCurrentIndex(atoi(pcdataXMLEle(ep))-1);
4510  captureFilterS->setCurrentText(pcdataXMLEle(ep));
4511  }
4512  else if (!strcmp(tagXMLEle(ep), "Type"))
4513  {
4514  captureTypeS->setCurrentText(pcdataXMLEle(ep));
4515  }
4516  else if (!strcmp(tagXMLEle(ep), "Prefix"))
4517  {
4518  subEP = findXMLEle(ep, "RawPrefix");
4519  if (subEP)
4520  filePrefixT->setText(pcdataXMLEle(subEP));
4521  subEP = findXMLEle(ep, "FilterEnabled");
4522  if (subEP)
4523  fileFilterS->setChecked(!strcmp("1", pcdataXMLEle(subEP)));
4524  subEP = findXMLEle(ep, "ExpEnabled");
4525  if (subEP)
4526  fileDurationS->setChecked(!strcmp("1", pcdataXMLEle(subEP)));
4527  subEP = findXMLEle(ep, "TimeStampEnabled");
4528  if (subEP)
4529  fileTimestampS->setChecked(!strcmp("1", pcdataXMLEle(subEP)));
4530  }
4531  else if (!strcmp(tagXMLEle(ep), "Count"))
4532  {
4533  captureCountN->setValue(cLocale.toInt(pcdataXMLEle(ep)));
4534  }
4535  else if (!strcmp(tagXMLEle(ep), "Delay"))
4536  {
4537  captureDelayN->setValue(cLocale.toInt(pcdataXMLEle(ep)));
4538  }
4539  else if (!strcmp(tagXMLEle(ep), "PostCaptureScript"))
4540  {
4541  m_Scripts[SCRIPT_POST_CAPTURE] = pcdataXMLEle(ep);
4542  }
4543  else if (!strcmp(tagXMLEle(ep), "PreCaptureScript"))
4544  {
4545  m_Scripts[SCRIPT_PRE_CAPTURE] = pcdataXMLEle(ep);
4546  }
4547  else if (!strcmp(tagXMLEle(ep), "PostJobScript"))
4548  {
4549  m_Scripts[SCRIPT_POST_JOB] = pcdataXMLEle(ep);
4550  }
4551  else if (!strcmp(tagXMLEle(ep), "PreJobScript"))
4552  {
4553  m_Scripts[SCRIPT_PRE_JOB] = pcdataXMLEle(ep);
4554  }
4555  else if (!strcmp(tagXMLEle(ep), "FITSDirectory"))
4556  {
4557  fileDirectoryT->setText(pcdataXMLEle(ep));
4558  }
4559  else if (!strcmp(tagXMLEle(ep), "RemoteDirectory"))
4560  {
4561  fileRemoteDirT->setText(pcdataXMLEle(ep));
4562  }
4563  else if (!strcmp(tagXMLEle(ep), "UploadMode"))
4564  {
4565  fileUploadModeS->setCurrentIndex(cLocale.toInt(pcdataXMLEle(ep)));
4566  }
4567  else if (!strcmp(tagXMLEle(ep), "ISOIndex"))
4568  {
4569  if (captureISOS)
4570  captureISOS->setCurrentIndex(cLocale.toInt(pcdataXMLEle(ep)));
4571  }
4572  else if (!strcmp(tagXMLEle(ep), "Rotation"))
4573  {
4574  rotatorSettings->setRotationEnforced(true);
4575  rotatorSettings->setTargetRotationPA(cLocale.toDouble(pcdataXMLEle(ep)));
4576  }
4577  else if (!strcmp(tagXMLEle(ep), "Properties"))
4578  {
4580 
4581  for (subEP = nextXMLEle(ep, 1); subEP != nullptr; subEP = nextXMLEle(ep, 0))
4582  {
4583  QMap<QString, QVariant> elements;
4584  XMLEle * oneElement = nullptr;
4585  for (oneElement = nextXMLEle(subEP, 1); oneElement != nullptr; oneElement = nextXMLEle(subEP, 0))
4586  {
4587  const char * name = findXMLAttValu(oneElement, "name");
4588  bool ok = false;
4589  // String
4590  auto xmlValue = pcdataXMLEle(oneElement);
4591  // Try to load it as double
4592  auto value = cLocale.toDouble(xmlValue, &ok);
4593  if (ok)
4594  elements[name] = value;
4595  else
4596  elements[name] = xmlValue;
4597  }
4598 
4599  const char * name = findXMLAttValu(subEP, "name");
4600  propertyMap[name] = elements;
4601  }
4602 
4603  customPropertiesDialog->setCustomProperties(propertyMap);
4604  const double gain = getGain();
4605  if (gain >= 0)
4606  captureGainN->setValue(gain);
4607  const double offset = getOffset();
4608  if (offset >= 0)
4609  captureOffsetN->setValue(offset);
4610  }
4611  else if (!strcmp(tagXMLEle(ep), "Calibration"))
4612  {
4613  subEP = findXMLEle(ep, "FlatSource");
4614  if (subEP)
4615  {
4616  XMLEle * typeEP = findXMLEle(subEP, "Type");
4617  if (typeEP)
4618  {
4619  if (!strcmp(pcdataXMLEle(typeEP), "Manual"))
4620  flatFieldSource = SOURCE_MANUAL;
4621  else if (!strcmp(pcdataXMLEle(typeEP), "FlatCap"))
4622  flatFieldSource = SOURCE_FLATCAP;
4623  else if (!strcmp(pcdataXMLEle(typeEP), "DarkCap"))
4624  flatFieldSource = SOURCE_DARKCAP;
4625  else if (!strcmp(pcdataXMLEle(typeEP), "Wall"))
4626  {
4627  XMLEle * azEP = findXMLEle(subEP, "Az");
4628  XMLEle * altEP = findXMLEle(subEP, "Alt");
4629 
4630  if (azEP && altEP)
4631  {
4632  flatFieldSource = SOURCE_WALL;
4633  wallCoord.setAz(cLocale.toDouble(pcdataXMLEle(azEP)));
4634  wallCoord.setAlt(cLocale.toDouble(pcdataXMLEle(altEP)));
4635  }
4636  }
4637  else
4638  flatFieldSource = SOURCE_DAWN_DUSK;
4639  }
4640  }
4641 
4642  subEP = findXMLEle(ep, "FlatDuration");
4643  if (subEP)
4644  {
4645  const char * dark = findXMLAttValu(subEP, "dark");
4646  isDarkFlat = !strcmp(dark, "true");
4647 
4648  XMLEle * typeEP = findXMLEle(subEP, "Type");
4649  if (typeEP)
4650  {
4651  if (!strcmp(pcdataXMLEle(typeEP), "Manual"))
4652  flatFieldDuration = DURATION_MANUAL;
4653  }
4654 
4655  XMLEle * aduEP = findXMLEle(subEP, "Value");
4656  if (aduEP)
4657  {
4658  flatFieldDuration = DURATION_ADU;
4659  targetADU = cLocale.toDouble(pcdataXMLEle(aduEP));
4660  }
4661 
4662  aduEP = findXMLEle(subEP, "Tolerance");
4663  if (aduEP)
4664  {
4665  targetADUTolerance = cLocale.toDouble(pcdataXMLEle(aduEP));
4666  }
4667  }
4668 
4669  subEP = findXMLEle(ep, "PreMountPark");
4670  if (subEP)
4671  {
4672  if (!strcmp(pcdataXMLEle(subEP), "True"))
4673  preMountPark = true;
4674  else
4675  preMountPark = false;
4676  }
4677 
4678  subEP = findXMLEle(ep, "PreDomePark");
4679  if (subEP)
4680  {
4681  if (!strcmp(pcdataXMLEle(subEP), "True"))
4682  preDomePark = true;
4683  else
4684  preDomePark = false;
4685  }
4686  }
4687  }
4688 
4689  addJob(false, isDarkFlat);
4690 
4691  return true;
4692 }
4693 
4694 void Capture::saveSequenceQueue()
4695 {
4696  QUrl backupCurrent = m_SequenceURL;
4697 
4698  if (m_SequenceURL.toLocalFile().startsWith(QLatin1String("/tmp/")) || m_SequenceURL.toLocalFile().contains("/Temp"))
4699  m_SequenceURL.clear();
4700 
4701  // If no changes made, return.
4702  if (m_Dirty == false && !m_SequenceURL.isEmpty())
4703  return;
4704 
4705  if (m_SequenceURL.isEmpty())
4706  {
4707  m_SequenceURL = QFileDialog::getSaveFileUrl(Ekos::Manager::Instance(), i18nc("@title:window", "Save Ekos Sequence Queue"),
4708  dirPath,
4709  "Ekos Sequence Queue (*.esq)");
4710  // if user presses cancel
4711  if (m_SequenceURL.isEmpty())
4712  {
4713  m_SequenceURL = backupCurrent;
4714  return;
4715  }
4716 
4717  dirPath = QUrl(m_SequenceURL.url(QUrl::RemoveFilename));
4718 
4719  if (m_SequenceURL.toLocalFile().endsWith(QLatin1String(".esq")) == false)
4720  m_SequenceURL.setPath(m_SequenceURL.toLocalFile() + ".esq");
4721 
4722  }
4723 
4724  if (m_SequenceURL.isValid())
4725  {
4726  if ((saveSequenceQueue(m_SequenceURL.toLocalFile())) == false)
4727  {
4728  KSNotification::error(i18n("Failed to save sequence queue"), i18n("Save"));
4729  return;
4730  }
4731 
4732  m_Dirty = false;
4733  }
4734  else
4735  {
4736  QString message = i18n("Invalid URL: %1", m_SequenceURL.url());
4737  KSNotification::sorry(message, i18n("Invalid URL"));
4738  }
4739 }
4740 
4741 void Capture::saveSequenceQueueAs()
4742 {
4743  m_SequenceURL.clear();
4745 }
4746 
4747 bool Capture::saveSequenceQueue(const QString &path)
4748 {
4749  QFile file;
4750  const QMap<QString, CCDFrameType> frameTypes =
4751  {
4752  { "Light", FRAME_LIGHT }, { "Dark", FRAME_DARK }, { "Bias", FRAME_BIAS }, { "Flat", FRAME_FLAT }
4753  };
4754 
4755  file.setFileName(path);
4756 
4757  if (!file.open(QIODevice::WriteOnly))
4758  {
4759  QString message = i18n("Unable to write to file %1", path);
4760  KSNotification::sorry(message, i18n("Could not open file"));
4761  return false;
4762  }
4763 
4764  QTextStream outstream(&file);
4765 
4766  // We serialize sequence data to XML using the C locale
4767  QLocale cLocale = QLocale::c();
4768 
4769  outstream << "<?xml version=\"1.0\" encoding=\"UTF-8\"?>" << Qt::endl;
4770  outstream << "<SequenceQueue version='" << SQ_FORMAT_VERSION << "'>" << Qt::endl;
4771  if (m_ObserverName.isEmpty() == false)
4772  outstream << "<Observer>" << m_ObserverName << "</Observer>" << Qt::endl;
4773  outstream << "<CCD>" << cameraS->currentText() << "</CCD>" << Qt::endl;
4774  outstream << "<FilterWheel>" << filterWheelS->currentText() << "</FilterWheel>" << Qt::endl;
4775  outstream << "<GuideDeviation enabled='" << (m_LimitsUI->limitGuideDeviationS->isChecked() ? "true" : "false") << "'>"
4776  << cLocale.toString(m_LimitsUI->limitGuideDeviationN->value()) << "</GuideDeviation>" << Qt::endl;
4777  outstream << "<GuideStartDeviation enabled='" << (m_LimitsUI->startGuiderDriftS->isChecked() ? "true" : "false") << "'>"
4778  << cLocale.toString(m_LimitsUI->startGuiderDriftN->value()) << "</GuideStartDeviation>" << Qt::endl;
4779  // Issue a warning when autofocus is enabled but Ekos options prevent HFR value from being written
4780  if (m_LimitsUI->limitFocusHFRS->isChecked() && !Options::saveHFRToFile())
4781  appendLogText(i18n(
4782  "Warning: HFR-based autofocus is set but option \"Save Sequence HFR Value to File\" is not enabled. "
4783  "Current HFR value will not be written to sequence file."));
4784  outstream << "<Autofocus enabled='" << (m_LimitsUI->limitFocusHFRS->isChecked() ? "true" : "false") << "'>"
4785  << cLocale.toString(Options::saveHFRToFile() ? m_LimitsUI->limitFocusHFRN->value() : 0) << "</Autofocus>" << Qt::endl;
4786  outstream << "<RefocusOnTemperatureDelta enabled='" << (m_LimitsUI->limitFocusDeltaTS->isChecked() ? "true" : "false") <<
4787  "'>"
4788  << cLocale.toString(m_LimitsUI->limitFocusDeltaTN->value()) << "</RefocusOnTemperatureDelta>" << Qt::endl;
4789  outstream << "<RefocusEveryN enabled='" << (m_LimitsUI->limitRefocusS->isChecked() ? "true" : "false") << "'>"
4790  << cLocale.toString(m_LimitsUI->limitRefocusN->value()) << "</RefocusEveryN>" << Qt::endl;
4791  outstream << "<RefocusOnMeridianFlip enabled='" << (m_LimitsUI->meridianRefocusS->isChecked() ? "true" : "false") << "'/>"
4792  << Qt::endl;
4793  for (auto &job : jobs)
4794  {
4795  auto rawPrefix = job->getCoreProperty(SequenceJob::SJ_RawPrefix).toString();
4796  auto filterEnabled = job->getCoreProperty(SequenceJob::SJ_FilterPrefixEnabled).toBool();
4797  auto expEnabled = job->getCoreProperty(SequenceJob::SJ_ExpPrefixEnabled).toBool();
4798  auto tsEnabled = job->getCoreProperty(SequenceJob::SJ_TimeStampPrefixEnabled).toBool();
4799  auto roi = job->getCoreProperty(SequenceJob::SJ_ROI).toRect();
4800 
4801  outstream << "<Job>" << Qt::endl;
4802 
4803  outstream << "<Exposure>" << cLocale.toString(job->getCoreProperty(SequenceJob::SJ_Exposure).toDouble()) << "</Exposure>" <<
4804  Qt::endl;
4805  outstream << "<Format>" << job->getCoreProperty(SequenceJob::SJ_Format).toString() << "</Format>" << Qt::endl;
4806  outstream << "<Encoding>" << job->getCoreProperty(SequenceJob::SJ_Encoding).toString() << "</Encoding>" << Qt::endl;
4807  outstream << "<Binning>" << Qt::endl;
4808  outstream << "<X>" << cLocale.toString(job->getCoreProperty(SequenceJob::SJ_Binning).toPoint().x()) << "</X>" << Qt::endl;
4809  outstream << "<Y>" << cLocale.toString(job->getCoreProperty(SequenceJob::SJ_Binning).toPoint().x()) << "</Y>" << Qt::endl;
4810  outstream << "</Binning>" << Qt::endl;
4811  outstream << "<Frame>" << Qt::endl;
4812  outstream << "<X>" << cLocale.toString(roi.x()) << "</X>" << Qt::endl;
4813  outstream << "<Y>" << cLocale.toString(roi.y()) << "</Y>" << Qt::endl;
4814  outstream << "<W>" << cLocale.toString(roi.width()) << "</W>" << Qt::endl;
4815  outstream << "<H>" << cLocale.toString(roi.height()) << "</H>" << Qt::endl;
4816  outstream << "</Frame>" << Qt::endl;
4817  if (job->getTargetTemperature() != Ekos::INVALID_VALUE)
4818  outstream << "<Temperature force='" << (job->getCoreProperty(SequenceJob::SJ_EnforceTemperature).toBool() ? "true" :
4819  "false") << "'>"
4820  << cLocale.toString(job->getTargetTemperature()) << "</Temperature>" << Qt::endl;
4821  if (job->getTargetFilter() >= 0)
4822  outstream << "<Filter>" << job->getCoreProperty(SequenceJob::SJ_Filter).toString() << "</Filter>" << Qt::endl;
4823  outstream << "<Type>" << frameTypes.key(job->getFrameType()) << "</Type>" << Qt::endl;
4824  outstream << "<Prefix>" << Qt::endl;
4825  //outstream << "<CompletePrefix>" << job->getPrefix() << "</CompletePrefix>" << Qt::endl;
4826  outstream << "<RawPrefix>" << rawPrefix << "</RawPrefix>" << Qt::endl;
4827  outstream << "<FilterEnabled>" << (filterEnabled ? 1 : 0) << "</FilterEnabled>" << Qt::endl;
4828  outstream << "<ExpEnabled>" << (expEnabled ? 1 : 0) << "</ExpEnabled>" << Qt::endl;
4829  outstream << "<TimeStampEnabled>" << (tsEnabled ? 1 : 0) << "</TimeStampEnabled>" << Qt::endl;
4830  outstream << "</Prefix>" << Qt::endl;
4831  outstream << "<Count>" << cLocale.toString(job->getCoreProperty(SequenceJob::SJ_Count).toInt()) << "</Count>" << Qt::endl;
4832  // ms to seconds
4833  outstream << "<Delay>" << cLocale.toString(job->getCoreProperty(SequenceJob::SJ_Delay).toInt() / 1000.0) << "</Delay>" <<
4834  Qt::endl;
4835  if (job->getScript(SCRIPT_PRE_CAPTURE).isEmpty() == false)
4836  outstream << "<PreCaptureScript>" << job->getScript(SCRIPT_PRE_CAPTURE) << "</PreCaptureScript>" << Qt::endl;
4837  if (job->getScript(SCRIPT_POST_CAPTURE).isEmpty() == false)
4838  outstream << "<PostCaptureScript>" << job->getScript(SCRIPT_POST_CAPTURE) << "</PostCaptureScript>" << Qt::endl;
4839  if (job->getScript(SCRIPT_PRE_JOB).isEmpty() == false)
4840  outstream << "<PreJobScript>" << job->getScript(SCRIPT_PRE_JOB) << "</PreJobScript>" << Qt::endl;
4841  if (job->getScript(SCRIPT_POST_JOB).isEmpty() == false)
4842  outstream << "<PostJobScript>" << job->getScript(SCRIPT_POST_JOB) << "</PostJobScript>" << Qt::endl;
4843  outstream << "<FITSDirectory>" << job->getCoreProperty(SequenceJob::SJ_LocalDirectory).toString() << "</FITSDirectory>" <<
4844  Qt::endl;
4845  outstream << "<UploadMode>" << job->getUploadMode() << "</UploadMode>" << Qt::endl;
4846  if (job->getCoreProperty(SequenceJob::SJ_RemoteDirectory).toString().isEmpty() == false)
4847  outstream << "<RemoteDirectory>" << job->getCoreProperty(SequenceJob::SJ_RemoteDirectory).toString() << "</RemoteDirectory>"
4848  << Qt::endl;
4849  if (job->getCoreProperty(SequenceJob::SJ_ISOIndex).toInt() != -1)
4850  outstream << "<ISOIndex>" << (job->getCoreProperty(SequenceJob::SJ_ISOIndex).toInt()) << "</ISOIndex>" << Qt::endl;
4851  if (job->getTargetRotation() != Ekos::INVALID_VALUE)
4852  outstream << "<Rotation>" << (job->getTargetRotation()) << "</Rotation>" << Qt::endl;
4853  QMapIterator<QString, QMap<QString, QVariant>> customIter(job->getCustomProperties());
4854  outstream << "<Properties>" << Qt::endl;
4855  while (customIter.hasNext())
4856  {
4857  customIter.next();
4858  outstream << "<PropertyVector name='" << customIter.key() << "'>" << Qt::endl;
4859  QMap<QString, QVariant> elements = customIter.value();
4860  QMapIterator<QString, QVariant> iter(elements);
4861  while (iter.hasNext())
4862  {
4863  iter.next();
4864  if (iter.value().type() == QVariant::String)
4865  {
4866  outstream << "<OneElement name='" << iter.key()
4867  << "'>" << iter.value().toString() << "</OneElement>" << Qt::endl;
4868  }
4869  else
4870  {
4871  outstream << "<OneElement name='" << iter.key()
4872  << "'>" << iter.value().toDouble() << "</OneElement>" << Qt::endl;
4873  }
4874  }
4875  outstream << "</PropertyVector>" << Qt::endl;
4876  }
4877  outstream << "</Properties>" << Qt::endl;
4878 
4879  outstream << "<Calibration>" << Qt::endl;
4880  outstream << "<FlatSource>" << Qt::endl;
4881  if (job->getFlatFieldSource() == SOURCE_MANUAL)
4882  outstream << "<Type>Manual</Type>" << Qt::endl;
4883  else if (job->getFlatFieldSource() == SOURCE_FLATCAP)
4884  outstream << "<Type>FlatCap</Type>" << Qt::endl;
4885  else if (job->getFlatFieldSource() == SOURCE_DARKCAP)
4886  outstream << "<Type>DarkCap</Type>" << Qt::endl;
4887  else if (job->getFlatFieldSource() == SOURCE_WALL)
4888  {
4889  outstream << "<Type>Wall</Type>" << Qt::endl;
4890  outstream << "<Az>" << cLocale.toString(job->getWallCoord().az().Degrees()) << "</Az>" << Qt::endl;
4891  outstream << "<Alt>" << cLocale.toString(job->getWallCoord().alt().Degrees()) << "</Alt>" << Qt::endl;
4892  }
4893  else
4894  outstream << "<Type>DawnDust</Type>" << Qt::endl;
4895  outstream << "</FlatSource>" << Qt::endl;
4896 
4897  outstream << "<FlatDuration dark='" << (job->getCoreProperty(SequenceJob::SJ_DarkFlat).toBool() ? "true" : "false")
4898  << "'>" << Qt::endl;
4899  if (job->getFlatFieldDuration() == DURATION_MANUAL)
4900  outstream << "<Type>Manual</Type>" << Qt::endl;
4901  else
4902  {
4903  outstream << "<Type>ADU</Type>" << Qt::endl;
4904  outstream << "<Value>" << cLocale.toString(job->getCoreProperty(SequenceJob::SJ_TargetADU).toDouble()) << "</Value>" <<
4905  Qt::endl;
4906  outstream << "<Tolerance>" << cLocale.toString(job->getCoreProperty(SequenceJob::SJ_TargetADUTolerance).toDouble()) <<
4907  "</Tolerance>" << Qt::endl;
4908  }
4909  outstream << "</FlatDuration>" << Qt::endl;
4910 
4911  outstream << "<PreMountPark>" << (job->getPreMountPark() ? "True" : "False") <<
4912  "</PreMountPark>" << Qt::endl;
4913  outstream << "<PreDomePark>" << (job->getPreDomePark() ? "True" : "False") <<
4914  "</PreDomePark>" << Qt::endl;
4915  outstream << "</Calibration>" << Qt::endl;
4916 
4917  outstream << "</Job>" << Qt::endl;
4918  }
4919 
4920  outstream << "</SequenceQueue>" << Qt::endl;
4921 
4922  appendLogText(i18n("Sequence queue saved to %1", path));
4923  file.flush();
4924  file.close();
4925  // update save button tool tip
4926  queueSaveB->setToolTip("Save to " + file.fileName());
4927 
4928  return true;
4929 }
4930 
4931 void Capture::resetJobs()
4932 {
4933  // Stop any running capture
4934  stop();
4935 
4936  // If a job is selected for edit, reset only that job
4937  if (m_JobUnderEdit == true)
4938  {
4939  SequenceJob * job = jobs.at(queueTable->currentRow());
4940  if (nullptr != job)
4941  job->resetStatus();
4942  }
4943  else
4944  {
4946  nullptr, i18n("Are you sure you want to reset status of all jobs?"), i18n("Reset job status"),
4947  KStandardGuiItem::cont(), KStandardGuiItem::cancel(), "reset_job_status_warning") != KMessageBox::Continue)
4948  {
4949  return;
4950  }
4951 
4952  foreach (SequenceJob * job, jobs)
4953  job->resetStatus();
4954  }
4955 
4956  // Also reset the storage count for all jobs
4957  capturedFramesMap.clear();
4958 
4959  // We're not controlled by the Scheduler, restore progress option
4960  ignoreJobProgress = Options::alwaysResetSequenceWhenStarting();
4961 }
4962 
4964 {
4965  // This function is called independently from the Scheduler or the UI, so honor the change
4966  ignoreJobProgress = true;
4967 }
4968 
4969 void Capture::syncGUIToJob(SequenceJob * job)
4970 {
4971  if (job == nullptr)
4972  {
4973  qWarning(KSTARS_EKOS_CAPTURE) << "syncGuiToJob with null job.";
4974  // Everything below depends on job. Just return.
4975  return;
4976  }
4977 
4978  auto rawPrefix = job->getCoreProperty(SequenceJob::SJ_RawPrefix).toString();
4979  auto filterEnabled = job->getCoreProperty(SequenceJob::SJ_FilterPrefixEnabled).toBool();
4980  auto expEnabled = job->getCoreProperty(SequenceJob::SJ_ExpPrefixEnabled).toBool();
4981  auto tsEnabled = job->getCoreProperty(SequenceJob::SJ_TimeStampPrefixEnabled).toBool();
4982  const auto roi = job->getCoreProperty(SequenceJob::SJ_ROI).toRect();
4983 
4984  captureFormatS->setCurrentText(job->getCoreProperty(SequenceJob::SJ_Format).toString());
4985  captureEncodingS->setCurrentText(job->getCoreProperty(SequenceJob::SJ_Encoding).toString());
4986  captureExposureN->setValue(job->getCoreProperty(SequenceJob::SJ_Exposure).toDouble());
4987  captureBinHN->setValue(job->getCoreProperty(SequenceJob::SJ_Binning).toPoint().x());
4988  captureBinVN->setValue(job->getCoreProperty(SequenceJob::SJ_Binning).toPoint().y());
4989  captureFrameXN->setValue(roi.x());
4990  captureFrameYN->setValue(roi.y());
4991  captureFrameWN->setValue(roi.width());
4992  captureFrameHN->setValue(roi.height());
4993  captureFilterS->setCurrentIndex(job->getTargetFilter() - 1);
4994  captureTypeS->setCurrentIndex(job->getFrameType());
4995  filePrefixT->setText(rawPrefix);
4996  fileFilterS->setChecked(filterEnabled);
4997  fileDurationS->setChecked(expEnabled);
4998  fileTimestampS->setChecked(tsEnabled);
4999  captureCountN->setValue(job->getCoreProperty(SequenceJob::SJ_Count).toInt());
5000  captureDelayN->setValue(job->getCoreProperty(SequenceJob::SJ_Delay).toInt() / 1000);
5001  fileUploadModeS->setCurrentIndex(job->getUploadMode());
5002  fileRemoteDirT->setEnabled(fileUploadModeS->currentIndex() != 0);
5003  fileRemoteDirT->setText(job->getCoreProperty(SequenceJob::SJ_RemoteDirectory).toString());
5004  fileDirectoryT->setText(job->getCoreProperty(SequenceJob::SJ_LocalDirectory).toString());
5005 
5006  // Temperature Options
5007  cameraTemperatureS->setChecked(job->getCoreProperty(SequenceJob::SJ_EnforceTemperature).toBool());
5008  if (job->getCoreProperty(SequenceJob::SJ_EnforceTemperature).toBool())
5009  cameraTemperatureN->setValue(job->getTargetTemperature());
5010 
5011  // Start guider drift options
5012  m_LimitsUI->startGuiderDriftS->setChecked(job->getCoreProperty(SequenceJob::SJ_EnforceStartGuiderDrift).toBool());
5013  if (job->getCoreProperty(SequenceJob::SJ_EnforceStartGuiderDrift).toBool())
5014  m_LimitsUI->startGuiderDriftN->setValue(job->getTargetStartGuiderDrift());
5015 
5016  // Flat field options
5017  calibrationB->setEnabled(job->getFrameType() != FRAME_LIGHT);
5018  generateDarkFlatsB->setEnabled(job->getFrameType() != FRAME_LIGHT);
5019  flatFieldDuration = job->getFlatFieldDuration();
5020  flatFieldSource = job->getFlatFieldSource();
5021  targetADU = job->getCoreProperty(SequenceJob::SJ_TargetADU).toDouble();
5022  targetADUTolerance = job->getCoreProperty(SequenceJob::SJ_TargetADUTolerance).toDouble();
5023  wallCoord = job->getWallCoord();
5024  preMountPark = job->getPreMountPark();
5025  preDomePark = job->getPreDomePark();
5026 
5027  // Script options
5028  m_Scripts = job->getScripts();
5029 
5030  // Custom Properties
5031  customPropertiesDialog->setCustomProperties(job->getCustomProperties());
5032 
5033  if (captureISOS)
5034  captureISOS->setCurrentIndex(job->getCoreProperty(SequenceJob::SJ_ISOIndex).toInt());
5035 
5036  double gain = getGain();
5037  if (gain >= 0)
5038  captureGainN->setValue(gain);
5039  else
5040  captureGainN->setValue(GainSpinSpecialValue);
5041 
5042  double offset = getOffset();
5043  if (offset >= 0)
5044  captureOffsetN->setValue(offset);
5045  else
5046  captureOffsetN->setValue(OffsetSpinSpecialValue);
5047 
5048  if (job->getTargetRotation() != Ekos::INVALID_VALUE)
5049  {
5050  rotatorSettings->setRotationEnforced(true);
5051  rotatorSettings->setTargetRotationPA(job->getTargetRotation());
5052  }
5053  else
5054  rotatorSettings->setRotationEnforced(false);
5055 
5056  // hide target drift if align check frequency is == 0
5057  if (Options::alignCheckFrequency() == 0)
5058  {
5059  targetDriftLabel->setVisible(false);
5060  targetDrift->setVisible(false);
5061  targetDriftUnit->setVisible(false);
5062  }
5063 
5064  emit settingsUpdated(getPresetSettings());
5065 }
5066 
5068 {
5069  QJsonObject settings;
5070 
5071  // Try to get settings value
5072  // if not found, fallback to camera value
5073  double gain = -1;
5074  if (GainSpinSpecialValue > INVALID_VALUE && captureGainN->value() > GainSpinSpecialValue)
5075  gain = captureGainN->value();
5076  else if (m_captureDeviceAdaptor->getActiveCamera() && m_captureDeviceAdaptor->getActiveCamera()->hasGain())
5077  m_captureDeviceAdaptor->getActiveCamera()->getGain(&gain);
5078 
5079  double offset = -1;
5080  if (OffsetSpinSpecialValue > INVALID_VALUE && captureOffsetN->value() > OffsetSpinSpecialValue)
5081  offset = captureOffsetN->value();
5082  else if (m_captureDeviceAdaptor->getActiveCamera() && m_captureDeviceAdaptor->getActiveCamera()->hasOffset())
5083  m_captureDeviceAdaptor->getActiveCamera()->getOffset(&offset);
5084 
5085  int iso = -1;
5086  if (captureISOS)
5087  iso = captureISOS->currentIndex();
5088  else if (m_captureDeviceAdaptor->getActiveCamera())
5089  iso = m_captureDeviceAdaptor->getActiveCamera()->getChip(ISD::CameraChip::PRIMARY_CCD)->getISOIndex();
5090 
5091  settings.insert("camera", cameraS->currentText());
5092  settings.insert("fw", filterWheelS->currentText());
5093  settings.insert("filter", captureFilterS->currentText());
5094  settings.insert("dark", darkB->isChecked());
5095  settings.insert("exp", captureExposureN->value());
5096  settings.insert("bin", captureBinHN->value());
5097  settings.insert("iso", iso);
5098  settings.insert("frameType", captureTypeS->currentIndex());
5099  settings.insert("captureFormat", captureFormatS->currentIndex());
5100  settings.insert("transferFormat", captureEncodingS->currentIndex());
5101  settings.insert("gain", gain);
5102  settings.insert("offset", offset);
5103  settings.insert("temperature", cameraTemperatureN->value());
5104 
5105  return settings;
5106 }
5107 
5108 void Capture::selectedJobChanged(QModelIndex current, QModelIndex previous)
5109 {
5110  Q_UNUSED(previous)
5111  selectJob(current);
5112 }
5113 
5114 bool Capture::selectJob(QModelIndex i)
5115 {
5116  if (i.row() < 0 || (i.row() + 1) > jobs.size())
5117  return false;
5118 
5119  SequenceJob * job = jobs.at(i.row());
5120 
5121  if (job == nullptr || job->getCoreProperty(SequenceJob::SJ_DarkFlat).toBool())
5122  return false;
5123 
5124  syncGUIToJob(job);
5125 
5126  if (isBusy)
5127  return false;
5128 
5129  if (jobs.size() >= 2)
5130  {
5131  queueUpB->setEnabled(i.row() > 0);
5132  queueDownB->setEnabled(i.row() + 1 < jobs.size());
5133  }
5134 
5135  return true;
5136 }
5137 
5138 void Capture::editJob(QModelIndex i)
5139 {
5140  // Try to select a job. If job not found or not editable return.
5141  if (selectJob(i) == false)
5142  return;
5143 
5144  appendLogText(i18n("Editing job #%1...", i.row() + 1));
5145 
5146  addToQueueB->setIcon(QIcon::fromTheme("dialog-ok-apply"));
5147  addToQueueB->setToolTip(i18n("Apply job changes."));
5148  removeFromQueueB->setToolTip(i18n("Cancel job changes."));
5149 
5150  // Make it sure if user presses enter, the job is validated.
5151  previewB->setDefault(false);
5152  addToQueueB->setDefault(true);
5153 
5154  m_JobUnderEdit = true;
5155 }
5156 
5157 void Capture::resetJobEdit()
5158 {
5159  if (m_JobUnderEdit)
5160  appendLogText(i18n("Editing job canceled."));
5161 
5162  m_JobUnderEdit = false;
5163  addToQueueB->setIcon(QIcon::fromTheme("list-add"));
5164 
5165  addToQueueB->setToolTip(i18n("Add job to sequence queue"));
5166  removeFromQueueB->setToolTip(i18n("Remove job from sequence queue"));
5167 
5168  addToQueueB->setDefault(false);
5169  previewB->setDefault(true);
5170 }
5171 
5173 {
5174  int totalImageCount = 0;
5175  int totalImageCompleted = 0;
5176 
5177  foreach (SequenceJob * job, jobs)
5178  {
5179  totalImageCount += job->getCoreProperty(SequenceJob::SJ_Count).toInt();
5180  totalImageCompleted += job->getCompleted();
5181  }
5182 
5183  if (totalImageCount != 0)
5184  return ((static_cast<double>(totalImageCompleted) / totalImageCount) * 100.0);
5185  else
5186  return -1;
5187 }
5188 
5190 {
5191  if (activeJob == nullptr)
5192  return -1;
5193 
5194  for (int i = 0; i < jobs.count(); i++)
5195  {
5196  if (activeJob == jobs[i])
5197  return i;
5198  }
5199 
5200  return -1;
5201 }
5202 
5204 {
5205  int completedJobs = 0;
5206 
5207  foreach (SequenceJob * job, jobs)
5208  {
5209  if (job->getStatus() == JOB_DONE)
5210  completedJobs++;
5211  }
5212 
5213  return (jobs.count() - completedJobs);
5214 }
5215 
5217 {
5218  if (id < jobs.count())
5219  {
5220  SequenceJob * job = jobs.at(id);
5221  return job->getStatusString();
5222  }
5223 
5224  return QString();
5225 }
5226 
5228 {
5229  if (id < jobs.count())
5230  {
5231  SequenceJob * job = jobs.at(id);
5232  return job->getCoreProperty(SequenceJob::SJ_Filter).toString();
5233  }
5234 
5235  return QString();
5236 }
5237 
5239 {
5240  if (id < jobs.count())
5241  {
5242  SequenceJob * job = jobs.at(id);
5243  return job->getCompleted();
5244  }
5245 
5246  return -1;
5247 }
5248 
5250 {
5251  if (id < jobs.count())
5252  {
5253  SequenceJob * job = jobs.at(id);
5254  return job->getCoreProperty(SequenceJob::SJ_Count).toInt();
5255  }
5256 
5257  return -1;
5258 }
5259 
5261 {
5262  if (id < jobs.count())
5263  {
5264  SequenceJob * job = jobs.at(id);
5265  return job->getExposeLeft();
5266  }
5267 
5268  return -1;
5269 }
5270 
5272 {
5273  if (id < jobs.count())
5274  {
5275  SequenceJob * job = jobs.at(id);
5276  return job->getCoreProperty(SequenceJob::SJ_Exposure).toDouble();
5277  }
5278 
5279  return -1;
5280 }
5281 
5282 CCDFrameType Capture::getJobFrameType(int id)
5283 {
5284  if (id < jobs.count())
5285  {
5286  SequenceJob * job = jobs.at(id);
5287  return job->getFrameType();
5288  }
5289 
5290  return FRAME_NONE;
5291 }
5292 
5294 {
5295  int remaining = 0;
5296  double estimatedDownloadTime = getEstimatedDownloadTime();
5297 
5298  foreach (SequenceJob * job, jobs)
5299  remaining += job->getJobRemainingTime(estimatedDownloadTime);
5300 
5301  return remaining;
5302 }
5303 
5305 {
5306  if (activeJob == nullptr)
5307  return -1;
5308 
5309  return activeJob->getJobRemainingTime(getEstimatedDownloadTime());
5310 }
5311 
5312 void Capture::setMaximumGuidingDeviation(bool enable, double value)
5313 {
5314  m_LimitsUI->limitGuideDeviationS->setChecked(enable);
5315  if (enable)
5316  m_LimitsUI->limitGuideDeviationN->setValue(value);
5317 }
5318 
5319 void Capture::setInSequenceFocus(bool enable, double HFR)
5320 {
5321  m_LimitsUI->limitFocusHFRS->setChecked(enable);
5322  if (enable)
5323  m_LimitsUI->limitFocusHFRN->setValue(HFR);
5324 }
5325 
5326 void Capture::setTargetTemperature(double temperature)
5327 {
5328  cameraTemperatureN->setValue(temperature);
5329 }
5330 
5332 {
5333  setActiveJob(nullptr);
5334  while (queueTable->rowCount() > 0)
5335  queueTable->removeRow(0);
5336  qDeleteAll(jobs);
5337  jobs.clear();
5338 
5339  while (m_SequenceArray.count())
5340  m_SequenceArray.pop_back();
5341  emit sequenceChanged(m_SequenceArray);
5342 }
5343 
5345 {
5346  if (jobs.count() == 0)
5347  return "Invalid";
5348 
5349  if (isBusy)
5350  return "Running";
5351 
5352  int idle = 0, error = 0, complete = 0, aborted = 0, running = 0;
5353 
5354  foreach (SequenceJob * job, jobs)
5355  {
5356  switch (job->getStatus())
5357  {
5358  case JOB_ABORTED:
5359  aborted++;
5360  break;
5361  case JOB_BUSY:
5362  running++;
5363  break;
5364  case JOB_DONE:
5365  complete++;
5366  break;
5367  case JOB_ERROR:
5368  error++;
5369  break;
5370  case JOB_IDLE:
5371  idle++;
5372  break;
5373  }
5374  }
5375 
5376  if (error > 0)
5377  return "Error";
5378 
5379  if (aborted > 0)
5380  {
5381  if (m_State == CAPTURE_SUSPENDED)
5382  return "Suspended";
5383  else
5384  return "Aborted";
5385  }
5386 
5387  if (running > 0)
5388  return "Running";
5389 
5390  if (idle == jobs.count())
5391  return "Idle";
5392 
5393  if (complete == jobs.count())
5394  return "Complete";
5395 
5396  return "Invalid";
5397 }
5398 
5399 void Capture::processTelescopeNumber(INumberVectorProperty * nvp)
5400 {
5401  // If it is not ours, return.
5402  if (nvp->device != m_captureDeviceAdaptor->getMount()->getDeviceName() || strstr(nvp->name, "EQUATORIAL_") == nullptr)
5403  return;
5404 
5405  switch (meridianFlipStage)
5406  {
5407  case MF_NONE:
5408  break;
5409  case MF_INITIATED:
5410  {
5411  if (nvp->s == IPS_BUSY)
5412  setMeridianFlipStage(MF_FLIPPING);
5413  }
5414  break;
5415 
5416  case MF_FLIPPING:
5417  {
5418  if (m_captureDeviceAdaptor->getMount() != nullptr && m_captureDeviceAdaptor->getMount()->isSlewing())
5419  setMeridianFlipStage(MF_SLEWING);
5420  }
5421  break;
5422 
5423  default:
5424  break;
5425  }
5426 }
5427 
5428 void Capture::processFlipCompleted()
5429 {
5430  // If dome is syncing, wait until it stops
5431  if (m_captureDeviceAdaptor->getDome() && m_captureDeviceAdaptor->getDome()->isMoving())
5432  return;
5433 
5434  appendLogText(i18n("Telescope completed the meridian flip."));
5435 
5436  KSNotification::event(QLatin1String("MeridianFlipCompleted"), i18n("Meridian flip is successfully completed"),
5437  KSNotification::EVENT_INFO);
5438 
5439 
5440  if (m_State == CAPTURE_IDLE || m_State == CAPTURE_ABORTED || m_State == CAPTURE_COMPLETE)
5441  {
5442  // reset the meridian flip stage and jump directly MF_NONE, since no
5443  // restart of guiding etc. necessary
5444  setMeridianFlipStage(MF_NONE);
5445  return;
5446  }
5447 }
5448 
5449 bool Capture::checkGuidingAfterFlip()
5450 {
5451  // if no meridian flip has completed, we do not touch guiding
5452  if (meridianFlipStage < MF_COMPLETED)
5453  return false;
5454  // If we're not autoguiding then we're done
5455  if (resumeGuidingAfterFlip == false)
5456  {
5457  setMeridianFlipStage(MF_NONE);
5458  return false;
5459  }
5460 
5461  // if we are waiting for a calibration, start it
5462  if (meridianFlipStage >= MF_COMPLETED && meridianFlipStage < MF_GUIDING)
5463  {
5464  appendLogText(i18n("Performing post flip re-calibration and guiding..."));
5465 
5466  m_State = CAPTURE_CALIBRATING;
5467  emit newStatus(Ekos::CAPTURE_CALIBRATING);
5468 
5469  setMeridianFlipStage(MF_GUIDING);
5470  emit meridianFlipCompleted();
5471  return true;
5472  }
5473  else if (m_State == CAPTURE_CALIBRATING && (m_GuideState == GUIDE_CALIBRATION_ERROR || m_GuideState == GUIDE_ABORTED))
5474  {
5475  // restart guiding after failure
5476  appendLogText(i18n("Post meridian flip calibration error. Restarting..."));
5477  emit meridianFlipCompleted();
5478  return true;
5479  }
5480  else
5481  // in all other cases, do not touch
5482  return false;
5483 }
5484 
5485 bool Capture::checkAlignmentAfterFlip()
5486 {
5487  // if no meridian flip has completed, we do not touch guiding
5488  if (meridianFlipStage < MF_COMPLETED)
5489  return false;
5490  // If we do not need to align then we're done
5491  if (resumeAlignmentAfterFlip == false)
5492  return false;
5493 
5494  // if we are waiting for a calibration, start it
5495  if (m_State < CAPTURE_ALIGNING)
5496  {
5497  appendLogText(i18n("Performing post flip re-alignment..."));
5498 
5499  retries = 0;
5500  m_State = CAPTURE_ALIGNING;
5501  emit newStatus(Ekos::CAPTURE_ALIGNING);
5502 
5503  setMeridianFlipStage(MF_ALIGNING);
5504  return true;
5505  }
5506  else
5507  // in all other cases, do not touch
5508  return false;
5509 }
5510 
5511 
5512 bool Capture::checkPausing()
5513 {
5514  if (m_State == CAPTURE_PAUSE_PLANNED)
5515  {
5516  appendLogText(i18n("Sequence paused."));
5517  m_State = CAPTURE_PAUSED;
5518  emit newStatus(m_State);
5519  // handle a requested meridian flip
5520  if (meridianFlipStage != MF_NONE)
5521  setMeridianFlipStage(MF_READY);
5522  // pause
5523  return true;
5524  }
5525  // no pause
5526  return false;
5527 }
5528 
5529 
5530 bool Capture::checkMeridianFlipReady()
5531 {
5532  if (m_captureDeviceAdaptor->getMount() == nullptr)
5533  return false;
5534 
5535  // If active job is taking flat field image at a wall source
5536  // then do not flip.
5537  if (activeJob && activeJob->getFrameType() == FRAME_FLAT && activeJob->getFlatFieldSource() == SOURCE_WALL)
5538  return false;
5539 
5540  if (meridianFlipStage != MF_REQUESTED)
5541  // if no flip has been requested or is already ongoing
5542  return false;
5543 
5544  // meridian flip requested or already in action
5545 
5546  // Reset frame if we need to do focusing later on
5547  if (isInSequenceFocus || (m_LimitsUI->limitRefocusS->isChecked() && getRefocusEveryNTimerElapsedSec() > 0))
5548  emit resetFocus();
5549 
5550  // signal that meridian flip may take place
5551  if (meridianFlipStage == MF_REQUESTED)
5552  setMeridianFlipStage(MF_READY);
5553 
5554 
5555  return true;
5556 }
5557 
5558 void Capture::checkGuideDeviationTimeout()
5559 {
5560  if (activeJob && activeJob->getStatus() == JOB_ABORTED && m_DeviationDetected)
5561  {
5562  appendLogText(i18n("Guide module timed out."));
5563  m_DeviationDetected = false;
5564 
5565  // If capture was suspended, it should be aborted (failed) now.
5566  if (m_State == CAPTURE_SUSPENDED)
5567  {
5568  m_State = CAPTURE_ABORTED;
5569  emit newStatus(m_State);
5570  }
5571  }
5572 }
5573 
5574 void Capture::setAlignStatus(AlignState state)
5575 {
5576  if (state != m_AlignState)
5577  qCDebug(KSTARS_EKOS_CAPTURE) << "Align State changed from" << Ekos::getAlignStatusString(
5578  m_AlignState) << "to" << Ekos::getAlignStatusString(state);
5579  m_AlignState = state;
5580 
5581  resumeAlignmentAfterFlip = true;
5582 
5583  switch (state)
5584  {
5585  case ALIGN_COMPLETE:
5586  if (meridianFlipStage == MF_ALIGNING)
5587  {
5588  appendLogText(i18n("Post flip re-alignment completed successfully."));
5589  retries = 0;
5590  // Trigger guiding if necessary.
5591  if (checkGuidingAfterFlip() == false)
5592  {
5593  // If no guiding is required, the meridian flip is complete
5594  setMeridianFlipStage(MF_NONE);
5595  m_State = CAPTURE_WAITING;
5596  }
5597  }
5598  break;
5599 
5600  case ALIGN_ABORTED:
5601  case ALIGN_FAILED:
5602  // TODO run it 3 times before giving up
5603  if (meridianFlipStage == MF_ALIGNING)
5604  {
5605  if (++retries == 3)
5606  {
5607  appendLogText(i18n("Post-flip alignment failed."));
5608  abort();
5609  }
5610  else
5611  {
5612  appendLogText(i18n("Post-flip alignment failed. Retrying..."));
5613 
5614  this->m_State = CAPTURE_ALIGNING;
5615  emit newStatus(Ekos::CAPTURE_ALIGNING);
5616 
5617  setMeridianFlipStage(MF_ALIGNING);
5618  }
5619  }
5620  break;
5621 
5622  default:
5623  break;
5624  }
5625 }
5626 
5627 void Capture::setGuideStatus(GuideState state)
5628 {
5629  if (state != m_GuideState)
5630  qCDebug(KSTARS_EKOS_CAPTURE) << "Guiding state changed from" << Ekos::getGuideStatusString(m_GuideState)
5631  << "to" << Ekos::getGuideStatusString(state);
5632  switch (state)
5633  {
5634  case GUIDE_IDLE:
5635  break;
5636 
5637  case GUIDE_GUIDING:
5638  case GUIDE_CALIBRATION_SUCCESS:
5639  autoGuideReady = true;
5640  break;
5641 
5642  case GUIDE_ABORTED:
5643  case GUIDE_CALIBRATION_ERROR:
5644  processGuidingFailed();
5645  m_GuideState = state;
5646  break;
5647 
5648  case GUIDE_DITHERING_SUCCESS:
5649  qCInfo(KSTARS_EKOS_CAPTURE) << "Dithering succeeded, capture state" << getCaptureStatusString(m_State);
5650  // do nothing if something happened during dithering
5651  appendLogText(i18n("Dithering succeeded."));
5652  if (m_State != CAPTURE_DITHERING)
5653  break;
5654 
5655  if (Options::guidingSettle() > 0)
5656  {
5657  // N.B. Do NOT convert to i18np since guidingRate is DOUBLE value (e.g. 1.36) so we always use plural with that.
5658  appendLogText(i18n("Dither complete. Resuming in %1 seconds...", Options::guidingSettle()));
5659  QTimer::singleShot(Options::guidingSettle() * 1000, this, [this]()
5660  {
5661  m_DitheringState = IPS_OK;
5662  });
5663  }
5664  else
5665  {
5666  appendLogText(i18n("Dither complete."));
5667  m_DitheringState = IPS_OK;
5668  }
5669  break;
5670 
5671  case GUIDE_DITHERING_ERROR:
5672  qCInfo(KSTARS_EKOS_CAPTURE) << "Dithering failed, capture state" << getCaptureStatusString(m_State);
5673  if (m_State != CAPTURE_DITHERING)
5674  break;
5675 
5676  if (Options::guidingSettle() > 0)
5677  {
5678  // N.B. Do NOT convert to i18np since guidingRate is DOUBLE value (e.g. 1.36) so we always use plural with that.
5679  appendLogText(i18n("Warning: Dithering failed. Resuming in %1 seconds...", Options::guidingSettle()));
5680  // set dithering state to OK after settling time and signal to proceed
5681  QTimer::singleShot(Options::guidingSettle() * 1000, this, [this]()
5682  {
5683  m_DitheringState = IPS_OK;
5684  });
5685  }
5686  else
5687  {
5688  appendLogText(i18n("Warning: Dithering failed."));
5689  // signal OK so that capturing may continue although dithering failed
5690  m_DitheringState = IPS_OK;
5691  }
5692 
5693  break;
5694 
5695  default:
5696  break;
5697  }
5698 
5699  m_GuideState = state;
5700  // forward it to the currently active sequence job
5701  if (activeJob != nullptr)
5702  activeJob->setCoreProperty(SequenceJob::SJ_GuiderActive, isActivelyGuiding());
5703 }
5704 
5705 
5706 void Capture::processGuidingFailed()
5707 {
5708  if (m_FocusState > FOCUS_PROGRESS)
5709  {
5710  appendLogText(i18n("Autoguiding stopped. Waiting for autofocus to finish..."));
5711  }
5712  // If Autoguiding was started before and now stopped, let's abort (unless we're doing a meridian flip)
5713  else if (isGuidingOn() && meridianFlipStage == MF_NONE &&
5714  // JM 2022.08.03: Only abort if the current job is LIGHT. For calibration frames, we can ignore guide failures.
5715  ((activeJob && activeJob->getStatus() == JOB_BUSY && activeJob->getFrameType() == FRAME_LIGHT) ||
5716  this->m_State == CAPTURE_SUSPENDED || this->m_State == CAPTURE_PAUSED))
5717  {
5718  appendLogText(i18n("Autoguiding stopped. Aborting..."));
5719  abort();
5720  }
5721  else if (meridianFlipStage == MF_GUIDING)
5722  {
5723  if (++retries >= 3)
5724  {
5725  appendLogText(i18n("Post meridian flip calibration error. Aborting..."));
5726  abort();
5727  }
5728  }
5729  autoGuideReady = false;
5730 }
5731 
5732 void Capture::checkFrameType(int index)
5733 {
5734  calibrationB->setEnabled(index != FRAME_LIGHT);
5735  generateDarkFlatsB->setEnabled(index != FRAME_LIGHT);
5736 }
5737 
5738 double Capture::setCurrentADU(double value)
5739 {
5740  if (activeJob == nullptr)
5741  {
5742  qWarning(KSTARS_EKOS_CAPTURE) << "setCurrentADU with null activeJob.";
5743  // Nothing good to do here. Just don't crash.
5744  return value;
5745  }
5746 
5747  double nextExposure = 0;
5748  double targetADU = activeJob->getCoreProperty(SequenceJob::SJ_TargetADU).toDouble();
5749  std::vector<double> coeff;
5750 
5751  // Check if saturated, then take shorter capture and discard value
5752  ExpRaw.append(activeJob->getCoreProperty(SequenceJob::SJ_Exposure).toDouble());
5753  ADURaw.append(value);
5754 
5755  qCDebug(KSTARS_EKOS_CAPTURE) << "Capture: Current ADU = " << value << " targetADU = " << targetADU
5756  << " Exposure Count: " << ExpRaw.count();
5757 
5758  // Most CCDs are quite linear so 1st degree polynomial is quite sufficient
5759  // But DSLRs can exhibit non-linear response curve and so a 2nd degree polynomial is more appropriate
5760  if (ExpRaw.count() >= 2)
5761  {
5762  if (ExpRaw.count() >= 5)
5763  {
5764  double chisq = 0;
5765 
5766  coeff = gsl_polynomial_fit(ADURaw.data(), ExpRaw.data(), ExpRaw.count(), 2, chisq);
5767  qCDebug(KSTARS_EKOS_CAPTURE) << "Running polynomial fitting. Found " << coeff.size() << " coefficients.";
5768  if (std::isnan(coeff[0]) || std::isinf(coeff[0]))
5769  {
5770  qCDebug(KSTARS_EKOS_CAPTURE) << "Coefficients are invalid.";
5771  targetADUAlgorithm = ADU_LEAST_SQUARES;
5772  }
5773  else
5774  {
5775  nextExposure = coeff[0] + (coeff[1] * targetADU) + (coeff[2] * pow(targetADU, 2));
5776  // If exposure is not valid or does not make sense, then we fall back to least squares
5777  if (nextExposure < 0 || (nextExposure > ExpRaw.last() || targetADU < ADURaw.last())
5778  || (nextExposure < ExpRaw.last() || targetADU > ADURaw.last()))
5779  {
5780  nextExposure = 0;
5781  targetADUAlgorithm = ADU_LEAST_SQUARES;
5782  }
5783  else
5784  {
5785  targetADUAlgorithm = ADU_POLYNOMIAL;
5786  for (size_t i = 0; i < coeff.size(); i++)
5787  qCDebug(KSTARS_EKOS_CAPTURE) << "Coeff #" << i << "=" << coeff[i];
5788  }
5789  }
5790  }
5791 
5792  bool looping = false;
5793  if (ExpRaw.count() >= 10)
5794  {
5795  int size = ExpRaw.count();
5796  looping = (std::fabs(ExpRaw[size - 1] - ExpRaw[size - 2] < 0.01)) &&
5797  (std::fabs(ExpRaw[size - 2] - ExpRaw[size - 3] < 0.01));
5798  if (looping && targetADUAlgorithm == ADU_POLYNOMIAL)
5799  {
5800  qWarning(KSTARS_EKOS_CAPTURE) << "Detected looping in polynomial results. Falling back to llsqr.";
5801  targetADUAlgorithm = ADU_LEAST_SQUARES;
5802  }
5803  }
5804 
5805  // If we get invalid data, let's fall back to llsq
5806  // Since polyfit can be unreliable at low counts, let's only use it at the 5th exposure
5807  // if we don't have results already.
5808  if (targetADUAlgorithm == ADU_LEAST_SQUARES)
5809  {
5810  double a = 0, b = 0;
5811  llsq(ExpRaw, ADURaw, a, b);
5812 
5813  // If we have valid results, let's calculate next exposure
5814  if (a != 0.0)
5815  {
5816  nextExposure = (targetADU - b) / a;
5817  // If we get invalid value, let's just proceed iteratively
5818  if (nextExposure < 0)
5819  nextExposure = 0;
5820  }
5821  }
5822  }
5823 
5824  // 2022.01.12 Put a hard limit to 180 seconds.
5825  // If it goes over this limit, the flat source is probably off.
5826  if (nextExposure == 0.0 || nextExposure > 180)
5827  {
5828  if (value < targetADU)
5829  nextExposure = activeJob->getCoreProperty(SequenceJob::SJ_Exposure).toDouble() * 1.25;
5830  else
5831  nextExposure = activeJob->getCoreProperty(SequenceJob::SJ_Exposure).toDouble() * .75;
5832  }
5833 
5834  qCDebug(KSTARS_EKOS_CAPTURE) << "next flat exposure is" << nextExposure;
5835 
5836  return nextExposure;
5837 }
5838 
5839 // Based on John Burkardt LLSQ (LGPL)
5840 void Capture::llsq(QVector<double> x, QVector<double> y, double &a, double &b)
5841 {
5842  double bot;
5843  int i;
5844  double top;
5845  double xbar;
5846  double ybar;
5847  int n = x.count();
5848  //
5849  // Special case.
5850  //
5851  if (n == 1)
5852  {
5853  a = 0.0;
5854  b = y[0];
5855  return;
5856  }
5857  //
5858  // Average X and Y.
5859  //
5860  xbar = 0.0;
5861  ybar = 0.0;
5862  for (i = 0; i < n; i++)
5863  {
5864  xbar = xbar + x[i];
5865  ybar = ybar + y[i];
5866  }
5867  xbar = xbar / static_cast<double>(n);
5868  ybar = ybar / static_cast<double>(n);
5869  //
5870  // Compute Beta.
5871  //
5872  top = 0.0;
5873  bot = 0.0;
5874  for (i = 0; i < n; i++)
5875  {
5876  top = top + (x[i] - xbar) * (y[i] - ybar);
5877  bot = bot + (x[i] - xbar) * (x[i] - xbar);
5878  }
5879 
5880  a = top / bot;
5881 
5882  b = ybar - a * xbar;
5883 }
5884 
5885 void Capture::setDirty()
5886 {
5887  m_Dirty = true;
5888 }
5889 
5890 
5892 {
5893  if (m_captureDeviceAdaptor->getActiveCamera() && m_captureDeviceAdaptor->getActiveCamera()->hasCoolerControl())
5894  return true;
5895 
5896  return false;
5897 }
5898 
5899 bool Capture::setCoolerControl(bool enable)
5900 {
5901  if (m_captureDeviceAdaptor->getActiveCamera() && m_captureDeviceAdaptor->getActiveCamera()->hasCoolerControl())
5902  return m_captureDeviceAdaptor->getActiveCamera()->setCoolerControl(enable);
5903 
5904  return false;
5905 }
5906 
5908 {
5909  // If HFR limit was set from file, we cannot override it.
5910  if (fileHFR > 0)
5911  return;
5912 
5913  m_LimitsUI->limitFocusHFRN->setValue(0);
5914  //firstAutoFocus = true;
5915 }
5916 
5917 void Capture::openCalibrationDialog()
5918 {
5919  QDialog calibrationDialog(this);
5920 
5921  Ui_calibrationOptions calibrationOptions;
5922  calibrationOptions.setupUi(&calibrationDialog);
5923 
5924  if (m_captureDeviceAdaptor->getMount())
5925  {
5926  calibrationOptions.parkMountC->setEnabled(m_captureDeviceAdaptor->getMount()->canPark());
5927  calibrationOptions.parkMountC->setChecked(preMountPark);
5928  }
5929  else
5930  calibrationOptions.parkMountC->setEnabled(false);
5931 
5932  if (m_captureDeviceAdaptor->getDome())
5933  {
5934  calibrationOptions.parkDomeC->setEnabled(m_captureDeviceAdaptor->getDome()->canPark());
5935  calibrationOptions.parkDomeC->setChecked(preDomePark);
5936  }
5937  else
5938  calibrationOptions.parkDomeC->setEnabled(false);
5939 
5940  switch (flatFieldSource)
5941  {
5942  case SOURCE_MANUAL:
5943  calibrationOptions.manualSourceC->setChecked(true);
5944  break;
5945 
5946  case SOURCE_FLATCAP:
5947  calibrationOptions.flatDeviceSourceC->setChecked(true);
5948  break;
5949 
5950  case SOURCE_DARKCAP:
5951  calibrationOptions.darkDeviceSourceC->setChecked(true);
5952  break;
5953 
5954  case SOURCE_WALL:
5955  calibrationOptions.wallSourceC->setChecked(true);
5956  calibrationOptions.azBox->setText(wallCoord.az().toDMSString());
5957  calibrationOptions.altBox->setText(wallCoord.alt().toDMSString());
5958  break;
5959 
5960  case SOURCE_DAWN_DUSK:
5961  calibrationOptions.dawnDuskFlatsC->setChecked(true);
5962  break;
5963  }
5964 
5965  switch (flatFieldDuration)
5966  {
5967  case DURATION_MANUAL:
5968  calibrationOptions.manualDurationC->setChecked(true);
5969  break;
5970 
5971  case DURATION_ADU:
5972  calibrationOptions.ADUC->setChecked(true);
5973  calibrationOptions.ADUValue->setValue(static_cast<int>(std::round(targetADU)));
5974  calibrationOptions.ADUTolerance->setValue(static_cast<int>(std::round(targetADUTolerance)));
5975  break;
5976  }
5977 
5978  if (calibrationDialog.exec() == QDialog::Accepted)
5979  {
5980  if (calibrationOptions.manualSourceC->isChecked())
5981  flatFieldSource = SOURCE_MANUAL;
5982  else if (calibrationOptions.flatDeviceSourceC->isChecked())
5983  flatFieldSource = SOURCE_FLATCAP;
5984  else if (calibrationOptions.darkDeviceSourceC->isChecked())
5985  flatFieldSource = SOURCE_DARKCAP;
5986  else if (calibrationOptions.wallSourceC->isChecked())
5987  {
5988  dms wallAz, wallAlt;
5989  bool azOk = false, altOk = false;
5990 
5991  wallAz = calibrationOptions.azBox->createDms(&azOk);
5992  wallAlt = calibrationOptions.altBox->createDms(&altOk);
5993 
5994  if (azOk && altOk)
5995  {
5996  flatFieldSource = SOURCE_WALL;
5997  wallCoord.setAz(wallAz);
5998  wallCoord.setAlt(wallAlt);
5999  }
6000  else
6001  {
6002  calibrationOptions.manualSourceC->setChecked(true);
6003  KSNotification::error(i18n("Wall coordinates are invalid."));
6004  }
6005  }
6006  else
6007  flatFieldSource = SOURCE_DAWN_DUSK;
6008 
6009  if (calibrationOptions.manualDurationC->isChecked())
6010  flatFieldDuration = DURATION_MANUAL;
6011  else
6012  {
6013  flatFieldDuration = DURATION_ADU;
6014  targetADU = calibrationOptions.ADUValue->value();
6015  targetADUTolerance = calibrationOptions.ADUTolerance->value();
6016  }
6017 
6018  preMountPark = calibrationOptions.parkMountC->isChecked();
6019  preDomePark = calibrationOptions.parkDomeC->isChecked();
6020 
6021  setDirty();
6022 
6023  Options::setCalibrationFlatSourceIndex(flatFieldSource);
6024  Options::setCalibrationFlatDurationIndex(flatFieldDuration);
6025  Options::setCalibrationWallAz(wallCoord.az().Degrees());
6026  Options::setCalibrationWallAlt(wallCoord.alt().Degrees());
6027  Options::setCalibrationADUValue(static_cast<uint>(std::round(targetADU)));
6028  Options::setCalibrationADUValueTolerance(static_cast<uint>(std::round(targetADUTolerance)));
6029  }
6030 }
6031 
6032 /**
6033  * @brief Check all tasks that might be pending before capturing may start.
6034  *
6035  * The following checks are executed:
6036  * 1. Are there any pending jobs that failed? If yes, return with IPS_ALERT.
6037  * 2. Has pausing been initiated (@see checkPausing()).
6038  * 3. Is a meridian flip already running (@see checkMeridianFlipRunning()) or ready
6039  * for execution (@see checkMeridianFlipReady()).
6040  * 4. Is a post meridian flip alignment running (@see checkAlignmentAfterFlip()).
6041  * 5. Is post flip guiding required or running (@see checkGuidingAfterFlip().
6042  * 6. Is the guiding deviation below the expected limit (@see setGuideDeviation(double,double)).
6043  * 7. Is dithering required or ongoing (@see checkDithering()).
6044  * 8. Is re-focusing required or ongoing (@see startFocusIfRequired()).
6045  * 9. Has guiding been resumed and needs to be restarted (@see resumeGuiding())
6046  *
6047  * If none of this is true, everything is ready and capturing may be started.
6048  *
6049  * @return IPS_OK iff no task is pending, IPS_BUSY otherwise (or IPS_ALERT if a problem occured)
6050  */
6051 IPState Capture::checkLightFramePendingTasks()
6052 {
6053  // step 1: did one of the pending jobs fail or has the user aborted the capture?
6054  if (m_State == CAPTURE_ABORTED)
6055  return IPS_ALERT;
6056 
6057  // step 2: check if pausing has been requested
6058  if (checkPausing() == true)
6059  {
6060  pauseFunction = &Capture::startNextExposure;
6061  return IPS_BUSY;
6062  }
6063 
6064  // step 3: check if meridian flip is already running or ready for execution
6065  if (checkMeridianFlipRunning() || checkMeridianFlipReady())
6066  return IPS_BUSY;
6067 
6068  // step 4: check if post flip alignment is running
6069  if (m_State == CAPTURE_ALIGNING || checkAlignmentAfterFlip())
6070  return IPS_BUSY;
6071 
6072  // step 5: check if post flip guiding is running
6073  // MF_NONE is set as soon as guiding is running and the guide deviation is below the limit
6074  if (meridianFlipStage >= MF_COMPLETED && m_GuideState != GUIDE_GUIDING && checkGuidingAfterFlip())
6075  return IPS_BUSY;
6076 
6077  // step 6: check guide deviation for non meridian flip stages if the initial guide limit is set.
6078  // Wait until the guide deviation is reported to be below the limit (@see setGuideDeviation(double, double)).
6079  if (m_State == CAPTURE_PROGRESS && m_GuideState == GUIDE_GUIDING && m_LimitsUI->startGuiderDriftS->isChecked())
6080  return IPS_BUSY;
6081 
6082  // step 7: in case that a meridian flip has been completed and a guide deviation limit is set, we wait
6083  // until the guide deviation is reported to be below the limit (@see setGuideDeviation(double, double)).
6084  // Otherwise the meridian flip is complete
6085  if (m_State == CAPTURE_CALIBRATING && meridianFlipStage == MF_GUIDING)
6086  {
6087  if (m_LimitsUI->limitGuideDeviationS->isChecked() || m_LimitsUI->startGuiderDriftS->isChecked())
6088  return IPS_BUSY;
6089  else
6090  setMeridianFlipStage(MF_NONE);
6091  }
6092 
6093  // step 8: check if dithering is required or running
6094  if ((m_State == CAPTURE_DITHERING && m_DitheringState != IPS_OK) || checkDithering())
6095  return IPS_BUSY;
6096 
6097  // step 9: check if re-focusing is required
6098  // Needs to be checked after dithering checks to avoid dithering in parallel
6099  // to focusing, since @startFocusIfRequired() might change its value over time
6100  if ((m_State == CAPTURE_FOCUSING && m_FocusState != FOCUS_COMPLETE && m_FocusState != FOCUS_ABORTED)
6101  || startFocusIfRequired())
6102  return IPS_BUSY;
6103 
6104  // step 10: resume guiding if it was suspended
6105  if (m_GuideState == GUIDE_SUSPENDED)
6106  {
6107  appendLogText(i18n("Autoguiding resumed."));
6108  emit resumeGuiding();
6109  // No need to return IPS_BUSY here, we can continue immediately.
6110  // In the case that the capturing sequence has a guiding limit,
6111  // capturing will be interrupted by setGuideDeviation().
6112  }
6113 
6114  // everything is ready for capturing light frames
6115  return IPS_OK;
6116 
6117 }
6118 
6119