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