File indexing completed on 2024-04-28 03:43:13

0001 /*
0002     SPDX-FileCopyrightText: 2012 Jasem Mutlaq <mutlaqja@ikarustech.com>
0003 
0004     SPDX-License-Identifier: GPL-2.0-or-later
0005 */
0006 
0007 #include "capture.h"
0008 
0009 #include "captureprocess.h"
0010 #include "capturemodulestate.h"
0011 #include "capturedeviceadaptor.h"
0012 #include "captureadaptor.h"
0013 #include "refocusstate.h"
0014 #include "kstars.h"
0015 #include "kstarsdata.h"
0016 #include "Options.h"
0017 #include "rotatorsettings.h"
0018 #include "sequencejob.h"
0019 #include "sequencequeue.h"
0020 #include "placeholderpath.h"
0021 #include "ui_calibrationoptions.h"
0022 #include "auxiliary/ksmessagebox.h"
0023 #include "ekos/manager.h"
0024 #include "ekos/auxiliary/darklibrary.h"
0025 #include "ekos/auxiliary/profilesettings.h"
0026 #include "ekos/auxiliary/opticaltrainmanager.h"
0027 #include "scriptsmanager.h"
0028 #include "fitsviewer/fitsdata.h"
0029 #include "indi/driverinfo.h"
0030 #include "indi/indifilterwheel.h"
0031 #include "indi/indicamera.h"
0032 #include "indi/indirotator.h"
0033 #include "oal/observeradd.h"
0034 #include "ekos/guide/guide.h"
0035 #include "exposurecalculator/exposurecalculatordialog.h"
0036 #include "dslrinfodialog.h"
0037 #include "ekos/auxiliary/rotatorutils.h"
0038 #include <basedevice.h>
0039 
0040 #include <ekos_capture_debug.h>
0041 
0042 #define MF_TIMER_TIMEOUT    90000
0043 #define MF_RA_DIFF_LIMIT    4
0044 
0045 // Qt version calming
0046 #include <qtendl.h>
0047 
0048 namespace
0049 {
0050 
0051 // Columns in the job table
0052 enum JobTableColumnIndex
0053 {
0054     JOBTABLE_COL_STATUS = 0,
0055     JOBTABLE_COL_FILTER,
0056     JOBTABLE_COL_COUNTS,
0057     JOBTABLE_COL_EXP,
0058     JOBTABLE_COL_TYPE,
0059     JOBTABLE_COL_BINNING,
0060     JOBTABLE_COL_ISO,
0061     JOBTABLE_COL_OFFSET
0062 };
0063 
0064 // Encode and decode for storing stand-alone options which are really QStringLists.
0065 QString standAloneEncode(const QStringList &list)
0066 {
0067     if (list.size() == 0)
0068         return "";
0069     QString encoding;
0070     encoding.append(list[0]);
0071     for (int i = 1; i < list.size(); ++i)
0072     {
0073         encoding.append(",");
0074         encoding.append(list[i]);
0075     }
0076     return encoding;
0077 }
0078 
0079 QStringList standAloneDecode(const QString &encoding)
0080 {
0081     auto dec = encoding.split(",");
0082     if (dec.size() == 1 && dec[0] == "")
0083         return QStringList();
0084     return dec;
0085 }
0086 
0087 // Adds the items to the QComboBox if they're not there already.
0088 void addToCombo(QComboBox *combo, const QStringList &items)
0089 {
0090     if (items.size() == 0)
0091         return;
0092     QStringList existingItems;
0093     for (int index = 0; index < combo->count(); index++)
0094         existingItems << combo->itemText(index);
0095 
0096     for (const auto &item : items)
0097         if (existingItems.indexOf(item) == -1)
0098             combo->addItem(item);
0099 }
0100 
0101 } // namespace
0102 
0103 namespace Ekos
0104 {
0105 
0106 // There are many widgets that are not used in stand-alone mode and should be made invisible and disabled.
0107 void Capture::initStandAlone()
0108 {
0109     QList<QWidget*> unusedWidgets =
0110     {
0111         opticalTrainLabel, opticalTrainCombo, trainB, cameraRowLabel, cameraLabel, restartCameraB,
0112         clearConfigurationB, coolerOnB, coolerOffB, setTemperatureB, temperatureRegulationB,
0113         previewB, loopB, liveVideoB, startB, pauseB, resetB, processGrid, darkB, darkLibraryB,
0114         filterManagerB
0115     };
0116     for (auto &widget : unusedWidgets)
0117     {
0118         widget->setEnabled(false);
0119         widget->setVisible(false);
0120     }
0121     CCDFWGroup->setTitle("Settings");
0122 }
0123 
0124 // Gets called when the stand-alone editor gets a show event.
0125 // Do this initialization here so that if the live capture module was
0126 // used after startup, it will have set more recent remembered values.
0127 void Capture::onStandAloneShow(QShowEvent* event)
0128 {
0129     Q_UNUSED(event);
0130     QSharedPointer<FilterManager> fm;
0131 
0132     // Default comment if there is no previously saved Options::CaptureStandAlone... parameters.
0133     QString comment = i18n("<b><font color=\"red\">Please run the Capture tab connected to INDI with your desired "
0134                            "camera/filterbank at least once before using the Sequence Editor. </font></b><p>");
0135     if (Options::captureStandAloneTimestamp().size() > 0)
0136         comment = i18n("<b>Using camera and filterwheel attributes from Capture session started at %1.</b>"
0137                        "<p>If you wish to use other cameras/filterbanks, please edit the sequence "
0138                        "using the Capture tab.<br>It is not recommended to overwrite a sequence file currently running, "
0139                        "please rename it instead.</p><p>", Options::captureStandAloneTimestamp());
0140     sequenceEditorComment->setVisible(true);
0141     sequenceEditorComment->setEnabled(true);
0142     sequenceEditorComment->setStyleSheet("{color: #C0BBFE}");
0143     sequenceEditorComment->setText(comment);
0144 
0145     // Add extra load and save buttons at the bottom of the window.
0146     loadSaveBox->setEnabled(true);
0147     loadSaveBox->setVisible(true);
0148     connect(esqSaveAsB, &QPushButton::clicked, this, &Capture::saveSequenceQueueAs);
0149     connect(esqLoadB, &QPushButton::clicked, this, static_cast<void(Capture::*)()>(&Capture::loadSequenceQueue));
0150 
0151     // This currently gets the filters from filter manager #0.
0152     // Could try all of them?
0153     bool ok = Manager::Instance()->getFilterManager(fm);
0154     if (ok)
0155         addToCombo(FilterPosCombo, fm->getFilterLabels());
0156     addToCombo(FilterPosCombo, standAloneDecode(Options::captureStandAloneFilters()));
0157 
0158     if (FilterPosCombo->count() > 0)
0159         filterEditB->setEnabled(true);
0160 
0161     captureGainN->setEnabled(true);
0162     captureGainN->setValue(GainSpinSpecialValue);
0163     captureGainN->setSpecialValueText(i18n("--"));
0164 
0165     captureOffsetN->setEnabled(true);
0166     captureOffsetN->setValue(OffsetSpinSpecialValue);
0167     captureOffsetN->setSpecialValueText(i18n("--"));
0168 
0169     // Always add these strings to the types menu. Might also add other ones
0170     // that were used in the last capture session.
0171     const QStringList frameTypes = {"Light", "Dark", "Bias", "Flat"};
0172     captureTypeS->clear();
0173     captureTypeS->addItems(frameTypes);
0174     addToCombo(captureTypeS, standAloneDecode(Options::captureStandAloneTypes()));
0175 
0176     // Always add these strings to the encodings menu. Might also add other ones
0177     // that were used in the last capture session.
0178     const QStringList frameEncodings = {"FITS", "Native", "XISF"};
0179     captureEncodingS->clear();
0180     captureEncodingS->addItems(frameEncodings);
0181     addToCombo(captureEncodingS, standAloneDecode(Options::captureStandAloneEncodings()));
0182 
0183     const QStringList frameFormats = {};
0184     captureFormatS->clear();
0185     if (frameFormats.size() > 0)
0186         captureFormatS->addItems(frameFormats);
0187     addToCombo(captureFormatS, standAloneDecode(Options::captureStandAloneFormats()));
0188 
0189     cameraTemperatureN->setEnabled(true);
0190 
0191     // No pre-configured ISOs are available--would be too much of a guess, but
0192     // we will use ISOs from the last live capture session.
0193     QStringList isoList = standAloneDecode(Options::captureStandAloneISOs());
0194     if (isoList.size() > 0)
0195     {
0196         captureISOS->clear();
0197         captureISOS->addItems(isoList);
0198         captureISOS->setCurrentIndex(Options::captureStandAloneISOIndex());
0199         captureISOS->blockSignals(false);
0200         captureISOS->setEnabled(true);
0201     }
0202     else
0203     {
0204         captureISOS->blockSignals(true);
0205         captureISOS->clear();
0206         captureISOS->setEnabled(false);
0207     }
0208 
0209     // Remember the sensor width and height from the last live session.
0210     // The user can always edit the input box.
0211     constexpr int maxFrame = 20000;
0212     captureFrameXN->setMaximum(static_cast<int>(maxFrame));
0213     captureFrameYN->setMaximum(static_cast<int>(maxFrame));
0214     captureFrameWN->setMaximum(static_cast<int>(maxFrame));
0215     captureFrameHN->setMaximum(static_cast<int>(maxFrame));
0216     QStringList whList = standAloneDecode(Options::captureStandAloneWHGO());
0217     if (whList.size() == 4)
0218     {
0219         captureFrameWN->setValue(whList[0].toInt());
0220         captureFrameHN->setValue(whList[1].toInt());
0221         m_standAloneUseCcdGain = whList[2] == "CCD_GAIN";
0222         m_standAloneUseCcdOffset = whList[3] == "CCD_OFFSET";
0223     }
0224 
0225     connect(captureGainN, &QDoubleSpinBox::editingFinished, this, [this]()
0226     {
0227         if (captureGainN->value() != GainSpinSpecialValue)
0228             setGain(captureGainN->value());
0229         else
0230             setGain(-1);
0231     });
0232 
0233     connect(captureOffsetN, &QDoubleSpinBox::editingFinished, this, [this]()
0234     {
0235         if (captureOffsetN->value() != OffsetSpinSpecialValue)
0236             setOffset(captureOffsetN->value());
0237         else
0238             setOffset(-1);
0239     });
0240 }
0241 
0242 Capture::Capture(bool standAlone) : m_standAlone(standAlone)
0243 {
0244     setupUi(this);
0245 
0246     if (!m_standAlone)
0247     {
0248         qRegisterMetaType<CaptureState>("CaptureState");
0249         qDBusRegisterMetaType<CaptureState>();
0250     }
0251     new CaptureAdaptor(this);
0252     m_captureModuleState.reset(new CaptureModuleState());
0253     m_captureDeviceAdaptor.reset(new CaptureDeviceAdaptor());
0254     m_captureProcess = new CaptureProcess(state(), m_captureDeviceAdaptor);
0255 
0256     state()->getSequenceQueue()->loadOptions();
0257 
0258     if (m_standAlone)
0259         initStandAlone();
0260 
0261     if (!m_standAlone)
0262     {
0263         QDBusConnection::sessionBus().registerObject("/KStars/Ekos/Capture", this);
0264         QPointer<QDBusInterface> ekosInterface = new QDBusInterface("org.kde.kstars", "/KStars/Ekos", "org.kde.kstars.Ekos",
0265                 QDBusConnection::sessionBus(), this);
0266 
0267         // Connecting DBus signals
0268         QDBusConnection::sessionBus().connect("org.kde.kstars", "/KStars/Ekos", "org.kde.kstars.Ekos", "newModule", this,
0269                                               SLOT(registerNewModule(QString)));
0270 
0271         // ensure that the mount interface is present
0272         registerNewModule("Mount");
0273     }
0274     KStarsData::Instance()->userdb()->GetAllDSLRInfos(state()->DSLRInfos());
0275 
0276     if (state()->DSLRInfos().count() > 0)
0277     {
0278         qCDebug(KSTARS_EKOS_CAPTURE) << "DSLR Cameras Info:";
0279         qCDebug(KSTARS_EKOS_CAPTURE) << state()->DSLRInfos();
0280     }
0281 
0282     m_LimitsDialog = new QDialog(this);
0283     m_LimitsUI.reset(new Ui::Limits());
0284     m_LimitsUI->setupUi(m_LimitsDialog);
0285     m_scriptsManager = new ScriptsManager(this);
0286     if (m_standAlone)
0287     {
0288         // Prepend "Capture Sequence Editor" to the two pop-up window titles, to differentiate them
0289         // from similar windows in the Capture tab.
0290         auto title = i18n("Capture Sequence Editor: %1", m_LimitsDialog->windowTitle());
0291         m_LimitsDialog->setWindowTitle(title);
0292         title = i18n("Capture Sequence Editor: %1", m_scriptsManager->windowTitle());
0293         m_scriptsManager->setWindowTitle(title);
0294     }
0295     dirPath = QUrl::fromLocalFile(QDir::homePath());
0296 
0297     //isAutoGuiding   = false;
0298 
0299     // hide avg. download time and target drift initially
0300     targetDriftLabel->setVisible(false);
0301     targetDrift->setVisible(false);
0302     targetDriftUnit->setVisible(false);
0303     avgDownloadTime->setVisible(false);
0304     avgDownloadLabel->setVisible(false);
0305     secLabel->setVisible(false);
0306 
0307     connect(&state()->getSeqDelayTimer(), &QTimer::timeout, m_captureProcess, &CaptureProcess::captureImage);
0308     state()->getCaptureDelayTimer().setSingleShot(true);
0309     connect(&state()->getCaptureDelayTimer(), &QTimer::timeout, this, &Capture::start, Qt::UniqueConnection);
0310 
0311     connect(startB, &QPushButton::clicked, this, &Capture::toggleSequence);
0312     connect(pauseB, &QPushButton::clicked, this, &Capture::pause);
0313     connect(darkLibraryB, &QPushButton::clicked, DarkLibrary::Instance(), &QDialog::show);
0314     connect(limitsB, &QPushButton::clicked, m_LimitsDialog, &QDialog::show);
0315     connect(temperatureRegulationB, &QPushButton::clicked, this, &Capture::showTemperatureRegulation);
0316 
0317     startB->setIcon(QIcon::fromTheme("media-playback-start"));
0318     startB->setAttribute(Qt::WA_LayoutUsesWidgetRect);
0319     pauseB->setIcon(QIcon::fromTheme("media-playback-pause"));
0320     pauseB->setAttribute(Qt::WA_LayoutUsesWidgetRect);
0321 
0322     filterManagerB->setIcon(QIcon::fromTheme("view-filter"));
0323     filterManagerB->setAttribute(Qt::WA_LayoutUsesWidgetRect);
0324 
0325     connect(captureBinHN, static_cast<void (QSpinBox::*)(int)>(&QSpinBox::valueChanged), captureBinVN, &QSpinBox::setValue);
0326 
0327     connect(liveVideoB, &QPushButton::clicked, this, &Capture::toggleVideo);
0328 
0329     connect(clearConfigurationB, &QPushButton::clicked, this, &Capture::clearCameraConfiguration);
0330 
0331     darkB->setChecked(Options::autoDark());
0332     connect(darkB, &QAbstractButton::toggled, this, [this]()
0333     {
0334         Options::setAutoDark(darkB->isChecked());
0335     });
0336 
0337     connect(restartCameraB, &QPushButton::clicked, this, [this]()
0338     {
0339         if (activeCamera())
0340             restartCamera(activeCamera()->getDeviceName());
0341     });
0342 
0343     connect(cameraTemperatureS, &QCheckBox::toggled, this, [this](bool toggled)
0344     {
0345         if (devices()->getActiveCamera())
0346         {
0347             QVariantMap auxInfo = devices()->getActiveCamera()->getDriverInfo()->getAuxInfo();
0348             auxInfo[QString("%1_TC").arg(devices()->getActiveCamera()->getDeviceName())] = toggled;
0349             devices()->getActiveCamera()->getDriverInfo()->setAuxInfo(auxInfo);
0350         }
0351     });
0352 
0353     connect(filterEditB, &QPushButton::clicked, this, &Capture::editFilterName);
0354 
0355     connect(FilterPosCombo, static_cast<void(QComboBox::*)(const QString &)>(&QComboBox::currentTextChanged),
0356             [ = ]()
0357     {
0358         state()->updateHFRThreshold();
0359         generatePreviewFilename();
0360     });
0361     connect(previewB, &QPushButton::clicked, this, &Capture::capturePreview);
0362     connect(loopB, &QPushButton::clicked, this, &Capture::startFraming);
0363 
0364     //connect( seqWatcher, SIGNAL(dirty(QString)), this, &Capture::checkSeqFile(QString)));
0365 
0366     connect(addToQueueB, &QPushButton::clicked, this, [this]()
0367     {
0368         if (m_JobUnderEdit)
0369             editJobFinished();
0370         else
0371             createJob();
0372     });
0373     connect(queueUpB, &QPushButton::clicked, [this]()
0374     {
0375         moveJob(true);
0376     });
0377     connect(queueDownB, &QPushButton::clicked, [this]()
0378     {
0379         moveJob(false);
0380     });
0381     connect(removeFromQueueB, &QPushButton::clicked, this, &Capture::removeJobFromQueue);
0382     connect(selectFileDirectoryB, &QPushButton::clicked, this, &Capture::saveFITSDirectory);
0383     connect(queueSaveB, &QPushButton::clicked, this, static_cast<void(Capture::*)()>(&Capture::saveSequenceQueue));
0384     connect(queueSaveAsB, &QPushButton::clicked, this, &Capture::saveSequenceQueueAs);
0385     connect(queueLoadB, &QPushButton::clicked, this, static_cast<void(Capture::*)()>(&Capture::loadSequenceQueue));
0386     connect(resetB, &QPushButton::clicked, this, &Capture::resetJobs);
0387     connect(queueTable->selectionModel(), &QItemSelectionModel::currentRowChanged, this, &Capture::selectedJobChanged);
0388     connect(queueTable, &QAbstractItemView::doubleClicked, this, &Capture::editJob);
0389     connect(queueTable, &QTableWidget::itemSelectionChanged, this, [&]()
0390     {
0391         resetJobEdit(m_JobUnderEdit);
0392     });
0393     connect(setTemperatureB, &QPushButton::clicked, this, [&]()
0394     {
0395         if (devices()->getActiveCamera())
0396             devices()->getActiveCamera()->setTemperature(cameraTemperatureN->value());
0397     });
0398     connect(coolerOnB, &QPushButton::clicked, this, [&]()
0399     {
0400         if (devices()->getActiveCamera())
0401             devices()->getActiveCamera()->setCoolerControl(true);
0402     });
0403     connect(coolerOffB, &QPushButton::clicked, this, [&]()
0404     {
0405         if (devices()->getActiveCamera())
0406             devices()->getActiveCamera()->setCoolerControl(false);
0407     });
0408     connect(cameraTemperatureN, &QDoubleSpinBox::editingFinished, setTemperatureB,
0409             static_cast<void (QPushButton::*)()>(&QPushButton::setFocus));
0410     connect(captureTypeS, static_cast<void (QComboBox::*)(int)>(&QComboBox::currentIndexChanged), this,
0411             &Capture::checkFrameType);
0412     connect(resetFrameB, &QPushButton::clicked, m_captureProcess, &CaptureProcess::resetFrame);
0413     connect(calibrationB, &QPushButton::clicked, this, &Capture::openCalibrationDialog);
0414     // connect(rotatorB, &QPushButton::clicked, m_RotatorControlPanel.get(), &Capture::show);
0415 
0416     connect(generateDarkFlatsB, &QPushButton::clicked, this, &Capture::generateDarkFlats);
0417     connect(scriptManagerB, &QPushButton::clicked, this, &Capture::handleScriptsManager);
0418     connect(resetFormatB, &QPushButton::clicked, this, [this]()
0419     {
0420         placeholderFormatT->setText(KSUtils::getDefaultPath("PlaceholderFormat"));
0421     });
0422 
0423     addToQueueB->setIcon(QIcon::fromTheme("list-add"));
0424     addToQueueB->setAttribute(Qt::WA_LayoutUsesWidgetRect);
0425     removeFromQueueB->setIcon(QIcon::fromTheme("list-remove"));
0426     removeFromQueueB->setAttribute(Qt::WA_LayoutUsesWidgetRect);
0427     queueUpB->setIcon(QIcon::fromTheme("go-up"));
0428     queueUpB->setAttribute(Qt::WA_LayoutUsesWidgetRect);
0429     queueDownB->setIcon(QIcon::fromTheme("go-down"));
0430     queueDownB->setAttribute(Qt::WA_LayoutUsesWidgetRect);
0431     selectFileDirectoryB->setIcon(QIcon::fromTheme("document-open-folder"));
0432     selectFileDirectoryB->setAttribute(Qt::WA_LayoutUsesWidgetRect);
0433     queueLoadB->setIcon(QIcon::fromTheme("document-open"));
0434     queueLoadB->setAttribute(Qt::WA_LayoutUsesWidgetRect);
0435     queueSaveB->setIcon(QIcon::fromTheme("document-save"));
0436     queueSaveB->setAttribute(Qt::WA_LayoutUsesWidgetRect);
0437     queueSaveAsB->setIcon(QIcon::fromTheme("document-save-as"));
0438     queueSaveAsB->setAttribute(Qt::WA_LayoutUsesWidgetRect);
0439     resetB->setIcon(QIcon::fromTheme("system-reboot"));
0440     resetB->setAttribute(Qt::WA_LayoutUsesWidgetRect);
0441     resetFrameB->setIcon(QIcon::fromTheme("view-refresh"));
0442     resetFrameB->setAttribute(Qt::WA_LayoutUsesWidgetRect);
0443     calibrationB->setIcon(QIcon::fromTheme("run-build"));
0444     calibrationB->setAttribute(Qt::WA_LayoutUsesWidgetRect);
0445     generateDarkFlatsB->setIcon(QIcon::fromTheme("tools-wizard"));
0446     generateDarkFlatsB->setAttribute(Qt::WA_LayoutUsesWidgetRect);
0447     // rotatorB->setIcon(QIcon::fromTheme("kstars_solarsystem"));
0448     rotatorB->setAttribute(Qt::WA_LayoutUsesWidgetRect);
0449 
0450     addToQueueB->setToolTip(i18n("Add job to sequence queue"));
0451     removeFromQueueB->setToolTip(i18n("Remove job from sequence queue"));
0452 
0453     ////////////////////////////////////////////////////////////////////////
0454     /// Device Adaptor
0455     ////////////////////////////////////////////////////////////////////////
0456     connect(m_captureDeviceAdaptor.data(), &CaptureDeviceAdaptor::newCCDTemperatureValue, this,
0457             &Capture::updateCCDTemperature, Qt::UniqueConnection);
0458     connect(m_captureDeviceAdaptor.data(), &CaptureDeviceAdaptor::newRotatorAngle, this,
0459             &Capture::updateRotatorAngle, Qt::UniqueConnection);
0460 
0461     ////////////////////////////////////////////////////////////////////////
0462     /// Settings
0463     ////////////////////////////////////////////////////////////////////////
0464     syncGUIToGeneralSettings();
0465     // Start Guide Deviation Check
0466     connect(m_LimitsUI->startGuiderDriftS, &QCheckBox::toggled, [ = ](bool checked)
0467     {
0468         // We don't want the editor to influence a concurrent live capture session.
0469         if (!m_standAlone)
0470             Options::setEnforceStartGuiderDrift(checked);
0471     });
0472 
0473     // Start Guide Deviation Value
0474     connect(m_LimitsUI->startGuiderDriftN, QOverload<double>::of(&QDoubleSpinBox::valueChanged), this, [this]()
0475     {
0476         // We don't want the editor to influence a concurrent live capture session.
0477         if (!m_standAlone)
0478             Options::setStartGuideDeviation(m_LimitsUI->startGuiderDriftN->value());
0479     });
0480 
0481     // Abort Guide Deviation Check
0482     connect(m_LimitsUI->limitGuideDeviationS, &QCheckBox::toggled, [ = ](bool checked)
0483     {
0484         // We don't want the editor to influence a concurrent live capture session.
0485         if (!m_standAlone)
0486             Options::setEnforceGuideDeviation(checked);
0487     });
0488 
0489     // Per job dither frequency count
0490     connect(m_LimitsUI->limitDitherFrequencyN, QOverload<int>::of(&QSpinBox::valueChanged), [this]()
0491     {
0492         // We don't want the editor to influence a concurrent live capture session.
0493         if (!m_standAlone)
0494             Options::setGuideDitherPerJobFrequency(m_LimitsUI->limitDitherFrequencyN->value());
0495     });
0496 
0497     // Guide Deviation Value
0498     connect(m_LimitsUI->limitGuideDeviationN, QOverload<double>::of(&QDoubleSpinBox::valueChanged), this, [this]()
0499     {
0500         // We don't want the editor to influence a concurrent live capture session.
0501         if (!m_standAlone)
0502             Options::setGuideDeviation(m_LimitsUI->limitGuideDeviationN->value());
0503     });
0504 
0505     connect(m_LimitsUI->limitGuideDeviationRepsN, QOverload<int>::of(&QSpinBox::valueChanged), this, [this]()
0506     {
0507         // We don't want the editor to influence a concurrent live capture session.
0508         if (!m_standAlone)
0509             Options::setGuideDeviationReps(static_cast<uint>(m_LimitsUI->limitGuideDeviationRepsN->value()));
0510     });
0511 
0512     // Autofocus HFR Check
0513     connect(m_LimitsUI->limitFocusHFRS, &QCheckBox::toggled, [ = ](bool checked)
0514     {
0515         // We don't want the editor to influence a concurrent live capture session.
0516         if (!m_standAlone)
0517             Options::setEnforceAutofocusHFR(checked);
0518         if (checked == false)
0519             state()->getRefocusState()->setInSequenceFocus(false);
0520     });
0521     m_LimitsUI->limitFocusHFRN->setValue(Options::hFRDeviation());
0522     connect(m_LimitsUI->limitFocusHFRN, QOverload<double>::of(&QDoubleSpinBox::valueChanged), this, [this]()
0523     {
0524         // We don't want the editor to influence a concurrent live capture session.
0525         if (!m_standAlone)
0526             Options::setHFRDeviation(m_LimitsUI->limitFocusHFRN->value());
0527     });
0528     m_LimitsUI->limitFocusHFRThresholdPercentage->setValue(Options::hFRThresholdPercentage());
0529     connect(m_LimitsUI->limitFocusHFRThresholdPercentage, QOverload<double>::of(&QDoubleSpinBox::valueChanged), this, [this]()
0530     {
0531         Options::setHFRThresholdPercentage(m_LimitsUI->limitFocusHFRThresholdPercentage->value());
0532         Capture::updateHFRCheckAlgo();
0533     });
0534     m_LimitsUI->limitFocusHFRCheckFrames->setValue(Options::inSequenceCheckFrames());
0535     connect(m_LimitsUI->limitFocusHFRCheckFrames, QOverload<int>::of(&QSpinBox::valueChanged), this, [this]()
0536     {
0537         Options::setInSequenceCheckFrames(m_LimitsUI->limitFocusHFRCheckFrames->value());
0538     });
0539     connect(m_captureModuleState.get(), &CaptureModuleState::newLimitFocusHFR, this, [this](double hfr)
0540     {
0541         m_LimitsUI->limitFocusHFRN->setValue(hfr);
0542     });
0543     m_LimitsUI->limitFocusHFRAlgorithm->setCurrentIndex(Options::hFRCheckAlgorithm());
0544     updateHFRCheckAlgo();
0545     connect(m_LimitsUI->limitFocusHFRAlgorithm, QOverload<int>::of(&QComboBox::currentIndexChanged), this, [this](int index)
0546     {
0547         Options::setHFRCheckAlgorithm(index);
0548         Capture::updateHFRCheckAlgo();
0549     });
0550 
0551     // Autofocus temperature Check
0552     connect(m_LimitsUI->limitFocusDeltaTS, &QCheckBox::toggled, this,  [ = ](bool checked)
0553     {
0554         // We don't want the editor to influence a concurrent live capture session.
0555         if (!m_standAlone)
0556             Options::setEnforceAutofocusOnTemperature(checked);
0557     });
0558 
0559     // Autofocus temperature Delta
0560     connect(m_LimitsUI->limitFocusDeltaTN, QOverload<double>::of(&QDoubleSpinBox::valueChanged), this, [this]()
0561     {
0562         // We don't want the editor to influence a concurrent live capture session.
0563         if (!m_standAlone)
0564             Options::setMaxFocusTemperatureDelta(m_LimitsUI->limitFocusDeltaTN->value());
0565     });
0566 
0567     // Refocus Every Check
0568     connect(m_LimitsUI->limitRefocusS, &QCheckBox::toggled, this, [ = ](bool checked)
0569     {
0570         // We don't want the editor to influence a concurrent live capture session.
0571         if (!m_standAlone)
0572             Options::setEnforceRefocusEveryN(checked);
0573     });
0574 
0575     // Refocus Every Value
0576     connect(m_LimitsUI->limitRefocusN, QOverload<int>::of(&QSpinBox::valueChanged), this, [this]()
0577     {
0578         // We don't want the editor to influence a concurrent live capture session.
0579         if (!m_standAlone)
0580             Options::setRefocusEveryN(static_cast<uint>(m_LimitsUI->limitRefocusN->value()));
0581     });
0582 
0583     // Refocus after meridian flip
0584     m_LimitsUI->meridianRefocusS->setChecked(Options::refocusAfterMeridianFlip());
0585     connect(m_LimitsUI->meridianRefocusS, &QCheckBox::toggled, [ = ](bool checked)
0586     {
0587         // We don't want the editor to influence a concurrent live capture session.
0588         if (!m_standAlone)
0589             Options::setRefocusAfterMeridianFlip(checked);
0590     });
0591 
0592     QCheckBox * const checkBoxes[] =
0593     {
0594         m_LimitsUI->limitGuideDeviationS,
0595         m_LimitsUI->startGuiderDriftS,
0596         m_LimitsUI->limitRefocusS,
0597         m_LimitsUI->limitFocusDeltaTS,
0598         m_LimitsUI->limitFocusHFRS,
0599         m_LimitsUI->meridianRefocusS,
0600     };
0601     for (const QCheckBox * control : checkBoxes)
0602         connect(control, &QCheckBox::toggled, this, [&]()
0603     {
0604         state()->setDirty(true);
0605     });
0606 
0607     QDoubleSpinBox * const dspinBoxes[]
0608     {
0609         m_LimitsUI->limitFocusHFRN,
0610         m_LimitsUI->limitFocusHFRThresholdPercentage,
0611         m_LimitsUI->limitFocusDeltaTN,
0612         m_LimitsUI->limitGuideDeviationN,
0613         m_LimitsUI->startGuiderDriftN
0614     };
0615     for (const QDoubleSpinBox * control : dspinBoxes)
0616         connect(control, static_cast<void (QDoubleSpinBox::*)(double)>(&QDoubleSpinBox::valueChanged), this, [&]()
0617     {
0618         state()->setDirty(true);
0619     });
0620 
0621     connect(m_LimitsUI->limitFocusHFRCheckFrames, QOverload<int>::of(&QSpinBox::valueChanged), this, [&]()
0622     {
0623         state()->setDirty(true);
0624     });
0625     connect(m_LimitsUI->limitFocusHFRAlgorithm, QOverload<int>::of(&QComboBox::currentIndexChanged), this, [&]()
0626     {
0627         state()->setDirty(true);
0628     });
0629 
0630     connect(fileUploadModeS, static_cast<void (QComboBox::*)(int)>(&QComboBox::currentIndexChanged), this, [&]()
0631     {
0632         state()->setDirty(true);
0633     });
0634     connect(fileRemoteDirT, &QLineEdit::editingFinished, this, [&]()
0635     {
0636         state()->setDirty(true);
0637     });
0638 
0639     observerB->setIcon(QIcon::fromTheme("im-user"));
0640     observerB->setAttribute(Qt::WA_LayoutUsesWidgetRect);
0641     connect(observerB, &QPushButton::clicked, this, &Capture::showObserverDialog);
0642 
0643     // Exposure Timeout
0644     state()->getCaptureTimeout().setSingleShot(true);
0645     connect(&state()->getCaptureTimeout(), &QTimer::timeout, m_captureProcess,
0646             &CaptureProcess::processCaptureTimeout);
0647 
0648     // Remote directory
0649     connect(fileUploadModeS, static_cast<void (QComboBox::*)(int)>(&QComboBox::currentIndexChanged), this,
0650             [&](int index)
0651     {
0652         fileRemoteDirT->setEnabled(index != 0);
0653     });
0654 
0655     customPropertiesDialog.reset(new CustomProperties());
0656     connect(customValuesB, &QPushButton::clicked, this, [&]()
0657     {
0658         customPropertiesDialog.get()->show();
0659         customPropertiesDialog.get()->raise();
0660     });
0661     connect(customPropertiesDialog.get(), &CustomProperties::valueChanged, this, [&]()
0662     {
0663         const double newGain = getGain();
0664         if (captureGainN && newGain >= 0)
0665             captureGainN->setValue(newGain);
0666         const int newOffset = getOffset();
0667         if (newOffset >= 0)
0668             captureOffsetN->setValue(newOffset);
0669     });
0670 
0671     if(!Options::captureDirectory().isEmpty())
0672         fileDirectoryT->setText(Options::captureDirectory());
0673     else
0674     {
0675         fileDirectoryT->setText(QDir::homePath() + QDir::separator() + "Pictures");
0676         Options::setCaptureDirectory(fileDirectoryT->text());
0677     }
0678 
0679     connect(fileDirectoryT, &QLineEdit::textChanged, this, [&]()
0680     {
0681         Options::setCaptureDirectory(fileDirectoryT->text());
0682         generatePreviewFilename();
0683     });
0684 
0685     if (Options::remoteCaptureDirectory().isEmpty() == false)
0686     {
0687         fileRemoteDirT->setText(Options::remoteCaptureDirectory());
0688     }
0689     connect(fileRemoteDirT, &QLineEdit::editingFinished, this, [&]()
0690     {
0691         Options::setRemoteCaptureDirectory(fileRemoteDirT->text());
0692         generatePreviewFilename();
0693     });
0694 
0695     //Note:  This is to prevent a button from being called the default button
0696     //and then executing when the user hits the enter key such as when on a Text Box
0697     QList<QPushButton *> qButtons = findChildren<QPushButton *>();
0698     for (auto &button : qButtons)
0699         button->setAutoDefault(false);
0700 
0701     DarkLibrary::Instance()->setCaptureModule(this);
0702 
0703     // display the capture status in the UI
0704     connect(this, &Capture::newStatus, captureStatusWidget, &LedStatusWidget::setCaptureState);
0705 
0706     // react upon state changes
0707     connect(m_captureModuleState.data(), &CaptureModuleState::captureBusy, this, &Capture::setBusy);
0708     connect(m_captureModuleState.data(), &CaptureModuleState::startCapture, this, &Capture::start);
0709     connect(m_captureModuleState.data(), &CaptureModuleState::abortCapture, this, &Capture::abort);
0710     connect(m_captureModuleState.data(), &CaptureModuleState::suspendCapture, this, &Capture::suspend);
0711     connect(m_captureModuleState.data(), &CaptureModuleState::executeActiveJob, m_captureProcess.data(),
0712             &CaptureProcess::executeJob);
0713     connect(m_captureModuleState.data(), &CaptureModuleState::updatePrepareState, this, &Capture::updatePrepareState);
0714     // forward signals from capture module state
0715     connect(m_captureModuleState.data(), &CaptureModuleState::captureStarted, m_captureProcess.data(),
0716             &CaptureProcess::captureStarted);
0717     connect(m_captureModuleState.data(), &CaptureModuleState::newLog, this, &Capture::appendLogText);
0718     connect(m_captureModuleState.data(), &CaptureModuleState::newStatus, this, &Capture::newStatus);
0719     connect(m_captureModuleState.data(), &CaptureModuleState::sequenceChanged, this, &Capture::sequenceChanged);
0720     connect(m_captureModuleState.data(), &CaptureModuleState::checkFocus, this, &Capture::checkFocus);
0721     connect(m_captureModuleState.data(), &CaptureModuleState::runAutoFocus, this, &Capture::runAutoFocus);
0722     connect(m_captureModuleState.data(), &CaptureModuleState::resetFocus, this, &Capture::resetFocus);
0723     connect(m_captureModuleState.data(), &CaptureModuleState::adaptiveFocus, this, &Capture::adaptiveFocus);
0724     connect(m_captureModuleState.data(), &CaptureModuleState::guideAfterMeridianFlip, this,
0725             &Capture::guideAfterMeridianFlip);
0726     connect(m_captureModuleState.data(), &CaptureModuleState::newFocusStatus, this, &Capture::updateFocusStatus);
0727     connect(m_captureModuleState.data(), &CaptureModuleState::newMeridianFlipStage, this, &Capture::updateMeridianFlipStage);
0728     connect(m_captureModuleState.data(), &CaptureModuleState::meridianFlipStarted, this, &Capture::meridianFlipStarted);
0729 
0730     // forward signals from capture process
0731     connect(m_captureProcess.data(), &CaptureProcess::cameraReady, this, &Capture::ready);
0732     connect(m_captureProcess.data(), &CaptureProcess::refreshCamera, this, &Capture::updateCamera);
0733     connect(m_captureProcess.data(), &CaptureProcess::refreshCameraSettings, this, &Capture::refreshCameraSettings);
0734     connect(m_captureProcess.data(), &CaptureProcess::refreshFilterSettings, this, &Capture::refreshFilterSettings);
0735     connect(m_captureProcess.data(), &CaptureProcess::newExposureProgress, this, &Capture::newExposureProgress);
0736     connect(m_captureProcess.data(), &CaptureProcess::newDownloadProgress, this, &Capture::updateDownloadProgress);
0737     connect(m_captureProcess.data(), &CaptureProcess::updateCaptureCountDown, this, &Capture::updateCaptureCountDown);
0738     connect(m_captureProcess.data(), &CaptureProcess::processingFITSfinished, this, &Capture::processingFITSfinished);
0739     connect(m_captureProcess.data(), &CaptureProcess::newImage, this, &Capture::newImage);
0740     connect(m_captureProcess.data(), &CaptureProcess::syncGUIToJob, this, &Capture::syncGUIToJob);
0741     connect(m_captureProcess.data(), &CaptureProcess::captureComplete, this, &Capture::captureComplete);
0742     connect(m_captureProcess.data(), &CaptureProcess::updateFrameProperties, this, &Capture::updateFrameProperties);
0743     connect(m_captureProcess.data(), &CaptureProcess::jobExecutionPreparationStarted, this,
0744             &Capture::jobExecutionPreparationStarted);
0745     connect(m_captureProcess.data(), &CaptureProcess::sequenceChanged, this, &Capture::sequenceChanged);
0746     connect(m_captureProcess.data(), &CaptureProcess::addJob, this, &Capture::addJob);
0747     connect(m_captureProcess.data(), &CaptureProcess::createJob, [this](SequenceJob::SequenceJobType jobType)
0748     {
0749         // report the result back to the process
0750         process()->jobCreated(createJob(jobType));
0751     });
0752     connect(m_captureProcess.data(), &CaptureProcess::jobPrepared, this, &Capture::jobPrepared);
0753     connect(m_captureProcess.data(), &CaptureProcess::captureImageStarted, this, &Capture::captureImageStarted);
0754     connect(m_captureProcess.data(), &CaptureProcess::captureTarget, this, &Capture::setTargetName);
0755     connect(m_captureProcess.data(), &CaptureProcess::downloadingFrame, this, [this]()
0756     {
0757         captureStatusWidget->setStatus(i18n("Downloading..."), Qt::yellow);
0758     });
0759     connect(m_captureProcess.data(), &CaptureProcess::captureAborted, this, &Capture::captureAborted);
0760     connect(m_captureProcess.data(), &CaptureProcess::captureStopped, this, &Capture::captureStopped);
0761     connect(m_captureProcess.data(), &CaptureProcess::updateJobTable, this, &Capture::updateJobTable);
0762     connect(m_captureProcess.data(), &CaptureProcess::abortFocus, this, &Capture::abortFocus);
0763     connect(m_captureProcess.data(), &CaptureProcess::updateMeridianFlipStage, this, &Capture::updateMeridianFlipStage);
0764     connect(m_captureProcess.data(), &CaptureProcess::darkFrameCompleted, this, &Capture::imageCapturingCompleted);
0765     connect(m_captureProcess.data(), &CaptureProcess::newLog, this, &Capture::appendLogText);
0766     connect(m_captureProcess.data(), &CaptureProcess::jobStarting, this, &Capture::jobStarting);
0767     connect(m_captureProcess.data(), &CaptureProcess::captureRunning, this, &Capture::captureRunning);
0768     connect(m_captureProcess.data(), &CaptureProcess::stopCapture, this, &Capture::stop);
0769     connect(m_captureProcess.data(), &CaptureProcess::suspendGuiding, this, &Capture::suspendGuiding);
0770     connect(m_captureProcess.data(), &CaptureProcess::resumeGuiding, this, &Capture::resumeGuiding);
0771     connect(m_captureProcess.data(), &CaptureProcess::driverTimedout, this, &Capture::driverTimedout);
0772     connect(m_captureProcess.data(), &CaptureProcess::rotatorReverseToggled, this, &Capture::setRotatorReversed);
0773     // connections between state machine and device adaptor
0774     connect(m_captureModuleState.data(), &CaptureModuleState::newFilterPosition,
0775             m_captureDeviceAdaptor.data(), &CaptureDeviceAdaptor::setFilterPosition);
0776     connect(m_captureModuleState.data(), &CaptureModuleState::abortFastExposure,
0777             m_captureDeviceAdaptor.data(), &CaptureDeviceAdaptor::abortFastExposure);
0778     connect(m_captureDeviceAdaptor.data(), &CaptureDeviceAdaptor::pierSideChanged,
0779             m_captureModuleState.data(), &CaptureModuleState::setPierSide);
0780     connect(m_captureDeviceAdaptor.data(), &CaptureDeviceAdaptor::newFilterWheel, this, &Capture::setFilterWheel);
0781     connect(m_captureDeviceAdaptor.data(), &CaptureDeviceAdaptor::CameraConnected, this, [this](bool connected)
0782     {
0783         CCDFWGroup->setEnabled(connected);
0784         sequenceBox->setEnabled(connected);
0785         for (auto &oneChild : sequenceControlsButtonGroup->buttons())
0786             oneChild->setEnabled(connected);
0787 
0788         if (! connected)
0789         {
0790             opticalTrainCombo->setEnabled(true);
0791             trainLabel->setEnabled(true);
0792         }
0793     });
0794     connect(m_captureDeviceAdaptor.data(), &CaptureDeviceAdaptor::FilterWheelConnected, this, [this](bool connected)
0795     {
0796         FilterPosLabel->setEnabled(connected);
0797         FilterPosCombo->setEnabled(connected);
0798         filterManagerB->setEnabled(connected);
0799     });
0800     connect(m_captureDeviceAdaptor.data(), &CaptureDeviceAdaptor::newRotator, this, &Capture::setRotator);
0801 
0802     setupOpticalTrainManager();
0803 
0804     // Generate Meridian Flip State
0805     getMeridianFlipState();
0806 
0807     //Update the filename preview
0808     placeholderFormatT->setText(Options::placeholderFormat());
0809     connect(placeholderFormatT, &QLineEdit::textChanged, this, [this]()
0810     {
0811         Options::setPlaceholderFormat(placeholderFormatT->text());
0812         generatePreviewFilename();
0813     });
0814     connect(formatSuffixN, QOverload<int>::of(&QSpinBox::valueChanged), this, &Capture::generatePreviewFilename);
0815     connect(captureExposureN, QOverload<double>::of(&QDoubleSpinBox::valueChanged), this,
0816             &Capture::generatePreviewFilename);
0817     connect(targetNameT, &QLineEdit::textEdited, this, [ = ]()
0818     {
0819         generatePreviewFilename();
0820         qCDebug(KSTARS_EKOS_CAPTURE) << "Changed target to" << targetNameT->text() << "because of user edit";
0821     });
0822     connect(captureTypeS, &QComboBox::currentTextChanged, this, &Capture::generatePreviewFilename);
0823 
0824     connect(exposureCalcB, &QPushButton::clicked, this, &Capture::openExposureCalculatorDialog);
0825 
0826 }
0827 
0828 Capture::~Capture()
0829 {
0830     qDeleteAll(state()->allJobs());
0831     state()->allJobs().clear();
0832 }
0833 
0834 void Capture::updateHFRCheckAlgo()
0835 {
0836     // Threshold % is not relevant for FIXED HFR do disable the field
0837     const bool threshold = (m_LimitsUI->limitFocusHFRAlgorithm->currentIndex() != HFR_CHECK_FIXED);
0838     m_LimitsUI->limitFocusHFRThresholdPercentage->setEnabled(threshold);
0839     m_LimitsUI->limitFocusHFRThresholdLabel->setEnabled(threshold);
0840     m_LimitsUI->limitFocusHFRPercentLabel->setEnabled(threshold);
0841     state()->updateHFRThreshold();
0842 }
0843 
0844 bool Capture::updateCamera()
0845 {
0846     auto isConnected = activeCamera() && activeCamera()->isConnected();
0847     CCDFWGroup->setEnabled(isConnected);
0848     sequenceBox->setEnabled(isConnected);
0849     for (auto &oneChild : sequenceControlsButtonGroup->buttons())
0850         oneChild->setEnabled(isConnected);
0851 
0852     QVariant trainID = ProfileSettings::Instance()->getOneSetting(ProfileSettings::CaptureOpticalTrain);
0853 
0854     if (activeCamera() && trainID.isValid())
0855     {
0856         opticalTrainCombo->setToolTip(QString("%1 @ %2").arg(activeCamera()->getDeviceName(), currentScope()["name"].toString()));
0857 
0858         cameraLabel->setText(activeCamera()->getDeviceName());
0859     }
0860     else
0861     {
0862         cameraLabel->clear();
0863         return false;
0864     }
0865 
0866     if (devices()->filterWheel())
0867         process()->updateFilterInfo();
0868 
0869     process()->checkCamera();
0870 
0871     emit settingsUpdated(getPresetSettings());
0872 
0873     return true;
0874 }
0875 
0876 
0877 
0878 void Capture::setFilterWheel(QString name)
0879 {
0880     // Should not happen
0881     if (m_standAlone)
0882         return;
0883 
0884     if (devices()->filterWheel() && devices()->filterWheel()->getDeviceName() == name)
0885     {
0886         refreshFilterSettings();
0887         return;
0888     }
0889 
0890     auto isConnected = devices()->filterWheel() && devices()->filterWheel()->isConnected();
0891     FilterPosLabel->setEnabled(isConnected);
0892     FilterPosCombo->setEnabled(isConnected);
0893     filterManagerB->setEnabled(isConnected);
0894 
0895     refreshFilterSettings();
0896 
0897     if (devices()->filterWheel())
0898         emit settingsUpdated(getPresetSettings());
0899 }
0900 
0901 bool Capture::setDome(ISD::Dome *device)
0902 {
0903     return m_captureProcess->setDome(device);
0904 }
0905 
0906 void Capture::setRotator(QString name)
0907 {
0908     ISD::Rotator *Rotator = devices()->rotator();
0909     // clear old rotator
0910     rotatorB->setEnabled(false);
0911     if (Rotator && !m_RotatorControlPanel.isNull())
0912         m_RotatorControlPanel->close();
0913 
0914     // set new rotator
0915     if (!name.isEmpty())  // start real rotator
0916     {
0917         Manager::Instance()->getRotatorController(name, m_RotatorControlPanel);
0918         m_RotatorControlPanel->initRotator(opticalTrainCombo->currentText(), m_captureDeviceAdaptor.data(), Rotator);
0919         connect(rotatorB, &QPushButton::clicked, this, [this]()
0920         {
0921             m_RotatorControlPanel->show();
0922             m_RotatorControlPanel->raise();
0923         });
0924         rotatorB->setEnabled(true);
0925     }
0926     else if (Options::astrometryUseRotator()) // start at least rotatorutils for "manual rotator"
0927     {
0928         RotatorUtils::Instance()->initRotatorUtils(opticalTrainCombo->currentText());
0929     }
0930 }
0931 
0932 void Capture::pause()
0933 {
0934     process()->pauseCapturing();
0935     updateStartButtons(false, true);
0936 }
0937 
0938 void Capture::toggleSequence()
0939 {
0940     const CaptureState capturestate = state()->getCaptureState();
0941     if (capturestate == CAPTURE_PAUSE_PLANNED || capturestate == CAPTURE_PAUSED)
0942         updateStartButtons(true, false);
0943 
0944     process()->toggleSequence();
0945 }
0946 
0947 void Capture::jobStarting()
0948 {
0949     if (m_LimitsUI->limitFocusHFRS->isChecked() && state()->getRefocusState()->isAutoFocusReady() == false)
0950         appendLogText(i18n("Warning: in-sequence focusing is selected but autofocus process was not started."));
0951     if (m_LimitsUI->limitFocusDeltaTS->isChecked() && state()->getRefocusState()->isAutoFocusReady() == false)
0952         appendLogText(i18n("Warning: temperature delta check is selected but autofocus process was not started."));
0953 
0954     updateStartButtons(true, false);
0955 }
0956 
0957 void Capture::registerNewModule(const QString &name)
0958 {
0959     if (m_standAlone)
0960         return;
0961     if (name == "Mount" && mountInterface == nullptr)
0962     {
0963         qCDebug(KSTARS_EKOS_CAPTURE) << "Registering new Module (" << name << ")";
0964         mountInterface = new QDBusInterface("org.kde.kstars", "/KStars/Ekos/Mount",
0965                                             "org.kde.kstars.Ekos.Mount", QDBusConnection::sessionBus(), this);
0966 
0967     }
0968 }
0969 
0970 QString Capture::camera()
0971 {
0972     if (devices()->getActiveCamera())
0973         return devices()->getActiveCamera()->getDeviceName();
0974 
0975     return QString();
0976 }
0977 
0978 void Capture::refreshCameraSettings()
0979 {
0980     // Make sure we have a valid chip and valid base device.
0981     // Make sure we are not in capture process.
0982     auto camera = activeCamera();
0983     auto targetChip = devices()->getActiveChip();
0984     // If camera is restarted, try again in one second
0985     if (!m_standAlone && (!camera || !targetChip || !targetChip->getCCD() || targetChip->isCapturing()))
0986     {
0987         QTimer::singleShot(1000, this, &Capture::refreshCameraSettings);
0988         return;
0989     }
0990 
0991     if (camera->hasCoolerControl())
0992     {
0993         coolerOnB->setEnabled(true);
0994         coolerOffB->setEnabled(true);
0995         coolerOnB->setChecked(camera->isCoolerOn());
0996         coolerOffB->setChecked(!camera->isCoolerOn());
0997     }
0998     else
0999     {
1000         coolerOnB->setEnabled(false);
1001         coolerOnB->setChecked(false);
1002         coolerOffB->setEnabled(false);
1003         coolerOffB->setChecked(false);
1004     }
1005 
1006     updateFrameProperties();
1007 
1008     updateCaptureFormats();
1009 
1010     customPropertiesDialog->setCCD(camera);
1011 
1012     liveVideoB->setEnabled(camera->hasVideoStream());
1013     if (camera->hasVideoStream())
1014         setVideoStreamEnabled(camera->isStreamingEnabled());
1015     else
1016         liveVideoB->setIcon(QIcon::fromTheme("camera-off"));
1017 
1018     connect(camera, &ISD::Camera::propertyUpdated, this, &Capture::processCameraNumber, Qt::UniqueConnection);
1019     connect(camera, &ISD::Camera::coolerToggled, this, &Capture::setCoolerToggled, Qt::UniqueConnection);
1020     connect(camera, &ISD::Camera::videoStreamToggled, this, &Capture::setVideoStreamEnabled, Qt::UniqueConnection);
1021     connect(camera, &ISD::Camera::ready, this, &Capture::ready, Qt::UniqueConnection);
1022     connect(camera, &ISD::Camera::error, m_captureProcess.data(), &CaptureProcess::processCaptureError,
1023             Qt::UniqueConnection);
1024 
1025     syncCameraInfo();
1026 
1027     // update values received by the device adaptor
1028     // connect(activeCamera(), &ISD::Camera::newTemperatureValue, this, &Capture::updateCCDTemperature, Qt::UniqueConnection);
1029 
1030     DarkLibrary::Instance()->checkCamera();
1031 }
1032 
1033 void Capture::updateCaptureFormats()
1034 {
1035     QStringList frameTypes = process()->frameTypes();
1036 
1037     captureTypeS->clear();
1038 
1039     if (frameTypes.isEmpty())
1040         captureTypeS->setEnabled(false);
1041     else
1042     {
1043         captureTypeS->setEnabled(true);
1044         captureTypeS->addItems(frameTypes);
1045         Options::setCaptureStandAloneTypes(standAloneEncode(frameTypes));
1046         captureTypeS->setCurrentIndex(devices()->getActiveChip()->getFrameType());
1047     }
1048 
1049     // Capture Format
1050     captureFormatS->blockSignals(true);
1051     captureFormatS->clear();
1052     captureFormatS->addItems(activeCamera()->getCaptureFormats());
1053     Options::setCaptureStandAloneFormats(standAloneEncode(activeCamera()->getCaptureFormats()));
1054     captureFormatS->setCurrentText(activeCamera()->getCaptureFormat());
1055     captureFormatS->blockSignals(false);
1056 
1057     // Encoding format
1058     captureEncodingS->blockSignals(true);
1059     captureEncodingS->clear();
1060     captureEncodingS->addItems(activeCamera()->getEncodingFormats());
1061     captureEncodingS->setCurrentText(activeCamera()->getEncodingFormat());
1062     Options::setCaptureStandAloneEncodings(standAloneEncode(activeCamera()->getEncodingFormats()));
1063     captureEncodingS->blockSignals(false);
1064 
1065     Options::setCaptureStandAloneTimestamp(KStarsData::Instance()->lt().toString("yyyy-MM-dd hh:mm"));
1066 }
1067 
1068 void Capture::syncCameraInfo()
1069 {
1070     if (!activeCamera())
1071         return;
1072 
1073     if (activeCamera()->hasCooler())
1074     {
1075         cameraTemperatureS->setEnabled(true);
1076         cameraTemperatureN->setEnabled(true);
1077 
1078         if (activeCamera()->getPermission("CCD_TEMPERATURE") != IP_RO)
1079         {
1080             double min, max, step;
1081             setTemperatureB->setEnabled(true);
1082             cameraTemperatureN->setReadOnly(false);
1083             cameraTemperatureS->setEnabled(true);
1084             temperatureRegulationB->setEnabled(true);
1085             activeCamera()->getMinMaxStep("CCD_TEMPERATURE", "CCD_TEMPERATURE_VALUE", &min, &max, &step);
1086             cameraTemperatureN->setMinimum(min);
1087             cameraTemperatureN->setMaximum(max);
1088             cameraTemperatureN->setSingleStep(1);
1089             bool isChecked = activeCamera()->getDriverInfo()->getAuxInfo().value(QString("%1_TC").arg(activeCamera()->getDeviceName()),
1090                              false).toBool();
1091             cameraTemperatureS->setChecked(isChecked);
1092         }
1093         else
1094         {
1095             setTemperatureB->setEnabled(false);
1096             cameraTemperatureN->setReadOnly(true);
1097             cameraTemperatureS->setEnabled(false);
1098             cameraTemperatureS->setChecked(false);
1099             temperatureRegulationB->setEnabled(false);
1100         }
1101 
1102         double temperature = 0;
1103         if (activeCamera()->getTemperature(&temperature))
1104         {
1105             temperatureOUT->setText(QString("%L1").arg(temperature, 0, 'f', 2));
1106             if (cameraTemperatureN->cleanText().isEmpty())
1107                 cameraTemperatureN->setValue(temperature);
1108         }
1109     }
1110     else
1111     {
1112         cameraTemperatureS->setEnabled(false);
1113         cameraTemperatureN->setEnabled(false);
1114         temperatureRegulationB->setEnabled(false);
1115         cameraTemperatureN->clear();
1116         temperatureOUT->clear();
1117         setTemperatureB->setEnabled(false);
1118     }
1119 
1120     auto isoList = devices()->getActiveChip()->getISOList();
1121     captureISOS->blockSignals(true);
1122     captureISOS->setEnabled(false);
1123     captureISOS->clear();
1124 
1125     // No ISO range available
1126     if (isoList.isEmpty())
1127     {
1128         captureISOS->setEnabled(false);
1129         Options::setCaptureStandAloneISOs("");
1130     }
1131     else
1132     {
1133         captureISOS->setEnabled(true);
1134         captureISOS->addItems(isoList);
1135         captureISOS->setCurrentIndex(devices()->getActiveChip()->getISOIndex());
1136         Options::setCaptureStandAloneISOs(standAloneEncode(isoList));
1137         Options::setCaptureStandAloneISOIndex(devices()->getActiveChip()->getISOIndex());
1138 
1139         uint16_t w, h;
1140         uint8_t bbp {8};
1141         double pixelX = 0, pixelY = 0;
1142         bool rc = devices()->getActiveChip()->getImageInfo(w, h, pixelX, pixelY, bbp);
1143         bool isModelInDB = state()->isModelinDSLRInfo(QString(activeCamera()->getDeviceName()));
1144         // If rc == true, then the property has been defined by the driver already
1145         // Only then we check if the pixels are zero
1146         if (rc == true && (pixelX == 0.0 || pixelY == 0.0 || isModelInDB == false))
1147         {
1148             // If model is already in database, no need to show dialog
1149             // The zeros above are the initial packets so we can safely ignore them
1150             if (isModelInDB == false)
1151             {
1152                 createDSLRDialog();
1153             }
1154             else
1155             {
1156                 QString model = QString(activeCamera()->getDeviceName());
1157                 process()->syncDSLRToTargetChip(model);
1158             }
1159         }
1160     }
1161     captureISOS->blockSignals(false);
1162 
1163     // Gain Check
1164     if (activeCamera()->hasGain())
1165     {
1166         double min, max, step, value, targetCustomGain;
1167         activeCamera()->getGainMinMaxStep(&min, &max, &step);
1168 
1169         // Allow the possibility of no gain value at all.
1170         GainSpinSpecialValue = min - step;
1171         captureGainN->setRange(GainSpinSpecialValue, max);
1172         captureGainN->setSpecialValueText(i18n("--"));
1173         captureGainN->setEnabled(true);
1174         captureGainN->setSingleStep(step);
1175         activeCamera()->getGain(&value);
1176         currentGainLabel->setText(QString::number(value, 'f', 0));
1177 
1178         targetCustomGain = getGain();
1179 
1180         // Set the custom gain if we have one
1181         // otherwise it will not have an effect.
1182         if (targetCustomGain > 0)
1183             captureGainN->setValue(targetCustomGain);
1184         else
1185             captureGainN->setValue(GainSpinSpecialValue);
1186 
1187         captureGainN->setReadOnly(activeCamera()->getGainPermission() == IP_RO);
1188 
1189         connect(captureGainN, &QDoubleSpinBox::editingFinished, this, [this]()
1190         {
1191             if (captureGainN->value() != GainSpinSpecialValue)
1192                 setGain(captureGainN->value());
1193             else
1194                 setGain(-1);
1195         });
1196     }
1197     else
1198     {
1199         captureGainN->setEnabled(false);
1200         currentGainLabel->clear();
1201     }
1202 
1203     // Offset checks
1204     if (activeCamera()->hasOffset())
1205     {
1206         double min, max, step, value, targetCustomOffset;
1207         activeCamera()->getOffsetMinMaxStep(&min, &max, &step);
1208 
1209         // Allow the possibility of no Offset value at all.
1210         OffsetSpinSpecialValue = min - step;
1211         captureOffsetN->setRange(OffsetSpinSpecialValue, max);
1212         captureOffsetN->setSpecialValueText(i18n("--"));
1213         captureOffsetN->setEnabled(true);
1214         captureOffsetN->setSingleStep(step);
1215         activeCamera()->getOffset(&value);
1216         currentOffsetLabel->setText(QString::number(value, 'f', 0));
1217 
1218         targetCustomOffset = getOffset();
1219 
1220         // Set the custom Offset if we have one
1221         // otherwise it will not have an effect.
1222         if (targetCustomOffset > 0)
1223             captureOffsetN->setValue(targetCustomOffset);
1224         else
1225             captureOffsetN->setValue(OffsetSpinSpecialValue);
1226 
1227         captureOffsetN->setReadOnly(activeCamera()->getOffsetPermission() == IP_RO);
1228 
1229         connect(captureOffsetN, &QDoubleSpinBox::editingFinished, this, [this]()
1230         {
1231             if (captureOffsetN->value() != OffsetSpinSpecialValue)
1232                 setOffset(captureOffsetN->value());
1233             else
1234                 setOffset(-1);
1235         });
1236     }
1237     else
1238     {
1239         captureOffsetN->setEnabled(false);
1240         currentOffsetLabel->clear();
1241     }
1242 }
1243 
1244 void Capture::setGuideChip(ISD::CameraChip * guideChip)
1245 {
1246     // We should suspend guide in two scenarios:
1247     // 1. If guide chip is within the primary CCD, then we cannot download any data from guide chip while primary CCD is downloading.
1248     // 2. If we have two CCDs running from ONE driver (Multiple-Devices-Per-Driver mpdp is true). Same issue as above, only one download
1249     // at a time.
1250     // After primary CCD download is complete, we resume guiding.
1251     if (!devices()->getActiveCamera())
1252         return;
1253 
1254     state()->setSuspendGuidingOnDownload((devices()->getActiveCamera()->getChip(
1255             ISD::CameraChip::GUIDE_CCD) == guideChip) ||
1256                                          (guideChip->getCCD() == devices()->getActiveCamera() &&
1257                                           devices()->getActiveCamera()->getDriverInfo()->getAuxInfo().value("mdpd", false).toBool()));
1258 }
1259 
1260 void Capture::resetFrameToZero()
1261 {
1262     captureFrameXN->setMinimum(0);
1263     captureFrameXN->setMaximum(0);
1264     captureFrameXN->setValue(0);
1265 
1266     captureFrameYN->setMinimum(0);
1267     captureFrameYN->setMaximum(0);
1268     captureFrameYN->setValue(0);
1269 
1270     captureFrameWN->setMinimum(0);
1271     captureFrameWN->setMaximum(0);
1272     captureFrameWN->setValue(0);
1273 
1274     captureFrameHN->setMinimum(0);
1275     captureFrameHN->setMaximum(0);
1276     captureFrameHN->setValue(0);
1277 }
1278 
1279 void Capture::updateFrameProperties(int reset)
1280 {
1281     if (!devices()->getActiveCamera())
1282         return;
1283 
1284     int binx = 1, biny = 1;
1285     double min, max, step;
1286     int xstep = 0, ystep = 0;
1287 
1288     QString frameProp    = state()->useGuideHead() ? QString("GUIDER_FRAME") : QString("CCD_FRAME");
1289     QString exposureProp = state()->useGuideHead() ? QString("GUIDER_EXPOSURE") : QString("CCD_EXPOSURE");
1290     QString exposureElem = state()->useGuideHead() ? QString("GUIDER_EXPOSURE_VALUE") :
1291                            QString("CCD_EXPOSURE_VALUE");
1292     devices()->setActiveChip(state()->useGuideHead() ?
1293                              devices()->getActiveCamera()->getChip(
1294                                  ISD::CameraChip::GUIDE_CCD) :
1295                              devices()->getActiveCamera()->getChip(ISD::CameraChip::PRIMARY_CCD));
1296 
1297     captureFrameWN->setEnabled(devices()->getActiveChip()->canSubframe());
1298     captureFrameHN->setEnabled(devices()->getActiveChip()->canSubframe());
1299     captureFrameXN->setEnabled(devices()->getActiveChip()->canSubframe());
1300     captureFrameYN->setEnabled(devices()->getActiveChip()->canSubframe());
1301 
1302     captureBinHN->setEnabled(devices()->getActiveChip()->canBin());
1303     captureBinVN->setEnabled(devices()->getActiveChip()->canBin());
1304 
1305     QList<double> exposureValues;
1306     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 <<
1307                    10 << 20 << 30 << 40 << 50 << 60 << 120 << 180 << 300 << 600 << 900 << 1200 << 1800;
1308 
1309     if (devices()->getActiveCamera()->getMinMaxStep(exposureProp, exposureElem, &min, &max, &step))
1310     {
1311         if (min < 0.001)
1312             captureExposureN->setDecimals(6);
1313         else
1314             captureExposureN->setDecimals(3);
1315         for(int i = 0; i < exposureValues.count(); i++)
1316         {
1317             double value = exposureValues.at(i);
1318             if(value < min || value > max)
1319             {
1320                 exposureValues.removeAt(i);
1321                 i--; //So we don't skip one
1322             }
1323         }
1324 
1325         exposureValues.prepend(min);
1326         exposureValues.append(max);
1327     }
1328 
1329     captureExposureN->setRecommendedValues(exposureValues);
1330     state()->setExposureRange(exposureValues.first(), exposureValues.last());
1331 
1332     if (devices()->getActiveCamera()->getMinMaxStep(frameProp, "WIDTH", &min, &max, &step))
1333     {
1334         if (min >= max)
1335         {
1336             resetFrameToZero();
1337             return;
1338         }
1339 
1340         if (step == 0.0)
1341             xstep = static_cast<int>(max * 0.05);
1342         else
1343             xstep = static_cast<int>(step);
1344 
1345         if (min >= 0 && max > 0)
1346         {
1347             captureFrameWN->setMinimum(static_cast<int>(min));
1348             captureFrameWN->setMaximum(static_cast<int>(max));
1349             captureFrameWN->setSingleStep(xstep);
1350         }
1351     }
1352     else
1353         return;
1354 
1355     if (devices()->getActiveCamera()->getMinMaxStep(frameProp, "HEIGHT", &min, &max, &step))
1356     {
1357         if (min >= max)
1358         {
1359             resetFrameToZero();
1360             return;
1361         }
1362 
1363         if (step == 0.0)
1364             ystep = static_cast<int>(max * 0.05);
1365         else
1366             ystep = static_cast<int>(step);
1367 
1368         if (min >= 0 && max > 0)
1369         {
1370             captureFrameHN->setMinimum(static_cast<int>(min));
1371             captureFrameHN->setMaximum(static_cast<int>(max));
1372             captureFrameHN->setSingleStep(ystep);
1373         }
1374     }
1375     else
1376         return;
1377 
1378     if (devices()->getActiveCamera()->getMinMaxStep(frameProp, "X", &min, &max, &step))
1379     {
1380         if (min >= max)
1381         {
1382             resetFrameToZero();
1383             return;
1384         }
1385 
1386         if (step == 0.0)
1387             step = xstep;
1388 
1389         if (min >= 0 && max > 0)
1390         {
1391             captureFrameXN->setMinimum(static_cast<int>(min));
1392             captureFrameXN->setMaximum(static_cast<int>(max));
1393             captureFrameXN->setSingleStep(static_cast<int>(step));
1394         }
1395     }
1396     else
1397         return;
1398 
1399     if (devices()->getActiveCamera()->getMinMaxStep(frameProp, "Y", &min, &max, &step))
1400     {
1401         if (min >= max)
1402         {
1403             resetFrameToZero();
1404             return;
1405         }
1406 
1407         if (step == 0.0)
1408             step = ystep;
1409 
1410         if (min >= 0 && max > 0)
1411         {
1412             captureFrameYN->setMinimum(static_cast<int>(min));
1413             captureFrameYN->setMaximum(static_cast<int>(max));
1414             captureFrameYN->setSingleStep(static_cast<int>(step));
1415         }
1416     }
1417     else
1418         return;
1419 
1420     // cull to camera limits, if there are any
1421     if (state()->useGuideHead() == false)
1422         cullToDSLRLimits();
1423 
1424     // Save the sensor's width and height for the stand-alone editor.
1425     Options::setCaptureStandAloneWHGO(
1426         standAloneEncode(
1427             QStringList({QString("%1").arg(captureFrameWN->value()),
1428                          QString("%1").arg(captureFrameHN->value()),
1429                          QString("%1").arg(devices()->getActiveCamera()->getProperty("CCD_GAIN") ? "CCD_GAIN" : "CCD_CONTROLS"),
1430                          QString("%1").arg(devices()->getActiveCamera()->getProperty("CCD_OFFSET") ? "CCD_OFFSET" : "CCD_CONTROLS")})));
1431 
1432     if (reset == 1 || state()->frameSettings().contains(devices()->getActiveChip()) == false)
1433     {
1434         QVariantMap settings;
1435 
1436         settings["x"]    = 0;
1437         settings["y"]    = 0;
1438         settings["w"]    = captureFrameWN->maximum();
1439         settings["h"]    = captureFrameHN->maximum();
1440         settings["binx"] = captureBinHN->value();
1441         settings["biny"] = captureBinVN->value();
1442 
1443         state()->frameSettings()[devices()->getActiveChip()] = settings;
1444     }
1445     else if (reset == 2 && state()->frameSettings().contains(devices()->getActiveChip()))
1446     {
1447         QVariantMap settings = state()->frameSettings()[devices()->getActiveChip()];
1448         int x, y, w, h;
1449 
1450         x = settings["x"].toInt();
1451         y = settings["y"].toInt();
1452         w = settings["w"].toInt();
1453         h = settings["h"].toInt();
1454 
1455         // Bound them
1456         x = qBound(captureFrameXN->minimum(), x, captureFrameXN->maximum() - 1);
1457         y = qBound(captureFrameYN->minimum(), y, captureFrameYN->maximum() - 1);
1458         w = qBound(captureFrameWN->minimum(), w, captureFrameWN->maximum());
1459         h = qBound(captureFrameHN->minimum(), h, captureFrameHN->maximum());
1460 
1461         settings["x"] = x;
1462         settings["y"] = y;
1463         settings["w"] = w;
1464         settings["h"] = h;
1465         settings["binx"] = captureBinHN->value();
1466         settings["biny"] = captureBinVN->value();
1467 
1468         state()->frameSettings()[devices()->getActiveChip()] = settings;
1469     }
1470 
1471     if (state()->frameSettings().contains(devices()->getActiveChip()))
1472     {
1473         QVariantMap settings = state()->frameSettings()[devices()->getActiveChip()];
1474         int x = settings["x"].toInt();
1475         int y = settings["y"].toInt();
1476         int w = settings["w"].toInt();
1477         int h = settings["h"].toInt();
1478 
1479         if (devices()->getActiveChip()->canBin())
1480         {
1481             devices()->getActiveChip()->getMaxBin(&binx, &biny);
1482             captureBinHN->setMaximum(binx);
1483             captureBinVN->setMaximum(biny);
1484 
1485             captureBinHN->setValue(settings["binx"].toInt());
1486             captureBinVN->setValue(settings["biny"].toInt());
1487         }
1488         else
1489         {
1490             captureBinHN->setValue(1);
1491             captureBinVN->setValue(1);
1492         }
1493 
1494         if (x >= 0)
1495             captureFrameXN->setValue(x);
1496         if (y >= 0)
1497             captureFrameYN->setValue(y);
1498         if (w > 0)
1499             captureFrameWN->setValue(w);
1500         if (h > 0)
1501             captureFrameHN->setValue(h);
1502     }
1503 }
1504 
1505 void Capture::processCameraNumber(INDI::Property prop)
1506 {
1507     if (devices()->getActiveCamera() == nullptr)
1508         return;
1509 
1510     if ((prop.isNameMatch("CCD_FRAME") && state()->useGuideHead() == false) ||
1511             (prop.isNameMatch("GUIDER_FRAME") && state()->useGuideHead()))
1512         updateFrameProperties();
1513     else if ((prop.isNameMatch("CCD_INFO") && state()->useGuideHead() == false) ||
1514              (prop.isNameMatch("GUIDER_INFO") && state()->useGuideHead()))
1515         updateFrameProperties(1);
1516     else if (prop.isNameMatch("CCD_TRANSFER_FORMAT") || prop.isNameMatch("CCD_CAPTURE_FORMAT"))
1517         updateCaptureFormats();
1518     else if (prop.isNameMatch("CCD_CONTROLS"))
1519     {
1520         auto nvp = prop.getNumber();
1521         auto gain = nvp->findWidgetByName("Gain");
1522         if (gain)
1523             currentGainLabel->setText(QString::number(gain->value, 'f', 0));
1524         auto offset = nvp->findWidgetByName("Offset");
1525         if (offset)
1526             currentOffsetLabel->setText(QString::number(offset->value, 'f', 0));
1527     }
1528     else if (prop.isNameMatch("CCD_GAIN"))
1529     {
1530         auto nvp = prop.getNumber();
1531         currentGainLabel->setText(QString::number(nvp->at(0)->getValue(), 'f', 0));
1532     }
1533     else if (prop.isNameMatch("CCD_OFFSET"))
1534     {
1535         auto nvp = prop.getNumber();
1536         currentOffsetLabel->setText(QString::number(nvp->at(0)->getValue(), 'f', 0));
1537     }
1538 }
1539 
1540 void Capture::syncFrameType(const QString &name)
1541 {
1542     if (!activeCamera() || name != activeCamera()->getDeviceName())
1543         return;
1544 
1545     QStringList frameTypes = process()->frameTypes();
1546 
1547     captureTypeS->clear();
1548 
1549     if (frameTypes.isEmpty())
1550         captureTypeS->setEnabled(false);
1551     else
1552     {
1553         captureTypeS->setEnabled(true);
1554         captureTypeS->addItems(frameTypes);
1555         ISD::CameraChip *tChip = devices()->getActiveCamera()->getChip(ISD::CameraChip::PRIMARY_CCD);
1556         captureTypeS->setCurrentIndex(tChip->getFrameType());
1557     }
1558 }
1559 
1560 QString Capture::filterWheel()
1561 {
1562     if (devices()->filterWheel())
1563         return devices()->filterWheel()->getDeviceName();
1564 
1565     return QString();
1566 }
1567 
1568 bool Capture::setFilter(const QString &filter)
1569 {
1570     if (devices()->filterWheel())
1571     {
1572         FilterPosCombo->setCurrentText(filter);
1573         return true;
1574     }
1575 
1576     return false;
1577 }
1578 
1579 QString Capture::filter()
1580 {
1581     return FilterPosCombo->currentText();
1582 }
1583 
1584 void Capture::updateCurrentFilterPosition()
1585 {
1586     const QString currentFilterText = FilterPosCombo->itemText(m_FilterManager->getFilterPosition() - 1);
1587     state()->setCurrentFilterPosition(m_FilterManager->getFilterPosition(),
1588                                       currentFilterText,
1589                                       m_FilterManager->getFilterLock(currentFilterText));
1590 }
1591 
1592 void Capture::refreshFilterSettings()
1593 {
1594     FilterPosCombo->clear();
1595 
1596     if (!devices()->filterWheel())
1597     {
1598         FilterPosLabel->setEnabled(false);
1599         FilterPosCombo->setEnabled(false);
1600         filterEditB->setEnabled(false);
1601 
1602         devices()->setFilterManager(m_FilterManager);
1603         return;
1604     }
1605 
1606     FilterPosLabel->setEnabled(true);
1607     FilterPosCombo->setEnabled(true);
1608     filterEditB->setEnabled(true);
1609 
1610     setupFilterManager();
1611 
1612     process()->updateFilterInfo();
1613 
1614     FilterPosCombo->addItems(process()->filterLabels());
1615     Options::setCaptureStandAloneFilters(standAloneEncode(process()->filterLabels()));
1616 
1617     updateCurrentFilterPosition();
1618 
1619     filterEditB->setEnabled(state()->getCurrentFilterPosition() > 0);
1620 
1621     FilterPosCombo->setCurrentIndex(state()->getCurrentFilterPosition() - 1);
1622 }
1623 
1624 void Capture::processingFITSfinished(bool success)
1625 {
1626     // do nothing in case of failure
1627     if (success == false)
1628         return;
1629 
1630     // If this is a preview job, make sure to enable preview button after
1631     if (devices()->getActiveCamera()
1632             && devices()->getActiveCamera()->getUploadMode() != ISD::Camera::UPLOAD_LOCAL)
1633         previewB->setEnabled(true);
1634 
1635     imageCapturingCompleted();
1636 }
1637 
1638 void Capture::imageCapturingCompleted()
1639 {
1640     SequenceJob *thejob = activeJob();
1641 
1642     if (!thejob)
1643         return;
1644 
1645     // In case we're framing, let's return quickly to continue the process.
1646     if (state()->isLooping())
1647     {
1648         captureStatusWidget->setStatus(i18n("Framing..."), Qt::darkGreen);
1649         return;
1650     }
1651 
1652     // If fast exposure is off, disconnect exposure progress
1653     // otherwise, keep it going since it fires off from driver continuous capture process.
1654     if (devices()->getActiveCamera()->isFastExposureEnabled() == false)
1655         DarkLibrary::Instance()->disconnect(this);
1656 
1657     // Do not display notifications for very short captures
1658     if (thejob->getCoreProperty(SequenceJob::SJ_Exposure).toDouble() >= 1)
1659         KSNotification::event(QLatin1String("EkosCaptureImageReceived"), i18n("Captured image received"),
1660                               KSNotification::Capture);
1661 
1662     // If it was initially set as pure preview job and NOT as preview for calibration
1663     if (thejob->jobType() == SequenceJob::JOBTYPE_PREVIEW)
1664         return;
1665 
1666     /* The image progress has now one more capture */
1667     imgProgress->setValue(thejob->getCompleted());
1668 }
1669 
1670 void Capture::captureStopped()
1671 {
1672     imgProgress->reset();
1673     imgProgress->setEnabled(false);
1674 
1675     frameRemainingTime->setText("--:--:--");
1676     jobRemainingTime->setText("--:--:--");
1677     frameInfoLabel->setText(i18n("Expose (-/-):"));
1678 
1679     // stopping to CAPTURE_IDLE means that capturing will continue automatically
1680     auto captureState = state()->getCaptureState();
1681     if (captureState == CAPTURE_ABORTED || captureState == CAPTURE_SUSPENDED || captureState == CAPTURE_COMPLETE)
1682         updateStartButtons(false, false);
1683 }
1684 
1685 void Capture::updateTargetDistance(double targetDiff)
1686 {
1687     // ensure that the drift is visible
1688     targetDriftLabel->setVisible(true);
1689     targetDrift->setVisible(true);
1690     targetDriftUnit->setVisible(true);
1691     // update the drift value
1692     targetDrift->setText(QString("%L1").arg(targetDiff, 0, 'd', 1));
1693 }
1694 
1695 void Capture::captureImageStarted()
1696 {
1697     if (devices()->filterWheel() != nullptr)
1698     {
1699         // JM 2021.08.23 Call filter info to set the active filter wheel in the camera driver
1700         // so that it may snoop on the active filter
1701         process()->updateFilterInfo();
1702         updateCurrentFilterPosition();
1703     }
1704 
1705     // necessary since the status widget doesn't store the calibration stage
1706     if (activeJob()->getCalibrationStage() == SequenceJobState::CAL_CALIBRATION)
1707         captureStatusWidget->setStatus(i18n("Calibrating..."), Qt::yellow);
1708 }
1709 
1710 namespace
1711 {
1712 QString frameLabel(CCDFrameType type, const QString &filter)
1713 {
1714     switch(type)
1715     {
1716         case FRAME_LIGHT:
1717             if (filter.size() == 0)
1718                 return CCDFrameTypeNames[type];
1719             else
1720                 return filter;
1721             break;
1722         case FRAME_FLAT:
1723             if (filter.size() == 0)
1724                 return CCDFrameTypeNames[type];
1725             else
1726                 return QString("%1 %2").arg(filter).arg(CCDFrameTypeNames[type]);
1727             break;
1728         case FRAME_BIAS:
1729         case FRAME_DARK:
1730         case FRAME_NONE:
1731         default:
1732             return CCDFrameTypeNames[type];
1733     }
1734 }
1735 }
1736 
1737 void Capture::captureRunning()
1738 {
1739     emit captureStarting(activeJob()->getCoreProperty(SequenceJob::SJ_Exposure).toDouble(),
1740                          activeJob()->getCoreProperty(SequenceJob::SJ_Filter).toString());
1741     appendLogText(i18n("Capturing %1-second %2 image...",
1742                        QString("%L1").arg(activeJob()->getCoreProperty(SequenceJob::SJ_Exposure).toDouble(), 0, 'f', 3),
1743                        activeJob()->getCoreProperty(SequenceJob::SJ_Filter).toString()));
1744     frameInfoLabel->setText(QString("%1 (%L3/%L4):").arg(frameLabel(activeJob()->getFrameType(),
1745                             activeJob()->getCoreProperty(SequenceJob::SJ_Filter).toString()))
1746                             .arg(activeJob()->getCompleted()).arg(activeJob()->getCoreProperty(
1747                                         SequenceJob::SJ_Count).toInt()));
1748     // ensure that the download time label is visible
1749     avgDownloadTime->setVisible(true);
1750     avgDownloadLabel->setVisible(true);
1751     secLabel->setVisible(true);
1752     // show estimated download time
1753     avgDownloadTime->setText(QString("%L1").arg(state()->averageDownloadTime(), 0, 'd', 2));
1754 }
1755 
1756 void Capture::appendLogText(const QString &text)
1757 {
1758     m_LogText.insert(0, i18nc("log entry; %1 is the date, %2 is the text", "%1 %2",
1759                               KStarsData::Instance()->lt().toString("yyyy-MM-ddThh:mm:ss"), text));
1760 
1761     qCInfo(KSTARS_EKOS_CAPTURE) << text;
1762 
1763     emit newLog(text);
1764 }
1765 
1766 void Capture::clearLog()
1767 {
1768     m_LogText.clear();
1769     emit newLog(QString());
1770 }
1771 
1772 void Capture::updateDownloadProgress(double downloadTimeLeft)
1773 {
1774     frameRemainingTime->setText(state()->imageCountDown().toString("hh:mm:ss"));
1775     emit newDownloadProgress(downloadTimeLeft);
1776 }
1777 
1778 void Capture::updateCaptureCountDown(int deltaMillis)
1779 {
1780     state()->imageCountDownAddMSecs(deltaMillis);
1781     state()->sequenceCountDownAddMSecs(deltaMillis);
1782     frameRemainingTime->setText(state()->imageCountDown().toString("hh:mm:ss"));
1783     jobRemainingTime->setText(state()->sequenceCountDown().toString("hh:mm:ss"));
1784 }
1785 
1786 void Capture::updateCCDTemperature(double value)
1787 {
1788     if (cameraTemperatureS->isEnabled() == false && devices()->getActiveCamera())
1789     {
1790         if (devices()->getActiveCamera()->getPermission("CCD_TEMPERATURE") != IP_RO)
1791             process()->checkCamera();
1792     }
1793 
1794     temperatureOUT->setText(QString("%L1").arg(value, 0, 'f', 2));
1795 
1796     if (cameraTemperatureN->cleanText().isEmpty())
1797         cameraTemperatureN->setValue(value);
1798 }
1799 
1800 void Capture::updateRotatorAngle(double value)
1801 {
1802     IPState RState = devices()->rotator()->absoluteAngleState();
1803     if (RState == IPS_OK)
1804         m_RotatorControlPanel->updateRotator(value);
1805     else
1806         m_RotatorControlPanel->updateGauge(value);
1807 }
1808 
1809 void Capture::addJob(SequenceJob *job)
1810 {
1811     // create a new row
1812     createNewJobTableRow(job);
1813 }
1814 
1815 SequenceJob *Capture::createJob(SequenceJob::SequenceJobType jobtype, FilenamePreviewType filenamePreview)
1816 {
1817     SequenceJob *job = new SequenceJob(devices(), state(), jobtype);
1818 
1819     updateJobFromUI(job, filenamePreview);
1820 
1821     // Nothing more to do if preview or for placeholder calculations
1822     if (jobtype == SequenceJob::JOBTYPE_PREVIEW || filenamePreview != NOT_PREVIEW)
1823         return job;
1824 
1825     // check if the upload paths are correct
1826     if (checkUploadPaths(filenamePreview) == false)
1827         return nullptr;
1828 
1829     // all other jobs will be added to the job list
1830     state()->allJobs().append(job);
1831 
1832     // create a new row
1833     createNewJobTableRow(job);
1834 
1835     return job;
1836 }
1837 
1838 void Ekos::Capture::createNewJobTableRow(SequenceJob *job)
1839 {
1840     int currentRow = queueTable->rowCount();
1841     queueTable->insertRow(currentRow);
1842 
1843     // create job table widgets
1844     QTableWidgetItem *status = new QTableWidgetItem();
1845     status->setTextAlignment(Qt::AlignHCenter);
1846     status->setFlags(Qt::ItemIsSelectable | Qt::ItemIsEnabled);
1847 
1848     QTableWidgetItem *filter = new QTableWidgetItem();
1849     filter->setTextAlignment(Qt::AlignHCenter);
1850     filter->setFlags(Qt::ItemIsSelectable | Qt::ItemIsEnabled);
1851 
1852     QTableWidgetItem *count = new QTableWidgetItem();
1853     count->setTextAlignment(Qt::AlignHCenter);
1854     count->setFlags(Qt::ItemIsSelectable | Qt::ItemIsEnabled);
1855 
1856     QTableWidgetItem *exp = new QTableWidgetItem();
1857     exp->setTextAlignment(Qt::AlignHCenter);
1858     exp->setFlags(Qt::ItemIsSelectable | Qt::ItemIsEnabled);
1859 
1860     QTableWidgetItem *type = new QTableWidgetItem();
1861     type->setTextAlignment(Qt::AlignHCenter);
1862     type->setFlags(Qt::ItemIsSelectable | Qt::ItemIsEnabled);
1863 
1864     QTableWidgetItem *bin = new QTableWidgetItem();
1865     bin->setTextAlignment(Qt::AlignHCenter);
1866     bin->setFlags(Qt::ItemIsSelectable | Qt::ItemIsEnabled);
1867 
1868     QTableWidgetItem *iso = new QTableWidgetItem();
1869     iso->setTextAlignment(Qt::AlignHCenter);
1870     iso->setFlags(Qt::ItemIsSelectable | Qt::ItemIsEnabled);
1871 
1872     QTableWidgetItem *offset = new QTableWidgetItem();
1873     offset->setTextAlignment(Qt::AlignHCenter);
1874     offset->setFlags(Qt::ItemIsSelectable | Qt::ItemIsEnabled);
1875 
1876     // add the widgets to the table
1877     queueTable->setItem(currentRow, JOBTABLE_COL_STATUS, status);
1878     queueTable->setItem(currentRow, JOBTABLE_COL_FILTER, filter);
1879     queueTable->setItem(currentRow, JOBTABLE_COL_COUNTS, count);
1880     queueTable->setItem(currentRow, JOBTABLE_COL_EXP, exp);
1881     queueTable->setItem(currentRow, JOBTABLE_COL_TYPE, type);
1882     queueTable->setItem(currentRow, JOBTABLE_COL_BINNING, bin);
1883     queueTable->setItem(currentRow, JOBTABLE_COL_ISO, iso);
1884     queueTable->setItem(currentRow, JOBTABLE_COL_OFFSET, offset);
1885 
1886     // full update to the job table row
1887     updateJobTable(job, true);
1888 
1889     // Create a new JSON object. Needs to be called after the new row has been filled
1890     QJsonObject jsonJob = createJsonJob(job, currentRow);
1891     state()->getSequence().append(jsonJob);
1892     emit sequenceChanged(state()->getSequence());
1893 
1894     removeFromQueueB->setEnabled(true);
1895 }
1896 
1897 
1898 void Capture::editJobFinished()
1899 {
1900     if (queueTable->currentRow() < 0)
1901         qCWarning(KSTARS_EKOS_CAPTURE()) << "Editing finished, but no row selected!";
1902 
1903     int currentRow = queueTable->currentRow();
1904     SequenceJob *job = state()->allJobs().at(currentRow);
1905     updateJobFromUI(job);
1906 
1907     // full update to the job table row
1908     updateJobTable(job, true);
1909 
1910     // Update the JSON object for the current row. Needs to be called after the new row has been filled
1911     QJsonObject jsonJob = createJsonJob(job, currentRow);
1912     state()->getSequence().replace(currentRow, jsonJob);
1913     emit sequenceChanged(state()->getSequence());
1914 
1915     resetJobEdit();
1916     appendLogText(i18n("Job #%1 changes applied.", currentRow + 1));
1917 }
1918 
1919 void Capture::removeJobFromQueue()
1920 {
1921     int currentRow = queueTable->currentRow();
1922 
1923     if (currentRow < 0)
1924         currentRow = queueTable->rowCount() - 1;
1925 
1926     removeJob(currentRow);
1927 
1928     // update selection
1929     if (queueTable->rowCount() == 0)
1930         return;
1931 
1932     if (currentRow > queueTable->rowCount())
1933         queueTable->selectRow(queueTable->rowCount() - 1);
1934     else
1935         queueTable->selectRow(currentRow);
1936 }
1937 
1938 bool Capture::removeJob(int index)
1939 {
1940     if (state()->getCaptureState() != CAPTURE_IDLE && state()->getCaptureState() != CAPTURE_ABORTED
1941             && state()->getCaptureState() != CAPTURE_COMPLETE)
1942         return false;
1943 
1944     if (m_JobUnderEdit)
1945     {
1946         resetJobEdit(true);
1947         return false;
1948     }
1949 
1950     if (index < 0 || index >= state()->allJobs().count())
1951         return false;
1952 
1953     queueTable->removeRow(index);
1954     QJsonArray seqArray = state()->getSequence();
1955     seqArray.removeAt(index);
1956     state()->setSequence(seqArray);
1957     emit sequenceChanged(seqArray);
1958 
1959     if (state()->allJobs().empty())
1960         return true;
1961 
1962     SequenceJob * job = state()->allJobs().at(index);
1963     // remove completed frame counts from frame count map
1964     state()->removeCapturedFrameCount(job->getSignature(), job->getCompleted());
1965     // remove the job
1966     state()->allJobs().removeOne(job);
1967     if (job == activeJob())
1968         state()->setActiveJob(nullptr);
1969 
1970     delete job;
1971 
1972     if (queueTable->rowCount() == 0)
1973         removeFromQueueB->setEnabled(false);
1974 
1975     if (queueTable->rowCount() == 1)
1976     {
1977         queueUpB->setEnabled(false);
1978         queueDownB->setEnabled(false);
1979     }
1980 
1981     if (index < queueTable->rowCount())
1982         queueTable->selectRow(index);
1983     else if (queueTable->rowCount() > 0)
1984         queueTable->selectRow(queueTable->rowCount() - 1);
1985 
1986     if (queueTable->rowCount() == 0)
1987     {
1988         queueSaveAsB->setEnabled(false);
1989         queueSaveB->setEnabled(false);
1990         resetB->setEnabled(false);
1991     }
1992 
1993     state()->setDirty(true);
1994 
1995     return true;
1996 }
1997 
1998 void Capture::moveJob(bool up)
1999 {
2000     int currentRow = queueTable->currentRow();
2001     int destinationRow = up ? currentRow - 1 : currentRow + 1;
2002 
2003     int columnCount = queueTable->columnCount();
2004 
2005     if (currentRow < 0 || destinationRow < 0 || destinationRow >= queueTable->rowCount())
2006         return;
2007 
2008     for (int i = 0; i < columnCount; i++)
2009     {
2010         QTableWidgetItem * selectedLine = queueTable->takeItem(currentRow, i);
2011         QTableWidgetItem * counterpart  = queueTable->takeItem(destinationRow, i);
2012 
2013         queueTable->setItem(destinationRow, i, selectedLine);
2014         queueTable->setItem(currentRow, i, counterpart);
2015     }
2016 
2017     SequenceJob * job = state()->allJobs().takeAt(currentRow);
2018 
2019     state()->allJobs().removeOne(job);
2020     state()->allJobs().insert(destinationRow, job);
2021 
2022     QJsonArray seqArray = state()->getSequence();
2023     QJsonObject currentJob = seqArray[currentRow].toObject();
2024     seqArray.replace(currentRow, seqArray[destinationRow]);
2025     seqArray.replace(destinationRow, currentJob);
2026     emit sequenceChanged(seqArray);
2027 
2028     queueTable->selectRow(destinationRow);
2029 
2030     state()->setDirty(true);
2031 }
2032 
2033 void Capture::newTargetName(const QString &name)
2034 {
2035     targetNameT->setText(name);
2036     generatePreviewFilename();
2037 }
2038 
2039 void Capture::setBusy(bool enable)
2040 {
2041     previewB->setEnabled(!enable);
2042     loopB->setEnabled(!enable);
2043     opticalTrainCombo->setEnabled(!enable);
2044     trainB->setEnabled(!enable);
2045 
2046     foreach (QAbstractButton * button, queueEditButtonGroup->buttons())
2047         button->setEnabled(!enable);
2048 }
2049 
2050 void Capture::jobPrepared(SequenceJob * job)
2051 {
2052 
2053     int index = state()->allJobs().indexOf(job);
2054     if (index >= 0)
2055         queueTable->selectRow(index);
2056 
2057     if (activeJob()->jobType() != SequenceJob::JOBTYPE_PREVIEW)
2058     {
2059         // set the progress info
2060         imgProgress->setEnabled(true);
2061         imgProgress->setMaximum(activeJob()->getCoreProperty(SequenceJob::SJ_Count).toInt());
2062         imgProgress->setValue(activeJob()->getCompleted());
2063     }
2064 }
2065 
2066 void Capture::jobExecutionPreparationStarted()
2067 {
2068     if (activeJob() == nullptr)
2069     {
2070         // this should never happen
2071         qWarning(KSTARS_EKOS_CAPTURE) << "jobExecutionPreparationStarted with null state()->getActiveJob().";
2072         return;
2073     }
2074     if (activeJob()->jobType() == SequenceJob::JOBTYPE_PREVIEW)
2075         updateStartButtons(true, false);
2076 }
2077 
2078 void Capture::updatePrepareState(CaptureState prepareState)
2079 {
2080     state()->setCaptureState(prepareState);
2081 
2082     if (activeJob() == nullptr)
2083     {
2084         qWarning(KSTARS_EKOS_CAPTURE) << "updatePrepareState with null activeJob().";
2085         // Everything below depends on activeJob(). Just return.
2086         return;
2087     }
2088 
2089     switch (prepareState)
2090     {
2091         case CAPTURE_SETTING_TEMPERATURE:
2092             appendLogText(i18n("Setting temperature to %1 °C...", activeJob()->getTargetTemperature()));
2093             captureStatusWidget->setStatus(i18n("Set Temp to %1 °C...", activeJob()->getTargetTemperature()),
2094                                            Qt::yellow);
2095             break;
2096         case CAPTURE_GUIDER_DRIFT:
2097             appendLogText(i18n("Waiting for guide drift below %1\"...", Options::startGuideDeviation()));
2098             captureStatusWidget->setStatus(i18n("Wait for Guider < %1\"...", Options::startGuideDeviation()), Qt::yellow);
2099             break;
2100 
2101         case CAPTURE_SETTING_ROTATOR:
2102             appendLogText(i18n("Setting camera to %1 degrees E of N...", activeJob()->getTargetRotation()));
2103             captureStatusWidget->setStatus(i18n("Set Camera to %1 deg...", activeJob()->getTargetRotation()),
2104                                            Qt::yellow);
2105             break;
2106 
2107         default:
2108             break;
2109 
2110     }
2111 }
2112 
2113 void Capture::setFocusTemperatureDelta(double focusTemperatureDelta, double absTemperture)
2114 {
2115     Q_UNUSED(absTemperture);
2116     // This produces too much log spam
2117     // Maybe add a threshold to report later?
2118     //qCDebug(KSTARS_EKOS_CAPTURE) << "setFocusTemperatureDelta: " << focusTemperatureDelta;
2119     state()->getRefocusState()->setFocusTemperatureDelta(focusTemperatureDelta);
2120 }
2121 
2122 void Capture::setGuideDeviation(double delta_ra, double delta_dec)
2123 {
2124     const double deviation_rms = std::hypot(delta_ra, delta_dec);
2125 
2126     // forward it to the state machine
2127     state()->setGuideDeviation(deviation_rms);
2128 
2129 }
2130 
2131 void Capture::setFocusStatus(FocusState newstate)
2132 {
2133     // directly forward it to the state machine
2134     state()->updateFocusState(newstate);
2135 }
2136 
2137 void Capture::updateFocusStatus(FocusState newstate)
2138 {
2139     if ((state()->getRefocusState()->isRefocusing()
2140             || state()->getRefocusState()->isInSequenceFocus()) && activeJob()
2141             && activeJob()->getStatus() == JOB_BUSY)
2142     {
2143         switch (newstate)
2144         {
2145             case FOCUS_COMPLETE:
2146                 appendLogText(i18n("Focus complete."));
2147                 captureStatusWidget->setStatus(i18n("Focus complete."), Qt::yellow);
2148                 break;
2149             case FOCUS_FAILED:
2150             case FOCUS_ABORTED:
2151                 captureStatusWidget->setStatus(i18n("Autofocus failed."), Qt::darkRed);
2152                 break;
2153             default:
2154                 // otherwise do nothing
2155                 break;
2156         }
2157     }
2158 }
2159 
2160 
2161 
2162 void Capture::updateMeridianFlipStage(MeridianFlipState::MFStage stage)
2163 {
2164     // update UI
2165     if (getMeridianFlipState()->getMeridianFlipStage() != stage)
2166     {
2167         switch (stage)
2168         {
2169             case MeridianFlipState::MF_READY:
2170                 if (state()->getCaptureState() == CAPTURE_PAUSED)
2171                 {
2172                     // paused after meridian flip requested
2173                     captureStatusWidget->setStatus(i18n("Paused..."), Qt::yellow);
2174                 }
2175                 break;
2176 
2177             case MeridianFlipState::MF_INITIATED:
2178                 captureStatusWidget->setStatus(i18n("Meridian Flip..."), Qt::yellow);
2179                 KSNotification::event(QLatin1String("MeridianFlipStarted"), i18n("Meridian flip started"), KSNotification::Capture);
2180                 break;
2181 
2182             case MeridianFlipState::MF_COMPLETED:
2183                 captureStatusWidget->setStatus(i18n("Flip complete."), Qt::yellow);
2184                 break;
2185 
2186             default:
2187                 break;
2188         }
2189     }
2190 }
2191 
2192 void Capture::setRotatorReversed(bool toggled)
2193 {
2194     m_RotatorControlPanel->reverseDirection->setEnabled(true);
2195 
2196     m_RotatorControlPanel->reverseDirection->blockSignals(true);
2197     m_RotatorControlPanel->reverseDirection->setChecked(toggled);
2198     m_RotatorControlPanel->reverseDirection->blockSignals(false);
2199 }
2200 
2201 void Capture::saveFITSDirectory()
2202 {
2203     QString dir =
2204         QFileDialog::getExistingDirectory(Manager::Instance(), i18nc("@title:window", "FITS Save Directory"),
2205                                           dirPath.toLocalFile());
2206     if (dir.isEmpty())
2207         return;
2208 
2209     fileDirectoryT->setText(QDir::toNativeSeparators(dir));
2210 }
2211 
2212 void Capture::loadSequenceQueue()
2213 {
2214     QUrl fileURL = QFileDialog::getOpenFileUrl(Manager::Instance(), i18nc("@title:window", "Open Ekos Sequence Queue"),
2215                    dirPath,
2216                    "Ekos Sequence Queue (*.esq)");
2217     if (fileURL.isEmpty())
2218         return;
2219 
2220     if (fileURL.isValid() == false)
2221     {
2222         QString message = i18n("Invalid URL: %1", fileURL.toLocalFile());
2223         KSNotification::sorry(message, i18n("Invalid URL"));
2224         return;
2225     }
2226 
2227     dirPath = QUrl(fileURL.url(QUrl::RemoveFilename));
2228 
2229     loadSequenceQueue(fileURL.toLocalFile());
2230 }
2231 
2232 bool Capture::loadSequenceQueue(const QString &fileURL, QString targetName)
2233 {
2234     QFile sFile(fileURL);
2235     if (!sFile.open(QIODevice::ReadOnly))
2236     {
2237         QString message = i18n("Unable to open file %1", fileURL);
2238         KSNotification::sorry(message, i18n("Could Not Open File"));
2239         return false;
2240     }
2241 
2242     state()->clearCapturedFramesMap();
2243     clearSequenceQueue();
2244 
2245     // !m_standAlone so the stand-alone editor doesn't influence a live capture sesion.
2246     const bool result = process()->loadSequenceQueue(fileURL, targetName, !m_standAlone);
2247     // cancel if loading fails
2248     if (result == false)
2249         return result;
2250 
2251     // update general settings
2252     setObserverName(state()->observerName());
2253     syncGUIToGeneralSettings();
2254 
2255     // select the first one of the loaded jobs
2256     if (state()->allJobs().size() > 0)
2257         syncGUIToJob(state()->allJobs().first());
2258 
2259     // update save button tool tip
2260     queueSaveB->setToolTip("Save to " + sFile.fileName());
2261 
2262     return true;
2263 }
2264 
2265 void Capture::saveSequenceQueue()
2266 {
2267     QUrl backupCurrent = state()->sequenceURL();
2268 
2269     if (state()->sequenceURL().toLocalFile().startsWith(QLatin1String("/tmp/"))
2270             || state()->sequenceURL().toLocalFile().contains("/Temp"))
2271         state()->setSequenceURL(QUrl(""));
2272 
2273     // If no changes made, return.
2274     if (state()->dirty() == false && !state()->sequenceURL().isEmpty())
2275         return;
2276 
2277     if (state()->sequenceURL().isEmpty())
2278     {
2279         state()->setSequenceURL(QFileDialog::getSaveFileUrl(Manager::Instance(), i18nc("@title:window",
2280                                 "Save Ekos Sequence Queue"),
2281                                 dirPath,
2282                                 "Ekos Sequence Queue (*.esq)"));
2283         // if user presses cancel
2284         if (state()->sequenceURL().isEmpty())
2285         {
2286             state()->setSequenceURL(backupCurrent);
2287             return;
2288         }
2289 
2290         dirPath = QUrl(state()->sequenceURL().url(QUrl::RemoveFilename));
2291 
2292         if (state()->sequenceURL().toLocalFile().endsWith(QLatin1String(".esq")) == false)
2293             state()->setSequenceURL(QUrl("file:" + state()->sequenceURL().toLocalFile() + ".esq"));
2294 
2295     }
2296 
2297     if (state()->sequenceURL().isValid())
2298     {
2299         // !m_standAlone so the stand-alone editor doesn't influence a live capture sesion.
2300         if ((process()->saveSequenceQueue(state()->sequenceURL().toLocalFile(), !m_standAlone)) == false)
2301         {
2302             KSNotification::error(i18n("Failed to save sequence queue"), i18n("Save"));
2303             return;
2304         }
2305 
2306         state()->setDirty(false);
2307     }
2308     else
2309     {
2310         QString message = i18n("Invalid URL: %1", state()->sequenceURL().url());
2311         KSNotification::sorry(message, i18n("Invalid URL"));
2312     }
2313 }
2314 
2315 void Capture::saveSequenceQueueAs()
2316 {
2317     state()->setSequenceURL(QUrl(""));
2318     saveSequenceQueue();
2319 }
2320 
2321 bool Capture::saveSequenceQueue(const QString &path)
2322 {
2323     // forward it to the process engine
2324     return process()->saveSequenceQueue(path);
2325 }
2326 
2327 void Capture::resetJobs()
2328 {
2329     // Stop any running capture
2330     stop();
2331 
2332     // If a job is selected for edit, reset only that job
2333     if (m_JobUnderEdit == true)
2334     {
2335         SequenceJob * job = state()->allJobs().at(queueTable->currentRow());
2336         if (nullptr != job)
2337         {
2338             job->resetStatus();
2339             updateJobTable(job);
2340         }
2341     }
2342     else
2343     {
2344         if (KMessageBox::warningContinueCancel(
2345                     nullptr, i18n("Are you sure you want to reset status of all jobs?"), i18n("Reset job status"),
2346                     KStandardGuiItem::cont(), KStandardGuiItem::cancel(), "reset_job_status_warning") != KMessageBox::Continue)
2347         {
2348             return;
2349         }
2350 
2351         foreach (SequenceJob * job, state()->allJobs())
2352         {
2353             job->resetStatus();
2354             updateJobTable(job);
2355         }
2356     }
2357 
2358     // Also reset the storage count for all jobs
2359     state()->clearCapturedFramesMap();
2360 
2361     // We're not controlled by the Scheduler, restore progress option
2362     state()->setIgnoreJobProgress(Options::alwaysResetSequenceWhenStarting());
2363 
2364     // enable start button
2365     startB->setEnabled(true);
2366 }
2367 
2368 void Capture::ignoreSequenceHistory()
2369 {
2370     // This function is called independently from the Scheduler or the UI, so honor the change
2371     state()->setIgnoreJobProgress(true);
2372 }
2373 
2374 void Capture::syncGUIToJob(SequenceJob * job)
2375 {
2376     if (job == nullptr)
2377     {
2378         qWarning(KSTARS_EKOS_CAPTURE) << "syncGuiToJob with null job.";
2379         // Everything below depends on job. Just return.
2380         return;
2381     }
2382 
2383     const auto roi = job->getCoreProperty(SequenceJob::SJ_ROI).toRect();
2384 
2385     captureFormatS->setCurrentText(job->getCoreProperty(SequenceJob::SJ_Format).toString());
2386     captureEncodingS->setCurrentText(job->getCoreProperty(SequenceJob::SJ_Encoding).toString());
2387     captureExposureN->setValue(job->getCoreProperty(SequenceJob::SJ_Exposure).toDouble());
2388     captureBinHN->setValue(job->getCoreProperty(SequenceJob::SJ_Binning).toPoint().x());
2389     captureBinVN->setValue(job->getCoreProperty(SequenceJob::SJ_Binning).toPoint().y());
2390     captureFrameXN->setValue(roi.x());
2391     captureFrameYN->setValue(roi.y());
2392     captureFrameWN->setValue(roi.width());
2393     captureFrameHN->setValue(roi.height());
2394     FilterPosCombo->setCurrentIndex(job->getTargetFilter() - 1);
2395     captureTypeS->setCurrentIndex(job->getFrameType());
2396     captureCountN->setValue(job->getCoreProperty(SequenceJob::SJ_Count).toInt());
2397     captureDelayN->setValue(job->getCoreProperty(SequenceJob::SJ_Delay).toInt() / 1000);
2398     targetNameT->setText(job->getCoreProperty(SequenceJob::SJ_TargetName).toString());
2399     fileDirectoryT->setText(job->getCoreProperty(SequenceJob::SJ_LocalDirectory).toString());
2400     fileUploadModeS->setCurrentIndex(job->getUploadMode());
2401     fileRemoteDirT->setEnabled(fileUploadModeS->currentIndex() != 0);
2402     fileRemoteDirT->setText(job->getCoreProperty(SequenceJob::SJ_RemoteDirectory).toString());
2403     placeholderFormatT->setText(job->getCoreProperty(SequenceJob::SJ_PlaceholderFormat).toString());
2404     formatSuffixN->setValue(job->getCoreProperty(SequenceJob::SJ_PlaceholderSuffix).toUInt());
2405     m_LimitsUI->limitDitherFrequencyN->setValue(job->getCoreProperty(SequenceJob::SJ_DitherPerJobFrequency).toInt());
2406 
2407     // Temperature Options
2408     cameraTemperatureS->setChecked(job->getCoreProperty(SequenceJob::SJ_EnforceTemperature).toBool());
2409     if (job->getCoreProperty(SequenceJob::SJ_EnforceTemperature).toBool())
2410         cameraTemperatureN->setValue(job->getTargetTemperature());
2411 
2412     // Start guider drift options
2413     m_LimitsUI->startGuiderDriftS->setChecked(Options::enforceStartGuiderDrift());
2414     if (Options::enforceStartGuiderDrift())
2415         m_LimitsUI->startGuiderDriftN->setValue(Options::startGuideDeviation());
2416 
2417     // Flat field options
2418     calibrationB->setEnabled(job->getFrameType() != FRAME_LIGHT);
2419     generateDarkFlatsB->setEnabled(job->getFrameType() != FRAME_LIGHT);
2420     state()->setFlatFieldDuration(job->getFlatFieldDuration());
2421     state()->setCalibrationPreAction(job->getCalibrationPreAction());
2422     state()->setTargetADU(job->getCoreProperty(SequenceJob::SJ_TargetADU).toDouble());
2423     state()->setTargetADUTolerance(job->getCoreProperty(SequenceJob::SJ_TargetADUTolerance).toDouble());
2424     state()->setWallCoord(job->getWallCoord());
2425     m_scriptsManager->setScripts(job->getScripts());
2426 
2427     // Custom Properties
2428     customPropertiesDialog->setCustomProperties(job->getCustomProperties());
2429 
2430     if (captureISOS)
2431         captureISOS->setCurrentIndex(job->getCoreProperty(SequenceJob::SJ_ISOIndex).toInt());
2432 
2433     double gain = getGain();
2434     if (gain >= 0)
2435         captureGainN->setValue(gain);
2436     else
2437         captureGainN->setValue(GainSpinSpecialValue);
2438 
2439     double offset = getOffset();
2440     if (offset >= 0)
2441         captureOffsetN->setValue(offset);
2442     else
2443         captureOffsetN->setValue(OffsetSpinSpecialValue);
2444 
2445     // update place holder typ
2446     generatePreviewFilename();
2447 
2448     if (m_RotatorControlPanel) // only if rotator is registered
2449     {
2450         if (job->getTargetRotation() != Ekos::INVALID_VALUE)
2451         {
2452             // remove enforceJobPA m_RotatorControlPanel->setRotationEnforced(true);
2453             m_RotatorControlPanel->setCameraPA(job->getTargetRotation());
2454         }
2455         // remove enforceJobPA
2456         // else
2457         //    m_RotatorControlPanel->setRotationEnforced(false);
2458     }
2459 
2460     // hide target drift if align check frequency is == 0
2461     if (Options::alignCheckFrequency() == 0)
2462     {
2463         targetDriftLabel->setVisible(false);
2464         targetDrift->setVisible(false);
2465         targetDriftUnit->setVisible(false);
2466     }
2467 
2468     emit settingsUpdated(getPresetSettings());
2469 }
2470 
2471 void Capture::syncGUIToGeneralSettings()
2472 {
2473     m_LimitsUI->startGuiderDriftS->setChecked(Options::enforceStartGuiderDrift());
2474     m_LimitsUI->startGuiderDriftN->setValue(Options::startGuideDeviation());
2475     m_LimitsUI->limitGuideDeviationS->setChecked(Options::enforceGuideDeviation());
2476     m_LimitsUI->limitGuideDeviationN->setValue(Options::guideDeviation());
2477     m_LimitsUI->limitGuideDeviationRepsN->setValue(static_cast<int>(Options::guideDeviationReps()));
2478     m_LimitsUI->limitFocusHFRS->setChecked(Options::enforceAutofocusHFR());
2479     m_LimitsUI->limitFocusHFRThresholdPercentage->setValue(Options::hFRThresholdPercentage());
2480     m_LimitsUI->limitFocusHFRN->setValue(Options::hFRDeviation());
2481     m_LimitsUI->limitFocusHFRCheckFrames->setValue(Options::inSequenceCheckFrames());
2482     m_LimitsUI->limitFocusHFRAlgorithm->setCurrentIndex(Options::hFRCheckAlgorithm());
2483     m_LimitsUI->limitFocusDeltaTS->setChecked(Options::enforceAutofocusOnTemperature());
2484     m_LimitsUI->limitFocusDeltaTN->setValue(Options::maxFocusTemperatureDelta());
2485     m_LimitsUI->limitRefocusS->setChecked(Options::enforceRefocusEveryN());
2486     m_LimitsUI->limitRefocusN->setValue(static_cast<int>(Options::refocusEveryN()));
2487     m_LimitsUI->meridianRefocusS->setChecked(Options::refocusAfterMeridianFlip());
2488 }
2489 
2490 QJsonObject Capture::getPresetSettings()
2491 {
2492     QJsonObject settings;
2493 
2494     // Try to get settings value
2495     // if not found, fallback to camera value
2496     double gain = -1;
2497     if (GainSpinSpecialValue > INVALID_VALUE && captureGainN->value() > GainSpinSpecialValue)
2498         gain = captureGainN->value();
2499     else if (devices()->getActiveCamera() && devices()->getActiveCamera()->hasGain())
2500         devices()->getActiveCamera()->getGain(&gain);
2501 
2502     double offset = -1;
2503     if (OffsetSpinSpecialValue > INVALID_VALUE && captureOffsetN->value() > OffsetSpinSpecialValue)
2504         offset = captureOffsetN->value();
2505     else if (devices()->getActiveCamera() && devices()->getActiveCamera()->hasOffset())
2506         devices()->getActiveCamera()->getOffset(&offset);
2507 
2508     int iso = -1;
2509     if (captureISOS)
2510         iso = captureISOS->currentIndex();
2511     else if (devices()->getActiveCamera())
2512         iso = devices()->getActiveCamera()->getChip(ISD::CameraChip::PRIMARY_CCD)->getISOIndex();
2513 
2514     settings.insert("optical_train", opticalTrainCombo->currentText());
2515     settings.insert("filter", FilterPosCombo->currentText());
2516     settings.insert("dark", darkB->isChecked());
2517     settings.insert("exp", captureExposureN->value());
2518     settings.insert("bin", captureBinHN->value());
2519     settings.insert("iso", iso);
2520     settings.insert("frameType", captureTypeS->currentIndex());
2521     settings.insert("captureFormat", captureFormatS->currentIndex());
2522     settings.insert("transferFormat", captureEncodingS->currentIndex());
2523     settings.insert("gain", gain);
2524     settings.insert("offset", offset);
2525     settings.insert("temperature", cameraTemperatureN->value());
2526     settings.insert("ditherPerJobFrequency", m_LimitsUI->limitDitherFrequencyN->value());
2527 
2528     return settings;
2529 }
2530 
2531 void Capture::selectedJobChanged(QModelIndex current, QModelIndex previous)
2532 {
2533     Q_UNUSED(previous)
2534     selectJob(current);
2535 }
2536 
2537 bool Capture::selectJob(QModelIndex i)
2538 {
2539     if (i.row() < 0 || (i.row() + 1) > state()->allJobs().size())
2540         return false;
2541 
2542     SequenceJob * job = state()->allJobs().at(i.row());
2543 
2544     if (job == nullptr || job->jobType() == SequenceJob::JOBTYPE_DARKFLAT)
2545         return false;
2546 
2547     syncGUIToJob(job);
2548 
2549     if (state()->isBusy())
2550         return false;
2551 
2552     if (state()->allJobs().size() >= 2)
2553     {
2554         queueUpB->setEnabled(i.row() > 0);
2555         queueDownB->setEnabled(i.row() + 1 < state()->allJobs().size());
2556     }
2557 
2558     return true;
2559 }
2560 
2561 void Capture::editJob(QModelIndex i)
2562 {
2563     // Try to select a job. If job not found or not editable return.
2564     if (selectJob(i) == false)
2565         return;
2566 
2567     appendLogText(i18n("Editing job #%1...", i.row() + 1));
2568 
2569     addToQueueB->setIcon(QIcon::fromTheme("dialog-ok-apply"));
2570     addToQueueB->setToolTip(i18n("Apply job changes."));
2571     removeFromQueueB->setToolTip(i18n("Cancel job changes."));
2572 
2573     // Make it sure if user presses enter, the job is validated.
2574     previewB->setDefault(false);
2575     addToQueueB->setDefault(true);
2576 
2577     m_JobUnderEdit = true;
2578 }
2579 
2580 void Capture::resetJobEdit(bool cancelled)
2581 {
2582     if (cancelled == true)
2583         appendLogText(i18n("Editing job canceled."));
2584 
2585     m_JobUnderEdit = false;
2586     addToQueueB->setIcon(QIcon::fromTheme("list-add"));
2587 
2588     addToQueueB->setToolTip(i18n("Add job to sequence queue"));
2589     removeFromQueueB->setToolTip(i18n("Remove job from sequence queue"));
2590 
2591     addToQueueB->setDefault(false);
2592     previewB->setDefault(true);
2593 }
2594 
2595 void Capture::setMaximumGuidingDeviation(bool enable, double value)
2596 {
2597     m_LimitsUI->limitGuideDeviationS->setChecked(enable);
2598     if (enable)
2599         m_LimitsUI->limitGuideDeviationN->setValue(value);
2600 }
2601 
2602 void Capture::setInSequenceFocus(bool enable, double HFR)
2603 {
2604     m_LimitsUI->limitFocusHFRS->setChecked(enable);
2605     if (enable)
2606         m_LimitsUI->limitFocusHFRN->setValue(HFR);
2607 }
2608 
2609 void Capture::clearSequenceQueue()
2610 {
2611     state()->setActiveJob(nullptr);
2612     while (queueTable->rowCount() > 0)
2613         queueTable->removeRow(0);
2614     qDeleteAll(state()->allJobs());
2615     state()->allJobs().clear();
2616 
2617     while (state()->getSequence().count())
2618         state()->getSequence().pop_back();
2619     emit sequenceChanged(state()->getSequence());
2620 }
2621 
2622 void Capture::setAlignStatus(AlignState newstate)
2623 {
2624     // forward it directly to the state machine
2625     state()->setAlignState(newstate);
2626 }
2627 
2628 void Capture::setGuideStatus(GuideState newstate)
2629 {
2630     // forward it directly to the state machine
2631     state()->setGuideState(newstate);
2632 }
2633 
2634 void Capture::checkFrameType(int index)
2635 {
2636     calibrationB->setEnabled(index != FRAME_LIGHT);
2637     generateDarkFlatsB->setEnabled(index != FRAME_LIGHT);
2638 }
2639 
2640 void Capture::clearAutoFocusHFR()
2641 {
2642     if (Options::hFRCheckAlgorithm() == HFR_CHECK_FIXED)
2643         return;
2644 
2645     m_LimitsUI->limitFocusHFRN->setValue(0);
2646     //firstAutoFocus = true;
2647 }
2648 
2649 void Capture::openCalibrationDialog()
2650 {
2651     QDialog calibrationDialog(this);
2652 
2653     Ui_calibrationOptions calibrationOptions;
2654     calibrationOptions.setupUi(&calibrationDialog);
2655 
2656     calibrationOptions.parkMountC->setEnabled(devices()->mount() && devices()->mount()->canPark());
2657     calibrationOptions.parkDomeC->setEnabled(devices()->dome() && devices()->dome()->canPark());
2658 
2659     calibrationOptions.parkMountC->setChecked(false);
2660     calibrationOptions.parkDomeC->setChecked(false);
2661     calibrationOptions.gotoWallC->setChecked(false);
2662 
2663     calibrationOptions.parkMountC->setChecked(state()->calibrationPreAction() & ACTION_PARK_MOUNT);
2664     calibrationOptions.parkDomeC->setChecked(state()->calibrationPreAction() & ACTION_PARK_DOME);
2665     if (state()->calibrationPreAction() & ACTION_WALL)
2666     {
2667         calibrationOptions.gotoWallC->setChecked(true);
2668         calibrationOptions.azBox->setText(state()->wallCoord().az().toDMSString());
2669         calibrationOptions.altBox->setText(state()->wallCoord().alt().toDMSString());
2670     }
2671 
2672     switch (state()->flatFieldDuration())
2673     {
2674         case DURATION_MANUAL:
2675             calibrationOptions.manualDurationC->setChecked(true);
2676             break;
2677 
2678         case DURATION_ADU:
2679             calibrationOptions.ADUC->setChecked(true);
2680             calibrationOptions.ADUValue->setValue(static_cast<int>(std::round(state()->targetADU())));
2681             calibrationOptions.ADUTolerance->setValue(static_cast<int>(std::round(state()->targetADUTolerance())));
2682             break;
2683     }
2684 
2685     // avoid combination of ACTION_WALL and ACTION_PARK_MOUNT
2686     connect(calibrationOptions.gotoWallC, &QCheckBox::clicked, [&](bool checked)
2687     {
2688         if (checked)
2689             calibrationOptions.parkMountC->setChecked(false);
2690     });
2691     connect(calibrationOptions.parkMountC, &QCheckBox::clicked, [&](bool checked)
2692     {
2693         if (checked)
2694             calibrationOptions.gotoWallC->setChecked(false);
2695     });
2696 
2697     if (calibrationDialog.exec() == QDialog::Accepted)
2698     {
2699         state()->setCalibrationPreAction(ACTION_NONE);
2700         if (calibrationOptions.parkMountC->isChecked())
2701             state()->setCalibrationPreAction(state()->calibrationPreAction() | ACTION_PARK_MOUNT);
2702         if (calibrationOptions.parkDomeC->isChecked())
2703             state()->setCalibrationPreAction(state()->calibrationPreAction() | ACTION_PARK_DOME);
2704         if (calibrationOptions.gotoWallC->isChecked())
2705         {
2706             dms wallAz, wallAlt;
2707             bool azOk = false, altOk = false;
2708 
2709             wallAz  = calibrationOptions.azBox->createDms(&azOk);
2710             wallAlt = calibrationOptions.altBox->createDms(&altOk);
2711 
2712             if (azOk && altOk)
2713             {
2714                 state()->setCalibrationPreAction((state()->calibrationPreAction() & ~ACTION_PARK_MOUNT) | ACTION_WALL);
2715                 state()->wallCoord().setAz(wallAz);
2716                 state()->wallCoord().setAlt(wallAlt);
2717             }
2718             else
2719             {
2720                 calibrationOptions.gotoWallC->setChecked(false);
2721                 KSNotification::error(i18n("Wall coordinates are invalid."));
2722             }
2723         }
2724 
2725         if (calibrationOptions.manualDurationC->isChecked())
2726             state()->setFlatFieldDuration(DURATION_MANUAL);
2727         else
2728         {
2729             state()->setFlatFieldDuration(DURATION_ADU);
2730             state()->setTargetADU(calibrationOptions.ADUValue->value());
2731             state()->setTargetADUTolerance(calibrationOptions.ADUTolerance->value());
2732         }
2733 
2734         state()->setDirty(true);
2735 
2736         if (!m_standAlone)
2737         {
2738             Options::setCalibrationPreActionIndex(state()->calibrationPreAction());
2739             Options::setCalibrationFlatDurationIndex(state()->flatFieldDuration());
2740             Options::setCalibrationWallAz(state()->wallCoord().az().Degrees());
2741             Options::setCalibrationWallAlt(state()->wallCoord().alt().Degrees());
2742             Options::setCalibrationADUValue(static_cast<uint>(std::round(state()->targetADU())));
2743             Options::setCalibrationADUValueTolerance(static_cast<uint>(std::round(state()->targetADUTolerance())));
2744         }
2745     }
2746 }
2747 
2748 bool Capture::setVideoLimits(uint16_t maxBufferSize, uint16_t maxPreviewFPS)
2749 {
2750     if (devices()->getActiveCamera() == nullptr)
2751         return false;
2752 
2753     return devices()->getActiveCamera()->setStreamLimits(maxBufferSize, maxPreviewFPS);
2754 }
2755 
2756 void Capture::setVideoStreamEnabled(bool enabled)
2757 {
2758     if (enabled)
2759     {
2760         liveVideoB->setChecked(true);
2761         liveVideoB->setIcon(QIcon::fromTheme("camera-on"));
2762     }
2763     else
2764     {
2765         liveVideoB->setChecked(false);
2766         liveVideoB->setIcon(QIcon::fromTheme("camera-ready"));
2767     }
2768 }
2769 
2770 void Capture::setMountStatus(ISD::Mount::Status newState)
2771 {
2772     switch (newState)
2773     {
2774         case ISD::Mount::MOUNT_PARKING:
2775         case ISD::Mount::MOUNT_SLEWING:
2776         case ISD::Mount::MOUNT_MOVING:
2777             previewB->setEnabled(false);
2778             liveVideoB->setEnabled(false);
2779             // Only disable when button is "Start", and not "Stopped"
2780             // If mount is in motion, Stopped button should always be enabled to terminate
2781             // the sequence
2782             if (state()->isBusy() == false)
2783                 startB->setEnabled(false);
2784             break;
2785 
2786         default:
2787             if (state()->isBusy() == false)
2788             {
2789                 previewB->setEnabled(true);
2790                 if (devices()->getActiveCamera())
2791                     liveVideoB->setEnabled(devices()->getActiveCamera()->hasVideoStream());
2792                 startB->setEnabled(true);
2793             }
2794 
2795             break;
2796     }
2797 }
2798 
2799 void Capture::showObserverDialog()
2800 {
2801     QList<OAL::Observer *> m_observerList;
2802     KStars::Instance()->data()->userdb()->GetAllObservers(m_observerList);
2803     QStringList observers;
2804     for (auto &o : m_observerList)
2805         observers << QString("%1 %2").arg(o->name(), o->surname());
2806 
2807     QDialog observersDialog(this);
2808     observersDialog.setWindowTitle(i18nc("@title:window", "Select Current Observer"));
2809 
2810     QLabel label(i18n("Current Observer:"));
2811 
2812     QComboBox observerCombo(&observersDialog);
2813     observerCombo.addItems(observers);
2814     observerCombo.setCurrentText(getObserverName());
2815     observerCombo.setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Fixed);
2816 
2817     QPushButton manageObserver(&observersDialog);
2818     manageObserver.setFixedSize(QSize(32, 32));
2819     manageObserver.setIcon(QIcon::fromTheme("document-edit"));
2820     manageObserver.setAttribute(Qt::WA_LayoutUsesWidgetRect);
2821     manageObserver.setToolTip(i18n("Manage Observers"));
2822     connect(&manageObserver, &QPushButton::clicked, this, [&]()
2823     {
2824         ObserverAdd add;
2825         add.exec();
2826 
2827         QList<OAL::Observer *> m_observerList;
2828         KStars::Instance()->data()->userdb()->GetAllObservers(m_observerList);
2829         QStringList observers;
2830         for (auto &o : m_observerList)
2831             observers << QString("%1 %2").arg(o->name(), o->surname());
2832 
2833         observerCombo.clear();
2834         observerCombo.addItems(observers);
2835         observerCombo.setCurrentText(getObserverName());
2836 
2837     });
2838 
2839     QHBoxLayout * layout = new QHBoxLayout;
2840     layout->addWidget(&label);
2841     layout->addWidget(&observerCombo);
2842     layout->addWidget(&manageObserver);
2843 
2844     observersDialog.setLayout(layout);
2845 
2846     observersDialog.exec();
2847     setObserverName(observerCombo.currentText());
2848 }
2849 
2850 void Capture::setAlignResults(double solverPA, double ra, double de, double pixscale)
2851 {
2852     Q_UNUSED(ra)
2853     Q_UNUSED(de)
2854     Q_UNUSED(pixscale)
2855     if (devices()->rotator() && m_RotatorControlPanel)
2856         m_RotatorControlPanel->refresh(solverPA);
2857 }
2858 
2859 void Capture::setFilterStatus(FilterState filterState)
2860 {
2861     if (filterState != state()->getFilterManagerState())
2862         qCDebug(KSTARS_EKOS_CAPTURE) << "Filter state changed from" << Ekos::getFilterStatusString(
2863                                          state()->getFilterManagerState()) << "to" << Ekos::getFilterStatusString(filterState);
2864     if (state()->getCaptureState() == CAPTURE_CHANGING_FILTER)
2865     {
2866         switch (filterState)
2867         {
2868             case FILTER_OFFSET:
2869                 appendLogText(i18n("Changing focus offset by %1 steps...",
2870                                    m_FilterManager->getTargetFilterOffset()));
2871                 break;
2872 
2873             case FILTER_CHANGE:
2874                 appendLogText(i18n("Changing filter to %1...",
2875                                    FilterPosCombo->itemText(m_FilterManager->getTargetFilterPosition() - 1)));
2876                 break;
2877 
2878             case FILTER_AUTOFOCUS:
2879                 appendLogText(i18n("Auto focus on filter change..."));
2880                 clearAutoFocusHFR();
2881                 break;
2882 
2883             case FILTER_IDLE:
2884                 if (state()->getFilterManagerState() == FILTER_CHANGE)
2885                 {
2886                     appendLogText(i18n("Filter set to %1.",
2887                                        FilterPosCombo->itemText(m_FilterManager->getTargetFilterPosition() - 1)));
2888                 }
2889                 break;
2890 
2891             default:
2892                 break;
2893         }
2894     }
2895     state()->setFilterManagerState(filterState);
2896 }
2897 
2898 void Capture::setupFilterManager()
2899 {
2900     // Do we have an existing filter manager?
2901     if (m_FilterManager)
2902         m_FilterManager->disconnect(this);
2903 
2904     // Create new or refresh device
2905     Manager::Instance()->createFilterManager(devices()->filterWheel());
2906 
2907     // Return global filter manager for this filter wheel.
2908     Manager::Instance()->getFilterManager(devices()->filterWheel()->getDeviceName(), m_FilterManager);
2909 
2910     devices()->setFilterManager(m_FilterManager);
2911 
2912     connect(m_FilterManager.get(), &FilterManager::updated, this, [this]()
2913     {
2914         emit filterManagerUpdated(devices()->filterWheel());
2915     });
2916 
2917     // display capture status changes
2918     connect(m_FilterManager.get(), &FilterManager::newStatus, this, &Capture::newFilterStatus);
2919 
2920     connect(filterManagerB, &QPushButton::clicked, this, [this]()
2921     {
2922         m_FilterManager->refreshFilterModel();
2923         m_FilterManager->show();
2924         m_FilterManager->raise();
2925     });
2926 
2927     connect(m_FilterManager.get(), &FilterManager::ready, this, &Capture::updateCurrentFilterPosition);
2928 
2929     connect(m_FilterManager.get(), &FilterManager::failed, this, [this]()
2930     {
2931         if (activeJob())
2932         {
2933             appendLogText(i18n("Filter operation failed."));
2934             abort();
2935         }
2936     });
2937 
2938     // filter changes
2939     connect(m_FilterManager.get(), &FilterManager::newStatus, this, &Capture::setFilterStatus);
2940 
2941     // display capture status changes
2942     connect(m_FilterManager.get(), &FilterManager::newStatus, captureStatusWidget, &LedStatusWidget::setFilterState);
2943 
2944     connect(m_FilterManager.get(), &FilterManager::labelsChanged, this, [this]()
2945     {
2946         FilterPosCombo->clear();
2947         FilterPosCombo->addItems(m_FilterManager->getFilterLabels());
2948         FilterPosCombo->setCurrentIndex(m_FilterManager->getFilterPosition() - 1);
2949         updateCurrentFilterPosition();
2950     });
2951 
2952     connect(m_FilterManager.get(), &FilterManager::positionChanged, this, [this]()
2953     {
2954         FilterPosCombo->setCurrentIndex(m_FilterManager->getFilterPosition() - 1);
2955         updateCurrentFilterPosition();
2956     });
2957 }
2958 
2959 void Capture::addDSLRInfo(const QString &model, uint32_t maxW, uint32_t maxH, double pixelW, double pixelH)
2960 {
2961     // Check if model already exists
2962     auto pos = std::find_if(state()->DSLRInfos().begin(), state()->DSLRInfos().end(), [model](const auto & oneDSLRInfo)
2963     {
2964         return (oneDSLRInfo["Model"] == model);
2965     });
2966 
2967     if (pos != state()->DSLRInfos().end())
2968     {
2969         KStarsData::Instance()->userdb()->DeleteDSLRInfo(model);
2970         state()->DSLRInfos().removeOne(*pos);
2971     }
2972 
2973     QMap<QString, QVariant> oneDSLRInfo;
2974     oneDSLRInfo["Model"] = model;
2975     oneDSLRInfo["Width"] = maxW;
2976     oneDSLRInfo["Height"] = maxH;
2977     oneDSLRInfo["PixelW"] = pixelW;
2978     oneDSLRInfo["PixelH"] = pixelH;
2979 
2980     KStarsData::Instance()->userdb()->AddDSLRInfo(oneDSLRInfo);
2981     KStarsData::Instance()->userdb()->GetAllDSLRInfos(state()->DSLRInfos());
2982 
2983     updateFrameProperties();
2984     process()->resetFrame();
2985     process()->syncDSLRToTargetChip(model);
2986 
2987     // In case the dialog was opened, let's close it
2988     if (dslrInfoDialog)
2989         dslrInfoDialog.reset();
2990 }
2991 
2992 void Capture::cullToDSLRLimits()
2993 {
2994     QString model(devices()->getActiveCamera()->getDeviceName());
2995 
2996     // Check if model already exists
2997     auto pos = std::find_if(state()->DSLRInfos().begin(),
2998                             state()->DSLRInfos().end(), [model](QMap<QString, QVariant> &oneDSLRInfo)
2999     {
3000         return (oneDSLRInfo["Model"] == model);
3001     });
3002 
3003     if (pos != state()->DSLRInfos().end())
3004     {
3005         if (captureFrameWN->maximum() == 0 || captureFrameWN->maximum() > (*pos)["Width"].toInt())
3006         {
3007             captureFrameWN->setValue((*pos)["Width"].toInt());
3008             captureFrameWN->setMaximum((*pos)["Width"].toInt());
3009         }
3010 
3011         if (captureFrameHN->maximum() == 0 || captureFrameHN->maximum() > (*pos)["Height"].toInt())
3012         {
3013             captureFrameHN->setValue((*pos)["Height"].toInt());
3014             captureFrameHN->setMaximum((*pos)["Height"].toInt());
3015         }
3016     }
3017 }
3018 
3019 void Capture::setPresetSettings(const QJsonObject &settings)
3020 {
3021     auto opticalTrain = settings["optical_train"].toString(opticalTrainCombo->currentText());
3022     auto targetFilter = settings["filter"].toString(FilterPosCombo->currentText());
3023 
3024     opticalTrainCombo->setCurrentText(opticalTrain);
3025     FilterPosCombo->setCurrentText(targetFilter);
3026 
3027     captureExposureN->setValue(settings["exp"].toDouble(1));
3028 
3029     int bin = settings["bin"].toInt(1);
3030     setBinning(bin, bin);
3031 
3032     if (settings["temperature"].isString() && settings["temperature"].toString() == "--")
3033         setForceTemperature(false);
3034     else
3035     {
3036         double temperature = settings["temperature"].toDouble(INVALID_VALUE);
3037         if (temperature > INVALID_VALUE && devices()->getActiveCamera()
3038                 && devices()->getActiveCamera()->hasCoolerControl())
3039         {
3040             setForceTemperature(true);
3041             setTargetTemperature(temperature);
3042         }
3043         else
3044             setForceTemperature(false);
3045     }
3046 
3047     if (settings["gain"].isString() && settings["gain"].toString() == "--")
3048         captureGainN->setValue(GainSpinSpecialValue);
3049     else
3050     {
3051         double gain = settings["gain"].toDouble(GainSpinSpecialValue);
3052         if (devices()->getActiveCamera() && devices()->getActiveCamera()->hasGain())
3053         {
3054             if (gain == GainSpinSpecialValue)
3055                 captureGainN->setValue(GainSpinSpecialValue);
3056             else
3057                 setGain(gain);
3058         }
3059     }
3060 
3061     if (settings["offset"].isString() && settings["offset"].toString() == "--")
3062         captureOffsetN->setValue(OffsetSpinSpecialValue);
3063     else
3064     {
3065         double offset = settings["offset"].toDouble(OffsetSpinSpecialValue);
3066         if (devices()->getActiveCamera() && devices()->getActiveCamera()->hasOffset())
3067         {
3068             if (offset == OffsetSpinSpecialValue)
3069                 captureOffsetN->setValue(OffsetSpinSpecialValue);
3070             else
3071                 setOffset(offset);
3072         }
3073     }
3074 
3075     int transferFormat = settings["transferFormat"].toInt(-1);
3076     if (transferFormat >= 0)
3077     {
3078         captureEncodingS->setCurrentIndex(transferFormat);
3079     }
3080 
3081     QString captureFormat = settings["captureFormat"].toString(captureFormatS->currentText());
3082     if (captureFormat != captureFormatS->currentText())
3083         captureFormatS->setCurrentText(captureFormat);
3084 
3085     captureTypeS->setCurrentIndex(qMax(0, settings["frameType"].toInt(0)));
3086 
3087     // ISO
3088     int isoIndex = settings["iso"].toInt(-1);
3089     if (isoIndex >= 0)
3090         setISO(isoIndex);
3091 
3092     bool dark = settings["dark"].toBool(darkB->isChecked());
3093     if (dark != darkB->isChecked())
3094         darkB->setChecked(dark);
3095 
3096     int ditherPerJobFrequency = settings["ditherPerJobFrequency"].toInt(0);
3097     m_LimitsUI->limitDitherFrequencyN->setValue(ditherPerJobFrequency);
3098 }
3099 
3100 void Capture::setFileSettings(const QJsonObject &settings)
3101 {
3102     const auto prefix = settings["prefix"].toString(targetNameT->text());
3103     const auto directory = settings["directory"].toString(fileDirectoryT->text());
3104     const auto upload = settings["upload"].toInt(fileUploadModeS->currentIndex());
3105     const auto remote = settings["remote"].toString(fileRemoteDirT->text());
3106     const auto format = settings["format"].toString(placeholderFormatT->text());
3107     const auto suffix = settings["suffix"].toInt(formatSuffixN->value());
3108 
3109     targetNameT->setText(prefix);
3110     fileDirectoryT->setText(directory);
3111     fileUploadModeS->setCurrentIndex(upload);
3112     fileRemoteDirT->setText(remote);
3113     placeholderFormatT->setText(format);
3114     formatSuffixN->setValue(suffix);
3115 }
3116 
3117 QJsonObject Capture::getFileSettings()
3118 {
3119     QJsonObject settings =
3120     {
3121         {"prefix", targetNameT->text()},
3122         {"directory", fileDirectoryT->text()},
3123         {"format", placeholderFormatT->text()},
3124         {"suffix", formatSuffixN->value()},
3125         {"upload", fileUploadModeS->currentIndex()},
3126         {"remote", fileRemoteDirT->text()}
3127     };
3128 
3129     return settings;
3130 }
3131 
3132 void Capture::setLimitSettings(const QJsonObject &settings)
3133 {
3134     const bool deviationCheck = settings["deviationCheck"].toBool(Options::enforceGuideDeviation());
3135     const double deviationValue = settings["deviationValue"].toDouble(Options::guideDeviation());
3136     const bool focusHFRCheck = settings["focusHFRCheck"].toBool(m_LimitsUI->limitFocusHFRS->isChecked());
3137     const double focusHFRThresholdPercentage = settings["hFRThresholdPercentage"].toDouble(
3138                 m_LimitsUI->limitFocusHFRThresholdPercentage->value());
3139     const double focusHFRValue = settings["focusHFRValue"].toDouble(m_LimitsUI->limitFocusHFRN->value());
3140     const int focusHFRCheckFrames = settings["inSequenceCheckFrames"].toInt(m_LimitsUI->limitFocusHFRCheckFrames->value());
3141     const int focusHFRAlgorithm = settings["hFRCheckAlgorithm"].toInt(m_LimitsUI->limitFocusHFRAlgorithm->currentIndex());
3142     const bool focusDeltaTCheck = settings["focusDeltaTCheck"].toBool(m_LimitsUI->limitFocusDeltaTS->isChecked());
3143     const double focusDeltaTValue = settings["focusDeltaTValue"].toDouble(m_LimitsUI->limitFocusDeltaTN->value());
3144     const bool refocusNCheck = settings["refocusNCheck"].toBool(m_LimitsUI->limitRefocusS->isChecked());
3145     const int refocusNValue = settings["refocusNValue"].toInt(m_LimitsUI->limitRefocusN->value());
3146     const int ditherPerJobFrequency = settings["ditherPerJobFrequency"].toInt(m_LimitsUI->limitDitherFrequencyN->value());
3147 
3148     if (deviationCheck)
3149     {
3150         m_LimitsUI->limitGuideDeviationS->setChecked(true);
3151         m_LimitsUI->limitGuideDeviationN->setValue(deviationValue);
3152     }
3153     else
3154         m_LimitsUI->limitGuideDeviationS->setChecked(false);
3155 
3156     if (focusHFRCheck)
3157     {
3158         m_LimitsUI->limitFocusHFRS->setChecked(true);
3159         m_LimitsUI->limitFocusHFRThresholdPercentage->setValue(focusHFRThresholdPercentage);
3160         m_LimitsUI->limitFocusHFRN->setValue(focusHFRValue);
3161         m_LimitsUI->limitFocusHFRCheckFrames->setValue(focusHFRCheckFrames);
3162         m_LimitsUI->limitFocusHFRAlgorithm->setCurrentIndex(focusHFRAlgorithm);
3163     }
3164     else
3165         m_LimitsUI->limitFocusHFRS->setChecked(false);
3166 
3167     if (focusDeltaTCheck)
3168     {
3169         m_LimitsUI->limitFocusDeltaTS->setChecked(true);
3170         m_LimitsUI->limitFocusDeltaTN->setValue(focusDeltaTValue);
3171     }
3172     else
3173         m_LimitsUI->limitFocusDeltaTS->setChecked(false);
3174 
3175     if (refocusNCheck)
3176     {
3177         m_LimitsUI->limitRefocusS->setChecked(true);
3178         m_LimitsUI->limitRefocusN->setValue(refocusNValue);
3179     }
3180     else
3181         m_LimitsUI->limitRefocusS->setChecked(false);
3182 
3183     m_LimitsUI->limitDitherFrequencyN->setValue(ditherPerJobFrequency);
3184 
3185     syncRefocusOptionsFromGUI();
3186 }
3187 
3188 QJsonObject Capture::getLimitSettings()
3189 {
3190     QJsonObject settings =
3191     {
3192         {"deviationCheck", Options::enforceGuideDeviation()},
3193         {"deviationValue", Options::guideDeviation()},
3194         {"ditherPerJobFrequency", m_LimitsUI->limitDitherFrequencyN->value()},
3195         {"focusHFRCheck", m_LimitsUI->limitFocusHFRS->isChecked()},
3196         {"hFRThresholdPercentage", m_LimitsUI->limitFocusHFRThresholdPercentage->value()},
3197         {"focusHFRValue", m_LimitsUI->limitFocusHFRN->value()},
3198         {"inSequenceCheckFrames", m_LimitsUI->limitFocusHFRCheckFrames->value()},
3199         {"hFRCheckAlgorithm", m_LimitsUI->limitFocusHFRAlgorithm->currentIndex()},
3200         {"focusDeltaTCheck", m_LimitsUI->limitFocusDeltaTS->isChecked()},
3201         {"focusDeltaTValue", m_LimitsUI->limitFocusDeltaTN->value()},
3202         {"refocusNCheck", m_LimitsUI->limitRefocusS->isChecked()},
3203         {"refocusNValue", m_LimitsUI->limitRefocusN->value()},
3204     };
3205 
3206     return settings;
3207 }
3208 
3209 void Capture::clearCameraConfiguration()
3210 {
3211     connect(KSMessageBox::Instance(), &KSMessageBox::accepted, this, [this]()
3212     {
3213         //QObject::disconnect(KSMessageBox::Instance(), &KSMessageBox::accepted, this, nullptr);
3214         KSMessageBox::Instance()->disconnect(this);
3215         devices()->getActiveCamera()->setConfig(PURGE_CONFIG);
3216         KStarsData::Instance()->userdb()->DeleteDSLRInfo(devices()->getActiveCamera()->getDeviceName());
3217 
3218         QStringList shutterfulCCDs  = Options::shutterfulCCDs();
3219         QStringList shutterlessCCDs = Options::shutterlessCCDs();
3220 
3221         // Remove camera from shutterful and shutterless CCDs
3222         if (shutterfulCCDs.contains(devices()->getActiveCamera()->getDeviceName()))
3223         {
3224             shutterfulCCDs.removeOne(devices()->getActiveCamera()->getDeviceName());
3225             Options::setShutterfulCCDs(shutterfulCCDs);
3226         }
3227         if (shutterlessCCDs.contains(devices()->getActiveCamera()->getDeviceName()))
3228         {
3229             shutterlessCCDs.removeOne(devices()->getActiveCamera()->getDeviceName());
3230             Options::setShutterlessCCDs(shutterlessCCDs);
3231         }
3232 
3233         // For DSLRs, immediately ask them to enter the values again.
3234         if (captureISOS && captureISOS->count() > 0)
3235         {
3236             createDSLRDialog();
3237         }
3238     });
3239 
3240     KSMessageBox::Instance()->questionYesNo( i18n("Reset %1 configuration to default?",
3241             devices()->getActiveCamera()->getDeviceName()),
3242             i18n("Confirmation"), 30);
3243 }
3244 
3245 void Capture::updateJobTable(SequenceJob *job, bool full)
3246 {
3247     if (job == nullptr)
3248     {
3249         QListIterator<SequenceJob *> iter(state()->allJobs());
3250         while (iter.hasNext())
3251             updateJobTable(iter.next(), full);
3252     }
3253     else
3254     {
3255         // find the job's row
3256         int row = state()->allJobs().indexOf(job);
3257         if (row >= 0 && row < queueTable->rowCount())
3258         {
3259             updateRowStyle(job);
3260             QTableWidgetItem *status = queueTable->item(row, JOBTABLE_COL_STATUS);
3261             QTableWidgetItem *count  = queueTable->item(row, JOBTABLE_COL_COUNTS);
3262             status->setText(job->getStatusString());
3263             updateJobTableCountCell(job, count);
3264 
3265             if (full)
3266             {
3267                 bool isDarkFlat = job->jobType() == SequenceJob::JOBTYPE_DARKFLAT;
3268 
3269                 QTableWidgetItem *filter = queueTable->item(row, JOBTABLE_COL_FILTER);
3270                 if (FilterPosCombo->findText(job->getCoreProperty(SequenceJob::SJ_Filter).toString()) >= 0 &&
3271                         (captureTypeS->currentIndex() == FRAME_LIGHT || captureTypeS->currentIndex() == FRAME_FLAT || isDarkFlat) )
3272                     filter->setText(job->getCoreProperty(SequenceJob::SJ_Filter).toString());
3273                 else
3274                     filter->setText("--");
3275 
3276                 QTableWidgetItem *exp = queueTable->item(row, JOBTABLE_COL_EXP);
3277                 exp->setText(QString("%L1").arg(job->getCoreProperty(SequenceJob::SJ_Exposure).toDouble(), 0, 'f',
3278                                                 captureExposureN->decimals()));
3279 
3280                 QTableWidgetItem *type = queueTable->item(row, JOBTABLE_COL_TYPE);
3281                 type->setText(isDarkFlat ? i18n("Dark Flat") : CCDFrameTypeNames[job->getFrameType()]);
3282 
3283                 QTableWidgetItem *bin = queueTable->item(row, JOBTABLE_COL_BINNING);
3284                 QPoint binning = job->getCoreProperty(SequenceJob::SJ_Binning).toPoint();
3285                 bin->setText(QString("%1x%2").arg(binning.x()).arg(binning.y()));
3286 
3287                 QTableWidgetItem *iso = queueTable->item(row, JOBTABLE_COL_ISO);
3288                 if (job->getCoreProperty(SequenceJob::SJ_ISOIndex).toInt() != -1)
3289                     iso->setText(captureISOS->itemText(job->getCoreProperty(SequenceJob::SJ_ISOIndex).toInt()));
3290                 else if (job->getCoreProperty(SequenceJob::SJ_Gain).toDouble() >= 0)
3291                     iso->setText(QString::number(job->getCoreProperty(SequenceJob::SJ_Gain).toDouble(), 'f', 1));
3292                 else
3293                     iso->setText("--");
3294 
3295                 QTableWidgetItem *offset = queueTable->item(row, JOBTABLE_COL_OFFSET);
3296                 if (job->getCoreProperty(SequenceJob::SJ_Offset).toDouble() >= 0)
3297                     offset->setText(QString::number(job->getCoreProperty(SequenceJob::SJ_Offset).toDouble(), 'f', 1));
3298                 else
3299                     offset->setText("--");
3300             }
3301 
3302             // update button enablement
3303             if (queueTable->rowCount() > 0)
3304             {
3305                 queueSaveAsB->setEnabled(true);
3306                 queueSaveB->setEnabled(true);
3307                 resetB->setEnabled(true);
3308                 state()->setDirty(true);
3309             }
3310 
3311             if (queueTable->rowCount() > 1)
3312             {
3313                 queueUpB->setEnabled(true);
3314                 queueDownB->setEnabled(true);
3315             }
3316         }
3317     }
3318 }
3319 
3320 void Capture::updateRowStyle(SequenceJob *job)
3321 {
3322     if (job == nullptr)
3323         return;
3324 
3325     // find the job's row
3326     int row = state()->allJobs().indexOf(job);
3327     if (row >= 0 && row < queueTable->rowCount())
3328     {
3329         updateCellStyle(queueTable->item(row, JOBTABLE_COL_STATUS), job->getStatus() == JOB_BUSY);
3330         updateCellStyle(queueTable->item(row, JOBTABLE_COL_FILTER), job->getStatus() == JOB_BUSY);
3331         updateCellStyle(queueTable->item(row, JOBTABLE_COL_COUNTS), job->getStatus() == JOB_BUSY);
3332         updateCellStyle(queueTable->item(row, JOBTABLE_COL_EXP), job->getStatus() == JOB_BUSY);
3333         updateCellStyle(queueTable->item(row, JOBTABLE_COL_TYPE), job->getStatus() == JOB_BUSY);
3334         updateCellStyle(queueTable->item(row, JOBTABLE_COL_BINNING), job->getStatus() == JOB_BUSY);
3335         updateCellStyle(queueTable->item(row, JOBTABLE_COL_ISO), job->getStatus() == JOB_BUSY);
3336         updateCellStyle(queueTable->item(row, JOBTABLE_COL_OFFSET), job->getStatus() == JOB_BUSY);
3337     }
3338 }
3339 
3340 void Capture::updateCellStyle(QTableWidgetItem *cell, bool active)
3341 {
3342     if (cell == nullptr)
3343         return;
3344 
3345     QFont font(cell->font());
3346     font.setBold(active);
3347     font.setItalic(active);
3348     cell->setFont(font);
3349 }
3350 
3351 void Capture::updateJobTableCountCell(SequenceJob *job, QTableWidgetItem *countCell)
3352 {
3353     countCell->setText(QString("%L1/%L2").arg(job->getCompleted()).arg(job->getCoreProperty(SequenceJob::SJ_Count).toInt()));
3354 }
3355 
3356 bool Capture::checkUploadPaths(FilenamePreviewType filenamePreview)
3357 {
3358     // only relevant if we do not generate file name previews
3359     if (filenamePreview != NOT_PREVIEW)
3360         return true;
3361 
3362     if (fileUploadModeS->currentIndex() != ISD::Camera::UPLOAD_CLIENT && fileRemoteDirT->text().isEmpty())
3363     {
3364         KSNotification::error(i18n("You must set remote directory for Local & Both modes."));
3365         return false;
3366     }
3367 
3368     if (fileUploadModeS->currentIndex() != ISD::Camera::UPLOAD_LOCAL && fileDirectoryT->text().isEmpty())
3369     {
3370         KSNotification::error(i18n("You must set local directory for Client & Both modes."));
3371         return false;
3372     }
3373     // everything OK
3374     return true;
3375 }
3376 
3377 QJsonObject Capture::createJsonJob(SequenceJob *job, int currentRow)
3378 {
3379     if (job == nullptr)
3380         return QJsonObject();
3381 
3382     QJsonObject jsonJob = {{"Status", "Idle"}};
3383     bool isDarkFlat = job->jobType() == SequenceJob::JOBTYPE_DARKFLAT;
3384     jsonJob.insert("Filter", FilterPosCombo->currentText());
3385     jsonJob.insert("Count", queueTable->item(currentRow, JOBTABLE_COL_COUNTS)->text());
3386     jsonJob.insert("Exp", queueTable->item(currentRow, JOBTABLE_COL_EXP)->text());
3387     jsonJob.insert("Type", isDarkFlat ? i18n("Dark Flat") : queueTable->item(currentRow, JOBTABLE_COL_TYPE)->text());
3388     jsonJob.insert("Bin", queueTable->item(currentRow, JOBTABLE_COL_BINNING)->text());
3389     jsonJob.insert("ISO/Gain", queueTable->item(currentRow, JOBTABLE_COL_ISO)->text());
3390     jsonJob.insert("Offset", queueTable->item(currentRow, JOBTABLE_COL_OFFSET)->text());
3391 
3392     return jsonJob;
3393 }
3394 
3395 void Capture::setCoolerToggled(bool enabled)
3396 {
3397     auto isToggled = (!enabled && coolerOnB->isChecked()) || (enabled && coolerOffB->isChecked());
3398 
3399     coolerOnB->blockSignals(true);
3400     coolerOnB->setChecked(enabled);
3401     coolerOnB->blockSignals(false);
3402 
3403     coolerOffB->blockSignals(true);
3404     coolerOffB->setChecked(!enabled);
3405     coolerOffB->blockSignals(false);
3406 
3407     if (isToggled)
3408         appendLogText(enabled ? i18n("Cooler is on") : i18n("Cooler is off"));
3409 }
3410 
3411 void Capture::createDSLRDialog()
3412 {
3413     dslrInfoDialog.reset(new DSLRInfo(this, devices()->getActiveCamera()));
3414 
3415     connect(dslrInfoDialog.get(), &DSLRInfo::infoChanged, this, [this]()
3416     {
3417         if (devices()->getActiveCamera())
3418             addDSLRInfo(QString(devices()->getActiveCamera()->getDeviceName()),
3419                         dslrInfoDialog->sensorMaxWidth,
3420                         dslrInfoDialog->sensorMaxHeight,
3421                         dslrInfoDialog->sensorPixelW,
3422                         dslrInfoDialog->sensorPixelH);
3423     });
3424 
3425     dslrInfoDialog->show();
3426 
3427     emit dslrInfoRequested(devices()->getActiveCamera()->getDeviceName());
3428 }
3429 
3430 void Capture::setStandAloneGain(double value)
3431 {
3432     QMap<QString, QMap<QString, QVariant> > propertyMap = customPropertiesDialog->getCustomProperties();
3433 
3434     if (m_standAloneUseCcdGain)
3435     {
3436         if (value >= 0)
3437         {
3438             QMap<QString, QVariant> ccdGain;
3439             ccdGain["GAIN"] = value;
3440             propertyMap["CCD_GAIN"] = ccdGain;
3441         }
3442         else
3443         {
3444             propertyMap["CCD_GAIN"].remove("GAIN");
3445             if (propertyMap["CCD_GAIN"].size() == 0)
3446                 propertyMap.remove("CCD_GAIN");
3447         }
3448     }
3449     else
3450     {
3451         if (value >= 0)
3452         {
3453             QMap<QString, QVariant> ccdGain = propertyMap["CCD_CONTROLS"];
3454             ccdGain["Gain"] = value;
3455             propertyMap["CCD_CONTROLS"] = ccdGain;
3456         }
3457         else
3458         {
3459             propertyMap["CCD_CONTROLS"].remove("Gain");
3460             if (propertyMap["CCD_CONTROLS"].size() == 0)
3461                 propertyMap.remove("CCD_CONTROLS");
3462         }
3463     }
3464 
3465     customPropertiesDialog->setCustomProperties(propertyMap);
3466 }
3467 
3468 void Capture::setStandAloneOffset(double value)
3469 {
3470     QMap<QString, QMap<QString, QVariant> > propertyMap = customPropertiesDialog->getCustomProperties();
3471 
3472     if (m_standAloneUseCcdOffset)
3473     {
3474         if (value >= 0)
3475         {
3476             QMap<QString, QVariant> ccdOffset;
3477             ccdOffset["OFFSET"] = value;
3478             propertyMap["CCD_OFFSET"] = ccdOffset;
3479         }
3480         else
3481         {
3482             propertyMap["CCD_OFFSET"].remove("OFFSET");
3483             if (propertyMap["CCD_OFFSET"].size() == 0)
3484                 propertyMap.remove("CCD_OFFSET");
3485         }
3486     }
3487     else
3488     {
3489         if (value >= 0)
3490         {
3491             QMap<QString, QVariant> ccdOffset = propertyMap["CCD_CONTROLS"];
3492             ccdOffset["Offset"] = value;
3493             propertyMap["CCD_CONTROLS"] = ccdOffset;
3494         }
3495         else
3496         {
3497             propertyMap["CCD_CONTROLS"].remove("Offset");
3498             if (propertyMap["CCD_CONTROLS"].size() == 0)
3499                 propertyMap.remove("CCD_CONTROLS");
3500         }
3501     }
3502 
3503     customPropertiesDialog->setCustomProperties(propertyMap);
3504 }
3505 void Capture::setGain(double value)
3506 {
3507     if (m_standAlone)
3508     {
3509         setStandAloneGain(value);
3510         return;
3511     }
3512     if (!devices()->getActiveCamera())
3513         return;
3514 
3515     QMap<QString, QMap<QString, QVariant> > customProps = customPropertiesDialog->getCustomProperties();
3516     process()->updateGain(value, customProps);
3517     customPropertiesDialog->setCustomProperties(customProps);
3518 }
3519 
3520 void Capture::setOffset(double value)
3521 {
3522     if (m_standAlone)
3523     {
3524         setStandAloneOffset(value);
3525         return;
3526     }
3527     if (!devices()->getActiveCamera())
3528         return;
3529 
3530     QMap<QString, QMap<QString, QVariant> > customProps = customPropertiesDialog->getCustomProperties();
3531 
3532     process()->updateOffset(value, customProps);
3533     customPropertiesDialog->setCustomProperties(customProps);
3534 }
3535 
3536 void Capture::editFilterName()
3537 {
3538     if (m_standAlone)
3539     {
3540         QStringList labels;
3541         for (int index = 0; index < FilterPosCombo->count(); index++)
3542             labels << FilterPosCombo->itemText(index);
3543         QStringList newLabels;
3544         if (editFilterNameInternal(labels, newLabels))
3545         {
3546             FilterPosCombo->clear();
3547             FilterPosCombo->addItems(newLabels);
3548         }
3549     }
3550     else
3551     {
3552         if (devices()->filterWheel() == nullptr || state()->getCurrentFilterPosition() < 1)
3553             return;
3554 
3555         QStringList labels = m_FilterManager->getFilterLabels();
3556         QStringList newLabels;
3557         if (editFilterNameInternal(labels, newLabels))
3558             m_FilterManager->setFilterNames(newLabels);
3559     }
3560 }
3561 
3562 bool Capture::editFilterNameInternal(const QStringList &labels, QStringList &newLabels)
3563 {
3564     QDialog filterDialog;
3565 
3566     QFormLayout *formLayout = new QFormLayout(&filterDialog);
3567     QVector<QLineEdit *> newLabelEdits;
3568 
3569     for (uint8_t i = 0; i < labels.count(); i++)
3570     {
3571         QLabel *existingLabel = new QLabel(QString("%1. <b>%2</b>").arg(i + 1).arg(labels[i]), &filterDialog);
3572         QLineEdit *newLabel = new QLineEdit(labels[i], &filterDialog);
3573         newLabelEdits.append(newLabel);
3574         formLayout->addRow(existingLabel, newLabel);
3575     }
3576 
3577     QString title = m_standAlone ?
3578                     "Edit Filter Names" : devices()->filterWheel()->getDeviceName();
3579     filterDialog.setWindowTitle(title);
3580     filterDialog.setLayout(formLayout);
3581     QDialogButtonBox *buttonBox = new QDialogButtonBox(QDialogButtonBox::Ok | QDialogButtonBox::Cancel, &filterDialog);
3582     connect(buttonBox, &QDialogButtonBox::accepted, &filterDialog, &QDialog::accept);
3583     connect(buttonBox, &QDialogButtonBox::rejected, &filterDialog, &QDialog::reject);
3584     filterDialog.layout()->addWidget(buttonBox);
3585 
3586     if (filterDialog.exec() == QDialog::Accepted)
3587     {
3588         QStringList results;
3589         for (uint8_t i = 0; i < labels.count(); i++)
3590             results << newLabelEdits[i]->text();
3591         newLabels = results;
3592         return true;
3593     }
3594     return false;
3595 }
3596 
3597 void Capture::handleScriptsManager()
3598 {
3599     QMap<ScriptTypes, QString> old_scripts = m_scriptsManager->getScripts();
3600 
3601     if (m_scriptsManager->exec() != QDialog::Accepted)
3602         // reset to old value
3603         m_scriptsManager->setScripts(old_scripts);
3604 }
3605 
3606 void Capture::showTemperatureRegulation()
3607 {
3608     if (!devices()->getActiveCamera())
3609         return;
3610 
3611     double currentRamp, currentThreshold;
3612     if (!devices()->getActiveCamera()->getTemperatureRegulation(currentRamp, currentThreshold))
3613         return;
3614 
3615 
3616     double rMin, rMax, rStep, tMin, tMax, tStep;
3617 
3618     devices()->getActiveCamera()->getMinMaxStep("CCD_TEMP_RAMP", "RAMP_SLOPE", &rMin, &rMax, &rStep);
3619     devices()->getActiveCamera()->getMinMaxStep("CCD_TEMP_RAMP", "RAMP_THRESHOLD", &tMin, &tMax, &tStep);
3620 
3621     QLabel rampLabel(i18nc("Maximum temperature variation over time when regulating.", "Ramp (°C/min):"));
3622     QDoubleSpinBox rampSpin;
3623     rampSpin.setMinimum(rMin);
3624     rampSpin.setMaximum(rMax);
3625     rampSpin.setSingleStep(rStep);
3626     rampSpin.setValue(currentRamp);
3627     rampSpin.setToolTip(i18n("<html><body>"
3628                              "<p>Maximum temperature change per minute when cooling or warming the camera. Set zero to disable."
3629                              "<p>This setting is read from and stored in the INDI camera driver configuration."
3630                              "</body></html>"));
3631 
3632     QLabel thresholdLabel(i18nc("Temperature threshold above which regulation triggers.", "Threshold (°C):"));
3633     QDoubleSpinBox thresholdSpin;
3634     thresholdSpin.setMinimum(tMin);
3635     thresholdSpin.setMaximum(tMax);
3636     thresholdSpin.setSingleStep(tStep);
3637     thresholdSpin.setValue(currentThreshold);
3638     thresholdSpin.setToolTip(i18n("<html><body>"
3639                                   "<p>Maximum difference between camera and target temperatures triggering regulation."
3640                                   "<p>This setting is read from and stored in the INDI camera driver configuration."
3641                                   "</body></html>"));
3642 
3643     QFormLayout layout;
3644     layout.addRow(&rampLabel, &rampSpin);
3645     layout.addRow(&thresholdLabel, &thresholdSpin);
3646 
3647     QPointer<QDialog> dialog = new QDialog(this);
3648     QDialogButtonBox buttonBox(QDialogButtonBox::Ok | QDialogButtonBox::Cancel, dialog);
3649     connect(&buttonBox, &QDialogButtonBox::accepted, dialog, &QDialog::accept);
3650     connect(&buttonBox, &QDialogButtonBox::rejected, dialog, &QDialog::reject);
3651     dialog->setWindowTitle(i18nc("@title:window", "Set Temperature Regulation"));
3652     layout.addWidget(&buttonBox);
3653     dialog->setLayout(&layout);
3654     dialog->setMinimumWidth(300);
3655 
3656     if (dialog->exec() == QDialog::Accepted)
3657     {
3658         if (devices()->getActiveCamera())
3659             devices()->getActiveCamera()->setTemperatureRegulation(rampSpin.value(), thresholdSpin.value());
3660     }
3661 }
3662 
3663 void Capture::updateStartButtons(bool start, bool pause)
3664 {
3665     if (start)
3666     {
3667         // start capturing, therefore next possible action is stopping
3668         startB->setIcon(QIcon::fromTheme("media-playback-stop"));
3669         startB->setToolTip(i18n("Stop Sequence"));
3670     }
3671     else
3672     {
3673         // stop capturing, therefore next possible action is starting
3674         startB->setIcon(QIcon::fromTheme("media-playback-start"));
3675         startB->setToolTip(i18n(pause ? "Resume Sequence" : "Start Sequence"));
3676     }
3677     pauseB->setEnabled(start && !pause);
3678 
3679 }
3680 
3681 void Capture::generateDarkFlats()
3682 {
3683     const auto existingJobs = state()->allJobs().size();
3684     uint8_t jobsAdded = 0;
3685 
3686     for (int i = 0; i < existingJobs; i++)
3687     {
3688         if (state()->allJobs().at(i)->getFrameType() != FRAME_FLAT)
3689             continue;
3690 
3691         syncGUIToJob(state()->allJobs().at(i));
3692 
3693         captureTypeS->setCurrentIndex(FRAME_DARK);
3694         createJob(SequenceJob::JOBTYPE_DARKFLAT);
3695         jobsAdded++;
3696     }
3697 
3698     if (jobsAdded > 0)
3699     {
3700         appendLogText(i18np("One dark flats job was created.", "%1 dark flats jobs were created.", jobsAdded));
3701     }
3702 }
3703 
3704 void Capture::updateJobFromUI(SequenceJob * job, FilenamePreviewType filenamePreview)
3705 {
3706     job->setCoreProperty(SequenceJob::SJ_Format, captureFormatS->currentText());
3707     job->setCoreProperty(SequenceJob::SJ_Encoding, captureEncodingS->currentText());
3708 
3709     if (captureISOS)
3710         job->setISO(captureISOS->currentIndex());
3711 
3712     job->setCoreProperty(SequenceJob::SJ_Gain, getGain());
3713     job->setCoreProperty(SequenceJob::SJ_Offset, getOffset());
3714 
3715     if (cameraTemperatureN->isEnabled())
3716     {
3717         job->setCoreProperty(SequenceJob::SJ_EnforceTemperature, cameraTemperatureS->isChecked());
3718         job->setTargetTemperature(cameraTemperatureN->value());
3719     }
3720 
3721     job->setScripts(m_scriptsManager->getScripts());
3722     job->setUploadMode(static_cast<ISD::Camera::UploadMode>(fileUploadModeS->currentIndex()));
3723     job->setFlatFieldDuration(state()->flatFieldDuration());
3724     job->setCalibrationPreAction(state()->calibrationPreAction());
3725     job->setWallCoord(state()->wallCoord());
3726     job->setCoreProperty(SequenceJob::SJ_TargetADU, state()->targetADU());
3727     job->setCoreProperty(SequenceJob::SJ_TargetADUTolerance, state()->targetADUTolerance());
3728     job->setFrameType(static_cast<CCDFrameType>(qMax(0, captureTypeS->currentIndex())));
3729 
3730     if (FilterPosCombo->currentIndex() != -1 && (m_standAlone || devices()->filterWheel() != nullptr))
3731         job->setTargetFilter(FilterPosCombo->currentIndex() + 1, FilterPosCombo->currentText());
3732 
3733     job->setCoreProperty(SequenceJob::SJ_Exposure, captureExposureN->value());
3734 
3735     job->setCoreProperty(SequenceJob::SJ_Count, captureCountN->value());
3736 
3737     job->setCoreProperty(SequenceJob::SJ_Binning, QPoint(captureBinHN->value(), captureBinVN->value()));
3738 
3739     /* in ms */
3740     job->setCoreProperty(SequenceJob::SJ_Delay, captureDelayN->value() * 1000);
3741 
3742     // Custom Properties
3743     job->setCustomProperties(customPropertiesDialog->getCustomProperties());
3744 
3745     job->setCoreProperty(SequenceJob::SJ_ROI, QRect(captureFrameXN->value(), captureFrameYN->value(), captureFrameWN->value(),
3746                          captureFrameHN->value()));
3747     job->setCoreProperty(SequenceJob::SJ_RemoteDirectory, fileRemoteDirT->text());
3748     job->setCoreProperty(SequenceJob::SJ_LocalDirectory, fileDirectoryT->text());
3749     job->setCoreProperty(SequenceJob::SJ_TargetName, targetNameT->text());
3750     job->setCoreProperty(SequenceJob::SJ_PlaceholderFormat, placeholderFormatT->text());
3751     job->setCoreProperty(SequenceJob::SJ_PlaceholderSuffix, formatSuffixN->value());
3752 
3753     job->setCoreProperty(SequenceJob::SJ_DitherPerJobFrequency, m_LimitsUI->limitDitherFrequencyN->value());
3754 
3755     auto placeholderPath = PlaceholderPath();
3756     placeholderPath.addJob(job, placeholderFormatT->text());
3757 
3758     QString signature = placeholderPath.generateSequenceFilename(*job,
3759                         filenamePreview != REMOTE_PREVIEW, true, 1,
3760                         ".fits", "", false, true);
3761     job->setCoreProperty(SequenceJob::SJ_Signature, signature);
3762 
3763     auto remoteUpload = placeholderPath.generateSequenceFilename(*job,
3764                         false,
3765                         true,
3766                         1,
3767                         ".fits",
3768                         "",
3769                         false,
3770                         true);
3771 
3772     auto lastSeparator = remoteUpload.lastIndexOf(QDir::separator());
3773     auto remoteDirectory = remoteUpload.mid(0, lastSeparator);
3774     auto remoteFilename = QString("%1_XXX").arg(remoteUpload.mid(lastSeparator + 1));
3775     job->setCoreProperty(SequenceJob::SJ_RemoteFormatDirectory, remoteDirectory);
3776     job->setCoreProperty(SequenceJob::SJ_RemoteFormatFilename, remoteFilename);
3777 }
3778 
3779 void Capture::setMeridianFlipState(QSharedPointer<MeridianFlipState> newstate)
3780 {
3781     state()->setMeridianFlipState(newstate);
3782     connect(state()->getMeridianFlipState().get(), &MeridianFlipState::newLog, this, &Capture::appendLogText);
3783 }
3784 
3785 void Capture::syncRefocusOptionsFromGUI()
3786 {
3787     Options::setEnforceAutofocusHFR(m_LimitsUI->limitFocusHFRS->isChecked());
3788     Options::setHFRThresholdPercentage(m_LimitsUI->limitFocusHFRThresholdPercentage->value());
3789     Options::setHFRDeviation(m_LimitsUI->limitFocusHFRN->value());
3790     Options::setInSequenceCheckFrames(m_LimitsUI->limitFocusHFRCheckFrames->value());
3791     Options::setHFRCheckAlgorithm(m_LimitsUI->limitFocusHFRAlgorithm->currentIndex());
3792     Options::setEnforceAutofocusOnTemperature(m_LimitsUI->limitFocusDeltaTS->isChecked());
3793     Options::setMaxFocusTemperatureDelta(m_LimitsUI->limitFocusDeltaTN->value());
3794     Options::setEnforceRefocusEveryN(m_LimitsUI->limitRefocusS->isChecked());
3795     Options::setRefocusEveryN(static_cast<uint>(m_LimitsUI->limitRefocusN->value()));
3796     Options::setRefocusAfterMeridianFlip(m_LimitsUI->meridianRefocusS->isChecked());
3797 }
3798 
3799 QJsonObject Capture::currentScope()
3800 {
3801     QVariant trainID = ProfileSettings::Instance()->getOneSetting(ProfileSettings::CaptureOpticalTrain);
3802     if (activeCamera() && trainID.isValid())
3803     {
3804         auto id = trainID.toUInt();
3805         auto name = OpticalTrainManager::Instance()->name(id);
3806         return OpticalTrainManager::Instance()->getScope(name);
3807     }
3808     // return empty JSON object
3809     return QJsonObject();
3810 }
3811 
3812 double Capture::currentReducer()
3813 {
3814     QVariant trainID = ProfileSettings::Instance()->getOneSetting(ProfileSettings::CaptureOpticalTrain);
3815     if (activeCamera() && trainID.isValid())
3816     {
3817         auto id = trainID.toUInt();
3818         auto name = OpticalTrainManager::Instance()->name(id);
3819         return OpticalTrainManager::Instance()->getReducer(name);
3820     }
3821     // no reducer available
3822     return 1.0;
3823 }
3824 
3825 double Capture::currentAperture()
3826 {
3827     auto scope = currentScope();
3828 
3829     double focalLength = scope["focal_length"].toDouble(-1);
3830     double aperture = scope["aperture"].toDouble(-1);
3831     double focalRatio = scope["focal_ratio"].toDouble(-1);
3832 
3833     // DSLR Lens Aperture
3834     if (aperture < 0 && focalRatio > 0)
3835         aperture = focalLength * focalRatio;
3836 
3837     return aperture;
3838 }
3839 
3840 void Capture::setupOpticalTrainManager()
3841 {
3842     connect(OpticalTrainManager::Instance(), &OpticalTrainManager::updated, this, &Capture::refreshOpticalTrain);
3843     connect(trainB, &QPushButton::clicked, this, [this]()
3844     {
3845         OpticalTrainManager::Instance()->openEditor(opticalTrainCombo->currentText());
3846     });
3847     connect(opticalTrainCombo, QOverload<int>::of(&QComboBox::currentIndexChanged), this, [this](int index)
3848     {
3849         ProfileSettings::Instance()->setOneSetting(ProfileSettings::CaptureOpticalTrain,
3850                 OpticalTrainManager::Instance()->id(opticalTrainCombo->itemText(index)));
3851         refreshOpticalTrain();
3852         emit trainChanged();
3853     });
3854 }
3855 
3856 void Capture::refreshOpticalTrain()
3857 {
3858     opticalTrainCombo->blockSignals(true);
3859     opticalTrainCombo->clear();
3860     opticalTrainCombo->addItems(OpticalTrainManager::Instance()->getTrainNames());
3861     trainB->setEnabled(true);
3862 
3863     QVariant trainID = ProfileSettings::Instance()->getOneSetting(ProfileSettings::CaptureOpticalTrain);
3864 
3865     if (trainID.isValid())
3866     {
3867         auto id = trainID.toUInt();
3868 
3869         // If train not found, select the first one available.
3870         if (OpticalTrainManager::Instance()->exists(id) == false)
3871         {
3872             qCWarning(KSTARS_EKOS_CAPTURE) << "Optical train doesn't exist for id" << id;
3873             id = OpticalTrainManager::Instance()->id(opticalTrainCombo->itemText(0));
3874         }
3875 
3876         auto name = OpticalTrainManager::Instance()->name(id);
3877 
3878         opticalTrainCombo->setCurrentText(name);
3879         process()->refreshOpticalTrain(name);
3880     }
3881 
3882     opticalTrainCombo->blockSignals(false);
3883 }
3884 
3885 void Capture::generatePreviewFilename()
3886 {
3887     if (state()->isCaptureRunning() == false)
3888     {
3889         placeholderFormatT->setToolTip(previewFilename( LOCAL_PREVIEW ));
3890         emit newLocalPreview(placeholderFormatT->toolTip());
3891 
3892         if (fileUploadModeS->currentIndex() != 0)
3893             fileRemoteDirT->setToolTip(previewFilename( REMOTE_PREVIEW ));
3894     }
3895 }
3896 
3897 QString Capture::previewFilename(FilenamePreviewType previewType)
3898 {
3899     QString previewText;
3900     QString m_format;
3901     auto separator = QDir::separator();
3902 
3903     if (previewType == LOCAL_PREVIEW)
3904     {
3905         if(!fileDirectoryT->text().endsWith(separator) && !placeholderFormatT->text().startsWith(separator))
3906             placeholderFormatT->setText(separator + placeholderFormatT->text());
3907         m_format = fileDirectoryT->text() + placeholderFormatT->text() + formatSuffixN->prefix() + formatSuffixN->cleanText();
3908     }
3909     else if (previewType == REMOTE_PREVIEW)
3910         m_format = fileRemoteDirT->text();
3911 
3912     //Guard against an empty format to avoid the empty directory warning pop-up in addjob
3913     if (m_format.isEmpty())
3914         return previewText;
3915     // Tags %d & %p disable for now for simplicity
3916     //    else if (state()->sequenceURL().toLocalFile().isEmpty() && (m_format.contains("%d") || m_format.contains("%p")
3917     //             || m_format.contains("%f")))
3918     else if (state()->sequenceURL().toLocalFile().isEmpty() && m_format.contains("%f"))
3919         previewText = ("Save the sequence file to show filename preview");
3920     else
3921     {
3922         // create temporarily a sequence job
3923         SequenceJob *m_job = createJob(SequenceJob::JOBTYPE_PREVIEW, previewType);
3924         if (m_job == nullptr)
3925             return previewText;
3926 
3927         QString previewSeq;
3928         if (state()->sequenceURL().toLocalFile().isEmpty())
3929         {
3930             if (m_format.startsWith(separator))
3931                 previewSeq = m_format.left(m_format.lastIndexOf(separator));
3932         }
3933         else
3934             previewSeq = state()->sequenceURL().toLocalFile();
3935         auto m_placeholderPath = PlaceholderPath(previewSeq);
3936 
3937         QString extension;
3938         if (captureEncodingS->currentText() == "FITS")
3939             extension = ".fits";
3940         else if (captureEncodingS->currentText() == "XISF")
3941             extension = ".xisf";
3942         else
3943             extension = ".[NATIVE]";
3944         previewText = m_placeholderPath.generateSequenceFilename(*m_job, previewType == LOCAL_PREVIEW, true, 1,
3945                       extension, "", false);
3946         previewText = QDir::toNativeSeparators(previewText);
3947         // we do not use it any more
3948         m_job->deleteLater();
3949     }
3950 
3951     // Must change directory separate to UNIX style for remote
3952     if (previewType == REMOTE_PREVIEW)
3953         previewText.replace(separator, "/");
3954 
3955     return previewText;
3956 }
3957 
3958 void Capture::openExposureCalculatorDialog()
3959 {
3960     qCInfo(KSTARS_EKOS_CAPTURE) << "Instantiating an Exposure Calculator";
3961 
3962     // Learn how to read these from indi
3963     double preferredSkyQuality = 20.5;
3964 
3965     auto scope = currentScope();
3966     double focalRatio = scope["focal_ratio"].toDouble(-1);
3967 
3968     auto reducedFocalLength = currentReducer() * scope["focal_length"].toDouble(-1);
3969     auto aperture = currentAperture();
3970     auto reducedFocalRatio = (focalRatio > 0 || aperture == 0) ? focalRatio : reducedFocalLength / aperture;
3971 
3972     if (devices()->getActiveCamera() != nullptr)
3973     {
3974         qCInfo(KSTARS_EKOS_CAPTURE) << "set ExposureCalculator preferred camera to active camera id: "
3975                                     << devices()->getActiveCamera()->getDeviceName();
3976     }
3977 
3978     QPointer<ExposureCalculatorDialog> anExposureCalculatorDialog(new ExposureCalculatorDialog(KStars::Instance(),
3979             preferredSkyQuality,
3980             reducedFocalRatio,
3981             devices()->getActiveCamera()->getDeviceName()));
3982     anExposureCalculatorDialog->setAttribute(Qt::WA_DeleteOnClose);
3983     anExposureCalculatorDialog->show();
3984 }
3985 
3986 bool Capture::hasCoolerControl()
3987 {
3988     return process()->hasCoolerControl();
3989 }
3990 
3991 bool Capture::setCoolerControl(bool enable)
3992 {
3993     return process()->setCoolerControl(enable);
3994 }
3995 
3996 void Capture::removeDevice(const QSharedPointer<ISD::GenericDevice> &device)
3997 {
3998     process()->removeDevice(device);
3999 }
4000 
4001 void Capture::start()
4002 {
4003     process()->startNextPendingJob();
4004 }
4005 
4006 void Capture::stop(CaptureState targetState)
4007 {
4008     process()->stopCapturing(targetState);
4009 }
4010 
4011 void Capture::toggleVideo(bool enabled)
4012 {
4013     process()->toggleVideo(enabled);
4014 }
4015 
4016 void Capture::setTargetName(const QString &newTargetName)
4017 {
4018     // target is changed only if no job is running
4019     if (activeJob() == nullptr)
4020     {
4021         // set the target name in the currently selected job
4022         targetNameT->setText(newTargetName);
4023         auto rows = queueTable->selectionModel()->selectedRows();
4024         if(rows.count() > 0)
4025         {
4026             // take the first one, since we are in single selection mode
4027             int pos = rows.constFirst().row();
4028 
4029             if (state()->allJobs().size() > pos)
4030                 state()->allJobs().at(pos)->setCoreProperty(SequenceJob::SJ_TargetName, newTargetName);
4031         }
4032 
4033         emit captureTarget(newTargetName);
4034     }
4035 }
4036 
4037 QString Capture::getTargetName()
4038 {
4039     if (activeJob())
4040         return activeJob()->getCoreProperty(SequenceJob::SJ_TargetName).toString();
4041     else
4042         return "";
4043 }
4044 
4045 void Capture::restartCamera(const QString &name)
4046 {
4047     process()->restartCamera(name);
4048 }
4049 
4050 void Capture::capturePreview()
4051 {
4052     process()->capturePreview();
4053 }
4054 
4055 void Capture::startFraming()
4056 {
4057     process()->capturePreview(true);
4058 }
4059 
4060 double Capture::getGain()
4061 {
4062     return devices()->cameraGain(customPropertiesDialog->getCustomProperties());
4063 }
4064 
4065 double Capture::getOffset()
4066 {
4067     return devices()->cameraOffset(customPropertiesDialog->getCustomProperties());
4068 }
4069 
4070 void Capture::setHFR(double newHFR, int, bool inAutofocus)
4071 {
4072     state()->getRefocusState()->setFocusHFR(newHFR, inAutofocus);
4073 }
4074 
4075 ISD::Camera *Capture::activeCamera()
4076 {
4077     return m_captureDeviceAdaptor->getActiveCamera();
4078 }
4079 }