diff --git a/kstars/ekos/auxiliary/filtermanager.cpp b/kstars/ekos/auxiliary/filtermanager.cpp index 6610e8e3d..0a36402cb 100644 --- a/kstars/ekos/auxiliary/filtermanager.cpp +++ b/kstars/ekos/auxiliary/filtermanager.cpp @@ -1,728 +1,738 @@ /* Ekos Filter Manager Copyright (C) 2017 Jasem Mutlaq This application is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version. */ #include "filtermanager.h" #include #include "indi_debug.h" #include "kstarsdata.h" #include "kstars.h" #include "Options.h" #include "auxiliary/kspaths.h" #include "auxiliary/ksmessagebox.h" #include "ekos/auxiliary/filterdelegate.h" #include #include #include #include #include #include namespace Ekos { FilterManager::FilterManager() : QDialog(KStars::Instance()) { #ifdef Q_OS_OSX setWindowFlags(Qt::Tool | Qt::WindowStaysOnTopHint); #endif setupUi(this); connect(buttonBox, SIGNAL(accepted()), this, SLOT(close())); connect(buttonBox, SIGNAL(rejected()), this, SLOT(close())); //QSqlDatabase::removeDatabase("filter_db"); QSqlDatabase userdb = QSqlDatabase::cloneDatabase(KStarsData::Instance()->userdb()->GetDatabase(), "filter_db"); userdb.open(); //delete (filterModel); filterModel = new QSqlTableModel(this, userdb); filterView->setModel(filterModel); // No Edit delegate noEditDelegate = new NotEditableDelegate(filterView); filterView->setItemDelegateForColumn(4, noEditDelegate); // Exposure delegate exposureDelegate = new ExposureDelegate(filterView); filterView->setItemDelegateForColumn(5, exposureDelegate); // Offset delegate offsetDelegate = new OffsetDelegate(filterView); filterView->setItemDelegateForColumn(6, offsetDelegate); // Auto Focus delegate useAutoFocusDelegate = new UseAutoFocusDelegate(filterView); filterView->setItemDelegateForColumn(7, useAutoFocusDelegate); // Set Delegates lockDelegate = new LockDelegate(m_currentFilterLabels, filterView); filterView->setItemDelegateForColumn(8, lockDelegate); // Absolute Focus Position filterView->setItemDelegateForColumn(9, noEditDelegate); connect(filterModel, &QSqlTableModel::dataChanged, [this](const QModelIndex & topLeft, const QModelIndex &, const QVector &) { reloadFilters(); if (topLeft.column() == 5) emit exposureChanged(filterModel->data(topLeft).toDouble()); }); } void FilterManager::refreshFilterModel() { if (m_currentFilterDevice == nullptr || m_currentFilterLabels.empty()) return; QString vendor(m_currentFilterDevice->getDeviceName()); //QSqlDatabase::removeDatabase("filter_db"); //QSqlDatabase userdb = QSqlDatabase::cloneDatabase(KStarsData::Instance()->userdb()->GetDatabase(), "filter_db"); //userdb.open(); //delete (filterModel); //filterModel = new QSqlTableModel(this, userdb); if (!filterModel) { QSqlDatabase userdb = QSqlDatabase::cloneDatabase(KStarsData::Instance()->userdb()->GetDatabase(), "filter_db"); userdb.open(); filterModel = new QSqlTableModel(this, userdb); filterView->setModel(filterModel); } filterModel->setTable("filter"); filterModel->setFilter(QString("vendor='%1'").arg(vendor)); filterModel->select(); filterModel->setEditStrategy(QSqlTableModel::OnFieldChange); // If we have an existing table but it doesn't match the number of current filters // then we remove it. if (filterModel->rowCount() > 0 && filterModel->rowCount() != m_currentFilterLabels.count()) { for (int i = 0; i < filterModel->rowCount(); i++) filterModel->removeRow(i); filterModel->select(); } // If it is first time, let's populate data if (filterModel->rowCount() == 0) { for (QString &filter : m_currentFilterLabels) KStarsData::Instance()->userdb()->AddFilter(vendor, "", "", filter, 0, 1.0, false, "--", 0); filterModel->select(); // Seems ->select() is not enough, have to create a new model. /*delete (filterModel); filterModel = new QSqlTableModel(this, userdb); filterModel->setTable("filter"); filterModel->setFilter(QString("vendor='%1'").arg(m_currentFilterDevice->getDeviceName())); filterModel->select(); filterModel->setEditStrategy(QSqlTableModel::OnManualSubmit);*/ } // Make sure all the filter colors match DB. If not update model to sync with INDI filter values else { for (int i = 0; i < filterModel->rowCount(); ++i) { QModelIndex index = filterModel->index(i, 4); if (filterModel->data(index).toString() != m_currentFilterLabels[i]) { filterModel->setData(index, m_currentFilterLabels[i]); } } } lockDelegate->setCurrentFilterList(m_currentFilterLabels); filterModel->setHeaderData(4, Qt::Horizontal, i18n("Filter")); filterModel->setHeaderData(5, Qt::Horizontal, i18n("Filter exposure time during focus"), Qt::ToolTipRole); filterModel->setHeaderData(5, Qt::Horizontal, i18n("Exposure")); filterModel->setHeaderData(6, Qt::Horizontal, i18n("Relative offset in steps"), Qt::ToolTipRole); filterModel->setHeaderData(6, Qt::Horizontal, i18n("Offset")); filterModel->setHeaderData(7, Qt::Horizontal, i18n("Start Auto Focus when filter is activated"), Qt::ToolTipRole); filterModel->setHeaderData(7, Qt::Horizontal, i18n("Auto Focus")); filterModel->setHeaderData(8, Qt::Horizontal, i18n("Lock specific filter when running Auto Focus"), Qt::ToolTipRole); filterModel->setHeaderData(8, Qt::Horizontal, i18n("Lock Filter")); filterModel->setHeaderData(9, Qt::Horizontal, i18n("Flat frames are captured at this focus position. It is updated automatically by focus process if enabled."), Qt::ToolTipRole); filterModel->setHeaderData(9, Qt::Horizontal, i18n("Flat Focus Position")); filterView->hideColumn(0); filterView->hideColumn(1); filterView->hideColumn(2); filterView->hideColumn(3); reloadFilters(); } void FilterManager::reloadFilters() { qDeleteAll(m_ActiveFilters); currentFilter = nullptr; targetFilter = nullptr; m_ActiveFilters.clear(); operationQueue.clear(); for (int i = 0; i < filterModel->rowCount(); ++i) { QSqlRecord record = filterModel->record(i); QString id = record.value("id").toString(); QString vendor = record.value("Vendor").toString(); QString model = record.value("Model").toString(); QString type = record.value("Type").toString(); QString color = record.value("Color").toString(); double exposure = record.value("Exposure").toDouble(); int offset = record.value("Offset").toInt(); QString lockedFilter = record.value("LockedFilter").toString(); bool useAutoFocus = record.value("UseAutoFocus").toInt() == 1; int absFocusPos = record.value("AbsoluteFocusPosition").toInt(); OAL::Filter *o = new OAL::Filter(id, model, vendor, type, color, exposure, offset, useAutoFocus, lockedFilter, absFocusPos); m_ActiveFilters.append(o); } } void FilterManager::setCurrentFilterWheel(ISD::GDInterface *filter) { if (m_currentFilterDevice == filter) return; else m_currentFilterDevice->disconnect(this); m_currentFilterDevice = filter; connect(filter, SIGNAL(textUpdated(ITextVectorProperty*)), this, SLOT(processText(ITextVectorProperty*))); connect(filter, SIGNAL(numberUpdated(INumberVectorProperty*)), this, SLOT(processNumber(INumberVectorProperty*))); connect(filter, SIGNAL(switchUpdated(ISwitchVectorProperty*)), this, SLOT(processSwitch(ISwitchVectorProperty*))); connect(filter, &ISD::GDInterface::Disconnected, [&]() { m_currentFilterLabels.clear(); m_currentFilterPosition = -1; //m_currentFilterDevice = nullptr; m_FilterNameProperty = nullptr; m_FilterPositionProperty = nullptr; }); m_FilterNameProperty = nullptr; m_FilterPositionProperty = nullptr; m_FilterConfirmSet = nullptr; initFilterProperties(); } void FilterManager::initFilterProperties() { if (m_FilterNameProperty && m_FilterPositionProperty) { if (m_FilterConfirmSet == nullptr) m_FilterConfirmSet = m_currentFilterDevice->getBaseDevice()->getSwitch("CONFIRM_FILTER_SET"); return; } filterNameLabel->setText(m_currentFilterDevice->getDeviceName()); m_currentFilterLabels.clear(); m_FilterNameProperty = m_currentFilterDevice->getBaseDevice()->getText("FILTER_NAME"); m_FilterPositionProperty = m_currentFilterDevice->getBaseDevice()->getNumber("FILTER_SLOT"); m_FilterConfirmSet = m_currentFilterDevice->getBaseDevice()->getSwitch("CONFIRM_FILTER_SET"); m_currentFilterPosition = getFilterPosition(true); m_currentFilterLabels = getFilterLabels(true); if (m_currentFilterLabels.isEmpty() == false) refreshFilterModel(); if (m_ActiveFilters.count() >= m_currentFilterPosition) lastFilterOffset = m_ActiveFilters[m_currentFilterPosition - 1]->offset(); } QStringList FilterManager::getFilterLabels(bool forceRefresh) { if (forceRefresh == false || m_FilterNameProperty == nullptr) return m_currentFilterLabels; QStringList filterList; QStringList filterAlias = Options::filterAlias(); for (int i = 0; i < m_FilterPositionProperty->np[0].max; i++) { QString item; if (m_FilterNameProperty != nullptr && (i < m_FilterNameProperty->ntp)) item = m_FilterNameProperty->tp[i].text; else if (i < filterAlias.count() && filterAlias[i].isEmpty() == false) item = filterAlias.at(i); filterList.append(item); } return filterList; } int FilterManager::getFilterPosition(bool forceRefresh) { if (forceRefresh == false) return m_currentFilterPosition; return static_cast(m_FilterPositionProperty->np[0].value); } bool FilterManager::setFilterPosition(uint8_t position, FilterPolicy policy) { // Position 1 to Max if (position > m_ActiveFilters.count()) return false; m_Policy = policy; currentFilter = m_ActiveFilters[m_currentFilterPosition - 1]; targetFilter = m_ActiveFilters[position - 1]; if (currentFilter == targetFilter) { emit ready(); return true; } buildOperationQueue(FILTER_CHANGE); executeOperationQueue(); return true; } void FilterManager::processNumber(INumberVectorProperty *nvp) { if (nvp->s != IPS_OK || strcmp(nvp->name, "FILTER_SLOT") || m_currentFilterDevice == nullptr || (nvp->device != m_currentFilterDevice->getDeviceName())) return; m_FilterPositionProperty = nvp; if (m_currentFilterPosition != static_cast(m_FilterPositionProperty->np[0].value)) { m_currentFilterPosition = static_cast(m_FilterPositionProperty->np[0].value); emit positionChanged(m_currentFilterPosition); } if (state == FILTER_CHANGE) executeOperationQueue(); // If filter is changed externally, record its current offset as the starting offset. else if (state == FILTER_IDLE && m_ActiveFilters.count() >= m_currentFilterPosition) lastFilterOffset = m_ActiveFilters[m_currentFilterPosition - 1]->offset(); // Check if we have to apply Focus Offset // Focus offsets are always applied first // Check if we have to start Auto Focus // If new filter position changed, and new filter policy is to perform auto-focus then autofocus is initiated. // Capture Module // 3x L ---> 3x HA ---> 3x L // Capture calls setFilterPosition("L"). // 0. Change filter to desired filter "L" // 1. Is there any offset from last offset that needs to be applied? // 1.1 Yes --> Apply focus offset and wait until position is changed: // 1.1.1 Position complete, now check for useAutoFocus policy (#2). // 1.1.2 Position failed, retry? // 1.2 No --> Go to #2 // 2. Is there autofocus policy for current filter? // 2.1. Yes --> Check filter lock policy // 2.1.1 If filter lock is another filter --> Change filter // 2.1.2 If filter lock is same filter --> proceed to 2.3 // 2.2 No --> Process to 2.3 // 2.3 filter lock policy filter is applied, start autofocus. // 2.4 Autofocus complete. Check filter lock policy // 2.4.1 If filter lock policy was applied --> revert filter // 2.4.1.1 If filter offset policy is applicable --> Apply offset // 2.4.1.2 If no filter offset policy is applicable --> Go to 2.5 // 2.4.2 No filter lock policy, go to 2.5 // 2.5 All complete, emit ready() // Example. Current filter L. setFilterPosition("HA"). AutoFocus = YES. HA lock policy: L, HA offset policy: +100 with respect to L // Operation Stack. offsetDiff = 100 // If AutoFocus && current filter = lock policy filter // AUTO_FOCUS (on L) // CHANGE_FILTER (to HA) // APPLY_OFFSET: +100 // Example. Current filter L. setFilterPosition("HA"). AutoFocus = No. HA lock policy: L, HA offset policy: +100 with respect to L // Operation Stack. offsetDiff = 100 // CHANGE_FILTER (to HA) // APPLY_OFFSET: +100 // Example. Current filter R. setFilterPosition("B"). AutoFocus = YES. B lock policy: "--", B offset policy: +70 with respect to L // R offset = -50 with respect to L // FILTER_CHANGE (to B) // FILTER_OFFSET (+120) // AUTO_FOCUS // Example. Current filter R. setFilterPosition("HA"). AutoFocus = YES. HA lock policy: L, HA offset policy: +100 with respect to L // R offset = -50 with respect to L // Operation Stack. offsetDiff = +150 // CHANGE_FILTER (to L) // APPLY_OFFSET: +50 (L - R) // AUTO_FOCUS // CHANGE_FILTER (HA) // APPLY_OFFSET: +100 // Example. Current filter R. setFilterPosition("HA"). AutoFocus = No. HA lock policy: L, HA offset policy: +100 with respect to L // R offset = -50 with respect to L // Operation Stack. offsetDiff = +150 // CHANGE_FILTER (to HA) // APPLY_OFFSET: +150 (HA - R) // Example. Current filter L. setFilterPosition("R"). AutoFocus = Yes. R lock policy: R, R offset policy: -50 with respect to L // Operation Stack. offsetDiff = -50 // CHANGE_FILTER (to R) // APPLY_OFFSET: -50 (R - L) // AUTO_FOCUS } void FilterManager::processText(ITextVectorProperty *tvp) { if (strcmp(tvp->name, "FILTER_NAME") || m_currentFilterDevice == nullptr || (tvp->device != m_currentFilterDevice->getDeviceName()) ) return; m_FilterNameProperty = tvp; QStringList newFilterLabels = getFilterLabels(true); if (newFilterLabels != m_currentFilterLabels) { m_currentFilterLabels = newFilterLabels; refreshFilterModel(); emit labelsChanged(newFilterLabels); } } void FilterManager::processSwitch(ISwitchVectorProperty *svp) { if (m_currentFilterDevice == nullptr || (svp->device != m_currentFilterDevice->getDeviceName())) return; } void FilterManager::buildOperationQueue(FilterState operation) { operationQueue.clear(); m_useTargetFilter = false; switch (operation) { case FILTER_CHANGE: { if ( (m_Policy & CHANGE_POLICY) && targetFilter != currentFilter) m_useTargetFilter = true; if (m_useTargetFilter) { operationQueue.enqueue(FILTER_CHANGE); if (m_FocusReady && (m_Policy & OFFSET_POLICY)) operationQueue.enqueue(FILTER_OFFSET); } if (m_FocusReady && (m_Policy & AUTOFOCUS_POLICY) && targetFilter->useAutoFocus()) operationQueue.enqueue(FILTER_AUTOFOCUS); } break; default: break; } } bool FilterManager::executeOperationQueue() { if (operationQueue.isEmpty()) { state = FILTER_IDLE; emit newStatus(state); emit ready(); return false; } FilterState nextOperation = operationQueue.dequeue(); bool actionRequired = true; switch (nextOperation) { case FILTER_CHANGE: { if (m_ConfirmationPending) return true; state = FILTER_CHANGE; if (m_useTargetFilter) targetFilterPosition = m_ActiveFilters.indexOf(targetFilter) + 1; m_currentFilterDevice->runCommand(INDI_SET_FILTER, &targetFilterPosition); emit newStatus(state); if (m_FilterConfirmSet) { // if (KMessageBox::questionYesNo(KStars::Instance(), // i18n("Set filter to %1. Is filter set?", targetFilter->color()), // i18n("Confirm Filter")) == KMessageBox::Yes) // m_currentFilterDevice->runCommand(INDI_CONFIRM_FILTER); connect(KSMessageBox::Instance(), &KSMessageBox::accepted, this, [this]() { //QObject::disconnect(KSMessageBox::Instance(), &KSMessageBox::accepted, this, nullptr); KSMessageBox::Instance()->disconnect(this); m_ConfirmationPending = false; m_currentFilterDevice->runCommand(INDI_CONFIRM_FILTER); }); connect(KSMessageBox::Instance(), &KSMessageBox::rejected, this, [this]() { //QObject::disconnect(KSMessageBox::Instance(), &KSMessageBox::accepted, this, nullptr); KSMessageBox::Instance()->disconnect(this); m_ConfirmationPending = false; }); m_ConfirmationPending = true; KSMessageBox::Instance()->questionYesNo(i18n("Set filter to %1. Is filter set?", targetFilter->color()), i18n("Confirm Filter")); } } break; case FILTER_OFFSET: { state = FILTER_OFFSET; if (m_useTargetFilter) { targetFilterOffset = targetFilter->offset() - lastFilterOffset; lastFilterOffset = targetFilter->offset(); currentFilter = targetFilter; m_useTargetFilter = false; } if (targetFilterOffset == 0) actionRequired = false; else { emit newFocusOffset(targetFilterOffset, false); emit newStatus(state); } } break; case FILTER_AUTOFOCUS: state = FILTER_AUTOFOCUS; qCDebug(KSTARS) << "FilterManager.cpp is triggering autofocus."; emit newStatus(state); emit checkFocus(0.01); break; default: break; } // If an additional action is required, return return and continue later if (actionRequired) return true; // Otherwise, continue processing the queue else return executeOperationQueue(); } bool FilterManager::executeOneOperation(FilterState operation) { bool actionRequired = false; switch (operation) { default: break; } return actionRequired; } void FilterManager::setFocusOffsetComplete() { if (state == FILTER_OFFSET) executeOperationQueue(); } double FilterManager::getFilterExposure(const QString &name) const { if (m_currentFilterLabels.empty() || m_currentFilterPosition < 1 || m_currentFilterPosition > m_currentFilterLabels.count()) return 1; QString color = name; if (color.isEmpty()) color = m_currentFilterLabels[m_currentFilterPosition - 1]; // Search for locked filter by filter color name auto pos = std::find_if(m_ActiveFilters.begin(), m_ActiveFilters.end(), [color](OAL::Filter * oneFilter) { return (oneFilter->color() == color); }); if (pos != m_ActiveFilters.end()) return (*pos)->exposure(); // Default value return 1; } bool FilterManager::setFilterExposure(int index, double exposure) { if (m_currentFilterLabels.empty()) return false; QString color = m_currentFilterLabels[index]; for (int i = 0; i < m_ActiveFilters.count(); i++) { if (color == m_ActiveFilters[i]->color()) { filterModel->setData(filterModel->index(i, 5), exposure); filterModel->submitAll(); refreshFilterModel(); return true; } } return false; } bool FilterManager::setFilterAbsoluteFocusPosition(int index, int absFocusPos) { if (index < 0 || index >= m_currentFilterLabels.count()) return false; QString color = m_currentFilterLabels[index]; for (int i = 0; i < m_ActiveFilters.count(); i++) { if (color == m_ActiveFilters[i]->color()) { filterModel->setData(filterModel->index(i, 9), absFocusPos); filterModel->submitAll(); refreshFilterModel(); return true; } } return false; } QString FilterManager::getFilterLock(const QString &name) const { // Search for locked filter by filter color name auto pos = std::find_if(m_ActiveFilters.begin(), m_ActiveFilters.end(), [name](OAL::Filter * oneFilter) { return (oneFilter->color() == name); }); if (pos != m_ActiveFilters.end()) return (*pos)->lockedFilter(); // Default value return "--"; } void FilterManager::removeDevice(ISD::GDInterface *device) { if (m_currentFilterDevice && (m_currentFilterDevice->getDeviceName() == device->getDeviceName())) { m_FilterNameProperty = nullptr; m_FilterPositionProperty = nullptr; m_currentFilterDevice = nullptr; m_currentFilterLabels.clear(); m_currentFilterPosition = 0; qDeleteAll(m_ActiveFilters); m_ActiveFilters.clear(); delete(filterModel); filterModel = nullptr; } } void FilterManager::setFocusStatus(Ekos::FocusState focusState) { if (state == FILTER_AUTOFOCUS) { switch (focusState) { case FOCUS_COMPLETE: executeOperationQueue(); break; case FOCUS_FAILED: if (++retries == 3) { retries = 0; emit failed(); return; } // Restart again emit checkFocus(0.01); break; default: break; } } } bool FilterManager::syncAbsoluteFocusPosition(int index) { if (index < 0 || index > m_ActiveFilters.count()) { qCWarning(KSTARS_INDI) << __FUNCTION__ << "index" << index << "is out of bounds."; return false; } int absFocusPos = m_ActiveFilters[index]->absoluteFocusPosition(); if (m_FocusAbsPosition == absFocusPos) { m_FocusAbsPositionPending = false; return true; } else if (m_FocusAbsPositionPending == false) { m_FocusAbsPositionPending = true; emit newFocusOffset(absFocusPos, true); } return false; } + +bool FilterManager::setFilterNames(const QStringList &newLabels) +{ + if (m_currentFilterDevice == nullptr || m_currentFilterLabels.empty()) + return false; + + m_currentFilterDevice->runCommand(INDI_SET_FILTER_NAMES, const_cast(&newLabels)); + return true; +} + } diff --git a/kstars/ekos/auxiliary/filtermanager.h b/kstars/ekos/auxiliary/filtermanager.h index 369d963a6..66dbcda94 100644 --- a/kstars/ekos/auxiliary/filtermanager.h +++ b/kstars/ekos/auxiliary/filtermanager.h @@ -1,215 +1,217 @@ /* Ekos Filter Manager Copyright (C) 2017 Jasem Mutlaq This application is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version. */ #pragma once #include "ui_filtersettings.h" #include "ekos/ekos.h" #include #include #include #include #include #include #include class QSqlTableModel; class LockDelegate; class NotEditableDelegate; class ExposureDelegate; class OffsetDelegate; class UseAutoFocusDelegate; namespace Ekos { class FilterManager : public QDialog, public Ui::FilterSettings { Q_OBJECT public: typedef enum { CHANGE_POLICY = 1 << 0, OFFSET_POLICY = 1 << 1, AUTOFOCUS_POLICY = 1 << 2, ALL_POLICIES = CHANGE_POLICY | OFFSET_POLICY | AUTOFOCUS_POLICY, NO_AUTOFOCUS_POLICY = CHANGE_POLICY | OFFSET_POLICY } FilterPolicy; FilterManager(); void refreshFilterModel(); QStringList getFilterLabels(bool forceRefresh = false); int getFilterPosition(bool forceRefresh = false); // The target position and offset int getTargetFilterPosition() { return targetFilterPosition; } int getTargetFilterOffset() { return targetFilterOffset; } bool setFilterAbsoluteFocusPosition(int index, int absFocusPos); // Set absolute focus position, if supported, to the current filter absolute focus position. bool syncAbsoluteFocusPosition(int index); /** * @brief getFilterExposure Get optimal exposure time for the specified filter * @param name filter to obtain exposure time for * @return exposure time in seconds. */ double getFilterExposure(const QString &name = QString()) const; bool setFilterExposure(int index, double exposure); /** * @brief getFilterLock Return filter that should be used when running autofocus for the supplied filter * For example, "Red" filter can be locked to use "Lum" when doing autofocus. "Green" filter can be locked to "--" * which means that no filter change is necessary. * @param name filter which we need to query its locked filter. * @return locked filter. "--" indicates no locked filter and whatever current filter should be used. * */ QString getFilterLock(const QString &name) const; /** * @brief setCurrentFilterWheel Set the FilterManager active filter wheel. * @param filter pointer to filter wheel device */ void setCurrentFilterWheel(ISD::GDInterface *filter); /** * @brief setFocusReady Set whether a focuser device is active and in use. * @param enabled true if focus is ready, false otherwise. */ void setFocusReady(bool enabled) { m_FocusReady = enabled; } /** * @brief applyFilterFocusPolicies Check if we need to apply any filter policies for focus operations. */ void applyFilterFocusPolicies(); public slots: // Position. if applyPolicy is true then all filter offsets and autofocus & lock policies are applied. bool setFilterPosition(uint8_t position, Ekos::FilterManager::FilterPolicy policy = ALL_POLICIES); + // Change filter names + bool setFilterNames(const QStringList &newLabels); // Offset Request completed void setFocusOffsetComplete(); // Remove Device void removeDevice(ISD::GDInterface *device); // Refresh Filters after model update void reloadFilters(); // Focus Status void setFocusStatus(Ekos::FocusState focusState); // Set absolute focus position void setFocusAbsolutePosition(int value) { m_FocusAbsPosition = value; } // Inti filter property after connection void initFilterProperties(); signals: // Emitted only when there is a change in the filter slot number void positionChanged(int); // Emitted when filter change operation completed successfully including any focus offsets or auto-focus operation void labelsChanged(QStringList); // Emitted when filter exposure duration changes void exposureChanged(double); // Emitted when filter change completed including all required actions void ready(); // Emitted when operation fails void failed(); // Status signal void newStatus(Ekos::FilterState state); // Check Focus void checkFocus(double); // New Focus offset requested void newFocusOffset(int value, bool useAbsoluteOffset); private slots: void processText(ITextVectorProperty *tvp); void processNumber(INumberVectorProperty *nvp); void processSwitch(ISwitchVectorProperty *svp); private: // Filter Wheel Devices ISD::GDInterface *m_currentFilterDevice = { nullptr }; // Position and Labels QStringList m_currentFilterLabels; int m_currentFilterPosition = { -1 }; double m_currentFilterExposure = { -1 }; // Filter Structure QList m_ActiveFilters; OAL::Filter *targetFilter = { nullptr }; OAL::Filter *currentFilter = { nullptr }; bool m_useTargetFilter = { false }; // Autofocus retries uint8_t retries = { 0 }; int16_t lastFilterOffset { 0 }; // Table model QSqlTableModel *filterModel = { nullptr }; // INDI Properties of current active filter ITextVectorProperty *m_FilterNameProperty { nullptr }; INumberVectorProperty *m_FilterPositionProperty { nullptr }; ISwitchVectorProperty *m_FilterConfirmSet { nullptr }; // Operation stack void buildOperationQueue(FilterState operation); bool executeOperationQueue(); bool executeOneOperation(FilterState operation); // Update model void syncDBToINDI(); // Operation Queue QQueue operationQueue; FilterState state = { FILTER_IDLE }; int targetFilterPosition { -1 }; int targetFilterOffset { - 1 }; bool m_FocusReady { false }; bool m_FocusAbsPositionPending { false}; int m_FocusAbsPosition { -1 }; // Delegates QPointer lockDelegate; QPointer noEditDelegate; QPointer exposureDelegate; QPointer offsetDelegate; QPointer useAutoFocusDelegate; // Policies FilterPolicy m_Policy = { ALL_POLICIES }; bool m_ConfirmationPending { false }; }; } diff --git a/kstars/ekos/capture/capture.cpp b/kstars/ekos/capture/capture.cpp index a9e694c05..946b68d35 100644 --- a/kstars/ekos/capture/capture.cpp +++ b/kstars/ekos/capture/capture.cpp @@ -1,6909 +1,6949 @@ /* Ekos Copyright (C) 2012 Jasem Mutlaq This application is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version. */ #include "capture.h" #include "captureadaptor.h" #include "kstars.h" #include "kstarsdata.h" #include "Options.h" #include "rotatorsettings.h" #include "sequencejob.h" #include "skymap.h" #include "ui_calibrationoptions.h" #include "auxiliary/QProgressIndicator.h" #include "auxiliary/ksmessagebox.h" #include "ekos/manager.h" #include "ekos/auxiliary/darklibrary.h" #include "fitsviewer/fitsdata.h" #include "fitsviewer/fitsview.h" #include "indi/driverinfo.h" #include "indi/indifilter.h" #include "indi/clientmanager.h" #include "oal/observeradd.h" #include #include #define MF_TIMER_TIMEOUT 90000 #define GD_TIMER_TIMEOUT 60000 #define MF_RA_DIFF_LIMIT 4 // Wait 3-minutes as maximum beyond exposure // value. #define CAPTURE_TIMEOUT_THRESHOLD 180000 // Current Sequence File Format: #define SQ_FORMAT_VERSION 2.0 // We accept file formats with version back to: #define SQ_COMPAT_VERSION 2.0 namespace Ekos { Capture::Capture() { setupUi(this); qRegisterMetaType("Ekos::CaptureState"); qDBusRegisterMetaType(); new CaptureAdaptor(this); QDBusConnection::sessionBus().registerObject("/KStars/Ekos/Capture", this); QPointer ekosInterface = new QDBusInterface("org.kde.kstars", "/KStars/Ekos", "org.kde.kstars.Ekos", QDBusConnection::sessionBus(), this); // Connecting DBus signals QDBusConnection::sessionBus().connect("org.kde.kstars", "/KStars/Ekos", "org.kde.kstars.Ekos", "newModule", this, SLOT(registerNewModule(QString))); //connect(ekosInterface, SIGNAL(newModule(QString)), this, SLOT(registerNewModule(QString))); // ensure that the mount interface is present registerNewModule("Mount"); KStarsData::Instance()->userdb()->GetAllDSLRInfos(DSLRInfos); if (DSLRInfos.count() > 0) { qCDebug(KSTARS_EKOS_CAPTURE) << "DSLR Cameras Info:"; qCDebug(KSTARS_EKOS_CAPTURE) << DSLRInfos; } dirPath = QUrl::fromLocalFile(QDir::homePath()); //isAutoGuiding = false; rotatorSettings.reset(new RotatorSettings(this)); pi = new QProgressIndicator(this); progressLayout->addWidget(pi, 0, 4, 1, 1); seqFileCount = 0; //seqWatcher = new KDirWatch(); seqTimer = new QTimer(this); connect(seqTimer, &QTimer::timeout, this, &Ekos::Capture::captureImage); connect(startB, &QPushButton::clicked, this, &Ekos::Capture::toggleSequence); connect(pauseB, &QPushButton::clicked, this, &Ekos::Capture::pause); startB->setIcon(QIcon::fromTheme("media-playback-start")); startB->setAttribute(Qt::WA_LayoutUsesWidgetRect); pauseB->setIcon(QIcon::fromTheme("media-playback-pause")); pauseB->setAttribute(Qt::WA_LayoutUsesWidgetRect); filterManagerB->setIcon(QIcon::fromTheme("view-filter")); filterManagerB->setAttribute(Qt::WA_LayoutUsesWidgetRect); FilterDevicesCombo->addItem("--"); connect(binXIN, static_cast(&QSpinBox::valueChanged), binYIN, &QSpinBox::setValue); connect(CCDCaptureCombo, static_cast(&QComboBox::activated), this, &Ekos::Capture::setDefaultCCD); connect(CCDCaptureCombo, static_cast(&QComboBox::activated), this, &Ekos::Capture::checkCCD); connect(liveVideoB, &QPushButton::clicked, this, &Ekos::Capture::toggleVideo); guideDeviationTimer.setInterval(GD_TIMER_TIMEOUT); connect(&guideDeviationTimer, &QTimer::timeout, this, &Ekos::Capture::checkGuideDeviationTimeout); connect(clearConfigurationB, &QPushButton::clicked, this, &Ekos::Capture::clearCameraConfiguration); connect(FilterDevicesCombo, static_cast(&QComboBox::activated), this, &Ekos::Capture::setDefaultFilterWheel); connect(FilterDevicesCombo, static_cast(&QComboBox::activated), this, &Ekos::Capture::checkFilter); connect(temperatureCheck, &QCheckBox::toggled, [this](bool toggled) { if (currentCCD) { QVariantMap auxInfo = currentCCD->getDriverInfo()->getAuxInfo(); auxInfo[QString("%1_TC").arg(currentCCD->getDeviceName())] = toggled; currentCCD->getDriverInfo()->setAuxInfo(auxInfo); } }); + connect(filterEditB, &QPushButton::clicked, this, &Ekos::Capture::editFilterName); + connect(FilterPosCombo, static_cast(&QComboBox::currentIndexChanged), [ = ]() { updateHFRThreshold(); }); connect(previewB, &QPushButton::clicked, this, &Ekos::Capture::captureOne); connect(loopB, &QPushButton::clicked, this, &Ekos::Capture::startFraming); //connect( seqWatcher, SIGNAL(dirty(QString)), this, &Ekos::Capture::checkSeqFile(QString))); connect(addToQueueB, &QPushButton::clicked, this, &Ekos::Capture::addJob); connect(removeFromQueueB, &QPushButton::clicked, this, &Ekos::Capture::removeJobFromQueue); connect(queueUpB, &QPushButton::clicked, this, &Ekos::Capture::moveJobUp); connect(queueDownB, &QPushButton::clicked, this, &Ekos::Capture::moveJobDown); connect(selectFITSDirB, &QPushButton::clicked, this, &Ekos::Capture::saveFITSDirectory); connect(queueSaveB, &QPushButton::clicked, this, static_cast(&Ekos::Capture::saveSequenceQueue)); connect(queueSaveAsB, &QPushButton::clicked, this, &Ekos::Capture::saveSequenceQueueAs); connect(queueLoadB, &QPushButton::clicked, this, static_cast(&Ekos::Capture::loadSequenceQueue)); connect(resetB, &QPushButton::clicked, this, &Ekos::Capture::resetJobs); connect(queueTable->selectionModel(), &QItemSelectionModel::currentRowChanged, this, &Ekos::Capture::selectedJobChanged); connect(queueTable, &QAbstractItemView::doubleClicked, this, &Ekos::Capture::editJob); connect(queueTable, &QTableWidget::itemSelectionChanged, this, &Ekos::Capture::resetJobEdit); connect(setTemperatureB, &QPushButton::clicked, [&]() { if (currentCCD) currentCCD->setTemperature(temperatureIN->value()); }); connect(coolerOnB, &QPushButton::clicked, [&]() { if (currentCCD) currentCCD->setCoolerControl(true); }); connect(coolerOffB, &QPushButton::clicked, [&]() { if (currentCCD) currentCCD->setCoolerControl(false); }); connect(temperatureIN, &QDoubleSpinBox::editingFinished, setTemperatureB, static_cast(&QPushButton::setFocus)); connect(frameTypeCombo, static_cast(&QComboBox::activated), this, &Ekos::Capture::checkFrameType); connect(resetFrameB, &QPushButton::clicked, this, &Ekos::Capture::resetFrame); connect(calibrationB, &QPushButton::clicked, this, &Ekos::Capture::openCalibrationDialog); connect(rotatorB, &QPushButton::clicked, rotatorSettings.get(), &Ekos::Capture::show); addToQueueB->setIcon(QIcon::fromTheme("list-add")); addToQueueB->setAttribute(Qt::WA_LayoutUsesWidgetRect); removeFromQueueB->setIcon(QIcon::fromTheme("list-remove")); removeFromQueueB->setAttribute(Qt::WA_LayoutUsesWidgetRect); queueUpB->setIcon(QIcon::fromTheme("go-up")); queueUpB->setAttribute(Qt::WA_LayoutUsesWidgetRect); queueDownB->setIcon(QIcon::fromTheme("go-down")); queueDownB->setAttribute(Qt::WA_LayoutUsesWidgetRect); selectFITSDirB->setIcon( QIcon::fromTheme("document-open-folder")); selectFITSDirB->setAttribute(Qt::WA_LayoutUsesWidgetRect); queueLoadB->setIcon(QIcon::fromTheme("document-open")); queueLoadB->setAttribute(Qt::WA_LayoutUsesWidgetRect); queueSaveB->setIcon(QIcon::fromTheme("document-save")); queueSaveB->setAttribute(Qt::WA_LayoutUsesWidgetRect); queueSaveAsB->setIcon(QIcon::fromTheme("document-save-as")); queueSaveAsB->setAttribute(Qt::WA_LayoutUsesWidgetRect); resetB->setIcon(QIcon::fromTheme("system-reboot")); resetB->setAttribute(Qt::WA_LayoutUsesWidgetRect); resetFrameB->setIcon(QIcon::fromTheme("view-refresh")); resetFrameB->setAttribute(Qt::WA_LayoutUsesWidgetRect); calibrationB->setIcon(QIcon::fromTheme("run-build")); calibrationB->setAttribute(Qt::WA_LayoutUsesWidgetRect); rotatorB->setIcon(QIcon::fromTheme("kstars_solarsystem")); rotatorB->setAttribute(Qt::WA_LayoutUsesWidgetRect); addToQueueB->setToolTip(i18n("Add job to sequence queue")); removeFromQueueB->setToolTip(i18n("Remove job from sequence queue")); fitsDir->setText(Options::fitsDir()); for (auto &filter : FITSViewer::filterTypes) filterCombo->addItem(filter); //////////////////////////////////////////////////////////////////////// /// Settings //////////////////////////////////////////////////////////////////////// // #1 Guide Deviation Check guideDeviationCheck->setChecked(Options::enforceGuideDeviation()); connect(guideDeviationCheck, &QCheckBox::toggled, [ = ](bool checked) { Options::setEnforceGuideDeviation(checked); }); // #2 Guide Deviation Value guideDeviation->setValue(Options::guideDeviation()); connect(guideDeviation, &QDoubleSpinBox::editingFinished, [ = ]() { Options::setGuideDeviation(guideDeviation->value()); }); // 3. Autofocus HFR Check autofocusCheck->setChecked(Options::enforceAutofocus()); connect(autofocusCheck, &QCheckBox::toggled, [ = ](bool checked) { Options::setEnforceAutofocus(checked); if (checked == false) isInSequenceFocus = false; }); // 4. Autofocus HFR Deviation HFRPixels->setValue(Options::hFRDeviation()); connect(HFRPixels, &QDoubleSpinBox::editingFinished, [ = ]() { Options::setHFRDeviation(HFRPixels->value()); }); // 5. Autofocus temperature Check temperatureDeltaCheck->setChecked(Options::enforceAutofocusOnTemperature()); connect(temperatureDeltaCheck, &QCheckBox::toggled, [ = ](bool checked) { Options::setEnforceAutofocusOnTemperature(checked); if (checked == false) isTemperatureDeltaCheckActive = false; }); // 6. Autofocus temperature Delta temperatureDelta->setValue(Options::maxFocusTemperatureDelta()); connect(temperatureDelta, &QDoubleSpinBox::editingFinished, [ = ]() { Options::setMaxFocusTemperatureDelta(temperatureDelta->value()); }); // 7. Refocus Every Check refocusEveryNCheck->setChecked(Options::enforceRefocusEveryN()); connect(refocusEveryNCheck, &QCheckBox::toggled, [ = ](bool checked) { Options::setEnforceRefocusEveryN(checked); }); // 8. Refocus Every Value refocusEveryN->setValue(Options::refocusEveryN()); connect(refocusEveryN, &QDoubleSpinBox::editingFinished, [ = ]() { Options::setRefocusEveryN(refocusEveryN->value()); }); // 9. File settings: filter name filterCheck->setChecked(Options::fileSettingsUseFilter()); connect(filterCheck, &QCheckBox::toggled, [ = ](bool checked) { Options::setFileSettingsUseFilter(checked); }); // 10. File settings: duration expDurationCheck->setChecked(Options::fileSettingsUseDuration()); connect(expDurationCheck, &QCheckBox::toggled, [ = ](bool checked) { Options::setFileSettingsUseDuration(checked); }); // 11. File settings: timestamp ISOCheck->setChecked(Options::fileSettingsUseTimestamp()); connect(ISOCheck, &QCheckBox::toggled, [ = ](bool checked) { Options::setFileSettingsUseTimestamp(checked); }); QCheckBox * const checkBoxes[] = { guideDeviationCheck, refocusEveryNCheck, guideDeviationCheck, }; for (const QCheckBox * control : checkBoxes) connect(control, &QCheckBox::toggled, this, &Ekos::Capture::setDirty); QDoubleSpinBox * const dspinBoxes[] { HFRPixels, temperatureDelta, guideDeviation, }; for (const QDoubleSpinBox * control : dspinBoxes) connect(control, static_cast(&QDoubleSpinBox::valueChanged), this, &Ekos::Capture::setDirty); connect(uploadModeCombo, static_cast(&QComboBox::activated), this, &Ekos::Capture::setDirty); connect(remoteDirIN, &QLineEdit::editingFinished, this, &Ekos::Capture::setDirty); m_ObserverName = Options::defaultObserver(); observerB->setIcon(QIcon::fromTheme("im-user")); observerB->setAttribute(Qt::WA_LayoutUsesWidgetRect); connect(observerB, &QPushButton::clicked, this, &Ekos::Capture::showObserverDialog); // Exposure Timeout captureTimeout.setSingleShot(true); connect(&captureTimeout, &QTimer::timeout, this, &Ekos::Capture::processCaptureTimeout); // Post capture script connect(&postCaptureScript, static_cast(&QProcess::finished), this, &Ekos::Capture::postScriptFinished); // Remote directory connect(uploadModeCombo, static_cast(&QComboBox::activated), this, [&](int index) { remoteDirIN->setEnabled(index != 0); }); customPropertiesDialog.reset(new CustomProperties()); connect(customValuesB, &QPushButton::clicked, [&]() { customPropertiesDialog.get()->show(); customPropertiesDialog.get()->raise(); }); connect(customPropertiesDialog.get(), &CustomProperties::valueChanged, [&]() { const double newGain = getGain(); if (GainSpin && newGain >= 0) GainSpin->setValue(newGain); }); flatFieldSource = static_cast(Options::calibrationFlatSourceIndex()); flatFieldDuration = static_cast(Options::calibrationFlatDurationIndex()); wallCoord.setAz(Options::calibrationWallAz()); wallCoord.setAlt(Options::calibrationWallAlt()); targetADU = Options::calibrationADUValue(); targetADUTolerance = Options::calibrationADUValueTolerance(); fitsDir->setText(Options::captureDirectory()); connect(fitsDir, &QLineEdit::textChanged, [&]() { Options::setCaptureDirectory(fitsDir->text()); }); if (Options::remoteCaptureDirectory().isEmpty() == false) { remoteDirIN->setText(Options::remoteCaptureDirectory()); } connect(remoteDirIN, &QLineEdit::editingFinished, [&]() { Options::setRemoteCaptureDirectory(remoteDirIN->text()); }); // Keep track of TARGET transfer format when changing CCDs (FITS or NATIVE). Actual format is not changed until capture connect( transferFormatCombo, static_cast(&QComboBox::activated), this, [&](int index) { if (currentCCD) currentCCD->setTargetTransferFormat(static_cast(index)); Options::setCaptureFormatIndex(index); }); // Load FIlter Offets //loadFilterOffsets(); //Note: This is to prevent a button from being called the default button //and then executing when the user hits the enter key such as when on a Text Box QList qButtons = findChildren(); for (auto &button : qButtons) button->setAutoDefault(false); //This Timer will update the Exposure time in the capture module to display the estimated download time left //It will also update the Exposure time left in the Summary Screen. //It fires every 100 ms while images are downloading. downloadProgressTimer.setInterval(100); connect(&downloadProgressTimer, &QTimer::timeout, this, &Ekos::Capture::setDownloadProgress); } Capture::~Capture() { qDeleteAll(jobs); } void Capture::setDefaultCCD(QString ccd) { Options::setDefaultCaptureCCD(ccd); } void Capture::setDefaultFilterWheel(QString filterWheel) { Options::setDefaultCaptureFilterWheel(filterWheel); } void Capture::addCCD(ISD::GDInterface * newCCD) { ISD::CCD * ccd = static_cast(newCCD); if (CCDs.contains(ccd)) return; CCDs.append(ccd); CCDCaptureCombo->addItem(ccd->getDeviceName()); if (Filters.count() > 0) syncFilterInfo(); checkCCD(); emit settingsUpdated(getSettings()); } void Capture::addGuideHead(ISD::GDInterface * newCCD) { QString guiderName = newCCD->getDeviceName() + QString(" Guider"); if (CCDCaptureCombo->findText(guiderName) == -1) { CCDCaptureCombo->addItem(guiderName); CCDs.append(static_cast(newCCD)); } } void Capture::addFilter(ISD::GDInterface * newFilter) { foreach (ISD::GDInterface * filter, Filters) { if (filter->getDeviceName() == newFilter->getDeviceName()) return; } FilterDevicesCombo->addItem(newFilter->getDeviceName()); Filters.append(static_cast(newFilter)); filterManagerB->setEnabled(true); int filterWheelIndex = 1; if (Options::defaultCaptureFilterWheel().isEmpty() == false) filterWheelIndex = FilterDevicesCombo->findText(Options::defaultCaptureFilterWheel()); if (filterWheelIndex < 1) filterWheelIndex = 1; checkFilter(filterWheelIndex); FilterDevicesCombo->setCurrentIndex(filterWheelIndex); emit settingsUpdated(getSettings()); } void Capture::pause() { if (m_State != CAPTURE_CAPTURING) { // Ensure that the pause function is only called during frame capturing // Handling it this way is by far easier than trying to enable/disable the pause button // Fixme: make pausing possible at all stages. This makes it necessary to separate the pausing states from CaptureState. appendLogText(i18n("Pausing only possible while frame capture is running.")); qCInfo(KSTARS_EKOS_CAPTURE) << "Pause button pressed while not capturing."; return; } pauseFunction = nullptr; m_State = CAPTURE_PAUSE_PLANNED; emit newStatus(Ekos::CAPTURE_PAUSE_PLANNED); appendLogText(i18n("Sequence shall be paused after current exposure is complete.")); pauseB->setEnabled(false); startB->setIcon(QIcon::fromTheme("media-playback-start")); startB->setToolTip(i18n("Resume Sequence")); } void Capture::toggleSequence() { if (m_State == CAPTURE_PAUSE_PLANNED || m_State == CAPTURE_PAUSED) { startB->setIcon( QIcon::fromTheme("media-playback-stop")); startB->setToolTip(i18n("Stop Sequence")); pauseB->setEnabled(true); m_State = CAPTURE_CAPTURING; emit newStatus(Ekos::CAPTURE_CAPTURING); appendLogText(i18n("Sequence resumed.")); // Call from where ever we have left of when we paused if (pauseFunction) (this->*pauseFunction)(); } else if (m_State == CAPTURE_IDLE || m_State == CAPTURE_ABORTED || m_State == CAPTURE_COMPLETE) { start(); } else { abort(); } } void Capture::registerNewModule(const QString &name) { qCDebug(KSTARS_EKOS_CAPTURE) << "Registering new Module (" << name << ")"; if (name == "Mount") { delete mountInterface; mountInterface = new QDBusInterface("org.kde.kstars", "/KStars/Ekos/Mount", "org.kde.kstars.Ekos.Mount", QDBusConnection::sessionBus(), this); } } void Capture::start() { if (darkSubCheck->isChecked()) { KSNotification::error(i18n("Auto dark subtract is not supported in batch mode.")); return; } // Reset progress option if there is no captured frame map set at the time of start - fixes the end-user setting the option just before starting ignoreJobProgress = !capturedFramesMap.count() && Options::alwaysResetSequenceWhenStarting(); if (queueTable->rowCount() == 0) { if (addJob() == false) return; } SequenceJob * first_job = nullptr; foreach (SequenceJob * job, jobs) { if (job->getStatus() == SequenceJob::JOB_IDLE || job->getStatus() == SequenceJob::JOB_ABORTED) { first_job = job; break; } } // If there are no idle nor aborted jobs, question is whether to reset and restart // Scheduler will start a non-empty new job each time and doesn't use this execution path if (first_job == nullptr) { // If we have at least one job that are in error, bail out, even if ignoring job progress foreach (SequenceJob * job, jobs) { if (job->getStatus() != SequenceJob::JOB_DONE) { appendLogText(i18n("No pending jobs found. Please add a job to the sequence queue.")); return; } } // If we only have completed jobs and we don't ignore job progress, ask the end-user what to do if (!ignoreJobProgress) if(KMessageBox::warningContinueCancel( nullptr, i18n("All jobs are complete. Do you want to reset the status of all jobs and restart capturing?"), i18n("Reset job status"), KStandardGuiItem::cont(), KStandardGuiItem::cancel(), "reset_job_complete_status_warning") != KMessageBox::Continue) return; // If the end-user accepted to reset, reset all jobs and restart foreach (SequenceJob * job, jobs) job->resetStatus(); first_job = jobs.first(); } // If we need to ignore job progress, systematically reset all jobs and restart // Scheduler will never ignore job progress and doesn't use this path else if (ignoreJobProgress) { appendLogText(i18n("Warning: option \"Always Reset Sequence When Starting\" is enabled and resets the sequence counts.")); foreach (SequenceJob * job, jobs) job->resetStatus(); } // Refocus timer should not be reset on deviation error if (m_DeviationDetected == false && m_State != CAPTURE_SUSPENDED) { // start timer to measure time until next forced refocus startRefocusEveryNTimer(); } // Only reset these counters if we are NOT restarting from deviation errors // So when starting a new job or fresh then we reset them. if (m_DeviationDetected == false) { ditherCounter = Options::ditherFrames(); inSequenceFocusCounter = Options::inSequenceCheckFrames(); } m_DeviationDetected = false; m_SpikeDetected = false; m_State = CAPTURE_PROGRESS; emit newStatus(Ekos::CAPTURE_PROGRESS); startB->setIcon(QIcon::fromTheme("media-playback-stop")); startB->setToolTip(i18n("Stop Sequence")); pauseB->setEnabled(true); setBusy(true); if (guideDeviationCheck->isChecked() && autoGuideReady == false) appendLogText(i18n("Warning: Guide deviation is selected but autoguide process was not started.")); if (autofocusCheck->isChecked() && m_AutoFocusReady == false) appendLogText(i18n("Warning: in-sequence focusing is selected but autofocus process was not started.")); if (temperatureDeltaCheck->isChecked() && m_AutoFocusReady == false) appendLogText(i18n("Warning: temperature delta check is selected but autofocus process was not started.")); prepareJob(first_job); } void Capture::stop(CaptureState targetState) { retries = 0; //seqTotalCount = 0; //seqCurrentCount = 0; captureTimeout.stop(); ADURaw.clear(); ExpRaw.clear(); if (activeJob) { if (activeJob->getStatus() == SequenceJob::JOB_BUSY) { QString stopText; switch (targetState) { case CAPTURE_IDLE: stopText = i18n("CCD capture stopped"); break; case CAPTURE_SUSPENDED: stopText = i18n("CCD capture suspended"); break; default: stopText = i18n("CCD capture aborted"); break; } KSNotification::event(QLatin1String("CaptureFailed"), stopText); appendLogText(stopText); activeJob->abort(); if (activeJob->isPreview() == false) { int index = jobs.indexOf(activeJob); QJsonObject oneSequence = m_SequenceArray[index].toObject(); oneSequence["Status"] = "Aborted"; m_SequenceArray.replace(index, oneSequence); emit sequenceChanged(m_SequenceArray); } } // In case of batch job if (activeJob->isPreview() == false) { activeJob->disconnect(this); activeJob->reset(); } // or preview job in calibration stage else if (calibrationStage == CAL_CALIBRATION) { activeJob->disconnect(this); activeJob->reset(); activeJob->setPreview(false); currentCCD->setUploadMode(rememberUploadMode); } // or regular preview job else { currentCCD->setUploadMode(rememberUploadMode); jobs.removeOne(activeJob); // Delete preview job delete (activeJob); activeJob = nullptr; emit newStatus(targetState); } } // Only emit a new status if there is an active job or if capturing is suspended. // The latter is necessary since suspending clears the active job, but the Capture // module keeps the control. if (activeJob != nullptr || m_State == CAPTURE_SUSPENDED) emit newStatus(targetState); calibrationStage = CAL_NONE; m_State = targetState; // Turn off any calibration light, IF they were turned on by Capture module if (currentDustCap && dustCapLightEnabled) { dustCapLightEnabled = false; currentDustCap->SetLightEnabled(false); } if (currentLightBox && lightBoxLightEnabled) { lightBoxLightEnabled = false; currentLightBox->SetLightEnabled(false); } if (meridianFlipStage == MF_NONE) secondsLabel->clear(); disconnect(currentCCD, &ISD::CCD::BLOBUpdated, this, &Ekos::Capture::newFITS); disconnect(currentCCD, &ISD::CCD::newExposureValue, this, &Ekos::Capture::setExposureProgress); disconnect(currentCCD, &ISD::CCD::previewFITSGenerated, this, &Ekos::Capture::setGeneratedPreviewFITS); disconnect(currentCCD, &ISD::CCD::ready, this, &Ekos::Capture::ready); currentCCD->setFITSDir(""); // In case of exposure looping, let's abort if (currentCCD->isLooping()) targetChip->abortExposure(); imgProgress->reset(); imgProgress->setEnabled(false); fullImgCountOUT->setText(QString()); currentImgCountOUT->setText(QString()); exposeOUT->setText(QString()); m_isLooping = false; setBusy(false); if (m_State == CAPTURE_ABORTED || m_State == CAPTURE_SUSPENDED) { startB->setIcon( QIcon::fromTheme("media-playback-start")); startB->setToolTip(i18n("Start Sequence")); pauseB->setEnabled(false); } //foreach (QAbstractButton *button, queueEditButtonGroup->buttons()) //button->setEnabled(true); seqTimer->stop(); activeJob = nullptr; // meridian flip may take place if requested setMeridianFlipStage(MF_READY); } void Capture::sendNewImage(const QString &filename, ISD::CCDChip * myChip) { if (activeJob && (myChip == nullptr || myChip == targetChip)) { activeJob->setProperty("filename", filename); emit newImage(activeJob); // We only emit this for client/both images since remote images already send this automatically if (currentCCD->getUploadMode() != ISD::CCD::UPLOAD_LOCAL && activeJob->isPreview() == false) { emit newSequenceImage(filename, m_GeneratedPreviewFITS); m_GeneratedPreviewFITS.clear(); } } } bool Capture::setCamera(const QString &device) { for (int i = 0; i < CCDCaptureCombo->count(); i++) if (device == CCDCaptureCombo->itemText(i)) { CCDCaptureCombo->setCurrentIndex(i); checkCCD(i); return true; } return false; } QString Capture::camera() { if (currentCCD) return currentCCD->getDeviceName(); return QString(); } void Capture::checkCCD(int ccdNum) { if (ccdNum == -1) { ccdNum = CCDCaptureCombo->currentIndex(); if (ccdNum == -1) return; } if (ccdNum < CCDs.count()) { // Check whether main camera or guide head only currentCCD = CCDs.at(ccdNum); if (CCDCaptureCombo->itemText(ccdNum).right(6) == QString("Guider")) { useGuideHead = true; targetChip = currentCCD->getChip(ISD::CCDChip::GUIDE_CCD); } else { currentCCD = CCDs.at(ccdNum); targetChip = currentCCD->getChip(ISD::CCDChip::PRIMARY_CCD); useGuideHead = false; } // Make sure we have a valid chip and valid base device. // Make sure we are not in capture process. if (!targetChip || !targetChip->getCCD() || !targetChip->getCCD()->getBaseDevice() || targetChip->isCapturing()) return; for (auto &ccd : CCDs) { disconnect(ccd, &ISD::CCD::numberUpdated, this, &Ekos::Capture::processCCDNumber); disconnect(ccd, &ISD::CCD::newTemperatureValue, this, &Ekos::Capture::updateCCDTemperature); disconnect(ccd, &ISD::CCD::coolerToggled, this, &Ekos::Capture::setCoolerToggled); disconnect(ccd, &ISD::CCD::newRemoteFile, this, &Ekos::Capture::setNewRemoteFile); disconnect(ccd, &ISD::CCD::videoStreamToggled, this, &Ekos::Capture::setVideoStreamEnabled); disconnect(ccd, &ISD::CCD::ready, this, &Ekos::Capture::ready); } if (currentCCD->hasCoolerControl()) { coolerOnB->setEnabled(true); coolerOffB->setEnabled(true); coolerOnB->setChecked(currentCCD->isCoolerOn()); coolerOffB->setChecked(!currentCCD->isCoolerOn()); } else { coolerOnB->setEnabled(false); coolerOnB->setChecked(false); coolerOffB->setEnabled(false); coolerOffB->setChecked(false); } if (currentCCD->hasCooler()) { temperatureCheck->setEnabled(true); temperatureIN->setEnabled(true); if (currentCCD->getBaseDevice()->getPropertyPermission("CCD_TEMPERATURE") != IP_RO) { double min, max, step; setTemperatureB->setEnabled(true); temperatureIN->setReadOnly(false); temperatureCheck->setEnabled(true); currentCCD->getMinMaxStep("CCD_TEMPERATURE", "CCD_TEMPERATURE_VALUE", &min, &max, &step); temperatureIN->setMinimum(min); temperatureIN->setMaximum(max); temperatureIN->setSingleStep(1); bool isChecked = currentCCD->getDriverInfo()->getAuxInfo().value(QString("%1_TC").arg(currentCCD->getDeviceName()), false).toBool(); temperatureCheck->setChecked(isChecked); } else { setTemperatureB->setEnabled(false); temperatureIN->setReadOnly(true); temperatureCheck->setEnabled(false); temperatureCheck->setChecked(false); } double temperature = 0; if (currentCCD->getTemperature(&temperature)) { temperatureOUT->setText(QString("%L1").arg(temperature, 0, 'f', 2)); if (temperatureIN->cleanText().isEmpty()) temperatureIN->setValue(temperature); } } else { temperatureCheck->setEnabled(false); temperatureIN->setEnabled(false); temperatureIN->clear(); temperatureOUT->clear(); setTemperatureB->setEnabled(false); } updateFrameProperties(); QStringList frameTypes = targetChip->getFrameTypes(); frameTypeCombo->clear(); if (frameTypes.isEmpty()) frameTypeCombo->setEnabled(false); else { frameTypeCombo->setEnabled(true); frameTypeCombo->addItems(frameTypes); frameTypeCombo->setCurrentIndex(targetChip->getFrameType()); } QStringList isoList = targetChip->getISOList(); //ISOCombo->clear(); transferFormatCombo->blockSignals(true); transferFormatCombo->clear(); delete (ISOCombo); delete (GainSpin); //ISOLabel->hide(); if (isoList.isEmpty()) { // Only one transfer format transferFormatCombo->addItem(i18n("FITS")); if (currentCCD->hasGain()) { ISOLabel->setText(QString("%1:").arg(i18nc("Camera Gain", "Gain"))); //ISOLabel->show(); GainSpin = new QDoubleSpinBox(CCDFWGroup); double min, max, step, value, targetCustomGain; currentCCD->getGainMinMaxStep(&min, &max, &step); // Allow the possibility of no gain value at all. GainSpinSpecialValue = min - step; GainSpin->setRange(GainSpinSpecialValue, max); GainSpin->setSpecialValueText(i18n("--")); GainSpin->setSingleStep(step); currentCCD->getGain(&value); targetCustomGain = getGain(); // Set the custom gain if we have one // otherwise it will not have an effect. if (targetCustomGain > 0) GainSpin->setValue(targetCustomGain); else GainSpin->setValue(GainSpinSpecialValue); GainSpin->setReadOnly(currentCCD->getGainPermission() == IP_RO); connect(GainSpin, &QDoubleSpinBox::editingFinished, [this]() { if (GainSpin->value() != GainSpinSpecialValue) setGain(GainSpin->value()); }); - gridLayout->addWidget(GainSpin, 4, 5, 1, 2); + gridLayout->addWidget(GainSpin, 4, 3, 1, 2); } } else { ISOLabel->setText(QString("%1:").arg(i18nc("Camera ISO", "ISO"))); //ISOLabel->show(); ISOCombo = new QComboBox(CCDFWGroup); ISOCombo->addItems(isoList); ISOCombo->setCurrentIndex(targetChip->getISOIndex()); - gridLayout->addWidget(ISOCombo, 4, 5, 1, 2); + gridLayout->addWidget(ISOCombo, 4, 3, 1, 2); // DSLRs have two transfer formats transferFormatCombo->addItem(i18n("FITS")); transferFormatCombo->addItem(i18n("Native")); //transferFormatCombo->setCurrentIndex(currentCCD->getTargetTransferFormat()); // 2018-05-07 JM: Set value to the value in options transferFormatCombo->setCurrentIndex(Options::captureFormatIndex()); uint16_t w, h; uint8_t bbp {8}; double pixelX = 0, pixelY = 0; bool rc = targetChip->getImageInfo(w, h, pixelX, pixelY, bbp); bool isModelInDB = isModelinDSLRInfo(QString(currentCCD->getDeviceName())); // If rc == true, then the property has been defined by the driver already // Only then we check if the pixels are zero if (rc == true && (pixelX == 0 || pixelY == 0 || isModelInDB == false)) { // If model is already in database, no need to show dialog // The zeros above are the initial packets so we can safely ignore them if (isModelInDB == false) { createDSLRDialog(); } else { QString model = QString(currentCCD->getDeviceName()); syncDSLRToTargetChip(model); } } } transferFormatCombo->blockSignals(false); customPropertiesDialog->setCCD(currentCCD); liveVideoB->setEnabled(currentCCD->hasVideoStream()); if (currentCCD->hasVideoStream()) setVideoStreamEnabled(currentCCD->isStreamingEnabled()); else liveVideoB->setIcon(QIcon::fromTheme("camera-off")); connect(currentCCD, &ISD::CCD::numberUpdated, this, &Ekos::Capture::processCCDNumber, Qt::UniqueConnection); connect(currentCCD, &ISD::CCD::newTemperatureValue, this, &Ekos::Capture::updateCCDTemperature, Qt::UniqueConnection); connect(currentCCD, &ISD::CCD::coolerToggled, this, &Ekos::Capture::setCoolerToggled, Qt::UniqueConnection); connect(currentCCD, &ISD::CCD::newRemoteFile, this, &Ekos::Capture::setNewRemoteFile); connect(currentCCD, &ISD::CCD::videoStreamToggled, this, &Ekos::Capture::setVideoStreamEnabled); connect(currentCCD, &ISD::CCD::ready, this, &Ekos::Capture::ready); } } void Capture::setGuideChip(ISD::CCDChip * chip) { guideChip = chip; // We should suspend guide in two scenarios: // 1. If guide chip is within the primary CCD, then we cannot download any data from guide chip while primary CCD is downloading. // 2. If we have two CCDs running from ONE driver (Multiple-Devices-Per-Driver mpdp is true). Same issue as above, only one download // at a time. // After primary CCD download is complete, we resume guiding. suspendGuideOnDownload = (currentCCD->getChip(ISD::CCDChip::GUIDE_CCD) == guideChip) || (guideChip->getCCD() == currentCCD && currentCCD->getDriverInfo()->getAuxInfo().value("mdpd", false).toBool()); } void Capture::resetFrameToZero() { frameXIN->setMinimum(0); frameXIN->setMaximum(0); frameXIN->setValue(0); frameYIN->setMinimum(0); frameYIN->setMaximum(0); frameYIN->setValue(0); frameWIN->setMinimum(0); frameWIN->setMaximum(0); frameWIN->setValue(0); frameHIN->setMinimum(0); frameHIN->setMaximum(0); frameHIN->setValue(0); } void Capture::updateFrameProperties(int reset) { int binx = 1, biny = 1; double min, max, step; int xstep = 0, ystep = 0; QString frameProp = useGuideHead ? QString("GUIDER_FRAME") : QString("CCD_FRAME"); QString exposureProp = useGuideHead ? QString("GUIDER_EXPOSURE") : QString("CCD_EXPOSURE"); QString exposureElem = useGuideHead ? QString("GUIDER_EXPOSURE_VALUE") : QString("CCD_EXPOSURE_VALUE"); targetChip = useGuideHead ? currentCCD->getChip(ISD::CCDChip::GUIDE_CCD) : currentCCD->getChip(ISD::CCDChip::PRIMARY_CCD); frameWIN->setEnabled(targetChip->canSubframe()); frameHIN->setEnabled(targetChip->canSubframe()); frameXIN->setEnabled(targetChip->canSubframe()); frameYIN->setEnabled(targetChip->canSubframe()); binXIN->setEnabled(targetChip->canBin()); binYIN->setEnabled(targetChip->canBin()); QList exposureValues; 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 << 10 << 20 << 30 << 40 << 50 << 60 << 120 << 180 << 300 << 600 << 900 << 1200 << 1800; if (currentCCD->getMinMaxStep(exposureProp, exposureElem, &min, &max, &step)) { if (min < 0.001) exposureIN->setDecimals(6); else exposureIN->setDecimals(3); for(int i = 0; i < exposureValues.count(); i++) { double value = exposureValues.at(i); if(value < min || value > max) { exposureValues.removeAt(i); i--; //So we don't skip one } } exposureValues.prepend(min); exposureValues.append(max); } exposureIN->setRecommendedValues(exposureValues); if (currentCCD->getMinMaxStep(frameProp, "WIDTH", &min, &max, &step)) { if (min >= max) { resetFrameToZero(); return; } if (step == 0) xstep = static_cast(max * 0.05); else xstep = step; if (min >= 0 && max > 0) { frameWIN->setMinimum(min); frameWIN->setMaximum(max); frameWIN->setSingleStep(xstep); } } else return; if (currentCCD->getMinMaxStep(frameProp, "HEIGHT", &min, &max, &step)) { if (min >= max) { resetFrameToZero(); return; } if (step == 0) ystep = static_cast(max * 0.05); else ystep = step; if (min >= 0 && max > 0) { frameHIN->setMinimum(min); frameHIN->setMaximum(max); frameHIN->setSingleStep(ystep); } } else return; if (currentCCD->getMinMaxStep(frameProp, "X", &min, &max, &step)) { if (min >= max) { resetFrameToZero(); return; } if (step == 0) step = xstep; if (min >= 0 && max > 0) { frameXIN->setMinimum(min); frameXIN->setMaximum(max); frameXIN->setSingleStep(step); } } else return; if (currentCCD->getMinMaxStep(frameProp, "Y", &min, &max, &step)) { if (min >= max) { resetFrameToZero(); return; } if (step == 0) step = ystep; if (min >= 0 && max > 0) { frameYIN->setMinimum(min); frameYIN->setMaximum(max); frameYIN->setSingleStep(step); } } else return; // cull to camera limits, if there are any if (useGuideHead == false) cullToDSLRLimits(); if (reset == 1 || frameSettings.contains(targetChip) == false) { QVariantMap settings; settings["x"] = 0; settings["y"] = 0; settings["w"] = frameWIN->maximum(); settings["h"] = frameHIN->maximum(); settings["binx"] = 1; settings["biny"] = 1; frameSettings[targetChip] = settings; } else if (reset == 2 && frameSettings.contains(targetChip)) { QVariantMap settings = frameSettings[targetChip]; int x, y, w, h; x = settings["x"].toInt(); y = settings["y"].toInt(); w = settings["w"].toInt(); h = settings["h"].toInt(); // Bound them x = qBound(frameXIN->minimum(), x, frameXIN->maximum() - 1); y = qBound(frameYIN->minimum(), y, frameYIN->maximum() - 1); w = qBound(frameWIN->minimum(), w, frameWIN->maximum()); h = qBound(frameHIN->minimum(), h, frameHIN->maximum()); settings["x"] = x; settings["y"] = y; settings["w"] = w; settings["h"] = h; frameSettings[targetChip] = settings; } if (frameSettings.contains(targetChip)) { QVariantMap settings = frameSettings[targetChip]; int x = settings["x"].toInt(); int y = settings["y"].toInt(); int w = settings["w"].toInt(); int h = settings["h"].toInt(); if (targetChip->canBin()) { targetChip->getMaxBin(&binx, &biny); binXIN->setMaximum(binx); binYIN->setMaximum(biny); binXIN->setValue(settings["binx"].toInt()); binYIN->setValue(settings["biny"].toInt()); } else { binXIN->setValue(1); binYIN->setValue(1); } if (x >= 0) frameXIN->setValue(x); if (y >= 0) frameYIN->setValue(y); if (w > 0) frameWIN->setValue(w); if (h > 0) frameHIN->setValue(h); } } void Capture::processCCDNumber(INumberVectorProperty * nvp) { if (currentCCD == nullptr) return; if ((!strcmp(nvp->name, "CCD_FRAME") && useGuideHead == false) || (!strcmp(nvp->name, "GUIDER_FRAME") && useGuideHead)) updateFrameProperties(); else if ((!strcmp(nvp->name, "CCD_INFO") && useGuideHead == false) || (!strcmp(nvp->name, "GUIDER_INFO") && useGuideHead)) updateFrameProperties(2); } void Capture::resetFrame() { targetChip = useGuideHead ? currentCCD->getChip(ISD::CCDChip::GUIDE_CCD) : currentCCD->getChip(ISD::CCDChip::PRIMARY_CCD); targetChip->resetFrame(); updateFrameProperties(1); } void Capture::syncFrameType(ISD::GDInterface * ccd) { if (ccd->getDeviceName() != CCDCaptureCombo->currentText().toLatin1()) return; ISD::CCDChip * tChip = (static_cast(ccd))->getChip(ISD::CCDChip::PRIMARY_CCD); QStringList frameTypes = tChip->getFrameTypes(); frameTypeCombo->clear(); if (frameTypes.isEmpty()) frameTypeCombo->setEnabled(false); else { frameTypeCombo->setEnabled(true); frameTypeCombo->addItems(frameTypes); frameTypeCombo->setCurrentIndex(tChip->getFrameType()); } } bool Capture::setFilterWheel(const QString &device) { bool deviceFound = false; for (int i = 0; i < FilterDevicesCombo->count(); i++) if (device == FilterDevicesCombo->itemText(i)) { // Check Combo if it was set to something else. if (FilterDevicesCombo->currentIndex() != i) { FilterDevicesCombo->blockSignals(true); FilterDevicesCombo->setCurrentIndex(i); FilterDevicesCombo->blockSignals(false); } checkFilter(i); deviceFound = true; break; } if (deviceFound == false) return false; return true; } QString Capture::filterWheel() { if (FilterDevicesCombo->currentIndex() >= 1) return FilterDevicesCombo->currentText(); return QString(); } bool Capture::setFilter(const QString &filter) { if (FilterDevicesCombo->currentIndex() >= 1) { FilterPosCombo->setCurrentText(filter); return true; } return false; } QString Capture::filter() { return FilterPosCombo->currentText(); } void Capture::checkFilter(int filterNum) { if (filterNum == -1) { filterNum = FilterDevicesCombo->currentIndex(); if (filterNum == -1) return; } // "--" is no filter if (filterNum == 0) { currentFilter = nullptr; m_CurrentFilterPosition = -1; + filterEditB->setEnabled(false); FilterPosCombo->clear(); syncFilterInfo(); return; } if (filterNum <= Filters.count()) currentFilter = Filters.at(filterNum - 1); filterManager->setCurrentFilterWheel(currentFilter); syncFilterInfo(); FilterPosCombo->clear(); FilterPosCombo->addItems(filterManager->getFilterLabels()); m_CurrentFilterPosition = filterManager->getFilterPosition(); + filterEditB->setEnabled(m_CurrentFilterPosition > 0); + FilterPosCombo->setCurrentIndex(m_CurrentFilterPosition - 1); /*if (activeJob && (activeJob->getStatus() == SequenceJob::JOB_ABORTED || activeJob->getStatus() == SequenceJob::JOB_IDLE)) activeJob->setCurrentFilter(currentFilterPosition);*/ } void Capture::syncFilterInfo() { if (currentCCD) { ITextVectorProperty * activeDevices = currentCCD->getBaseDevice()->getText("ACTIVE_DEVICES"); if (activeDevices) { IText * activeFilter = IUFindText(activeDevices, "ACTIVE_FILTER"); if (activeFilter) { if (currentFilter != nullptr && (activeFilter->text != currentFilter->getDeviceName())) { m_FilterOverride = true; activeFilter->aux0 = &m_FilterOverride; IUSaveText(activeFilter, currentFilter->getDeviceName().toLatin1().constData()); currentCCD->getDriverInfo()->getClientManager()->sendNewText(activeDevices); } // Reset filter name in CCD driver else if (currentFilter == nullptr && strlen(activeFilter->text) > 0) { m_FilterOverride = true; activeFilter->aux0 = &m_FilterOverride; IUSaveText(activeFilter, ""); currentCCD->getDriverInfo()->getClientManager()->sendNewText(activeDevices); } } } } } bool Capture::startNextExposure() { // check if pausing has been requested if (checkPausing() == true) { pauseFunction = &Capture::startNextExposure; return false; } if (checkMeridianFlip()) // execute flip before next capture return false; if (startFocusIfRequired()) // re-focus before next capture return false; if (seqDelay > 0) { secondsLabel->setText(i18n("Waiting...")); m_State = CAPTURE_WAITING; emit newStatus(Ekos::CAPTURE_WAITING); } seqTimer->start(seqDelay); return true; } void Capture::newFITS(IBLOB * bp) { ISD::CCDChip * tChip = nullptr; // If there is no active job, ignore if (activeJob == nullptr) { qCWarning(KSTARS_EKOS_CAPTURE) << "Ignoring received FITS" << bp->name << "as active job is null."; return; } if (meridianFlipStage >= MF_ALIGNING) { qCWarning(KSTARS_EKOS_CAPTURE) << "Ignoring Received FITS" << bp->name << "as meridian flip stage is" << meridianFlipStage; return; } if (currentCCD->getUploadMode() != ISD::CCD::UPLOAD_LOCAL) { if (bp == nullptr) { appendLogText(i18n("Failed to save file to %1", activeJob->getSignature())); abort(); return; } if (m_State == CAPTURE_IDLE || m_State == CAPTURE_ABORTED) { qCWarning(KSTARS_EKOS_CAPTURE) << "Ignoring Received FITS" << bp->name << "as current capture state is not active" << m_State; return; } if (!strcmp(bp->name, "CCD2")) tChip = currentCCD->getChip(ISD::CCDChip::GUIDE_CCD); else tChip = currentCCD->getChip(ISD::CCDChip::PRIMARY_CCD); if (tChip != targetChip) { if (guideState == GUIDE_IDLE) qCWarning(KSTARS_EKOS_CAPTURE) << "Ignoring Received FITS" << bp->name << "as it does not correspond to the target chip" << targetChip->getType(); return; } if (targetChip->getCaptureMode() == FITS_FOCUS || targetChip->getCaptureMode() == FITS_GUIDE) { qCWarning(KSTARS_EKOS_CAPTURE) << "Ignoring Received FITS" << bp->name << "as it has the wrong capture mode" << targetChip->getCaptureMode(); return; } // If the FITS is not for our device, simply ignore //if (QString(bp->bvp->device) != currentCCD->getDeviceName() || (startB->isEnabled() && previewB->isEnabled())) if (QString(bp->bvp->device) != currentCCD->getDeviceName()) { qCWarning(KSTARS_EKOS_CAPTURE) << "Ignoring Received FITS" << bp->name << "as the blob device name" << bp->bvp->device << "does not equal active camera" << currentCCD->getDeviceName(); return; } // If this is a preview job, make sure to enable preview button after // we receive the FITS if (activeJob->isPreview() && previewB->isEnabled() == false) previewB->setEnabled(true); // m_isLooping client-side looping (next capture starts after image is downloaded to client) // currentCCD->isLooping driver side looping (without any delays, next capture starts after driver reads data) if (m_isLooping == false && currentCCD->isLooping() == false) { disconnect(currentCCD, &ISD::CCD::BLOBUpdated, this, &Ekos::Capture::newFITS); if (useGuideHead == false && darkSubCheck->isChecked() && activeJob->isPreview()) { FITSView * currentImage = targetChip->getImageView(FITS_NORMAL); FITSData * darkData = DarkLibrary::Instance()->getDarkFrame(targetChip, activeJob->getExposure()); uint16_t offsetX = activeJob->getSubX() / activeJob->getXBin(); uint16_t offsetY = activeJob->getSubY() / activeJob->getYBin(); connect(DarkLibrary::Instance(), &DarkLibrary::darkFrameCompleted, this, [&](bool completed) { if (currentCCD->isLooping() == false) DarkLibrary::Instance()->disconnect(this); if (completed) setCaptureComplete(); else abort(); }); connect(DarkLibrary::Instance(), &DarkLibrary::newLog, this, &Ekos::Capture::appendLogText); if (darkData) DarkLibrary::Instance()->subtract(darkData, currentImage, activeJob->getCaptureFilter(), offsetX, offsetY); else DarkLibrary::Instance()->captureAndSubtract(targetChip, currentImage, activeJob->getExposure(), offsetX, offsetY); return; } } } blobChip = bp ? static_cast(bp->aux0) : nullptr; blobFilename = bp ? static_cast(bp->aux2) : QString(); setCaptureComplete(); } bool Capture::setCaptureComplete() { captureTimeout.stop(); m_CaptureTimeoutCounter = 0; downloadProgressTimer.stop(); // In case we're framing, let's start if (m_isLooping) { sendNewImage(blobFilename, blobChip); secondsLabel->setText(i18n("Framing...")); activeJob->capture(darkSubCheck->isChecked() ? true : false); return true; } if (currentCCD->isLooping() == false) { disconnect(currentCCD, &ISD::CCD::newExposureValue, this, &Ekos::Capture::setExposureProgress); DarkLibrary::Instance()->disconnect(this); } // Do not calculate download time for images stored on server. // Only calculate for longer exposures. if (currentCCD->getUploadMode() != ISD::CCD::UPLOAD_LOCAL) { //This determines the time since the image started downloading //Then it gets the estimated time left and displays it in the log. double currentDownloadTime = downloadTimer.elapsed() / 1000.0; downloadTimes << currentDownloadTime; QString dLTimeString = QString::number(currentDownloadTime, 'd', 2); QString estimatedTimeString = QString::number(getEstimatedDownloadTime(), 'd', 2); appendLogText(i18n("Download Time: %1 s, New Download Time Estimate: %2 s.", dLTimeString, estimatedTimeString)); } secondsLabel->setText(i18n("Complete.")); // Do not display notifications for very short captures if (activeJob->getExposure() >= 1) KSNotification::event(QLatin1String("EkosCaptureImageReceived"), i18n("Captured image received"), KSNotification::EVENT_INFO); // If it was initially set as pure preview job and NOT as preview for calibration if (activeJob->isPreview() && calibrationStage != CAL_CALIBRATION) { sendNewImage(blobFilename, blobChip); jobs.removeOne(activeJob); // Reset upload mode if it was changed by preview currentCCD->setUploadMode(rememberUploadMode); delete (activeJob); // Reset active job pointer activeJob = nullptr; abort(); if (guideState == GUIDE_SUSPENDED && suspendGuideOnDownload) emit resumeGuiding(); m_State = CAPTURE_IDLE; emit newStatus(Ekos::CAPTURE_IDLE); return true; } // check if pausing has been requested if (checkPausing() == true) { pauseFunction = &Capture::setCaptureComplete; return false; } if (! activeJob->isPreview()) { /* Increase the sequence's current capture count */ activeJob->setCompleted(activeJob->getCompleted() + 1); /* Decrease the counter for in-sequence focusing */ inSequenceFocusCounter--; } // Do not send new image if the image was stored on the server. if (currentCCD->getUploadMode() != ISD::CCD::UPLOAD_LOCAL) sendNewImage(blobFilename, blobChip); /* If we were assigned a captured frame map, also increase the relevant counter for prepareJob */ SchedulerJob::CapturedFramesMap::iterator frame_item = capturedFramesMap.find(activeJob->getSignature()); if (capturedFramesMap.end() != frame_item) frame_item.value()++; if (activeJob->getFrameType() != FRAME_LIGHT) { if (processPostCaptureCalibrationStage() == false) return true; if (calibrationStage == CAL_CALIBRATION_COMPLETE) calibrationStage = CAL_CAPTURING; } /* The image progress has now one more capture */ imgProgress->setValue(activeJob->getCompleted()); appendLogText(i18n("Received image %1 out of %2.", activeJob->getCompleted(), activeJob->getCount())); m_State = CAPTURE_IMAGE_RECEIVED; emit newStatus(Ekos::CAPTURE_IMAGE_RECEIVED); currentImgCountOUT->setText(QString("%L1").arg(activeJob->getCompleted())); // Check if we need to execute post capture script first if (activeJob->getPostCaptureScript().isEmpty() == false) { postCaptureScript.start(activeJob->getPostCaptureScript()); appendLogText(i18n("Executing post capture script %1", activeJob->getPostCaptureScript())); return true; } // if we're done if (activeJob->getCount() <= activeJob->getCompleted()) { processJobCompletion(); return true; } return resumeSequence(); } void Capture::processJobCompletion() { activeJob->done(); if (activeJob->isPreview() == false) { int index = jobs.indexOf(activeJob); QJsonObject oneSequence = m_SequenceArray[index].toObject(); oneSequence["Status"] = "Complete"; m_SequenceArray.replace(index, oneSequence); emit sequenceChanged(m_SequenceArray); } stop(); // Check if there are more pending jobs and execute them if (resumeSequence()) return; // Otherwise, we're done. We park if required and resume guiding if no parking is done and autoguiding was engaged before. else { //KNotification::event(QLatin1String("CaptureSuccessful"), i18n("CCD capture sequence completed")); KSNotification::event(QLatin1String("CaptureSuccessful"), i18n("CCD capture sequence completed"), KSNotification::EVENT_INFO); abort(); m_State = CAPTURE_COMPLETE; emit newStatus(Ekos::CAPTURE_COMPLETE); //Resume guiding if it was suspended before //if (isAutoGuiding && currentCCD->getChip(ISD::CCDChip::GUIDE_CCD) == guideChip) if (guideState == GUIDE_SUSPENDED && suspendGuideOnDownload) emit resumeGuiding(); } } bool Capture::resumeSequence() { if (m_State == CAPTURE_PAUSED) { pauseFunction = &Capture::resumeSequence; appendLogText(i18n("Sequence paused.")); secondsLabel->setText(i18n("Paused...")); return false; } // If no job is active, we have to find if there are more pending jobs in the queue if (!activeJob) { SequenceJob * next_job = nullptr; foreach (SequenceJob * job, jobs) { if (job->getStatus() == SequenceJob::JOB_IDLE || job->getStatus() == SequenceJob::JOB_ABORTED) { next_job = job; break; } } if (next_job) { prepareJob(next_job); //Resume guiding if it was suspended before //if (isAutoGuiding && currentCCD->getChip(ISD::CCDChip::GUIDE_CCD) == guideChip) if (guideState == GUIDE_SUSPENDED && suspendGuideOnDownload) { qCDebug(KSTARS_EKOS_CAPTURE) << "Resuming guiding..."; emit resumeGuiding(); } return true; } else { qCDebug(KSTARS_EKOS_CAPTURE) << "All capture jobs complete."; return false; } } // Otherwise, let's prepare for next exposure after making sure in-sequence focus and dithering are complete if applicable. else { isInSequenceFocus = (m_AutoFocusReady && autofocusCheck->isChecked()/* && HFRPixels->value() > 0*/); // if (isInSequenceFocus) // requiredAutoFocusStarted = false; isTemperatureDeltaCheckActive = (m_AutoFocusReady && temperatureDeltaCheck->isChecked()); // Reset HFR pixels to file value after meridian flip if (isInSequenceFocus && meridianFlipStage != MF_NONE && meridianFlipStage != MF_READY) { qCDebug(KSTARS_EKOS_CAPTURE) << "Resetting HFR value to file value of" << fileHFR << "pixels after meridian flip."; //firstAutoFocus = true; HFRPixels->setValue(fileHFR); } // If we suspended guiding due to primary chip download, resume guide chip guiding now if (guideState == GUIDE_SUSPENDED && suspendGuideOnDownload) { qCInfo(KSTARS_EKOS_CAPTURE) << "Resuming guiding..."; emit resumeGuiding(); } // Dither either when guiding or IF Non-Guide either option is enabled if ( (Options::ditherEnabled() || Options::ditherNoGuiding()) // 2017-09-20 Jasem: No need to dither after post meridian flip guiding && meridianFlipStage != MF_GUIDING // If CCD is looping, we cannot dither UNLESS a different camera and NOT a guide chip is doing the guiding for us. && (currentCCD->isLooping() == false || guideChip == nullptr) // We must be either in guide mode or if non-guide dither (via pulsing) is enabled && (guideState == GUIDE_GUIDING || Options::ditherNoGuiding()) // Must be only done for light frames && activeJob->getFrameType() == FRAME_LIGHT // Check dither counter && --ditherCounter == 0) { ditherCounter = Options::ditherFrames(); secondsLabel->setText(i18n("Dithering...")); qCInfo(KSTARS_EKOS_CAPTURE) << "Dithering..."; if (currentCCD->isLooping()) targetChip->abortExposure(); m_State = CAPTURE_DITHERING; emit newStatus(Ekos::CAPTURE_DITHERING); } #if 0 else if (isRefocus && activeJob->getFrameType() == FRAME_LIGHT) { appendLogText(i18n("Scheduled refocus starting after %1 seconds...", getRefocusEveryNTimerElapsedSec())); secondsLabel->setText(i18n("Focusing...")); if (currentCCD->isLooping()) targetChip->abortExposure(); // If we are over 30 mins since last autofocus, we'll reset frame. if (refocusEveryN->value() >= 30) emit resetFocus(); // force refocus qCDebug(KSTARS_EKOS_CAPTURE) << "Capture is triggering autofocus on line 1812."; emit checkFocus(0.1); m_State = CAPTURE_FOCUSING; emit newStatus(Ekos::CAPTURE_FOCUSING); } else if (isInSequenceFocus && activeJob->getFrameType() == FRAME_LIGHT && --inSequenceFocusCounter == 0) { inSequenceFocusCounter = Options::inSequenceCheckFrames(); // Post meridian flip we need to reset filter _before_ running in-sequence focusing // as it could have changed for whatever reason (e.g. alignment used a different filter). // Then when focus process begins with the _target_ filter in place, it should take all the necessary actions to make it // work for the next set of captures. This is direct reset to the filter device, not via Filter Manager. if (meridianFlipStage != MF_NONE && currentFilter) { int targetFilterPosition = activeJob->getTargetFilter(); int currentFilterPosition = filterManager->getFilterPosition(); if (targetFilterPosition > 0 && targetFilterPosition != currentFilterPosition) currentFilter->runCommand(INDI_SET_FILTER, &targetFilterPosition); } secondsLabel->setText(i18n("Focusing...")); if (currentCCD->isLooping()) targetChip->abortExposure(); if (HFRPixels->value() == 0) { qCDebug(KSTARS_EKOS_CAPTURE) << "Capture is triggering autofocus on line 1841."; emit checkFocus(0.1); } else { qCDebug(KSTARS_EKOS_CAPTURE) << "Capture is triggering autofocus on line 1846."; emit checkFocus(HFRPixels->value()); } qCDebug(KSTARS_EKOS_CAPTURE) << "In-sequence focusing started..."; m_State = CAPTURE_FOCUSING; emit newStatus(Ekos::CAPTURE_FOCUSING); } #endif // Check if we need to do autofocus, if not let's check if we need looping or start next exposure else if (startFocusIfRequired() == false) { // If looping, we just increment the file system image count if (currentCCD->isLooping()) { if (currentCCD->getUploadMode() != ISD::CCD::UPLOAD_LOCAL) { checkSeqBoundary(activeJob->getSignature()); currentCCD->setNextSequenceID(nextSequenceID); } } else startNextExposure(); } } return true; } bool Capture::startFocusIfRequired() { if (activeJob == nullptr || activeJob->getFrameType() != FRAME_LIGHT) return false; isRefocus = false; // check if time for forced refocus if (refocusEveryNCheck->isChecked()) { qCDebug(KSTARS_EKOS_CAPTURE) << "Focus elapsed time (secs): " << getRefocusEveryNTimerElapsedSec() << ". Requested Interval (secs): " << refocusEveryN->value() * 60; if (getRefocusEveryNTimerElapsedSec() >= refocusEveryN->value() * 60) { isRefocus = true; appendLogText(i18n("Scheduled refocus starting after %1 seconds...", getRefocusEveryNTimerElapsedSec())); } } if (!isRefocus && isTemperatureDeltaCheckActive) { qCDebug(KSTARS_EKOS_CAPTURE) << "Focus temperature delta (°C): " << focusTemperatureDelta << ". Requested maximum delta (°C): " << temperatureDelta->value(); if (focusTemperatureDelta > temperatureDelta->value()) { isRefocus = true; appendLogText(i18n("Refocus starting because of temperature change of %1 °C...", focusTemperatureDelta)); } } // Either it is time to force autofocus or temperature has changed if (isRefocus) { secondsLabel->setText(i18n("Focusing...")); if (currentCCD->isLooping()) targetChip->abortExposure(); // If we are over 30 mins since last autofocus, we'll reset frame. if (refocusEveryN->value() >= 30) emit resetFocus(); // force refocus qCDebug(KSTARS_EKOS_CAPTURE) << "Capture is triggering autofocus on line " << __LINE__; emit checkFocus(0.1); m_State = CAPTURE_FOCUSING; emit newStatus(Ekos::CAPTURE_FOCUSING); return true; } else if (isInSequenceFocus && inSequenceFocusCounter == 0) { inSequenceFocusCounter = Options::inSequenceCheckFrames(); // Post meridian flip we need to reset filter _before_ running in-sequence focusing // as it could have changed for whatever reason (e.g. alignment used a different filter). // Then when focus process begins with the _target_ filter in place, it should take all the necessary actions to make it // work for the next set of captures. This is direct reset to the filter device, not via Filter Manager. if (meridianFlipStage != MF_NONE && currentFilter) { int targetFilterPosition = activeJob->getTargetFilter(); int currentFilterPosition = filterManager->getFilterPosition(); if (targetFilterPosition > 0 && targetFilterPosition != currentFilterPosition) currentFilter->runCommand(INDI_SET_FILTER, &targetFilterPosition); } secondsLabel->setText(i18n("Focusing...")); if (currentCCD->isLooping()) targetChip->abortExposure(); if (HFRPixels->value() == 0) { qCDebug(KSTARS_EKOS_CAPTURE) << "Capture is triggering autofocus on line " << __LINE__; emit checkFocus(0.1); } else { qCDebug(KSTARS_EKOS_CAPTURE) << "Capture is triggering autofocus on line " << __LINE__; emit checkFocus(HFRPixels->value()); } qCDebug(KSTARS_EKOS_CAPTURE) << "In-sequence focusing started..."; m_State = CAPTURE_FOCUSING; emit newStatus(Ekos::CAPTURE_FOCUSING); return true; } return false; } void Capture::captureOne() { //if (currentCCD->getUploadMode() == ISD::CCD::UPLOAD_LOCAL) /*if (uploadModeCombo->currentIndex() != ISD::CCD::UPLOAD_CLIENT) { appendLogText(i18n("Cannot take preview image while CCD upload mode is set to local or both. Please change " "upload mode to client and try again.")); return; }*/ if (transferFormatCombo->currentIndex() == ISD::CCD::FORMAT_NATIVE && darkSubCheck->isChecked()) { appendLogText(i18n("Cannot perform auto dark subtraction of native DSLR formats.")); return; } if (addJob(true)) prepareJob(jobs.last()); } void Capture::startFraming() { m_isLooping = true; appendLogText(i18n("Starting framing...")); captureOne(); } void Capture::captureImage() { if (activeJob == nullptr) return; captureTimeout.stop(); seqTimer->stop(); SequenceJob::CAPTUREResult rc = SequenceJob::CAPTURE_OK; if (currentCCD->isConnected() == false) { appendLogText(i18n("Error: Lost connection to CCD.")); abort(); return; } // This test must be placed before the FOCUS_PROGRESS test, // as sometimes the FilterManager can cause an auto-focus. // If the filterManager is not IDLE, then try again in 1 second. switch (filterManagerState) { case FILTER_IDLE: secondsLabel->clear(); break; case FILTER_AUTOFOCUS: secondsLabel->setText(i18n("Focusing...")); QTimer::singleShot(1000, this, &Ekos::Capture::captureImage); return; case FILTER_CHANGE: secondsLabel->setText(i18n("Changing Filters...")); QTimer::singleShot(1000, this, &Ekos::Capture::captureImage); return; case FILTER_OFFSET: secondsLabel->setText(i18n("Adjusting Filter Offset...")); QTimer::singleShot(1000, this, &Ekos::Capture::captureImage); return; } if (focusState >= FOCUS_PROGRESS) { appendLogText(i18n("Cannot capture while focus module is busy.")); abort(); return; } /* if (filterSlot != nullptr) { currentFilterPosition = (int)filterSlot->np[0].value; activeJob->setCurrentFilter(currentFilterPosition); }*/ if (currentFilter != nullptr) { m_CurrentFilterPosition = filterManager->getFilterPosition(); activeJob->setCurrentFilter(m_CurrentFilterPosition); } if (currentCCD->hasCooler()) { double temperature = 0; currentCCD->getTemperature(&temperature); activeJob->setCurrentTemperature(temperature); } if (currentCCD->isLooping()) { int remaining = activeJob->getCount() - activeJob->getCompleted(); if (remaining > 1) currentCCD->setExposureLoopCount(remaining); } connect(currentCCD, &ISD::CCD::BLOBUpdated, this, &Ekos::Capture::newFITS, Qt::UniqueConnection); connect(currentCCD, &ISD::CCD::previewFITSGenerated, this, &Ekos::Capture::setGeneratedPreviewFITS, Qt::UniqueConnection); if (activeJob->getFrameType() == FRAME_FLAT) { // If we have to calibrate ADU levels, first capture must be preview and not in batch mode if (activeJob->isPreview() == false && activeJob->getFlatFieldDuration() == DURATION_ADU && calibrationStage == CAL_PRECAPTURE_COMPLETE) { if (currentCCD->getTransferFormat() == ISD::CCD::FORMAT_NATIVE) { appendLogText(i18n("Cannot calculate ADU levels in non-FITS images.")); abort(); return; } calibrationStage = CAL_CALIBRATION; // We need to be in preview mode and in client mode for this to work activeJob->setPreview(true); } } // Temporary change upload mode to client when requesting previews if (activeJob->isPreview()) { rememberUploadMode = activeJob->getUploadMode(); currentCCD->setUploadMode(ISD::CCD::UPLOAD_CLIENT); } if (currentCCD->getUploadMode() != ISD::CCD::UPLOAD_LOCAL) { checkSeqBoundary(activeJob->getSignature()); currentCCD->setNextSequenceID(nextSequenceID); } m_State = CAPTURE_CAPTURING; //if (activeJob->isPreview() == false) // NOTE: Why we didn't emit this before for preview? emit newStatus(Ekos::CAPTURE_CAPTURING); if (frameSettings.contains(activeJob->getActiveChip())) { QVariantMap settings; settings["x"] = activeJob->getSubX(); settings["y"] = activeJob->getSubY(); settings["w"] = activeJob->getSubW(); settings["h"] = activeJob->getSubH(); settings["binx"] = activeJob->getXBin(); settings["biny"] = activeJob->getYBin(); frameSettings[activeJob->getActiveChip()] = settings; } // If using DSLR, make sure it is set to correct transfer format currentCCD->setTransformFormat(activeJob->getTransforFormat()); connect(currentCCD, &ISD::CCD::newExposureValue, this, &Ekos::Capture::setExposureProgress, Qt::UniqueConnection); rc = activeJob->capture(darkSubCheck->isChecked() ? true : false); if (rc != SequenceJob::CAPTURE_OK) { disconnect(currentCCD, &ISD::CCD::newExposureValue, this, &Ekos::Capture::setExposureProgress); } switch (rc) { case SequenceJob::CAPTURE_OK: { appendLogText(i18n("Capturing %1-second %2 image...", QString("%L1").arg(activeJob->getExposure(), 0, 'f', 3), activeJob->getFilterName())); captureTimeout.start(activeJob->getExposure() * 1000 + CAPTURE_TIMEOUT_THRESHOLD); if (activeJob->isPreview() == false) { int index = jobs.indexOf(activeJob); QJsonObject oneSequence = m_SequenceArray[index].toObject(); oneSequence["Status"] = "In Progress"; m_SequenceArray.replace(index, oneSequence); emit sequenceChanged(m_SequenceArray); } } break; case SequenceJob::CAPTURE_FRAME_ERROR: appendLogText(i18n("Failed to set sub frame.")); abort(); break; case SequenceJob::CAPTURE_BIN_ERROR: appendLogText(i18n("Failed to set binning.")); abort(); break; case SequenceJob::CAPTURE_FILTER_BUSY: // Try again in 1 second if filter is busy secondsLabel->setText(i18n("Changing filter...")); QTimer::singleShot(1000, this, &Ekos::Capture::captureImage); break; case SequenceJob::CAPTURE_FOCUS_ERROR: appendLogText(i18n("Cannot capture while focus module is busy.")); abort(); break; } } bool Capture::resumeCapture() { if (m_State == CAPTURE_PAUSED) { pauseFunction = &Capture::resumeCapture; appendLogText(i18n("Sequence paused.")); secondsLabel->setText(i18n("Paused...")); return false; } #if 0 /* Refresh isRefocus when resuming */ if (autoFocusReady && refocusEveryNCheck->isChecked()) { qCDebug(KSTARS_EKOS_CAPTURE) << "NFocus Elapsed Time (secs): " << getRefocusEveryNTimerElapsedSec() << " Requested Interval (secs): " << refocusEveryN->value() * 60; isRefocus = getRefocusEveryNTimerElapsedSec() >= refocusEveryN->value() * 60; } // FIXME ought to be able to combine these - only different is value passed // to checkFocus() // 2018-08-23 Jasem: For now in-sequence-focusing takes precedence. if (isInSequenceFocus && requiredAutoFocusStarted == false) { requiredAutoFocusStarted = true; secondsLabel->setText(i18n("Focusing...")); qCDebug(KSTARS_EKOS_CAPTURE) << "Requesting focusing if HFR >" << HFRPixels->value(); qCDebug(KSTARS_EKOS_CAPTURE) << "Capture is triggering autofocus on line 2186."; emit checkFocus(HFRPixels->value()); m_State = CAPTURE_FOCUSING; emit newStatus(Ekos::CAPTURE_FOCUSING); return true; } else if (isRefocus) { appendLogText(i18n("Scheduled refocus started...")); secondsLabel->setText(i18n("Focusing...")); qCDebug(KSTARS_EKOS_CAPTURE) << "Capture is triggering autofocus on line 2197."; emit checkFocus(0.1); m_State = CAPTURE_FOCUSING; emit newStatus(Ekos::CAPTURE_FOCUSING); return true; } #endif if (m_State == CAPTURE_DITHERING && m_AutoFocusReady && startFocusIfRequired()) return true; startNextExposure(); return true; } /*******************************************************************************/ /* Update the prefix for the sequence of images to be captured */ /*******************************************************************************/ void Capture::updateSequencePrefix(const QString &newPrefix, const QString &dir) { seqPrefix = newPrefix; // If it doesn't exist, create it QDir().mkpath(dir); nextSequenceID = 1; } /*******************************************************************************/ /* Determine the next file number sequence. That is, if we have file1.png */ /* and file2.png, then the next sequence should be file3.png */ /*******************************************************************************/ void Capture::checkSeqBoundary(const QString &path) { int newFileIndex = -1; QFileInfo const path_info(path); QString const sig_dir(path_info.dir().path()); QString const sig_file(path_info.completeBaseName()); QString tempName; // seqFileCount = 0; // No updates during meridian flip if (meridianFlipStage >= MF_ALIGNING) return; QDirIterator it(sig_dir, QDir::Files); while (it.hasNext()) { tempName = it.next(); QFileInfo info(tempName); // This returns the filename without the extension tempName = info.completeBaseName(); // This remove any additional extension (e.g. m42_001.fits.fz) // the completeBaseName() would return m42_001.fits // and this remove .fits so we end up with m42_001 tempName = tempName.remove(".fits"); QString finalSeqPrefix = seqPrefix; finalSeqPrefix.remove(SequenceJob::ISOMarker); // find the prefix first if (tempName.startsWith(finalSeqPrefix, Qt::CaseInsensitive) == false) continue; /* Do not change the number of captures. * - If the sequence is required by the end-user, unconditionally run what each sequence item is requiring. * - If the sequence is required by the scheduler, use capturedFramesMap to determine when to stop capturing. */ //seqFileCount++; int lastUnderScoreIndex = tempName.lastIndexOf("_"); if (lastUnderScoreIndex > 0) { bool indexOK = false; newFileIndex = tempName.midRef(lastUnderScoreIndex + 1).toInt(&indexOK); if (indexOK && newFileIndex >= nextSequenceID) nextSequenceID = newFileIndex + 1; } } } void Capture::appendLogText(const QString &text) { m_LogText.insert(0, i18nc("log entry; %1 is the date, %2 is the text", "%1 %2", KStarsData::Instance()->lt().toString("yyyy-MM-ddThh:mm:ss"), text)); qCInfo(KSTARS_EKOS_CAPTURE) << text; emit newLog(text); } void Capture::clearLog() { m_LogText.clear(); emit newLog(QString()); } //This method will update the Capture Module and Summary Screen's estimate of how much time is left in the download void Capture::setDownloadProgress() { if (activeJob) { double downloadTimeLeft = getEstimatedDownloadTime() - downloadTimer.elapsed() / 1000.0; if(downloadTimeLeft > 0) { exposeOUT->setText(QString("%L1").arg(downloadTimeLeft, 0, 'd', 2)); emit newDownloadProgress(downloadTimeLeft); } } } void Capture::setExposureProgress(ISD::CCDChip * tChip, double value, IPState state) { if (targetChip != tChip || targetChip->getCaptureMode() != FITS_NORMAL || meridianFlipStage >= MF_ALIGNING) return; exposeOUT->setText(QString("%L1").arg(value, 0, 'd', 2)); if (activeJob) { activeJob->setExposeLeft(value); emit newExposureProgress(activeJob); } if (activeJob && state == IPS_ALERT) { int retries = activeJob->getCaptureRetires() + 1; activeJob->setCaptureRetires(retries); appendLogText(i18n("Capture failed. Check INDI Control Panel for details.")); if (retries == 3) { abort(); return; } appendLogText(i18n("Restarting capture attempt #%1", retries)); nextSequenceID = 1; captureImage(); return; } //qDebug() << "Exposure with value " << value << "state" << pstateStr(state); if (activeJob != nullptr && state == IPS_OK) { activeJob->setCaptureRetires(0); activeJob->setExposeLeft(0); if (currentCCD && currentCCD->getUploadMode() == ISD::CCD::UPLOAD_LOCAL) { if (activeJob && activeJob->getStatus() == SequenceJob::JOB_BUSY) { newFITS(nullptr); return; } } //if (isAutoGuiding && Options::useEkosGuider() && currentCCD->getChip(ISD::CCDChip::GUIDE_CCD) == guideChip) if (guideState == GUIDE_GUIDING && Options::guiderType() == 0 && suspendGuideOnDownload) { qCDebug(KSTARS_EKOS_CAPTURE) << "Autoguiding suspended until primary CCD chip completes downloading..."; emit suspendGuiding(); } secondsLabel->setText(i18n("Downloading...")); //This will start the clock to see how long the download takes. downloadTimer.start(); downloadProgressTimer.start(); //disconnect(currentCCD, &ISD::CCD::newExposureValue(ISD::CCDChip*,double,IPState)), this, &Ekos::Capture::updateCaptureProgress(ISD::CCDChip*,double,IPState))); } // JM: Don't change to i18np, value is DOUBLE, not Integer. else if (value <= 1) secondsLabel->setText(i18n("second left")); else secondsLabel->setText(i18n("seconds left")); } void Capture::updateCCDTemperature(double value) { if (temperatureCheck->isEnabled() == false) { if (currentCCD->getBaseDevice()->getPropertyPermission("CCD_TEMPERATURE") != IP_RO) checkCCD(); } temperatureOUT->setText(QString("%L1").arg(value, 0, 'f', 2)); if (temperatureIN->cleanText().isEmpty()) temperatureIN->setValue(value); //if (activeJob && (activeJob->getStatus() == SequenceJob::JOB_ABORTED || activeJob->getStatus() == SequenceJob::JOB_IDLE)) if (activeJob) activeJob->setCurrentTemperature(value); } void Capture::updateRotatorNumber(INumberVectorProperty * nvp) { if (!strcmp(nvp->name, "ABS_ROTATOR_ANGLE")) { // Update widget rotator position rotatorSettings->setCurrentAngle(nvp->np[0].value); //if (activeJob && (activeJob->getStatus() == SequenceJob::JOB_ABORTED || activeJob->getStatus() == SequenceJob::JOB_IDLE)) if (activeJob) activeJob->setCurrentRotation(rotatorSettings->getCurrentRotationPA()); } } bool Capture::addJob(bool preview) { if (m_State != CAPTURE_IDLE && m_State != CAPTURE_ABORTED && m_State != CAPTURE_COMPLETE) return false; SequenceJob * job = nullptr; QString imagePrefix; if (preview == false && darkSubCheck->isChecked()) { KSNotification::error(i18n("Auto dark subtract is not supported in batch mode.")); return false; } if (uploadModeCombo->currentIndex() != ISD::CCD::UPLOAD_CLIENT && remoteDirIN->text().isEmpty()) { KSNotification::error(i18n("You must set remote directory for Local & Both modes.")); return false; } if (uploadModeCombo->currentIndex() != ISD::CCD::UPLOAD_LOCAL && fitsDir->text().isEmpty()) { KSNotification::error(i18n("You must set local directory for Client & Both modes.")); return false; } if (m_JobUnderEdit) job = jobs.at(queueTable->currentRow()); else { job = new SequenceJob(); job->setFilterManager(filterManager); } if (job == nullptr) { qWarning() << "Job is nullptr!" << endl; return false; } if (ISOCombo) job->setISOIndex(ISOCombo->currentIndex()); if (getGain() >= 0) job->setGain(getGain()); job->setTransforFormat(static_cast(transferFormatCombo->currentIndex())); job->setPreview(preview); if (temperatureIN->isEnabled()) { double currentTemperature; currentCCD->getTemperature(¤tTemperature); job->setEnforceTemperature(temperatureCheck->isChecked()); job->setTargetTemperature(temperatureIN->value()); job->setCurrentTemperature(currentTemperature); } job->setCaptureFilter(static_cast(filterCombo->currentIndex())); job->setUploadMode(static_cast(uploadModeCombo->currentIndex())); job->setPostCaptureScript(postCaptureScriptIN->text()); job->setFlatFieldDuration(flatFieldDuration); job->setFlatFieldSource(flatFieldSource); job->setPreMountPark(preMountPark); job->setPreDomePark(preDomePark); job->setWallCoord(wallCoord); job->setTargetADU(targetADU); job->setTargetADUTolerance(targetADUTolerance); imagePrefix = prefixIN->text(); // JM 2019-11-26: In case there is no raw prefix set // BUT target name is set, we update the prefix to include // the target name, which is usually set by the scheduler. if (imagePrefix.isEmpty() && !m_TargetName.isEmpty()) { prefixIN->setText(m_TargetName); imagePrefix = m_TargetName; } constructPrefix(imagePrefix); job->setPrefixSettings(prefixIN->text(), filterCheck->isChecked(), expDurationCheck->isChecked(), ISOCheck->isChecked()); job->setFrameType(static_cast(frameTypeCombo->currentIndex())); job->setFullPrefix(imagePrefix); //if (filterSlot != nullptr && currentFilter != nullptr) if (FilterPosCombo->currentIndex() != -1 && currentFilter != nullptr) job->setTargetFilter(FilterPosCombo->currentIndex() + 1, FilterPosCombo->currentText()); job->setExposure(exposureIN->value()); job->setCount(countIN->value()); job->setBin(binXIN->value(), binYIN->value()); job->setDelay(delayIN->value() * 1000); /* in ms */ job->setActiveChip(targetChip); job->setActiveCCD(currentCCD); job->setActiveFilter(currentFilter); // Custom Properties job->setCustomProperties(customPropertiesDialog->getCustomProperties()); if (currentRotator && rotatorSettings->isRotationEnforced()) { job->setActiveRotator(currentRotator); job->setTargetRotation(rotatorSettings->getTargetRotationPA()); job->setCurrentRotation(rotatorSettings->getCurrentRotationPA()); } job->setFrame(frameXIN->value(), frameYIN->value(), frameWIN->value(), frameHIN->value()); job->setRemoteDir(remoteDirIN->text()); job->setLocalDir(fitsDir->text()); if (m_JobUnderEdit == false) { // JM 2018-09-24: If this is the first job added // We always ignore job progress by default. if (jobs.isEmpty() && preview == false) ignoreJobProgress = true; jobs.append(job); // Nothing more to do if preview if (preview) return true; } QJsonObject jsonJob = {{"Status", "Idle"}}; QString directoryPostfix; /* FIXME: Refactor directoryPostfix assignment, whose code is duplicated in scheduler.cpp */ if (m_TargetName.isEmpty()) directoryPostfix = QLatin1String("/") + frameTypeCombo->currentText(); else directoryPostfix = QLatin1String("/") + m_TargetName + QLatin1String("/") + frameTypeCombo->currentText(); if ((job->getFrameType() == FRAME_LIGHT || job->getFrameType() == FRAME_FLAT) && job->getFilterName().isEmpty() == false) directoryPostfix += QLatin1String("/") + job->getFilterName(); job->setDirectoryPostfix(directoryPostfix); int currentRow = 0; if (m_JobUnderEdit == false) { currentRow = queueTable->rowCount(); queueTable->insertRow(currentRow); } else currentRow = queueTable->currentRow(); QTableWidgetItem * status = m_JobUnderEdit ? queueTable->item(currentRow, 0) : new QTableWidgetItem(); job->setStatusCell(status); QTableWidgetItem * filter = m_JobUnderEdit ? queueTable->item(currentRow, 1) : new QTableWidgetItem(); filter->setText("--"); jsonJob.insert("Filter", "--"); /*if (frameTypeCombo->currentText().compare("Bias", Qt::CaseInsensitive) && frameTypeCombo->currentText().compare("Dark", Qt::CaseInsensitive) && FilterPosCombo->count() > 0)*/ if (FilterPosCombo->count() > 0 && (frameTypeCombo->currentIndex() == FRAME_LIGHT || frameTypeCombo->currentIndex() == FRAME_FLAT)) { filter->setText(FilterPosCombo->currentText()); jsonJob.insert("Filter", FilterPosCombo->currentText()); } filter->setTextAlignment(Qt::AlignHCenter); filter->setFlags(Qt::ItemIsSelectable | Qt::ItemIsEnabled); QTableWidgetItem * type = m_JobUnderEdit ? queueTable->item(currentRow, 2) : new QTableWidgetItem(); type->setText(frameTypeCombo->currentText()); type->setTextAlignment(Qt::AlignHCenter); type->setFlags(Qt::ItemIsSelectable | Qt::ItemIsEnabled); jsonJob.insert("Type", type->text()); QTableWidgetItem * bin = m_JobUnderEdit ? queueTable->item(currentRow, 3) : new QTableWidgetItem(); bin->setText(QString("%1x%2").arg(binXIN->value()).arg(binYIN->value())); bin->setTextAlignment(Qt::AlignHCenter); bin->setFlags(Qt::ItemIsSelectable | Qt::ItemIsEnabled); jsonJob.insert("Bin", bin->text()); QTableWidgetItem * exp = m_JobUnderEdit ? queueTable->item(currentRow, 4) : new QTableWidgetItem(); exp->setText(QString("%L1").arg(exposureIN->value(), 0, 'f', exposureIN->decimals())); exp->setTextAlignment(Qt::AlignHCenter); exp->setFlags(Qt::ItemIsSelectable | Qt::ItemIsEnabled); jsonJob.insert("Exp", exp->text()); QTableWidgetItem * iso = m_JobUnderEdit ? queueTable->item(currentRow, 5) : new QTableWidgetItem(); if (ISOCombo && ISOCombo->currentIndex() != -1) { iso->setText(ISOCombo->currentText()); jsonJob.insert("ISO/Gain", iso->text()); } else if (GainSpin && GainSpin->value() >= 0 && std::fabs(GainSpin->value() - GainSpinSpecialValue) > 0) { iso->setText(GainSpin->cleanText()); jsonJob.insert("ISO/Gain", iso->text()); } else { iso->setText("--"); jsonJob.insert("ISO/Gain", "--"); } iso->setTextAlignment(Qt::AlignHCenter); iso->setFlags(Qt::ItemIsSelectable | Qt::ItemIsEnabled); QTableWidgetItem * count = m_JobUnderEdit ? queueTable->item(currentRow, 6) : new QTableWidgetItem(); job->setCountCell(count); jsonJob.insert("Count", count->text()); if (m_JobUnderEdit == false) { queueTable->setItem(currentRow, 0, status); queueTable->setItem(currentRow, 1, filter); queueTable->setItem(currentRow, 2, type); queueTable->setItem(currentRow, 3, bin); queueTable->setItem(currentRow, 4, exp); queueTable->setItem(currentRow, 5, iso); queueTable->setItem(currentRow, 6, count); m_SequenceArray.append(jsonJob); emit sequenceChanged(m_SequenceArray); } removeFromQueueB->setEnabled(true); if (queueTable->rowCount() > 0) { queueSaveAsB->setEnabled(true); queueSaveB->setEnabled(true); resetB->setEnabled(true); m_Dirty = true; } if (queueTable->rowCount() > 1) { queueUpB->setEnabled(true); queueDownB->setEnabled(true); } if (m_JobUnderEdit) { m_JobUnderEdit = false; resetJobEdit(); appendLogText(i18n("Job #%1 changes applied.", currentRow + 1)); m_SequenceArray.replace(currentRow, jsonJob); emit sequenceChanged(m_SequenceArray); } return true; } void Capture::removeJobFromQueue() { int currentRow = queueTable->currentRow(); if (currentRow < 0) currentRow = queueTable->rowCount() - 1; removeJob(currentRow); // update selection if (queueTable->rowCount() == 0) return; if (currentRow > queueTable->rowCount()) queueTable->selectRow(queueTable->rowCount() - 1); else queueTable->selectRow(currentRow); } void Capture::removeJob(int index) { if (m_State != CAPTURE_IDLE && m_State != CAPTURE_ABORTED && m_State != CAPTURE_COMPLETE) return; if (m_JobUnderEdit) { resetJobEdit(); return; } if (index < 0) return; queueTable->removeRow(index); m_SequenceArray.removeAt(index); emit sequenceChanged(m_SequenceArray); SequenceJob * job = jobs.at(index); jobs.removeOne(job); if (job == activeJob) activeJob = nullptr; delete job; if (queueTable->rowCount() == 0) removeFromQueueB->setEnabled(false); if (queueTable->rowCount() == 1) { queueUpB->setEnabled(false); queueDownB->setEnabled(false); } for (int i = 0; i < jobs.count(); i++) jobs.at(i)->setStatusCell(queueTable->item(i, 0)); if (index < queueTable->rowCount()) queueTable->selectRow(index); else if (queueTable->rowCount() > 0) queueTable->selectRow(queueTable->rowCount() - 1); if (queueTable->rowCount() == 0) { queueSaveAsB->setEnabled(false); queueSaveB->setEnabled(false); resetB->setEnabled(false); } m_Dirty = true; } void Capture::moveJobUp() { int currentRow = queueTable->currentRow(); int columnCount = queueTable->columnCount(); if (currentRow <= 0 || queueTable->rowCount() == 1) return; int destinationRow = currentRow - 1; for (int i = 0; i < columnCount; i++) { QTableWidgetItem * downItem = queueTable->takeItem(currentRow, i); QTableWidgetItem * upItem = queueTable->takeItem(destinationRow, i); queueTable->setItem(destinationRow, i, downItem); queueTable->setItem(currentRow, i, upItem); } SequenceJob * job = jobs.takeAt(currentRow); jobs.removeOne(job); jobs.insert(destinationRow, job); QJsonObject currentJob = m_SequenceArray[currentRow].toObject(); m_SequenceArray.replace(currentRow, m_SequenceArray[destinationRow]); m_SequenceArray.replace(destinationRow, currentJob); emit sequenceChanged(m_SequenceArray); queueTable->selectRow(destinationRow); for (int i = 0; i < jobs.count(); i++) jobs.at(i)->setStatusCell(queueTable->item(i, 0)); m_Dirty = true; } void Capture::moveJobDown() { int currentRow = queueTable->currentRow(); int columnCount = queueTable->columnCount(); if (currentRow < 0 || queueTable->rowCount() == 1 || (currentRow + 1) == queueTable->rowCount()) return; int destinationRow = currentRow + 1; for (int i = 0; i < columnCount; i++) { QTableWidgetItem * downItem = queueTable->takeItem(currentRow, i); QTableWidgetItem * upItem = queueTable->takeItem(destinationRow, i); queueTable->setItem(destinationRow, i, downItem); queueTable->setItem(currentRow, i, upItem); } SequenceJob * job = jobs.takeAt(currentRow); jobs.removeOne(job); jobs.insert(destinationRow, job); QJsonObject currentJob = m_SequenceArray[currentRow].toObject(); m_SequenceArray.replace(currentRow, m_SequenceArray[destinationRow]); m_SequenceArray.replace(destinationRow, currentJob); emit sequenceChanged(m_SequenceArray); queueTable->selectRow(destinationRow); for (int i = 0; i < jobs.count(); i++) jobs.at(i)->setStatusCell(queueTable->item(i, 0)); m_Dirty = true; } void Capture::setBusy(bool enable) { isBusy = enable; enable ? pi->startAnimation() : pi->stopAnimation(); previewB->setEnabled(!enable); loopB->setEnabled(!enable); foreach (QAbstractButton * button, queueEditButtonGroup->buttons()) button->setEnabled(!enable); } void Capture::prepareJob(SequenceJob * job) { activeJob = job; if (m_isLooping == false) qCDebug(KSTARS_EKOS_CAPTURE) << "Preparing capture job" << job->getSignature() << "for execution."; int index = jobs.indexOf(job); if (index >= 0) queueTable->selectRow(index); if (activeJob->getActiveCCD() != currentCCD) { setCamera(activeJob->getActiveCCD()->getDeviceName()); } /*if (activeJob->isPreview()) seqTotalCount = -1; else seqTotalCount = activeJob->getCount();*/ seqDelay = activeJob->getDelay(); // seqCurrentCount = activeJob->getCompleted(); if (activeJob->isPreview() == false) { fullImgCountOUT->setText(QString("%L1").arg(activeJob->getCount())); currentImgCountOUT->setText(QString("%L1").arg(activeJob->getCompleted())); // set the progress info imgProgress->setEnabled(true); imgProgress->setMaximum(activeJob->getCount()); imgProgress->setValue(activeJob->getCompleted()); if (currentCCD->getUploadMode() != ISD::CCD::UPLOAD_LOCAL) updateSequencePrefix(activeJob->getFullPrefix(), QFileInfo(activeJob->getSignature()).path()); } // We check if the job is already fully or partially complete by checking how many files of its type exist on the file system if (activeJob->isPreview() == false) { // The signature is the unique identification path in the system for a particular job. Format is "///". // If the Scheduler is requesting the Capture tab to process a sequence job, a target name will be inserted after the sequence file storage field (e.g. /path/to/storage/target/Light/...) // If the end-user is requesting the Capture tab to process a sequence job, the sequence file storage will be used as is (e.g. /path/to/storage/Light/...) QString signature = activeJob->getSignature(); // Now check on the file system ALL the files that exist with the above signature // If 29 files exist for example, then nextSequenceID would be the NEXT file number (30) // Therefore, we know how to number the next file. // However, we do not deduce the number of captures to process from this function. checkSeqBoundary(signature); // Captured Frames Map contains a list of signatures:count of _already_ captured files in the file system. // This map is set by the Scheduler in order to complete efficiently the required captures. // When the end-user requests a sequence to be processed, that map is empty. // // Example with a 5xL-5xR-5xG-5xB sequence // // When the end-user loads and runs this sequence, each filter gets to capture 5 frames, then the procedure stops. // When the Scheduler executes a job with this sequence, the procedure depends on what is in the storage. // // Let's consider the Scheduler has 3 instances of this job to run. // // When the first job completes the sequence, there are 20 images in the file system (5 for each filter). // When the second job starts, Scheduler finds those 20 images but requires 20 more images, thus sets the frames map counters to 0 for all LRGB frames. // When the third job starts, Scheduler now has 40 images, but still requires 20 more, thus again sets the frames map counters to 0 for all LRGB frames. // // Now let's consider something went wrong, and the third job was aborted before getting to 60 images, say we have full LRG, but only 1xB. // When Scheduler attempts to run the aborted job again, it will count captures in storage, subtract previous job requirements, and set the frames map counters to 0 for LRG, and 4 for B. // When the sequence runs, the procedure will bypass LRG and proceed to capture 4xB. if (capturedFramesMap.contains(signature)) { // Get the current capture count from the map int count = capturedFramesMap[signature]; // Count how many captures this job has to process, given that previous jobs may have done some work already foreach (SequenceJob * a_job, jobs) if (a_job == activeJob) break; else if (a_job->getSignature() == activeJob->getSignature()) count -= a_job->getCompleted(); // This is the current completion count of the current job activeJob->setCompleted(count); } // JM 2018-09-24: Only set completed jobs to 0 IF the scheduler set captured frames map to begin with // If the map is empty, then no scheduler is used and it should proceed as normal. else if (capturedFramesMap.count() > 0) { // No preliminary information, we reset the job count and run the job unconditionally to clarify the behavior activeJob->setCompleted(0); } // JM 2018-09-24: In case ignoreJobProgress is enabled // We check if this particular job progress ignore flag is set. If not, // then we set it and reset completed to zero. Next time it is evaluated here again // It will maintain its count regardless else if (ignoreJobProgress && activeJob->getJobProgressIgnored() == false) { activeJob->setJobProgressIgnored(true); activeJob->setCompleted(0); } // We cannot rely on sequenceID to give us a count - if we don't ignore job progress, we leave the count as it was originally #if 0 // If we cannot ignore job progress, then we set completed job number according to what // was found on the file system. else if (ignoreJobProgress == false) { int count = nextSequenceID - 1; if (count < activeJob->getCount()) activeJob->setCompleted(count); else activeJob->setCompleted(activeJob->getCount()); } #endif // Check whether active job is complete by comparing required captures to what is already available if (activeJob->getCount() <= activeJob->getCompleted()) { activeJob->setCompleted(activeJob->getCount()); appendLogText(i18n("Job requires %1-second %2 images, has already %3/%4 captures and does not need to run.", QString("%L1").arg(job->getExposure(), 0, 'f', 3), job->getFilterName(), activeJob->getCompleted(), activeJob->getCount())); processJobCompletion(); /* FIXME: find a clearer way to exit here */ return; } else { // There are captures to process currentImgCountOUT->setText(QString("%L1").arg(activeJob->getCompleted())); appendLogText(i18n("Job requires %1-second %2 images, has %3/%4 frames captured and will be processed.", QString("%L1").arg(job->getExposure(), 0, 'f', 3), job->getFilterName(), activeJob->getCompleted(), activeJob->getCount())); // Emit progress update - done a few lines below // emit newImage(nullptr, activeJob); currentCCD->setNextSequenceID(nextSequenceID); } } if (currentCCD->isBLOBEnabled() == false) { // FIXME: Move this warning pop-up elsewhere, it will interfere with automation. // if (Options::guiderType() != Ekos::Guide::GUIDE_INTERNAL || KMessageBox::questionYesNo(nullptr, i18n("Image transfer is disabled for this camera. Would you like to enable it?")) == // KMessageBox::Yes) if (Options::guiderType() != Ekos::Guide::GUIDE_INTERNAL) { currentCCD->setBLOBEnabled(true); } else { connect(KSMessageBox::Instance(), &KSMessageBox::accepted, this, [this]() { //QObject::disconnect(KSMessageBox::Instance(), &KSMessageBox::accepted, this, nullptr); KSMessageBox::Instance()->disconnect(this); currentCCD->setBLOBEnabled(true); prepareActiveJob(); }); connect(KSMessageBox::Instance(), &KSMessageBox::rejected, this, [this]() { //QObject::disconnect(KSMessageBox::Instance(), &KSMessageBox::rejected, this, nullptr); KSMessageBox::Instance()->disconnect(this); currentCCD->setBLOBEnabled(true); setBusy(false); }); KSMessageBox::Instance()->questionYesNo(i18n("Image transfer is disabled for this camera. Would you like to enable it?"), i18n("Image Transfer"), 15); return; } } prepareActiveJob(); } void Capture::prepareActiveJob() { // Just notification of active job stating up emit newImage(activeJob); //connect(job, SIGNAL(checkFocus()), this, &Ekos::Capture::startPostFilterAutoFocus())); // Reset calibration stage if (calibrationStage == CAL_CAPTURING) { if (activeJob->getFrameType() != FRAME_LIGHT) calibrationStage = CAL_PRECAPTURE_COMPLETE; else calibrationStage = CAL_NONE; } /* Disable this restriction, let the sequence run even if focus did not run prior to the capture. * Besides, this locks up the Scheduler when the Capture module starts a sequence without any prior focus procedure done. * This is quite an old code block. The message "Manual scheduled" seems to even refer to some manual intervention? * With the new HFR threshold, it might be interesting to prevent the execution because we actually need an HFR value to * begin capturing, but even there, on one hand it makes sense for the end-user to know what HFR to put in the edit box, * and on the other hand the focus procedure will deduce the next HFR automatically. * But in the end, it's not entirely clear what the intent was. Note there is still a warning that a preliminary autofocus * procedure is important to avoid any surprise that could make the whole schedule ineffective. */ #if 0 // If we haven't performed a single autofocus yet, we stop //if (!job->isPreview() && Options::enforceRefocusEveryN() && autoFocusReady && isInSequenceFocus == false && firstAutoFocus == true) if (!job->isPreview() && Options::enforceRefocusEveryN() && autoFocusReady == false && isInSequenceFocus == false) { appendLogText(i18n("Manual scheduled focusing is not supported. Run Autofocus process before trying again.")); abort(); return; } #endif #if 0 if (currentFilterPosition > 0) { // If we haven't performed a single autofocus yet, we stop if (!job->isPreview() && Options::autoFocusOnFilterChange() && (isInSequenceFocus == false && firstAutoFocus == true)) { appendLogText(i18n( "Manual focusing post filter change is not supported. Run Autofocus process before trying again.")); abort(); return; } /* if (currentFilterPosition != activeJob->getTargetFilter() && filterFocusOffsets.empty() == false) { int16_t targetFilterOffset = 0; foreach (FocusOffset *offset, filterFocusOffsets) { if (offset->filter == activeJob->getFilterName()) { targetFilterOffset = offset->offset - lastFilterOffset; lastFilterOffset = offset->offset; break; } } if (targetFilterOffset != 0 && (activeJob->getFrameType() == FRAME_LIGHT || activeJob->getFrameType() == FRAME_FLAT)) { appendLogText(i18n("Adjust focus offset by %1 steps", targetFilterOffset)); secondsLabel->setText(i18n("Adjusting filter offset")); if (activeJob->isPreview() == false) { state = CAPTURE_FILTER_FOCUS; emit newStatus(Ekos::CAPTURE_FILTER_FOCUS); } setBusy(true); emit newFocusOffset(targetFilterOffset); return; } } */ } #endif preparePreCaptureActions(); } void Capture::preparePreCaptureActions() { // Update position if (m_CurrentFilterPosition > 0) activeJob->setCurrentFilter(m_CurrentFilterPosition); // update temperature if (currentCCD->hasCooler() && activeJob->getEnforceTemperature()) { double temperature = 0; currentCCD->getTemperature(&temperature); activeJob->setCurrentTemperature(temperature); } // update rotator angle if (currentRotator != nullptr && activeJob->getTargetRotation() != Ekos::INVALID_VALUE) activeJob->setCurrentRotation(rotatorSettings->getCurrentRotationPA()); setBusy(true); if (activeJob->isPreview()) { startB->setIcon( QIcon::fromTheme("media-playback-stop")); startB->setToolTip(i18n("Stop")); } connect(activeJob, &SequenceJob::prepareState, this, &Ekos::Capture::updatePrepareState); connect(activeJob, &SequenceJob::prepareComplete, this, &Ekos::Capture::executeJob); activeJob->prepareCapture(); } void Capture::updatePrepareState(Ekos::CaptureState prepareState) { m_State = prepareState; emit newStatus(prepareState); switch (prepareState) { case CAPTURE_SETTING_TEMPERATURE: appendLogText(i18n("Setting temperature to %1 C...", activeJob->getTargetTemperature())); secondsLabel->setText(i18n("Set %1 C...", activeJob->getTargetTemperature())); break; case CAPTURE_SETTING_ROTATOR: appendLogText(i18n("Setting rotation to %1 degrees E of N...", activeJob->getTargetRotation())); secondsLabel->setText(i18n("Set Rotator %1...", activeJob->getTargetRotation())); break; default: break; } } void Capture::executeJob() { activeJob->disconnect(this); QMap FITSHeader; QString rawPrefix = activeJob->property("rawPrefix").toString(); if (m_ObserverName.isEmpty() == false) FITSHeader["FITS_OBSERVER"] = m_ObserverName; if (m_TargetName.isEmpty() == false) FITSHeader["FITS_OBJECT"] = m_TargetName; else if (rawPrefix.isEmpty() == false) { FITSHeader["FITS_OBJECT"] = rawPrefix; } if (FITSHeader.count() > 0) currentCCD->setFITSHeader(FITSHeader); // Update button status setBusy(true); useGuideHead = (activeJob->getActiveChip()->getType() == ISD::CCDChip::PRIMARY_CCD) ? false : true; syncGUIToJob(activeJob); calibrationCheckType = CAL_CHECK_TASK; updatePreCaptureCalibrationStatus(); // Check calibration frame requirements #if 0 if (activeJob->getFrameType() != FRAME_LIGHT && activeJob->isPreview() == false) { updatePreCaptureCalibrationStatus(); return; } captureImage(); #endif } void Capture::updatePreCaptureCalibrationStatus() { // If process was aborted or stopped by the user if (isBusy == false) { appendLogText(i18n("Warning: Calibration process was prematurely terminated.")); return; } IPState rc = processPreCaptureCalibrationStage(); if (rc == IPS_ALERT) return; else if (rc == IPS_BUSY) { // Clear the label if we are neither executing a meridian flip nor re-focusing if ((meridianFlipStage == MF_NONE || meridianFlipStage == MF_READY) && m_State != CAPTURE_FOCUSING) secondsLabel->clear(); QTimer::singleShot(1000, this, &Ekos::Capture::updatePreCaptureCalibrationStatus); return; } captureImage(); } void Capture::setFocusTemperatureDelta(double focusTemperatureDelta) { qCDebug(KSTARS_EKOS_CAPTURE) << "setFocusTemperatureDelta: " << focusTemperatureDelta; this->focusTemperatureDelta = focusTemperatureDelta; } void Capture::setGuideDeviation(double delta_ra, double delta_dec) { // if (activeJob == nullptr) // { // if (deviationDetected == false) // return; // // Try to find first job that was aborted due to deviation // for(SequenceJob *job : jobs) // { // if (job->getStatus() == SequenceJob::JOB_ABORTED) // { // activeJob = job; // break; // } // } // if (activeJob == nullptr) // return; // } // If guiding is started after a meridian flip we will start getting guide deviations again // if the guide deviations are within our limits, we resume the sequence if (meridianFlipStage == MF_GUIDING) { double deviation_rms = std::hypot(delta_ra, delta_dec); // If the user didn't select any guiding deviation, we fall through // otherwise we can for deviation RMS if (guideDeviationCheck->isChecked() == false || deviation_rms < guideDeviation->value()) { appendLogText(i18n("Post meridian flip calibration completed successfully.")); resumeSequence(); // N.B. Set meridian flip stage AFTER resumeSequence() always setMeridianFlipStage(MF_NONE); return; } } // We don't enforce limit on previews if (guideDeviationCheck->isChecked() == false || (activeJob && (activeJob->isPreview() || activeJob->getExposeLeft() == 0))) return; double deviation_rms = sqrt( (delta_ra * delta_ra + delta_dec * delta_dec) / 2.0); QString deviationText = QString("%1").arg(deviation_rms, 0, 'f', 3); // If we have an active busy job, let's abort it if guiding deviation is exceeded. // And we accounted for the spike if (activeJob && activeJob->getStatus() == SequenceJob::JOB_BUSY && activeJob->getFrameType() == FRAME_LIGHT) { if (deviation_rms > guideDeviation->value()) { // Ignore spikes ONCE if (m_SpikeDetected == false) { m_SpikeDetected = true; return; } appendLogText(i18n("Guiding deviation %1 exceeded limit value of %2 arcsecs, " "suspending exposure and waiting for guider up to %3 seconds.", deviationText, guideDeviation->value(), QString("%L1").arg(guideDeviationTimer.interval() / 1000.0, 0, 'f', 3))); suspend(); m_SpikeDetected = false; // Check if we need to start meridian flip if (checkMeridianFlip()) return; m_DeviationDetected = true; guideDeviationTimer.start(); } return; } // Find the first aborted job SequenceJob * abortedJob = nullptr; for(SequenceJob * job : jobs) { if (job->getStatus() == SequenceJob::JOB_ABORTED) { abortedJob = job; break; } } if (abortedJob && m_DeviationDetected) { if (deviation_rms <= guideDeviation->value()) { guideDeviationTimer.stop(); if (seqDelay == 0) appendLogText(i18n("Guiding deviation %1 is now lower than limit value of %2 arcsecs, " "resuming exposure.", deviationText, guideDeviation->value())); else appendLogText(i18n("Guiding deviation %1 is now lower than limit value of %2 arcsecs, " "resuming exposure in %3 seconds.", deviationText, guideDeviation->value(), seqDelay / 1000.0)); QTimer::singleShot(seqDelay, this, &Ekos::Capture::start); return; } else appendLogText(i18n("Guiding deviation %1 is still higher than limit value of %2 arcsecs.", deviationText, guideDeviation->value())); } } void Capture::setFocusStatus(FocusState state) { qCDebug(KSTARS_EKOS_CAPTURE) << "setFocusStatus: " << state; focusState = state; if (focusState > FOCUS_ABORTED) return; if (focusState == FOCUS_COMPLETE) { // enable option to have a refocus event occur if HFR goes over threshold m_AutoFocusReady = true; //if (HFRPixels->value() == 0.0 && fileHFR == 0.0) if (fileHFR == 0.0) { QList filterHFRList; if (m_CurrentFilterPosition > 0) { // If we are using filters, then we retrieve which filter is currently active. // We check if filter lock is used, and store that instead of the current filter. // e.g. If current filter HA, but lock filter is L, then the HFR value is stored for L filter. // If no lock filter exists, then we store as is (HA) QString currentFilterText = FilterPosCombo->itemText(m_CurrentFilterPosition - 1); //QString filterLock = filterManager.data()->getFilterLock(currentFilterText); //QString finalFilter = (filterLock == "--" ? currentFilterText : filterLock); //filterHFRList = HFRMap[finalFilter]; filterHFRList = HFRMap[currentFilterText]; filterHFRList.append(focusHFR); //HFRMap[finalFilter] = filterHFRList; HFRMap[currentFilterText] = filterHFRList; } // No filters else { filterHFRList = HFRMap["--"]; filterHFRList.append(focusHFR); HFRMap["--"] = filterHFRList; } double median = focusHFR; int count = filterHFRList.size(); if (Options::useMedianFocus() && count > 1) median = (count % 2) ? filterHFRList[count / 2] : (filterHFRList[count / 2 - 1] + filterHFRList[count / 2]) / 2.0; // Add 2.5% (default) to the automatic initial HFR value to allow for minute changes in HFR without need to refocus // in case in-sequence-focusing is used. HFRPixels->setValue(median + (median * (Options::hFRThresholdPercentage() / 100.0))); } #if 0 if (focusHFR > 0 && firstAutoFocus && HFRPixels->value() == 0 && fileHFR == 0) { firstAutoFocus = false; // Add 2.5% (default) to the automatic initial HFR value to allow for minute changes in HFR without need to refocus // in case in-sequence-focusing is used. HFRPixels->setValue(focusHFR + (focusHFR * (Options::hFRThresholdPercentage() / 100.0))); } #endif // successful focus so reset elapsed time restartRefocusEveryNTimer(); } #if 0 if (activeJob && (activeJob->getStatus() == SequenceJob::JOB_ABORTED || activeJob->getStatus() == SequenceJob::JOB_IDLE)) { if (focusState == FOCUS_COMPLETE) { //HFRPixels->setValue(focusHFR + (focusHFR * 0.025)); appendLogText(i18n("Focus complete.")); } else if (focusState == FOCUS_FAILED) { appendLogText(i18n("Autofocus failed. Aborting exposure...")); secondsLabel->setText(""); abort(); } return; } #endif if ((isRefocus || isInSequenceFocus) && activeJob && activeJob->getStatus() == SequenceJob::JOB_BUSY) { // if the focusing has been started during the post-calibration, return to the calibration if (calibrationStage < CAL_PRECAPTURE_COMPLETE && m_State == CAPTURE_FOCUSING) { if (focusState == FOCUS_COMPLETE) { appendLogText(i18n("Focus complete.")); secondsLabel->setText(i18n("Focus complete.")); m_State = CAPTURE_PROGRESS; } else if (focusState == FOCUS_FAILED) { appendLogText(i18n("Autofocus failed.")); secondsLabel->setText(i18n("Autofocus failed.")); abort(); } } else if (focusState == FOCUS_COMPLETE) { appendLogText(i18n("Focus complete.")); secondsLabel->setText(i18n("Focus complete.")); startNextExposure(); } else if (focusState == FOCUS_FAILED) { appendLogText(i18n("Autofocus failed. Aborting exposure...")); secondsLabel->setText(i18n("Autofocus failed.")); abort(); } } } void Capture::updateHFRThreshold() { if (fileHFR != 0.0) return; QList filterHFRList; if (FilterPosCombo->currentIndex() != -1) { // If we are using filters, then we retrieve which filter is currently active. // We check if filter lock is used, and store that instead of the current filter. // e.g. If current filter HA, but lock filter is L, then the HFR value is stored for L filter. // If no lock filter exists, then we store as is (HA) QString currentFilterText = FilterPosCombo->currentText(); QString filterLock = filterManager.data()->getFilterLock(currentFilterText); QString finalFilter = (filterLock == "--" ? currentFilterText : filterLock); filterHFRList = HFRMap[finalFilter]; } // No filters else { filterHFRList = HFRMap["--"]; } if (filterHFRList.empty()) { HFRPixels->setValue(Options::hFRDeviation()); return; } double median = 0; int count = filterHFRList.size(); if (count > 1) median = (count % 2) ? filterHFRList[count / 2] : (filterHFRList[count / 2 - 1] + filterHFRList[count / 2]) / 2.0; else if (count == 1) median = filterHFRList[0]; // Add 2.5% (default) to the automatic initial HFR value to allow for minute changes in HFR without need to refocus // in case in-sequence-focusing is used. HFRPixels->setValue(median + (median * (Options::hFRThresholdPercentage() / 100.0))); } void Capture::setMeridianFlipStage(MFStage stage) { qCDebug(KSTARS_EKOS_CAPTURE) << "setMeridianFlipStage: " << MFStageString(stage); if (meridianFlipStage != stage) { switch (stage) { case MF_NONE: if (m_State == CAPTURE_PAUSED) secondsLabel->setText(i18n("Paused...")); meridianFlipStage = stage; emit newMeridianFlipStatus(Mount::FLIP_NONE); break; case MF_READY: if (meridianFlipStage == MF_REQUESTED) { // we keep the stage on requested until the mount starts the meridian flip emit newMeridianFlipStatus(Mount::FLIP_ACCEPTED); } else if (m_State == CAPTURE_PAUSED) { // paused after meridian flip requested secondsLabel->setText(i18n("Paused...")); meridianFlipStage = stage; emit newMeridianFlipStatus(Mount::FLIP_ACCEPTED); } // in any other case, ignore it break; case MF_INITIATED: meridianFlipStage = MF_INITIATED; emit meridianFlipStarted(); secondsLabel->setText(i18n("Meridian Flip...")); KSNotification::event(QLatin1String("MeridianFlipStarted"), i18n("Meridian flip started"), KSNotification::EVENT_INFO); break; case MF_REQUESTED: if (m_State == CAPTURE_PAUSED) // paused before meridian flip requested emit newMeridianFlipStatus(Mount::FLIP_ACCEPTED); else emit newMeridianFlipStatus(Mount::FLIP_WAITING); meridianFlipStage = stage; break; case MF_COMPLETED: secondsLabel->setText(i18n("Flip complete.")); break; default: meridianFlipStage = stage; break; } } } void Capture::meridianFlipStatusChanged(Mount::MeridianFlipStatus status) { qCDebug(KSTARS_EKOS_CAPTURE) << "meridianFlipStatusChanged: " << Mount::meridianFlipStatusString(status); switch (status) { case Mount::FLIP_NONE: // MF_NONE as external signal ignored so that re-alignment and guiding are processed first if (meridianFlipStage < MF_COMPLETED) setMeridianFlipStage(MF_NONE); break; case Mount::FLIP_PLANNED: if (meridianFlipStage > MF_NONE) { // it seems like the meridian flip had been postponed resumeSequence(); return; } else { // If we are autoguiding, we should resume autoguiding after flip resumeGuidingAfterFlip = isGuidingActive(); if (m_State == CAPTURE_IDLE || m_State == CAPTURE_ABORTED || m_State == CAPTURE_COMPLETE || m_State == CAPTURE_PAUSED) { setMeridianFlipStage(MF_INITIATED); emit newMeridianFlipStatus(Mount::FLIP_ACCEPTED); } else setMeridianFlipStage(MF_REQUESTED); } break; case Mount::FLIP_RUNNING: setMeridianFlipStage(MF_INITIATED); emit newStatus(Ekos::CAPTURE_MERIDIAN_FLIP); break; case Mount::FLIP_COMPLETED: setMeridianFlipStage(MF_COMPLETED); emit newStatus(Ekos::CAPTURE_IDLE); processFlipCompleted(); break; default: break; } } int Capture::getTotalFramesCount(QString signature) { int result = 0; bool found = false; foreach (SequenceJob * job, jobs) { // FIXME: this should be part of SequenceJob QString sig = job->getSignature(); if (sig == signature) { result += job->getCount(); found = true; } } if (found) return result; else return -1; } void Capture::setRotator(ISD::GDInterface * newRotator) { currentRotator = newRotator; connect(currentRotator, &ISD::GDInterface::numberUpdated, this, &Ekos::Capture::updateRotatorNumber, Qt::UniqueConnection); rotatorB->setEnabled(true); rotatorSettings->setRotator(newRotator); INumberVectorProperty * nvp = newRotator->getBaseDevice()->getNumber("ABS_ROTATOR_ANGLE"); rotatorSettings->setCurrentAngle(nvp->np[0].value); } void Capture::setTelescope(ISD::GDInterface * newTelescope) { currentTelescope = static_cast(newTelescope); currentTelescope->disconnect(this); connect(currentTelescope, &ISD::GDInterface::numberUpdated, this, &Ekos::Capture::processTelescopeNumber); connect(currentTelescope, &ISD::Telescope::newTarget, [&](const QString & target) { if (m_State == CAPTURE_IDLE || m_State == CAPTURE_COMPLETE) { QString sanitized = target; // Remove illegal characters that can be problematic sanitized = sanitized.replace( QRegularExpression("\\s|/|\\(|\\)|:|\\*|~|\"" ), "_" ) // Remove any two or more __ .replace( QRegularExpression("_{2,}"), "_") // Remove any _ at the end .replace( QRegularExpression("_$"), ""); prefixIN->setText(sanitized); } }); syncTelescopeInfo(); } void Capture::syncTelescopeInfo() { if (currentTelescope && currentTelescope->isConnected()) { // Sync ALL CCDs to current telescope for (ISD::CCD * oneCCD : CCDs) { ITextVectorProperty * activeDevices = oneCCD->getBaseDevice()->getText("ACTIVE_DEVICES"); if (activeDevices) { IText * activeTelescope = IUFindText(activeDevices, "ACTIVE_TELESCOPE"); if (activeTelescope) { IUSaveText(activeTelescope, currentTelescope->getDeviceName().toLatin1().constData()); oneCCD->getDriverInfo()->getClientManager()->sendNewText(activeDevices); } } } } } void Capture::saveFITSDirectory() { QString dir = QFileDialog::getExistingDirectory(KStars::Instance(), i18n("FITS Save Directory"), dirPath.toLocalFile()); if (dir.isEmpty()) return; fitsDir->setText(dir); } void Capture::loadSequenceQueue() { QUrl fileURL = QFileDialog::getOpenFileUrl(KStars::Instance(), i18n("Open Ekos Sequence Queue"), dirPath, "Ekos Sequence Queue (*.esq)"); if (fileURL.isEmpty()) return; if (fileURL.isValid() == false) { QString message = i18n("Invalid URL: %1", fileURL.toLocalFile()); KSNotification::sorry(message, i18n("Invalid URL")); return; } dirPath = QUrl(fileURL.url(QUrl::RemoveFilename)); loadSequenceQueue(fileURL.toLocalFile()); } bool Capture::loadSequenceQueue(const QString &fileURL) { QFile sFile(fileURL); if (!sFile.open(QIODevice::ReadOnly)) { QString message = i18n("Unable to open file %1", fileURL); KSNotification::sorry(message, i18n("Could Not Open File")); return false; } capturedFramesMap.clear(); clearSequenceQueue(); LilXML * xmlParser = newLilXML(); char errmsg[MAXRBUF]; XMLEle * root = nullptr; XMLEle * ep = nullptr; char c; // We expect all data read from the XML to be in the C locale - QLocale::c(). QLocale cLocale = QLocale::c(); while (sFile.getChar(&c)) { root = readXMLEle(xmlParser, c, errmsg); if (root) { double sqVersion = cLocale.toFloat(findXMLAttValu(root, "version")); if (sqVersion < SQ_COMPAT_VERSION) { appendLogText(i18n("Deprecated sequence file format version %1. Please construct a new sequence file.", sqVersion)); return false; } for (ep = nextXMLEle(root, 1); ep != nullptr; ep = nextXMLEle(root, 0)) { if (!strcmp(tagXMLEle(ep), "Observer")) { m_ObserverName = QString(pcdataXMLEle(ep)); } else if (!strcmp(tagXMLEle(ep), "GuideDeviation")) { guideDeviationCheck->setChecked(!strcmp(findXMLAttValu(ep, "enabled"), "true")); guideDeviation->setValue(cLocale.toDouble(pcdataXMLEle(ep))); } else if (!strcmp(tagXMLEle(ep), "Autofocus")) { autofocusCheck->setChecked(!strcmp(findXMLAttValu(ep, "enabled"), "true")); double const HFRValue = cLocale.toDouble(pcdataXMLEle(ep)); // Set the HFR value from XML, or reset it to zero, don't let another unrelated older HFR be used // Note that HFR value will only be serialized to XML when option "Save Sequence HFR to File" is enabled fileHFR = HFRValue > 0.0 ? HFRValue : 0.0; HFRPixels->setValue(fileHFR); } else if (!strcmp(tagXMLEle(ep), "RefocusOnTemperatureDelta")) { temperatureDeltaCheck->setChecked(!strcmp(findXMLAttValu(ep, "enabled"), "true")); double const deltaValue = cLocale.toDouble(pcdataXMLEle(ep)); temperatureDelta->setValue(deltaValue); } else if (!strcmp(tagXMLEle(ep), "RefocusEveryN")) { refocusEveryNCheck->setChecked(!strcmp(findXMLAttValu(ep, "enabled"), "true")); int const minutesValue = cLocale.toInt(pcdataXMLEle(ep)); // Set the refocus period from XML, or reset it to zero, don't let another unrelated older refocus period be used. refocusEveryNMinutesValue = minutesValue > 0 ? minutesValue : 0; refocusEveryN->setValue(refocusEveryNMinutesValue); } else if (!strcmp(tagXMLEle(ep), "MeridianFlip")) { // meridian flip is managed by the mount only // older files might nevertheless contain MF settings if (! strcmp(findXMLAttValu(ep, "enabled"), "true")) appendLogText( i18n("Meridian flip configuration has been shifted to the mount module. Please configure the meridian flip there.")); } else if (!strcmp(tagXMLEle(ep), "CCD")) { CCDCaptureCombo->setCurrentText(pcdataXMLEle(ep)); // Signal "activated" of QComboBox does not fire when changing the text programmatically setCamera(pcdataXMLEle(ep)); } else if (!strcmp(tagXMLEle(ep), "FilterWheel")) { FilterDevicesCombo->setCurrentText(pcdataXMLEle(ep)); checkFilter(); } else { processJobInfo(ep); } } delXMLEle(root); } else if (errmsg[0]) { appendLogText(QString(errmsg)); delLilXML(xmlParser); return false; } } m_SequenceURL = QUrl::fromLocalFile(fileURL); m_Dirty = false; delLilXML(xmlParser); // update save button tool tip queueSaveB->setToolTip("Save to " + sFile.fileName()); return true; } bool Capture::processJobInfo(XMLEle * root) { XMLEle * ep; XMLEle * subEP; rotatorSettings->setRotationEnforced(false); QLocale cLocale = QLocale::c(); for (ep = nextXMLEle(root, 1); ep != nullptr; ep = nextXMLEle(root, 0)) { if (!strcmp(tagXMLEle(ep), "Exposure")) exposureIN->setValue(cLocale.toDouble(pcdataXMLEle(ep))); else if (!strcmp(tagXMLEle(ep), "Binning")) { subEP = findXMLEle(ep, "X"); if (subEP) binXIN->setValue(cLocale.toInt(pcdataXMLEle(subEP))); subEP = findXMLEle(ep, "Y"); if (subEP) binYIN->setValue(cLocale.toInt(pcdataXMLEle(subEP))); } else if (!strcmp(tagXMLEle(ep), "Frame")) { subEP = findXMLEle(ep, "X"); if (subEP) frameXIN->setValue(cLocale.toInt(pcdataXMLEle(subEP))); subEP = findXMLEle(ep, "Y"); if (subEP) frameYIN->setValue(cLocale.toInt(pcdataXMLEle(subEP))); subEP = findXMLEle(ep, "W"); if (subEP) frameWIN->setValue(cLocale.toInt(pcdataXMLEle(subEP))); subEP = findXMLEle(ep, "H"); if (subEP) frameHIN->setValue(cLocale.toInt(pcdataXMLEle(subEP))); } else if (!strcmp(tagXMLEle(ep), "Temperature")) { if (temperatureIN->isEnabled()) temperatureIN->setValue(cLocale.toDouble(pcdataXMLEle(ep))); // If force attribute exist, we change temperatureCheck, otherwise do nothing. if (!strcmp(findXMLAttValu(ep, "force"), "true")) temperatureCheck->setChecked(true); else if (!strcmp(findXMLAttValu(ep, "force"), "false")) temperatureCheck->setChecked(false); } else if (!strcmp(tagXMLEle(ep), "Filter")) { //FilterPosCombo->setCurrentIndex(atoi(pcdataXMLEle(ep))-1); FilterPosCombo->setCurrentText(pcdataXMLEle(ep)); } else if (!strcmp(tagXMLEle(ep), "Type")) { frameTypeCombo->setCurrentText(pcdataXMLEle(ep)); } else if (!strcmp(tagXMLEle(ep), "Prefix")) { subEP = findXMLEle(ep, "RawPrefix"); if (subEP) prefixIN->setText(pcdataXMLEle(subEP)); subEP = findXMLEle(ep, "FilterEnabled"); if (subEP) filterCheck->setChecked(!strcmp("1", pcdataXMLEle(subEP))); subEP = findXMLEle(ep, "ExpEnabled"); if (subEP) expDurationCheck->setChecked(!strcmp("1", pcdataXMLEle(subEP))); subEP = findXMLEle(ep, "TimeStampEnabled"); if (subEP) ISOCheck->setChecked(!strcmp("1", pcdataXMLEle(subEP))); } else if (!strcmp(tagXMLEle(ep), "Count")) { countIN->setValue(cLocale.toInt(pcdataXMLEle(ep))); } else if (!strcmp(tagXMLEle(ep), "Delay")) { delayIN->setValue(cLocale.toInt(pcdataXMLEle(ep))); } else if (!strcmp(tagXMLEle(ep), "PostCaptureScript")) { postCaptureScriptIN->setText(pcdataXMLEle(ep)); } else if (!strcmp(tagXMLEle(ep), "FITSDirectory")) { fitsDir->setText(pcdataXMLEle(ep)); } else if (!strcmp(tagXMLEle(ep), "RemoteDirectory")) { remoteDirIN->setText(pcdataXMLEle(ep)); } else if (!strcmp(tagXMLEle(ep), "UploadMode")) { uploadModeCombo->setCurrentIndex(cLocale.toInt(pcdataXMLEle(ep))); } else if (!strcmp(tagXMLEle(ep), "ISOIndex")) { if (ISOCombo) ISOCombo->setCurrentIndex(cLocale.toInt(pcdataXMLEle(ep))); } else if (!strcmp(tagXMLEle(ep), "FormatIndex")) { transferFormatCombo->setCurrentIndex(cLocale.toInt(pcdataXMLEle(ep))); } else if (!strcmp(tagXMLEle(ep), "Rotation")) { rotatorSettings->setRotationEnforced(true); rotatorSettings->setTargetRotationPA(cLocale.toDouble(pcdataXMLEle(ep))); } else if (!strcmp(tagXMLEle(ep), "Properties")) { QMap> propertyMap; for (subEP = nextXMLEle(ep, 1); subEP != nullptr; subEP = nextXMLEle(ep, 0)) { QMap numbers; XMLEle * oneNumber = nullptr; for (oneNumber = nextXMLEle(subEP, 1); oneNumber != nullptr; oneNumber = nextXMLEle(subEP, 0)) { const char * name = findXMLAttValu(oneNumber, "name"); numbers[name] = cLocale.toDouble(pcdataXMLEle(oneNumber)); } const char * name = findXMLAttValu(subEP, "name"); propertyMap[name] = numbers; } customPropertiesDialog->setCustomProperties(propertyMap); const double gain = getGain(); if (GainSpin && gain >= 0) GainSpin->setValue(gain); } else if (!strcmp(tagXMLEle(ep), "Calibration")) { subEP = findXMLEle(ep, "FlatSource"); if (subEP) { XMLEle * typeEP = findXMLEle(subEP, "Type"); if (typeEP) { if (!strcmp(pcdataXMLEle(typeEP), "Manual")) flatFieldSource = SOURCE_MANUAL; else if (!strcmp(pcdataXMLEle(typeEP), "FlatCap")) flatFieldSource = SOURCE_FLATCAP; else if (!strcmp(pcdataXMLEle(typeEP), "DarkCap")) flatFieldSource = SOURCE_DARKCAP; else if (!strcmp(pcdataXMLEle(typeEP), "Wall")) { XMLEle * azEP = findXMLEle(subEP, "Az"); XMLEle * altEP = findXMLEle(subEP, "Alt"); if (azEP && altEP) { flatFieldSource = SOURCE_WALL; wallCoord.setAz(cLocale.toDouble(pcdataXMLEle(azEP))); wallCoord.setAlt(cLocale.toDouble(pcdataXMLEle(altEP))); } } else flatFieldSource = SOURCE_DAWN_DUSK; } } subEP = findXMLEle(ep, "FlatDuration"); if (subEP) { XMLEle * typeEP = findXMLEle(subEP, "Type"); if (typeEP) { if (!strcmp(pcdataXMLEle(typeEP), "Manual")) flatFieldDuration = DURATION_MANUAL; } XMLEle * aduEP = findXMLEle(subEP, "Value"); if (aduEP) { flatFieldDuration = DURATION_ADU; targetADU = cLocale.toDouble(pcdataXMLEle(aduEP)); } aduEP = findXMLEle(subEP, "Tolerance"); if (aduEP) { targetADUTolerance = cLocale.toDouble(pcdataXMLEle(aduEP)); } } subEP = findXMLEle(ep, "PreMountPark"); if (subEP) { if (!strcmp(pcdataXMLEle(subEP), "True")) preMountPark = true; else preMountPark = false; } subEP = findXMLEle(ep, "PreDomePark"); if (subEP) { if (!strcmp(pcdataXMLEle(subEP), "True")) preDomePark = true; else preDomePark = false; } } } addJob(false); return true; } void Capture::saveSequenceQueue() { QUrl backupCurrent = m_SequenceURL; if (m_SequenceURL.toLocalFile().startsWith(QLatin1String("/tmp/")) || m_SequenceURL.toLocalFile().contains("/Temp")) m_SequenceURL.clear(); // If no changes made, return. if (m_Dirty == false && !m_SequenceURL.isEmpty()) return; if (m_SequenceURL.isEmpty()) { m_SequenceURL = QFileDialog::getSaveFileUrl(KStars::Instance(), i18n("Save Ekos Sequence Queue"), dirPath, "Ekos Sequence Queue (*.esq)"); // if user presses cancel if (m_SequenceURL.isEmpty()) { m_SequenceURL = backupCurrent; return; } dirPath = QUrl(m_SequenceURL.url(QUrl::RemoveFilename)); if (m_SequenceURL.toLocalFile().endsWith(QLatin1String(".esq")) == false) m_SequenceURL.setPath(m_SequenceURL.toLocalFile() + ".esq"); /*if (QFile::exists(sequenceURL.toLocalFile())) { int r = KMessageBox::warningContinueCancel(0, i18n("A file named \"%1\" already exists. " "Overwrite it?", sequenceURL.fileName()), i18n("Overwrite File?"), KStandardGuiItem::overwrite()); if (r == KMessageBox::Cancel) return; }*/ } if (m_SequenceURL.isValid()) { if ((saveSequenceQueue(m_SequenceURL.toLocalFile())) == false) { KSNotification::error(i18n("Failed to save sequence queue"), i18n("Save")); return; } m_Dirty = false; } else { QString message = i18n("Invalid URL: %1", m_SequenceURL.url()); KSNotification::sorry(message, i18n("Invalid URL")); } } void Capture::saveSequenceQueueAs() { m_SequenceURL.clear(); saveSequenceQueue(); } bool Capture::saveSequenceQueue(const QString &path) { QFile file; QString rawPrefix; bool filterEnabled, expEnabled, tsEnabled; const QMap frameTypes = { { "Light", FRAME_LIGHT }, { "Dark", FRAME_DARK }, { "Bias", FRAME_BIAS }, { "Flat", FRAME_FLAT } }; file.setFileName(path); if (!file.open(QIODevice::WriteOnly)) { QString message = i18n("Unable to write to file %1", path); KSNotification::sorry(message, i18n("Could not open file")); return false; } QTextStream outstream(&file); // We serialize sequence data to XML using the C locale QLocale cLocale = QLocale::c(); outstream << "" << endl; outstream << "" << endl; if (m_ObserverName.isEmpty() == false) outstream << "" << m_ObserverName << "" << endl; outstream << "" << CCDCaptureCombo->currentText() << "" << endl; outstream << "" << FilterDevicesCombo->currentText() << "" << endl; outstream << "" << cLocale.toString(guideDeviation->value()) << "" << endl; // Issue a warning when autofocus is enabled but Ekos options prevent HFR value from being written if (autofocusCheck->isChecked() && !Options::saveHFRToFile()) appendLogText(i18n( "Warning: HFR-based autofocus is set but option \"Save Sequence HFR Value to File\" is not enabled. " "Current HFR value will not be written to sequence file.")); outstream << "" << cLocale.toString(Options::saveHFRToFile() ? HFRPixels->value() : 0) << "" << endl; outstream << "" << cLocale.toString(temperatureDelta->value()) << "" << endl; outstream << "" << cLocale.toString(refocusEveryN->value()) << "" << endl; foreach (SequenceJob * job, jobs) { job->getPrefixSettings(rawPrefix, filterEnabled, expEnabled, tsEnabled); outstream << "" << endl; outstream << "" << cLocale.toString(job->getExposure()) << "" << endl; outstream << "" << endl; outstream << "" << cLocale.toString(job->getXBin()) << "" << endl; outstream << "" << cLocale.toString(job->getXBin()) << "" << endl; outstream << "" << endl; outstream << "" << endl; outstream << "" << cLocale.toString(job->getSubX()) << "" << endl; outstream << "" << cLocale.toString(job->getSubY()) << "" << endl; outstream << "" << cLocale.toString(job->getSubW()) << "" << endl; outstream << "" << cLocale.toString(job->getSubH()) << "" << endl; outstream << "" << endl; if (job->getTargetTemperature() != Ekos::INVALID_VALUE) outstream << "" << cLocale.toString(job->getTargetTemperature()) << "" << endl; if (job->getTargetFilter() >= 0) //outstream << "" << job->getTargetFilter() << "" << endl; outstream << "" << job->getFilterName() << "" << endl; outstream << "" << frameTypes.key(job->getFrameType()) << "" << endl; outstream << "" << endl; //outstream << "" << job->getPrefix() << "" << endl; outstream << "" << rawPrefix << "" << endl; outstream << "" << (filterEnabled ? 1 : 0) << "" << endl; outstream << "" << (expEnabled ? 1 : 0) << "" << endl; outstream << "" << (tsEnabled ? 1 : 0) << "" << endl; outstream << "" << endl; outstream << "" << cLocale.toString(job->getCount()) << "" << endl; // ms to seconds outstream << "" << cLocale.toString(job->getDelay() / 1000.0) << "" << endl; if (job->getPostCaptureScript().isEmpty() == false) outstream << "" << job->getPostCaptureScript() << "" << endl; outstream << "" << job->getLocalDir() << "" << endl; outstream << "" << job->getUploadMode() << "" << endl; if (job->getRemoteDir().isEmpty() == false) outstream << "" << job->getRemoteDir() << "" << endl; if (job->getISOIndex() != -1) outstream << "" << (job->getISOIndex()) << "" << endl; outstream << "" << (job->getTransforFormat()) << "" << endl; if (job->getTargetRotation() != Ekos::INVALID_VALUE) outstream << "" << (job->getTargetRotation()) << "" << endl; QMapIterator> customIter(job->getCustomProperties()); outstream << "" << endl; while (customIter.hasNext()) { customIter.next(); outstream << "" << endl; QMap numbers = customIter.value(); QMapIterator numberIter(numbers); while (numberIter.hasNext()) { numberIter.next(); outstream << "" << cLocale.toString(numberIter.value()) << "" << endl; } outstream << "" << endl; } outstream << "" << endl; outstream << "" << endl; outstream << "" << endl; if (job->getFlatFieldSource() == SOURCE_MANUAL) outstream << "Manual" << endl; else if (job->getFlatFieldSource() == SOURCE_FLATCAP) outstream << "FlatCap" << endl; else if (job->getFlatFieldSource() == SOURCE_DARKCAP) outstream << "DarkCap" << endl; else if (job->getFlatFieldSource() == SOURCE_WALL) { outstream << "Wall" << endl; outstream << "" << cLocale.toString(job->getWallCoord().az().Degrees()) << "" << endl; outstream << "" << cLocale.toString(job->getWallCoord().alt().Degrees()) << "" << endl; } else outstream << "DawnDust" << endl; outstream << "" << endl; outstream << "" << endl; if (job->getFlatFieldDuration() == DURATION_MANUAL) outstream << "Manual" << endl; else { outstream << "ADU" << endl; outstream << "" << cLocale.toString(job->getTargetADU()) << "" << endl; outstream << "" << cLocale.toString(job->getTargetADUTolerance()) << "" << endl; } outstream << "" << endl; outstream << "" << (job->isPreMountPark() ? "True" : "False") << "" << endl; outstream << "" << (job->isPreDomePark() ? "True" : "False") << "" << endl; outstream << "" << endl; outstream << "" << endl; } outstream << "" << endl; appendLogText(i18n("Sequence queue saved to %1", path)); file.flush(); file.close(); // update save button tool tip queueSaveB->setToolTip("Save to " + file.fileName()); return true; } void Capture::resetJobs() { // Stop any running capture stop(); // If a job is selected for edit, reset only that job if (m_JobUnderEdit == true) { SequenceJob * job = jobs.at(queueTable->currentRow()); if (nullptr != job) job->resetStatus(); } else { if (KMessageBox::warningContinueCancel( nullptr, i18n("Are you sure you want to reset status of all jobs?"), i18n("Reset job status"), KStandardGuiItem::cont(), KStandardGuiItem::cancel(), "reset_job_status_warning") != KMessageBox::Continue) { return; } foreach (SequenceJob * job, jobs) job->resetStatus(); } // Also reset the storage count for all jobs capturedFramesMap.clear(); // We're not controlled by the Scheduler, restore progress option ignoreJobProgress = Options::alwaysResetSequenceWhenStarting(); } void Capture::ignoreSequenceHistory() { // This function is called independently from the Scheduler or the UI, so honor the change ignoreJobProgress = true; } void Capture::syncGUIToJob(SequenceJob * job) { QString rawPrefix; bool filterEnabled, expEnabled, tsEnabled; job->getPrefixSettings(rawPrefix, filterEnabled, expEnabled, tsEnabled); exposureIN->setValue(job->getExposure()); binXIN->setValue(job->getXBin()); binYIN->setValue(job->getYBin()); frameXIN->setValue(job->getSubX()); frameYIN->setValue(job->getSubY()); frameWIN->setValue(job->getSubW()); frameHIN->setValue(job->getSubH()); FilterPosCombo->setCurrentIndex(job->getTargetFilter() - 1); frameTypeCombo->setCurrentIndex(job->getFrameType()); prefixIN->setText(rawPrefix); filterCheck->setChecked(filterEnabled); expDurationCheck->setChecked(expEnabled); ISOCheck->setChecked(tsEnabled); countIN->setValue(job->getCount()); delayIN->setValue(job->getDelay() / 1000); postCaptureScriptIN->setText(job->getPostCaptureScript()); uploadModeCombo->setCurrentIndex(job->getUploadMode()); remoteDirIN->setEnabled(uploadModeCombo->currentIndex() != 0); remoteDirIN->setText(job->getRemoteDir()); fitsDir->setText(job->getLocalDir()); // Temperature Options temperatureCheck->setChecked(job->getEnforceTemperature()); if (job->getEnforceTemperature()) temperatureIN->setValue(job->getTargetTemperature()); // Flat field options calibrationB->setEnabled(job->getFrameType() != FRAME_LIGHT); flatFieldDuration = job->getFlatFieldDuration(); flatFieldSource = job->getFlatFieldSource(); targetADU = job->getTargetADU(); targetADUTolerance = job->getTargetADUTolerance(); wallCoord = job->getWallCoord(); preMountPark = job->isPreMountPark(); preDomePark = job->isPreDomePark(); // Custom Properties customPropertiesDialog->setCustomProperties(job->getCustomProperties()); if (ISOCombo) ISOCombo->setCurrentIndex(job->getISOIndex()); if (GainSpin) { double value = getGain(); if (value >= 0) GainSpin->setValue(value); else GainSpin->setValue(GainSpinSpecialValue); } transferFormatCombo->setCurrentIndex(job->getTransforFormat()); if (job->getTargetRotation() != Ekos::INVALID_VALUE) { rotatorSettings->setRotationEnforced(true); rotatorSettings->setTargetRotationPA(job->getTargetRotation()); } else rotatorSettings->setRotationEnforced(false); emit settingsUpdated(getSettings()); } QJsonObject Capture::getSettings() { QJsonObject settings; // Try to get settings value // if not found, fallback to camera value double gain = -1; if (GainSpin && GainSpin->value() != GainSpinSpecialValue) gain = GainSpin->value(); else if (currentCCD && currentCCD->hasGain()) currentCCD->getGain(&gain); int iso = -1; if (ISOCombo) iso = ISOCombo->currentIndex(); else if (currentCCD) iso = currentCCD->getChip(ISD::CCDChip::PRIMARY_CCD)->getISOIndex(); settings.insert("camera", CCDCaptureCombo->currentText()); settings.insert("fw", FilterDevicesCombo->currentText()); settings.insert("filter", FilterPosCombo->currentText()); settings.insert("exp", exposureIN->value()); settings.insert("bin", binXIN->value()); settings.insert("iso", iso); settings.insert("frameType", frameTypeCombo->currentIndex()); settings.insert("format", transferFormatCombo->currentIndex()); settings.insert("gain", gain); settings.insert("temperature", temperatureIN->value()); return settings; } void Capture::selectedJobChanged(QModelIndex current, QModelIndex previous) { Q_UNUSED(previous) selectJob(current); } void Capture::selectJob(QModelIndex i) { if (i.row() < 0 || (i.row() + 1) > jobs.size()) return; SequenceJob * job = jobs.at(i.row()); if (job == nullptr) return; syncGUIToJob(job); if (isBusy || jobs.size() < 2) return; queueUpB->setEnabled(i.row() > 0); queueDownB->setEnabled(i.row() + 1 < jobs.size()); } void Capture::editJob(QModelIndex i) { selectJob(i); appendLogText(i18n("Editing job #%1...", i.row() + 1)); addToQueueB->setIcon(QIcon::fromTheme("dialog-ok-apply")); addToQueueB->setToolTip(i18n("Apply job changes.")); removeFromQueueB->setToolTip(i18n("Cancel job changes.")); m_JobUnderEdit = true; } void Capture::resetJobEdit() { if (m_JobUnderEdit) appendLogText(i18n("Editing job canceled.")); m_JobUnderEdit = false; addToQueueB->setIcon(QIcon::fromTheme("list-add")); addToQueueB->setToolTip(i18n("Add job to sequence queue")); removeFromQueueB->setToolTip(i18n("Remove job from sequence queue")); } void Capture::constructPrefix(QString &imagePrefix) { if (imagePrefix.isEmpty() == false) imagePrefix += '_'; imagePrefix += frameTypeCombo->currentText(); /*if (filterCheck->isChecked() && FilterPosCombo->currentText().isEmpty() == false && frameTypeCombo->currentText().compare("Bias", Qt::CaseInsensitive) && frameTypeCombo->currentText().compare("Dark", Qt::CaseInsensitive))*/ if (filterCheck->isChecked() && FilterPosCombo->currentText().isEmpty() == false && (frameTypeCombo->currentIndex() == FRAME_LIGHT || frameTypeCombo->currentIndex() == FRAME_FLAT)) { imagePrefix += '_'; imagePrefix += FilterPosCombo->currentText(); } if (expDurationCheck->isChecked()) { //if (imagePrefix.isEmpty() == false || frameTypeCheck->isChecked()) imagePrefix += '_'; double exposureValue = exposureIN->value(); // Don't use the locale for exposure value in the capture file name, so that we get a "." as decimal separator if (exposureValue == static_cast(exposureValue)) // Whole number imagePrefix += QString::number(exposureIN->value(), 'd', 0) + QString("_secs"); else { // Decimal if (exposureIN->value() >= 0.001) imagePrefix += QString::number(exposureIN->value(), 'f', 3) + QString("_secs"); else imagePrefix += QString::number(exposureIN->value(), 'f', 6) + QString("_secs"); } } if (ISOCheck->isChecked()) { imagePrefix += SequenceJob::ISOMarker; } } double Capture::getProgressPercentage() { int totalImageCount = 0; int totalImageCompleted = 0; foreach (SequenceJob * job, jobs) { totalImageCount += job->getCount(); totalImageCompleted += job->getCompleted(); } if (totalImageCount != 0) return ((static_cast(totalImageCompleted) / totalImageCount) * 100.0); else return -1; } int Capture::getActiveJobID() { if (activeJob == nullptr) return -1; for (int i = 0; i < jobs.count(); i++) { if (activeJob == jobs[i]) return i; } return -1; } int Capture::getPendingJobCount() { int completedJobs = 0; foreach (SequenceJob * job, jobs) { if (job->getStatus() == SequenceJob::JOB_DONE) completedJobs++; } return (jobs.count() - completedJobs); } QString Capture::getJobState(int id) { if (id < jobs.count()) { SequenceJob * job = jobs.at(id); return job->getStatusString(); } return QString(); } int Capture::getJobImageProgress(int id) { if (id < jobs.count()) { SequenceJob * job = jobs.at(id); return job->getCompleted(); } return -1; } int Capture::getJobImageCount(int id) { if (id < jobs.count()) { SequenceJob * job = jobs.at(id); return job->getCount(); } return -1; } double Capture::getJobExposureProgress(int id) { if (id < jobs.count()) { SequenceJob * job = jobs.at(id); return job->getExposeLeft(); } return -1; } double Capture::getJobExposureDuration(int id) { if (id < jobs.count()) { SequenceJob * job = jobs.at(id); return job->getExposure(); } return -1; } int Capture::getJobRemainingTime(SequenceJob * job) { int remaining = (job->getExposure() + getEstimatedDownloadTime() + job->getDelay() / 1000) * (job->getCount() - job->getCompleted()); if (job->getStatus() == SequenceJob::JOB_BUSY) remaining += job->getExposeLeft() + getEstimatedDownloadTime(); return remaining; } int Capture::getOverallRemainingTime() { double remaining = 0; foreach (SequenceJob * job, jobs) remaining += getJobRemainingTime(job); return remaining; } int Capture::getActiveJobRemainingTime() { if (activeJob == nullptr) return -1; return getJobRemainingTime(activeJob); } void Capture::setMaximumGuidingDeviation(bool enable, double value) { guideDeviationCheck->setChecked(enable); if (enable) guideDeviation->setValue(value); } void Capture::setInSequenceFocus(bool enable, double HFR) { autofocusCheck->setChecked(enable); if (enable) HFRPixels->setValue(HFR); } void Capture::setTargetTemperature(double temperature) { temperatureIN->setValue(temperature); } void Capture::clearSequenceQueue() { activeJob = nullptr; //m_TargetName.clear(); //stop(); while (queueTable->rowCount() > 0) queueTable->removeRow(0); qDeleteAll(jobs); jobs.clear(); } QString Capture::getSequenceQueueStatus() { if (jobs.count() == 0) return "Invalid"; if (isBusy) return "Running"; int idle = 0, error = 0, complete = 0, aborted = 0, running = 0; foreach (SequenceJob * job, jobs) { switch (job->getStatus()) { case SequenceJob::JOB_ABORTED: aborted++; break; case SequenceJob::JOB_BUSY: running++; break; case SequenceJob::JOB_DONE: complete++; break; case SequenceJob::JOB_ERROR: error++; break; case SequenceJob::JOB_IDLE: idle++; break; } } if (error > 0) return "Error"; if (aborted > 0) { if (m_State == CAPTURE_SUSPENDED) return "Suspended"; else return "Aborted"; } if (running > 0) return "Running"; if (idle == jobs.count()) return "Idle"; if (complete == jobs.count()) return "Complete"; return "Invalid"; } void Capture::processTelescopeNumber(INumberVectorProperty * nvp) { // If it is not ours, return. if (nvp->device != currentTelescope->getDeviceName() || strstr(nvp->name, "EQUATORIAL_") == nullptr) return; switch (meridianFlipStage) { case MF_NONE: break; case MF_INITIATED: { if (nvp->s == IPS_BUSY) setMeridianFlipStage(MF_FLIPPING); } break; case MF_FLIPPING: { if (currentTelescope != nullptr && currentTelescope->isSlewing()) setMeridianFlipStage(MF_SLEWING); } break; default: break; } } void Capture::processFlipCompleted() { // If dome is syncing, wait until it stops if (currentDome && currentDome->isMoving()) return; appendLogText(i18n("Telescope completed the meridian flip.")); //KNotification::event(QLatin1String("MeridianFlipCompleted"), i18n("Meridian flip is successfully completed")); KSNotification::event(QLatin1String("MeridianFlipCompleted"), i18n("Meridian flip is successfully completed"), KSNotification::EVENT_INFO); // resume only if capturing was running if (m_State == CAPTURE_IDLE || m_State == CAPTURE_ABORTED || m_State == CAPTURE_COMPLETE || m_State == CAPTURE_PAUSED) return; if (resumeAlignmentAfterFlip == true) { appendLogText(i18n("Performing post flip re-alignment...")); secondsLabel->setText(i18n("Aligning...")); retries = 0; m_State = CAPTURE_ALIGNING; emit newStatus(Ekos::CAPTURE_ALIGNING); setMeridianFlipStage(MF_ALIGNING); //QTimer::singleShot(Options::settlingTime(), [this]() {emit meridialFlipTracked();}); //emit meridialFlipTracked(); return; } retries = 0; checkGuidingAfterFlip(); } void Capture::checkGuidingAfterFlip() { // If we're not autoguiding then we're done if (resumeGuidingAfterFlip == false) { resumeSequence(); // N.B. Set meridian flip stage AFTER resumeSequence() always setMeridianFlipStage(MF_NONE); } else { appendLogText(i18n("Performing post flip re-calibration and guiding...")); secondsLabel->setText(i18n("Calibrating...")); m_State = CAPTURE_CALIBRATING; emit newStatus(Ekos::CAPTURE_CALIBRATING); setMeridianFlipStage(MF_GUIDING); emit meridianFlipCompleted(); } } bool Capture::checkPausing() { if (m_State == CAPTURE_PAUSE_PLANNED) { appendLogText(i18n("Sequence paused.")); secondsLabel->setText(i18n("Paused...")); m_State = CAPTURE_PAUSED; // handle a requested meridian flip if (meridianFlipStage != MF_NONE) setMeridianFlipStage(MF_READY); // pause return true; } // no pause return false; } bool Capture::checkMeridianFlip() { if (currentTelescope == nullptr) return false; // If active job is taking flat field image at a wall source // then do not flip. if (activeJob && activeJob->getFrameType() == FRAME_FLAT && activeJob->getFlatFieldSource() == SOURCE_WALL) return false; if (meridianFlipStage != MF_REQUESTED) // if no flip has been requested or is already ongoing return false; // meridian flip requested or already in action // Reset frame if we need to do focusing later on if (isInSequenceFocus || (refocusEveryNCheck->isChecked() && getRefocusEveryNTimerElapsedSec() > 0)) emit resetFocus(); // signal that meridian flip may take place if (meridianFlipStage == MF_REQUESTED) setMeridianFlipStage(MF_READY); return true; } void Capture::checkGuideDeviationTimeout() { if (activeJob && activeJob->getStatus() == SequenceJob::JOB_ABORTED && m_DeviationDetected) { appendLogText(i18n("Guide module timed out.")); m_DeviationDetected = false; // If capture was suspended, it should be aborted (failed) now. if (m_State == CAPTURE_SUSPENDED) { m_State = CAPTURE_ABORTED; emit newStatus(m_State); } } } void Capture::setAlignStatus(AlignState state) { qCDebug(KSTARS_EKOS_CAPTURE) << "setAlignStatus: " << state; alignState = state; resumeAlignmentAfterFlip = true; switch (state) { case ALIGN_COMPLETE: if (meridianFlipStage == MF_ALIGNING) { appendLogText(i18n("Post flip re-alignment completed successfully.")); retries = 0; checkGuidingAfterFlip(); } break; case ALIGN_ABORTED: case ALIGN_FAILED: // TODO run it 3 times before giving up if (meridianFlipStage == MF_ALIGNING) { if (++retries == 3) { appendLogText(i18n("Post-flip alignment failed.")); abort(); } else { appendLogText(i18n("Post-flip alignment failed. Retrying...")); secondsLabel->setText(i18n("Aligning...")); this->m_State = CAPTURE_ALIGNING; emit newStatus(Ekos::CAPTURE_ALIGNING); setMeridianFlipStage(MF_ALIGNING); } } break; default: break; } } void Capture::setGuideStatus(GuideState state) { if (state != guideState) qCDebug(KSTARS_EKOS_CAPTURE) << "Guiding state changed from" << getGuideStatusString(guideState) << "to" << getGuideStatusString(state); switch (state) { case GUIDE_IDLE: case GUIDE_ABORTED: // If Autoguiding was started before and now stopped, let's abort (unless we're doing a meridian flip) if (isGuidingActive() && meridianFlipStage == MF_NONE && ((activeJob && activeJob->getStatus() == SequenceJob::JOB_BUSY) || this->m_State == CAPTURE_SUSPENDED || this->m_State == CAPTURE_PAUSED)) { appendLogText(i18n("Autoguiding stopped. Aborting...")); abort(); } break; case GUIDE_GUIDING: case GUIDE_CALIBRATION_SUCESS: autoGuideReady = true; break; case GUIDE_CALIBRATION_ERROR: // TODO try restarting calibration a couple of times before giving up if (meridianFlipStage == MF_GUIDING) { if (++retries == 3) { appendLogText(i18n("Post meridian flip calibration error. Aborting...")); abort(); } else { appendLogText(i18n("Post meridian flip calibration error. Restarting...")); checkGuidingAfterFlip(); } } autoGuideReady = false; break; case GUIDE_DITHERING_SUCCESS: qCInfo(KSTARS_EKOS_CAPTURE) << "Dithering succeeded, capture state" << getCaptureStatusString(m_State); // do nothing if something happened during dithering if (m_State != CAPTURE_DITHERING) break; if (Options::guidingSettle() > 0) { // N.B. Do NOT convert to i18np since guidingRate is DOUBLE value (e.g. 1.36) so we always use plural with that. appendLogText(i18n("Dither complete. Resuming capture in %1 seconds...", Options::guidingSettle())); QTimer::singleShot(Options::guidingSettle() * 1000, this, &Ekos::Capture::resumeCapture); } else { appendLogText(i18n("Dither complete.")); resumeCapture(); } break; case GUIDE_DITHERING_ERROR: qCInfo(KSTARS_EKOS_CAPTURE) << "Dithering failed, capture state" << getCaptureStatusString(m_State); if (m_State != CAPTURE_DITHERING) break; if (Options::guidingSettle() > 0) { // N.B. Do NOT convert to i18np since guidingRate is DOUBLE value (e.g. 1.36) so we always use plural with that. appendLogText(i18n("Warning: Dithering failed. Resuming capture in %1 seconds...", Options::guidingSettle())); QTimer::singleShot(Options::guidingSettle() * 1000, this, &Ekos::Capture::resumeCapture); } else { appendLogText(i18n("Warning: Dithering failed.")); resumeCapture(); } break; default: break; } guideState = state; } void Capture::checkFrameType(int index) { if (index == FRAME_LIGHT) calibrationB->setEnabled(false); else calibrationB->setEnabled(true); } double Capture::setCurrentADU(double value) { double nextExposure = 0; double targetADU = activeJob->getTargetADU(); std::vector coeff; // Check if saturated, then take shorter capture and discard value ExpRaw.append(activeJob->getExposure()); ADURaw.append(value); qCDebug(KSTARS_EKOS_CAPTURE) << "Capture: Current ADU = " << value << " targetADU = " << targetADU << " Exposure Count: " << ExpRaw.count(); // Most CCDs are quite linear so 1st degree polynomial is quite sufficient // But DSLRs can exhibit non-linear response curve and so a 2nd degree polynomial is more appropriate if (ExpRaw.count() >= 2) { if (ExpRaw.count() >= 5) { double chisq = 0; coeff = gsl_polynomial_fit(ADURaw.data(), ExpRaw.data(), ExpRaw.count(), 2, chisq); qCDebug(KSTARS_EKOS_CAPTURE) << "Running polynomial fitting. Found " << coeff.size() << " coefficients."; if (std::isnan(coeff[0]) || std::isinf(coeff[0])) { qCDebug(KSTARS_EKOS_CAPTURE) << "Coefficients are invalid."; targetADUAlgorithm = ADU_LEAST_SQUARES; } else { nextExposure = coeff[0] + (coeff[1] * targetADU) + (coeff[2] * pow(targetADU, 2)); // If exposure is not valid or does not make sense, then we fall back to least squares if (nextExposure < 0 || (nextExposure > ExpRaw.last() || targetADU < ADURaw.last()) || (nextExposure < ExpRaw.last() || targetADU > ADURaw.last())) { nextExposure = 0; targetADUAlgorithm = ADU_LEAST_SQUARES; } else { targetADUAlgorithm = ADU_POLYNOMIAL; for (size_t i = 0; i < coeff.size(); i++) qCDebug(KSTARS_EKOS_CAPTURE) << "Coeff #" << i << "=" << coeff[i]; } } } bool looping = false; if (ExpRaw.count() >= 10) { int size = ExpRaw.count(); looping = (std::fabs(ExpRaw[size - 1] - ExpRaw[size - 2] < 0.01)) && (std::fabs(ExpRaw[size - 2] - ExpRaw[size - 3] < 0.01)); if (looping && targetADUAlgorithm == ADU_POLYNOMIAL) { qWarning(KSTARS_EKOS_CAPTURE) << "Detected looping in polynomial results. Falling back to llsqr."; targetADUAlgorithm = ADU_LEAST_SQUARES; } } // If we get invalid data, let's fall back to llsq // Since polyfit can be unreliable at low counts, let's only use it at the 5th exposure // if we don't have results already. if (targetADUAlgorithm == ADU_LEAST_SQUARES) { double a = 0, b = 0; llsq(ExpRaw, ADURaw, a, b); // If we have valid results, let's calculate next exposure if (a != 0) { nextExposure = (targetADU - b) / a; // If we get invalid value, let's just proceed iteratively if (nextExposure < 0) nextExposure = 0; } } } if (nextExposure == 0) { if (value < targetADU) nextExposure = activeJob->getExposure() * 1.25; else nextExposure = activeJob->getExposure() * .75; } qCDebug(KSTARS_EKOS_CAPTURE) << "next flat exposure is" << nextExposure; return nextExposure; } // Based on John Burkardt LLSQ (LGPL) void Capture::llsq(QVector x, QVector y, double &a, double &b) { double bot; int i; double top; double xbar; double ybar; int n = x.count(); // // Special case. // if (n == 1) { a = 0.0; b = y[0]; return; } // // Average X and Y. // xbar = 0.0; ybar = 0.0; for (i = 0; i < n; i++) { xbar = xbar + x[i]; ybar = ybar + y[i]; } xbar = xbar / static_cast(n); ybar = ybar / static_cast(n); // // Compute Beta. // top = 0.0; bot = 0.0; for (i = 0; i < n; i++) { top = top + (x[i] - xbar) * (y[i] - ybar); bot = bot + (x[i] - xbar) * (x[i] - xbar); } a = top / bot; b = ybar - a * xbar; } void Capture::setDirty() { m_Dirty = true; } bool Capture::hasCoolerControl() { if (currentCCD && currentCCD->hasCoolerControl()) return true; return false; } bool Capture::setCoolerControl(bool enable) { if (currentCCD && currentCCD->hasCoolerControl()) return currentCCD->setCoolerControl(enable); return false; } void Capture::clearAutoFocusHFR() { // If HFR limit was set from file, we cannot override it. if (fileHFR > 0) return; HFRPixels->setValue(0); //firstAutoFocus = true; } void Capture::openCalibrationDialog() { QDialog calibrationDialog; Ui_calibrationOptions calibrationOptions; calibrationOptions.setupUi(&calibrationDialog); if (currentTelescope) { calibrationOptions.parkMountC->setEnabled(currentTelescope->canPark()); calibrationOptions.parkMountC->setChecked(preMountPark); } else calibrationOptions.parkMountC->setEnabled(false); if (currentDome) { calibrationOptions.parkDomeC->setEnabled(currentDome->canPark()); calibrationOptions.parkDomeC->setChecked(preDomePark); } else calibrationOptions.parkDomeC->setEnabled(false); //connect(calibrationOptions.wallSourceC, SIGNAL(toggled(bool)), calibrationOptions.parkC, &Ekos::Capture::setDisabled(bool))); switch (flatFieldSource) { case SOURCE_MANUAL: calibrationOptions.manualSourceC->setChecked(true); break; case SOURCE_FLATCAP: calibrationOptions.flatDeviceSourceC->setChecked(true); break; case SOURCE_DARKCAP: calibrationOptions.darkDeviceSourceC->setChecked(true); break; case SOURCE_WALL: calibrationOptions.wallSourceC->setChecked(true); calibrationOptions.azBox->setText(wallCoord.az().toDMSString()); calibrationOptions.altBox->setText(wallCoord.alt().toDMSString()); break; case SOURCE_DAWN_DUSK: calibrationOptions.dawnDuskFlatsC->setChecked(true); break; } switch (flatFieldDuration) { case DURATION_MANUAL: calibrationOptions.manualDurationC->setChecked(true); break; case DURATION_ADU: calibrationOptions.ADUC->setChecked(true); calibrationOptions.ADUValue->setValue(targetADU); calibrationOptions.ADUTolerance->setValue(targetADUTolerance); break; } if (calibrationDialog.exec() == QDialog::Accepted) { if (calibrationOptions.manualSourceC->isChecked()) flatFieldSource = SOURCE_MANUAL; else if (calibrationOptions.flatDeviceSourceC->isChecked()) flatFieldSource = SOURCE_FLATCAP; else if (calibrationOptions.darkDeviceSourceC->isChecked()) flatFieldSource = SOURCE_DARKCAP; else if (calibrationOptions.wallSourceC->isChecked()) { dms wallAz, wallAlt; bool azOk = false, altOk = false; wallAz = calibrationOptions.azBox->createDms(true, &azOk); wallAlt = calibrationOptions.altBox->createDms(true, &altOk); if (azOk && altOk) { flatFieldSource = SOURCE_WALL; wallCoord.setAz(wallAz); wallCoord.setAlt(wallAlt); } else { calibrationOptions.manualSourceC->setChecked(true); KSNotification::error(i18n("Wall coordinates are invalid.")); } } else flatFieldSource = SOURCE_DAWN_DUSK; if (calibrationOptions.manualDurationC->isChecked()) flatFieldDuration = DURATION_MANUAL; else { flatFieldDuration = DURATION_ADU; targetADU = calibrationOptions.ADUValue->value(); targetADUTolerance = calibrationOptions.ADUTolerance->value(); } preMountPark = calibrationOptions.parkMountC->isChecked(); preDomePark = calibrationOptions.parkDomeC->isChecked(); setDirty(); Options::setCalibrationFlatSourceIndex(flatFieldSource); Options::setCalibrationFlatDurationIndex(flatFieldDuration); Options::setCalibrationWallAz(wallCoord.az().Degrees()); Options::setCalibrationWallAlt(wallCoord.alt().Degrees()); Options::setCalibrationADUValue(targetADU); Options::setCalibrationADUValueTolerance(targetADUTolerance); } } IPState Capture::checkLightFrameAuxiliaryTasks() { // step 2: check if meridian flip already is ongoing if (meridianFlipStage != MF_NONE && meridianFlipStage != MF_READY) return IPS_BUSY; // step 3: check if meridian flip is required if (checkMeridianFlip()) return IPS_BUSY; // step 4: check if re-focusing is required if (m_State == CAPTURE_FOCUSING || startFocusIfRequired()) { m_State = CAPTURE_FOCUSING; return IPS_BUSY; } if (guideState == GUIDE_SUSPENDED) { appendLogText(i18n("Autoguiding resumed.")); emit resumeGuiding(); } calibrationStage = CAL_PRECAPTURE_COMPLETE; return IPS_OK; } IPState Capture::checkLightFramePendingTasks() { switch (activeJob->getFlatFieldSource()) { // All these are considered MANUAL when it comes to light frames case SOURCE_MANUAL: case SOURCE_DAWN_DUSK: case SOURCE_WALL: // If telescopes were MANUALLY covered before // we need to manually uncover them. if (m_TelescopeCoveredDarkExposure || m_TelescopeCoveredFlatExposure) { // Uncover telescope // N.B. This operation cannot be autonomous // if (KMessageBox::warningContinueCancel( // nullptr, i18n("Remove cover from the telescope in order to continue."), i18n("Telescope Covered"), // KStandardGuiItem::cont(), KStandardGuiItem::cancel(), // "uncover_scope_dialog_notification", KMessageBox::WindowModal | KMessageBox::Notify) == KMessageBox::Cancel) // { // return IPS_ALERT; // } // If we already asked for confirmation and waiting for it // let us see if the confirmation is fulfilled // otherwise we return. if (calibrationCheckType == CAL_CHECK_CONFIRMATION) return IPS_BUSY; // Otherwise, we ask user to confirm manually calibrationCheckType = CAL_CHECK_CONFIRMATION; connect(KSMessageBox::Instance(), &KSMessageBox::accepted, this, [this]() { //QObject::disconnect(KSMessageBox::Instance(), &KSMessageBox::accepted, this, nullptr); KSMessageBox::Instance()->disconnect(this); m_TelescopeCoveredDarkExposure = false; m_TelescopeCoveredFlatExposure = false; calibrationCheckType = CAL_CHECK_TASK; }); KSMessageBox::Instance()->warningContinueCancel(i18n("Remove cover from the telescope in order to continue."), i18n("Telescope Covered"), Options::manualCoverTimeout()); return IPS_BUSY; } break; case SOURCE_FLATCAP: case SOURCE_DARKCAP: if (currentDustCap == nullptr) { qCWarning(KSTARS_EKOS_CAPTURE) << "Skipping flat/dark cap since it is not connected."; break; } // { // appendLogText(i18n("Cap device is missing but the job requires flat or dark cap device.")); // return IPS_ALERT; // } // If dust cap HAS light and light is ON, then turn it off. if (currentDustCap->hasLight() && currentDustCap->isLightOn() == true) { dustCapLightEnabled = false; currentDustCap->SetLightEnabled(false); } // If cap is parked, we need to unpark it if (calibrationStage < CAL_DUSTCAP_UNPARKING && currentDustCap->isParked()) { if (currentDustCap->UnPark()) { calibrationStage = CAL_DUSTCAP_UNPARKING; appendLogText(i18n("Unparking dust cap...")); return IPS_BUSY; } else { appendLogText(i18n("Unparking dust cap failed, aborting...")); abort(); return IPS_ALERT; } } // Wait until cap is unparked if (calibrationStage == CAL_DUSTCAP_UNPARKING) { if (currentDustCap->isUnParked() == false) return IPS_BUSY; else { calibrationStage = CAL_DUSTCAP_UNPARKED; appendLogText(i18n("Dust cap unparked.")); } } break; } return checkLightFrameAuxiliaryTasks(); } IPState Capture::checkDarkFramePendingTasks() { QStringList shutterfulCCDs = Options::shutterfulCCDs(); QStringList shutterlessCCDs = Options::shutterlessCCDs(); QString deviceName = currentCCD->getDeviceName(); bool hasShutter = shutterfulCCDs.contains(deviceName); bool hasNoShutter = shutterlessCCDs.contains(deviceName) || (ISOCombo && ISOCombo->count() > 0); // If we have no information, we ask before we proceed. if (hasShutter == false && hasNoShutter == false) { // Awaiting user input if (calibrationCheckType == CAL_CHECK_CONFIRMATION) return IPS_BUSY; // // This action cannot be autonomous // if (KMessageBox::questionYesNo(nullptr, i18n("Does %1 have a shutter?", deviceName), // i18n("Dark Exposure")) == KMessageBox::Yes) // { // hasNoShutter = false; // shutterfulCCDs.append(deviceName); // Options::setShutterfulCCDs(shutterfulCCDs); // } // else // { // hasNoShutter = true; // shutterlessCCDs.append(deviceName); // Options::setShutterlessCCDs(shutterlessCCDs); // } connect(KSMessageBox::Instance(), &KSMessageBox::accepted, this, [&]() { //QObject::disconnect(KSMessageBox::Instance(), &KSMessageBox::accepted, this, nullptr); KSMessageBox::Instance()->disconnect(this); QStringList shutterfulCCDs = Options::shutterfulCCDs(); QString deviceName = currentCCD->getDeviceName(); shutterfulCCDs.append(deviceName); Options::setShutterfulCCDs(shutterfulCCDs); calibrationCheckType = CAL_CHECK_TASK; }); connect(KSMessageBox::Instance(), &KSMessageBox::rejected, this, [&]() { //QObject::disconnect(KSMessageBox::Instance(), &KSMessageBox::rejected, this, nullptr); KSMessageBox::Instance()->disconnect(this); QStringList shutterlessCCDs = Options::shutterlessCCDs(); QString deviceName = currentCCD->getDeviceName(); shutterlessCCDs.append(deviceName); Options::setShutterlessCCDs(shutterlessCCDs); calibrationCheckType = CAL_CHECK_TASK; }); calibrationCheckType = CAL_CHECK_CONFIRMATION; KSMessageBox::Instance()->questionYesNo(i18n("Does %1 have a shutter?", deviceName), i18n("Dark Exposure")); return IPS_BUSY; } switch (activeJob->getFlatFieldSource()) { // All these are manual when it comes to dark frames case SOURCE_MANUAL: case SOURCE_DAWN_DUSK: // For cameras without a shutter, we need to ask the user to cover the telescope // if the telescope is not already covered. if (hasNoShutter && !m_TelescopeCoveredDarkExposure) { if (calibrationCheckType == CAL_CHECK_CONFIRMATION) return IPS_BUSY; // Continue connect(KSMessageBox::Instance(), &KSMessageBox::accepted, this, [&]() { //QObject::disconnect(KSMessageBox::Instance(), &KSMessageBox::accepted, this, nullptr); KSMessageBox::Instance()->disconnect(this); m_TelescopeCoveredDarkExposure = true; m_TelescopeCoveredFlatExposure = false; calibrationCheckType = CAL_CHECK_TASK; }); // Cancel connect(KSMessageBox::Instance(), &KSMessageBox::rejected, this, [&]() { //QObject::disconnect(KSMessageBox::Instance(), &KSMessageBox::rejected, this, nullptr); KSMessageBox::Instance()->disconnect(this); calibrationCheckType = CAL_CHECK_TASK; abort(); }); // if (KMessageBox::warningContinueCancel( // nullptr, i18n("Cover the telescope in order to take a dark exposure."), i18n("Dark Exposure"), // KStandardGuiItem::cont(), KStandardGuiItem::cancel(), // "cover_scope_dialog_notification", KMessageBox::WindowModal | KMessageBox::Notify) == KMessageBox::Cancel) // { // abort(); // return IPS_ALERT; // } calibrationCheckType = CAL_CHECK_CONFIRMATION; KSMessageBox::Instance()->warningContinueCancel(i18n("Cover the telescope in order to take a dark exposure.") , i18n("Dark Exposure"), Options::manualCoverTimeout()); return IPS_BUSY; } break; case SOURCE_FLATCAP: case SOURCE_DARKCAP: if (currentDustCap == nullptr) { qCWarning(KSTARS_EKOS_CAPTURE) << "Skipping flat/dark cap since it is not connected."; break; } // When using a cap, we need to park, if not already parked. // Need to turn off light, if light exists and was on. // if (!currentDustCap) // { // appendLogText(i18n("Cap device is missing but the job requires flat or dark cap device.")); // abort(); // return IPS_ALERT; // } // If cap is not park, park it if (calibrationStage < CAL_DUSTCAP_PARKING && currentDustCap->isParked() == false) { if (currentDustCap->Park()) { calibrationStage = CAL_DUSTCAP_PARKING; appendLogText(i18n("Parking dust cap...")); return IPS_BUSY; } else { appendLogText(i18n("Parking dust cap failed, aborting...")); abort(); return IPS_ALERT; } } // Wait until cap is parked if (calibrationStage == CAL_DUSTCAP_PARKING) { if (currentDustCap->isParked() == false) return IPS_BUSY; else { calibrationStage = CAL_DUSTCAP_PARKED; appendLogText(i18n("Dust cap parked.")); } } // Turn off light if it exists and was on. if (currentDustCap->hasLight() && currentDustCap->isLightOn() == true) { dustCapLightEnabled = false; currentDustCap->SetLightEnabled(false); } break; case SOURCE_WALL: if (currentTelescope) { if (calibrationStage < CAL_SLEWING) { wallCoord = activeJob->getWallCoord(); wallCoord.HorizontalToEquatorial(KStarsData::Instance()->lst(), KStarsData::Instance()->geo()->lat()); currentTelescope->Slew(&wallCoord); appendLogText(i18n("Mount slewing to wall position...")); calibrationStage = CAL_SLEWING; return IPS_BUSY; } // Check if slewing is complete if (calibrationStage == CAL_SLEWING) { if (currentTelescope->isSlewing() == false) { // Disable mount tracking if supported by the driver. currentTelescope->setTrackEnabled(false); calibrationStage = CAL_SLEWING_COMPLETE; appendLogText(i18n("Slew to wall position complete.")); } else return IPS_BUSY; } if (currentLightBox && currentLightBox->isLightOn() == true) { lightBoxLightEnabled = false; currentLightBox->SetLightEnabled(false); } } break; } calibrationStage = CAL_PRECAPTURE_COMPLETE; return IPS_OK; } IPState Capture::checkFlatFramePendingTasks() { switch (activeJob->getFlatFieldSource()) { case SOURCE_MANUAL: // Manual mode we need to cover mount with evenly illuminated field. if (m_TelescopeCoveredFlatExposure == false) { if (calibrationCheckType == CAL_CHECK_CONFIRMATION) return IPS_BUSY; // This action cannot be autonomous // if (KMessageBox::warningContinueCancel( // nullptr, i18n("Cover telescope with evenly illuminated light source."), i18n("Flat Frame"), // KStandardGuiItem::cont(), KStandardGuiItem::cancel(), // "flat_light_cover_dialog_notification", KMessageBox::WindowModal | KMessageBox::Notify) == KMessageBox::Cancel) // { // abort(); // return IPS_ALERT; // } // m_TelescopeCoveredFlatExposure = true; // m_TelescopeCoveredDarkExposure = false; // Continue connect(KSMessageBox::Instance(), &KSMessageBox::accepted, this, [&]() { //QObject::disconnect(KSMessageBox::Instance(), &KSMessageBox::accepted, this, nullptr); KSMessageBox::Instance()->disconnect(this); m_TelescopeCoveredFlatExposure = true; m_TelescopeCoveredDarkExposure = false; calibrationCheckType = CAL_CHECK_TASK; }); // Cancel connect(KSMessageBox::Instance(), &KSMessageBox::rejected, this, [&]() { //QObject::disconnect(KSMessageBox::Instance(), &KSMessageBox::rejected, this, nullptr); KSMessageBox::Instance()->disconnect(this); calibrationCheckType = CAL_CHECK_TASK; abort(); }); calibrationCheckType = CAL_CHECK_CONFIRMATION; KSMessageBox::Instance()->warningContinueCancel(i18n("Cover telescope with evenly illuminated light source."), i18n("Flat Frame"), Options::manualCoverTimeout()); return IPS_BUSY; } break; // Not implemented. case SOURCE_DAWN_DUSK: break; case SOURCE_FLATCAP: if (currentDustCap == nullptr) { qCWarning(KSTARS_EKOS_CAPTURE) << "Skipping flat/dark cap since it is not connected."; break; } // if (!currentDustCap) // { // appendLogText(i18n("Cap device is missing but the job requires flat cap device.")); // abort(); // return IPS_ALERT; // } // If cap is not park, park it if (calibrationStage < CAL_DUSTCAP_PARKING && currentDustCap->isParked() == false) { if (currentDustCap->Park()) { calibrationStage = CAL_DUSTCAP_PARKING; appendLogText(i18n("Parking dust cap...")); return IPS_BUSY; } else { appendLogText(i18n("Parking dust cap failed, aborting...")); abort(); return IPS_ALERT; } } // Wait until cap is parked if (calibrationStage == CAL_DUSTCAP_PARKING) { if (currentDustCap->isParked() == false) return IPS_BUSY; else { calibrationStage = CAL_DUSTCAP_PARKED; appendLogText(i18n("Dust cap parked.")); } } // If light is not on, turn it on. if (currentDustCap->hasLight() && currentDustCap->isLightOn() == false) { dustCapLightEnabled = true; currentDustCap->SetLightEnabled(true); } break; case SOURCE_WALL: if (currentTelescope) { if (calibrationStage < CAL_SLEWING) { wallCoord = activeJob->getWallCoord(); wallCoord.HorizontalToEquatorial(KStarsData::Instance()->lst(), KStarsData::Instance()->geo()->lat()); currentTelescope->Slew(&wallCoord); appendLogText(i18n("Mount slewing to wall position...")); calibrationStage = CAL_SLEWING; return IPS_BUSY; } // Check if slewing is complete if (calibrationStage == CAL_SLEWING) { if (currentTelescope->isSlewing() == false) { // Disable mount tracking if supported by the driver. currentTelescope->setTrackEnabled(false); calibrationStage = CAL_SLEWING_COMPLETE; appendLogText(i18n("Slew to wall position complete.")); } else return IPS_BUSY; } if (currentLightBox) { // Check if we have a light box to turn on if (activeJob->getFrameType() == FRAME_FLAT && currentLightBox->isLightOn() == false) { lightBoxLightEnabled = true; currentLightBox->SetLightEnabled(true); } else if (activeJob->getFrameType() != FRAME_FLAT && currentLightBox->isLightOn() == true) { lightBoxLightEnabled = false; currentLightBox->SetLightEnabled(false); } } } break; case SOURCE_DARKCAP: if (currentDustCap == nullptr) { qCWarning(KSTARS_EKOS_CAPTURE) << "Skipping flat/dark cap since it is not connected."; break; } // if (!currentDustCap) // { // appendLogText(i18n("Cap device is missing but the job requires dark cap device.")); // abort(); // return IPS_ALERT; // } // If cap is parked, unpark it since dark cap uses external light source. if (calibrationStage < CAL_DUSTCAP_UNPARKING && currentDustCap->isParked() == true) { if (currentDustCap->UnPark()) { calibrationStage = CAL_DUSTCAP_UNPARKING; appendLogText(i18n("UnParking dust cap...")); return IPS_BUSY; } else { appendLogText(i18n("UnParking dust cap failed, aborting...")); abort(); return IPS_ALERT; } } // Wait until cap is unparked if (calibrationStage == CAL_DUSTCAP_UNPARKING) { if (currentDustCap->isUnParked() == false) return IPS_BUSY; else { calibrationStage = CAL_DUSTCAP_UNPARKED; appendLogText(i18n("Dust cap unparked.")); } } // If light is off, turn it on. if (currentDustCap->hasLight() && currentDustCap->isLightOn() == false) { dustCapLightEnabled = true; currentDustCap->SetLightEnabled(true); } break; } // Check if we need to perform mount prepark if (preMountPark && currentTelescope && activeJob->getFlatFieldSource() != SOURCE_WALL) { if (calibrationStage < CAL_MOUNT_PARKING && currentTelescope->isParked() == false) { if (currentTelescope->Park()) { calibrationStage = CAL_MOUNT_PARKING; //emit mountParking(); appendLogText(i18n("Parking mount prior to calibration frames capture...")); return IPS_BUSY; } else { appendLogText(i18n("Parking mount failed, aborting...")); abort(); return IPS_ALERT; } } if (calibrationStage == CAL_MOUNT_PARKING) { // If not parked yet, check again in 1 second // Otherwise proceed to the rest of the algorithm if (currentTelescope->isParked() == false) return IPS_BUSY; else { calibrationStage = CAL_MOUNT_PARKED; appendLogText(i18n("Mount parked.")); } } } // Check if we need to perform dome prepark if (preDomePark && currentDome) { if (calibrationStage < CAL_DOME_PARKING && currentDome->isParked() == false) { if (currentDome->Park()) { calibrationStage = CAL_DOME_PARKING; //emit mountParking(); appendLogText(i18n("Parking dome...")); return IPS_BUSY; } else { appendLogText(i18n("Parking dome failed, aborting...")); abort(); return IPS_ALERT; } } if (calibrationStage == CAL_DOME_PARKING) { // If not parked yet, check again in 1 second // Otherwise proceed to the rest of the algorithm if (currentDome->isParked() == false) return IPS_BUSY; else { calibrationStage = CAL_DOME_PARKED; appendLogText(i18n("Dome parked.")); } } } // If we used AUTOFOCUS before for a specific frame (e.g. Lum) // then the absolute focus position for Lum is recorded in the filter manager // when we take flats again, we always go back to the same focus position as the light frames to ensure // near identical focus for both frames. if (activeJob->getFrameType() == FRAME_FLAT && m_AutoFocusReady && currentFilter != nullptr && Options::flatSyncFocus()) { if (filterManager->syncAbsoluteFocusPosition(activeJob->getTargetFilter() - 1) == false) return IPS_BUSY; } calibrationStage = CAL_PRECAPTURE_COMPLETE; return IPS_OK; } IPState Capture::processPreCaptureCalibrationStage() { // If we are currently guide and the frame is NOT a light frame, then we shopld suspend. // N.B. The guide camera could be on its own scope unaffected but it doesn't hurt to stop // guiding since it is no longer used anyway. if (activeJob->getFrameType() != FRAME_LIGHT && guideState == GUIDE_GUIDING) { appendLogText(i18n("Autoguiding suspended.")); emit suspendGuiding(); } // Run necessary tasks for each frame type switch (activeJob->getFrameType()) { case FRAME_LIGHT: return checkLightFramePendingTasks(); case FRAME_BIAS: case FRAME_DARK: return checkDarkFramePendingTasks(); case FRAME_FLAT: return checkFlatFramePendingTasks(); } return IPS_OK; } bool Capture::processPostCaptureCalibrationStage() { // If there are no more images to capture, do not bother calculating next exposure if (calibrationStage == CAL_CALIBRATION_COMPLETE) if (activeJob && activeJob->getCount() <= activeJob->getCompleted()) return true; // Check if we need to do flat field slope calculation if the user specified a desired ADU value if (activeJob->getFrameType() == FRAME_FLAT && activeJob->getFlatFieldDuration() == DURATION_ADU && activeJob->getTargetADU() > 0) { if (Options::useFITSViewer() == false) { Options::setUseFITSViewer(true); qCInfo(KSTARS_EKOS_CAPTURE) << "Enabling FITS Viewer..."; } FITSData * image_data = nullptr; FITSView * currentImage = targetChip->getImageView(FITS_NORMAL); if (currentImage) { image_data = currentImage->getImageData(); double currentADU = image_data->getADU(); bool outOfRange = false, saturated = false; switch (image_data->bpp()) { case 8: if (activeJob->getTargetADU() > UINT8_MAX) outOfRange = true; else if (currentADU / UINT8_MAX > 0.95) saturated = true; break; case 16: if (activeJob->getTargetADU() > UINT16_MAX) outOfRange = true; else if (currentADU / UINT16_MAX > 0.95) saturated = true; break; case 32: if (activeJob->getTargetADU() > UINT32_MAX) outOfRange = true; else if (currentADU / UINT32_MAX > 0.95) saturated = true; break; default: break; } if (outOfRange) { appendLogText(i18n("Flat calibration failed. Captured image is only %1-bit while requested ADU is %2.", QString::number(image_data->bpp()) , QString::number(activeJob->getTargetADU(), 'f', 2))); abort(); return false; } else if (saturated) { double nextExposure = activeJob->getExposure() * 0.1; nextExposure = qBound(exposureIN->minimum(), nextExposure, exposureIN->maximum()); appendLogText(i18n("Current image is saturated (%1). Next exposure is %2 seconds.", QString::number(currentADU, 'f', 0), QString("%L1").arg(nextExposure, 0, 'f', 6))); calibrationStage = CAL_CALIBRATION; activeJob->setExposure(nextExposure); activeJob->setPreview(true); rememberUploadMode = activeJob->getUploadMode(); currentCCD->setUploadMode(ISD::CCD::UPLOAD_CLIENT); startNextExposure(); return false; } double ADUDiff = fabs(currentADU - activeJob->getTargetADU()); // If it is within tolerance range of target ADU if (ADUDiff <= targetADUTolerance) { if (calibrationStage == CAL_CALIBRATION) { appendLogText( i18n("Current ADU %1 within target ADU tolerance range.", QString::number(currentADU, 'f', 0))); activeJob->setPreview(false); currentCCD->setUploadMode(rememberUploadMode); // Get raw prefix exposureIN->setValue(activeJob->getExposure()); QString imagePrefix = activeJob->property("rawPrefix").toString(); constructPrefix(imagePrefix); activeJob->setFullPrefix(imagePrefix); seqPrefix = imagePrefix; currentCCD->setSeqPrefix(imagePrefix); currentCCD->updateUploadSettings(activeJob->getRemoteDir() + activeJob->getDirectoryPostfix()); calibrationStage = CAL_CALIBRATION_COMPLETE; startNextExposure(); return false; } return true; } double nextExposure = -1; // If value is saturated, try to reduce it to valid range first if (std::fabs(image_data->getMax(0) - image_data->getMin(0)) < 10) nextExposure = activeJob->getExposure() * 0.5; else nextExposure = setCurrentADU(currentADU); if (nextExposure <= 0 || std::isnan(nextExposure)) { appendLogText( i18n("Unable to calculate optimal exposure settings, please capture the flats manually.")); //activeJob->setTargetADU(0); //targetADU = 0; abort(); return false; } // Limit to minimum and maximum values nextExposure = qBound(exposureIN->minimum(), nextExposure, exposureIN->maximum()); appendLogText(i18n("Current ADU is %1 Next exposure is %2 seconds.", QString::number(currentADU, 'f', 0), QString("%L1").arg(nextExposure, 0, 'f', 6))); calibrationStage = CAL_CALIBRATION; activeJob->setExposure(nextExposure); activeJob->setPreview(true); rememberUploadMode = activeJob->getUploadMode(); currentCCD->setUploadMode(ISD::CCD::UPLOAD_CLIENT); startNextExposure(); return false; // Start next exposure in case ADU Slope is not calculated yet /*if (currentSlope == 0) { startNextExposure(); return; }*/ } else { appendLogText(i18n("An empty image is received, aborting...")); abort(); return false; } } calibrationStage = CAL_CALIBRATION_COMPLETE; return true; } void Capture::setNewRemoteFile(QString file) { appendLogText(i18n("Remote image saved to %1", file)); emit newSequenceImage(file, QString()); } /* void Capture::startPostFilterAutoFocus() { if (focusState >= FOCUS_PROGRESS || state == CAPTURE_FOCUSING) return; secondsLabel->setText(i18n("Focusing...")); state = CAPTURE_FOCUSING; emit newStatus(Ekos::CAPTURE_FOCUSING); appendLogText(i18n("Post filter change Autofocus...")); // Force it to always run autofocus routine emit checkFocus(0.1); } */ void Capture::postScriptFinished(int exitCode, QProcess::ExitStatus status) { Q_UNUSED(status) appendLogText(i18n("Post capture script finished with code %1.", exitCode)); // If we're done, proceed to completion. if (activeJob->getCount() <= activeJob->getCompleted()) { processJobCompletion(); } // Else check if meridian condition is met. else if (checkMeridianFlip()) { appendLogText(i18n("Processing meridian flip...")); } // Then if nothing else, just resume sequence. else { appendLogText(i18n("Resuming sequence...")); resumeSequence(); } } void Capture::toggleVideo(bool enabled) { if (currentCCD == nullptr) return; if (currentCCD->isBLOBEnabled() == false) { if (Options::guiderType() != Ekos::Guide::GUIDE_INTERNAL) currentCCD->setBLOBEnabled(true); else { connect(KSMessageBox::Instance(), &KSMessageBox::accepted, this, [this, enabled]() { //QObject::disconnect(KSMessageBox::Instance(), &KSMessageBox::accepted, this, nullptr); KSMessageBox::Instance()->disconnect(this); currentCCD->setBLOBEnabled(true); currentCCD->setVideoStreamEnabled(enabled); }); KSMessageBox::Instance()->questionYesNo(i18n("Image transfer is disabled for this camera. Would you like to enable it?"), i18n("Image Transfer"), 15); return; } } currentCCD->setVideoStreamEnabled(enabled); } void Capture::setVideoStreamEnabled(bool enabled) { if (enabled) { liveVideoB->setChecked(true); liveVideoB->setIcon(QIcon::fromTheme("camera-on")); //liveVideoB->setStyleSheet("color:red;"); } else { liveVideoB->setChecked(false); liveVideoB->setIcon(QIcon::fromTheme("camera-ready")); //liveVideoB->setStyleSheet(QString()); } } void Capture::setMountStatus(ISD::Telescope::Status newState) { switch (newState) { case ISD::Telescope::MOUNT_PARKING: case ISD::Telescope::MOUNT_SLEWING: case ISD::Telescope::MOUNT_MOVING: previewB->setEnabled(false); liveVideoB->setEnabled(false); // Only disable when button is "Start", and not "Stopped" // If mount is in motion, Stopped button should always be enabled to terminate // the sequence if (pi->isAnimated() == false) startB->setEnabled(false); break; default: if (pi->isAnimated() == false) { previewB->setEnabled(true); if (currentCCD) liveVideoB->setEnabled(currentCCD->hasVideoStream()); startB->setEnabled(true); } break; } } void Capture::showObserverDialog() { QList m_observerList; KStars::Instance()->data()->userdb()->GetAllObservers(m_observerList); QStringList observers; for (auto &o : m_observerList) observers << QString("%1 %2").arg(o->name(), o->surname()); QDialog observersDialog(this); observersDialog.setWindowTitle(i18n("Select Current Observer")); QLabel label(i18n("Current Observer:")); QComboBox observerCombo(&observersDialog); observerCombo.addItems(observers); observerCombo.setCurrentText(m_ObserverName); observerCombo.setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Fixed); QPushButton manageObserver(&observersDialog); manageObserver.setFixedSize(QSize(32, 32)); manageObserver.setIcon(QIcon::fromTheme("document-edit")); manageObserver.setAttribute(Qt::WA_LayoutUsesWidgetRect); manageObserver.setToolTip(i18n("Manage Observers")); connect(&manageObserver, &QPushButton::clicked, this, [&]() { ObserverAdd add; add.exec(); QList m_observerList; KStars::Instance()->data()->userdb()->GetAllObservers(m_observerList); QStringList observers; for (auto &o : m_observerList) observers << QString("%1 %2").arg(o->name(), o->surname()); observerCombo.clear(); observerCombo.addItems(observers); observerCombo.setCurrentText(m_ObserverName); }); QHBoxLayout * layout = new QHBoxLayout; layout->addWidget(&label); layout->addWidget(&observerCombo); layout->addWidget(&manageObserver); observersDialog.setLayout(layout); observersDialog.exec(); m_ObserverName = observerCombo.currentText(); Options::setDefaultObserver(m_ObserverName); } void Capture::startRefocusTimer(bool forced) { /* If refocus is requested, only restart timer if not already running in order to keep current elapsed time since last refocus */ if (refocusEveryNCheck->isChecked()) { // How much time passed since we last started the time uint32_t elapsedSecs = refocusEveryNTimer.elapsed() / 1000; // How many seconds do we wait for between focusing (60 mins ==> 3600 secs) uint32_t totalSecs = refocusEveryN->value() * 60; if (!refocusEveryNTimer.isValid() || forced) { appendLogText(i18n("Ekos will refocus in %1 seconds.", totalSecs)); refocusEveryNTimer.restart(); } else if (elapsedSecs < totalSecs) { //appendLogText(i18n("Ekos will refocus in %1 seconds, last procedure was %2 seconds ago.", refocusEveryNTimer.elapsed()/1000-refocusEveryNTimer.elapsed()*60, refocusEveryNTimer.elapsed()/1000)); appendLogText(i18n("Ekos will refocus in %1 seconds, last procedure was %2 seconds ago.", totalSecs - elapsedSecs, elapsedSecs)); } else { appendLogText(i18n("Ekos will refocus as soon as possible, last procedure was %1 seconds ago.", elapsedSecs)); } } } int Capture::getRefocusEveryNTimerElapsedSec() { /* If timer isn't valid, consider there is no focus to be done, that is, that focus was just done */ return refocusEveryNTimer.isValid() ? refocusEveryNTimer.elapsed() / 1000 : 0; } void Capture::setAlignResults(double orientation, double ra, double de, double pixscale) { Q_UNUSED(orientation) Q_UNUSED(ra) Q_UNUSED(de) Q_UNUSED(pixscale) if (currentRotator == nullptr) return; rotatorSettings->refresh(); } void Capture::setFilterManager(const QSharedPointer &manager) { filterManager = manager; connect(filterManagerB, &QPushButton::clicked, [this]() { filterManager->show(); filterManager->raise(); }); connect(filterManager.data(), &FilterManager::ready, [this]() { m_CurrentFilterPosition = filterManager->getFilterPosition(); // Due to race condition, focusState = FOCUS_IDLE; if (activeJob) activeJob->setCurrentFilter(m_CurrentFilterPosition); } ); connect(filterManager.data(), &FilterManager::failed, [this]() { if (activeJob) { appendLogText(i18n("Filter operation failed.")); abort(); } } ); connect(filterManager.data(), &FilterManager::newStatus, [this](Ekos::FilterState filterState) { filterManagerState = filterState; if (m_State == CAPTURE_CHANGING_FILTER) { secondsLabel->setText(Ekos::getFilterStatusString(filterState)); switch (filterState) { case FILTER_OFFSET: appendLogText(i18n("Changing focus offset by %1 steps...", filterManager->getTargetFilterOffset())); break; case FILTER_CHANGE: appendLogText(i18n("Changing filter to %1...", FilterPosCombo->itemText(filterManager->getTargetFilterPosition() - 1))); break; case FILTER_AUTOFOCUS: appendLogText(i18n("Auto focus on filter change...")); clearAutoFocusHFR(); break; default: break; } } }); connect(filterManager.data(), &FilterManager::labelsChanged, this, [this]() { FilterPosCombo->clear(); FilterPosCombo->addItems(filterManager->getFilterLabels()); m_CurrentFilterPosition = filterManager->getFilterPosition(); FilterPosCombo->setCurrentIndex(m_CurrentFilterPosition - 1); }); connect(filterManager.data(), &FilterManager::positionChanged, this, [this]() { m_CurrentFilterPosition = filterManager->getFilterPosition(); FilterPosCombo->setCurrentIndex(m_CurrentFilterPosition - 1); }); } void Capture::addDSLRInfo(const QString &model, uint32_t maxW, uint32_t maxH, double pixelW, double pixelH) { // Check if model already exists auto pos = std::find_if(DSLRInfos.begin(), DSLRInfos.end(), [model](QMap &oneDSLRInfo) { return (oneDSLRInfo["Model"] == model); }); if (pos != DSLRInfos.end()) { KStarsData::Instance()->userdb()->DeleteDSLRInfo(model); DSLRInfos.removeOne(*pos); } QMap oneDSLRInfo; oneDSLRInfo["Model"] = model; oneDSLRInfo["Width"] = maxW; oneDSLRInfo["Height"] = maxH; oneDSLRInfo["PixelW"] = pixelW; oneDSLRInfo["PixelH"] = pixelH; KStarsData::Instance()->userdb()->AddDSLRInfo(oneDSLRInfo); KStarsData::Instance()->userdb()->GetAllDSLRInfos(DSLRInfos); updateFrameProperties(); resetFrame(); syncDSLRToTargetChip(model); // In case the dialog was opened, let's close it if (dslrInfoDialog) dslrInfoDialog.reset(); } bool Capture::isModelinDSLRInfo(const QString &model) { auto pos = std::find_if(DSLRInfos.begin(), DSLRInfos.end(), [model](QMap &oneDSLRInfo) { return (oneDSLRInfo["Model"] == model); }); return (pos != DSLRInfos.end()); } #if 0 void Capture::syncDriverToDSLRLimits() { if (targetChip == nullptr) return; QString model(currentCCD->getDeviceName()); // Check if model already exists auto pos = std::find_if(DSLRInfos.begin(), DSLRInfos.end(), [model](QMap &oneDSLRInfo) { return (oneDSLRInfo["Model"] == model); }); if (pos != DSLRInfos.end()) targetChip->setImageInfo((*pos)["Width"].toInt(), (*pos)["Height"].toInt(), (*pos)["PixelW"].toDouble(), (*pos)["PixelH"].toDouble(), 8); } #endif void Capture::cullToDSLRLimits() { QString model(currentCCD->getDeviceName()); // Check if model already exists auto pos = std::find_if(DSLRInfos.begin(), DSLRInfos.end(), [model](QMap &oneDSLRInfo) { return (oneDSLRInfo["Model"] == model); }); if (pos != DSLRInfos.end()) { if (frameWIN->maximum() == 0 || frameWIN->maximum() > (*pos)["Width"].toInt()) { frameWIN->setValue((*pos)["Width"].toInt()); frameWIN->setMaximum((*pos)["Width"].toInt()); } if (frameHIN->maximum() == 0 || frameHIN->maximum() > (*pos)["Height"].toInt()) { frameHIN->setValue((*pos)["Height"].toInt()); frameHIN->setMaximum((*pos)["Height"].toInt()); } } } void Capture::setCapturedFramesMap(const QString &signature, int count) { capturedFramesMap[signature] = count; qCDebug(KSTARS_EKOS_CAPTURE) << QString("Client module indicates that storage for '%1' has already %2 captures processed.").arg(signature).arg(count); // Scheduler's captured frame map overrides the progress option of the Capture module ignoreJobProgress = false; } void Capture::setSettings(const QJsonObject &settings) { // FIXME: QComboBox signal "activated" does not trigger when setting text programmatically. const QString targetCamera = settings["camera"].toString(); const QString targetFW = settings["fw"].toString(); const QString targetFilter = settings["filter"].toString(); if (CCDCaptureCombo->currentText() != targetCamera) { const int index = CCDCaptureCombo->findText(targetCamera); CCDCaptureCombo->setCurrentIndex(index); checkCCD(index); } if (!targetFW.isEmpty() && FilterDevicesCombo->currentText() != targetFW) { const int index = FilterDevicesCombo->findText(targetFW); FilterDevicesCombo->setCurrentIndex(index); checkFilter(index); } if (!targetFilter.isEmpty() && FilterPosCombo->currentText() != targetFilter) { FilterPosCombo->setCurrentIndex(FilterPosCombo->findText(targetFilter)); } exposureIN->setValue(settings["exp"].toDouble(1)); int bin = settings["bin"].toInt(1); setBinning(bin, bin); double temperature = settings["temperature"].toDouble(100); if (temperature < 100 && currentCCD && currentCCD->hasCoolerControl()) { setForceTemperature(true); setTargetTemperature(temperature); } double gain = settings["gain"].toDouble(-1); if (gain >= 0 && currentCCD && currentCCD->hasGain()) { setGain(gain); } int format = settings["format"].toInt(-1); if (format >= 0) { transferFormatCombo->setCurrentIndex(format); } frameTypeCombo->setCurrentIndex(qMax(0, settings["frameType"].toInt(0))); // ISO int isoIndex = settings["iso"].toInt(-1); if (isoIndex >= 0) setISO(isoIndex); } void Capture::clearCameraConfiguration() { //if (!Options::autonomousMode() && KMessageBox::questionYesNo(nullptr, i18n("Reset %1 configuration to default?", currentCCD->getDeviceName()), i18n("Confirmation")) == KMessageBox::No) // return; connect(KSMessageBox::Instance(), &KSMessageBox::accepted, this, [this]() { //QObject::disconnect(KSMessageBox::Instance(), &KSMessageBox::accepted, this, nullptr); KSMessageBox::Instance()->disconnect(this); currentCCD->setConfig(PURGE_CONFIG); KStarsData::Instance()->userdb()->DeleteDSLRInfo(currentCCD->getDeviceName()); QStringList shutterfulCCDs = Options::shutterfulCCDs(); QStringList shutterlessCCDs = Options::shutterlessCCDs(); // Remove camera from shutterful and shutterless CCDs if (shutterfulCCDs.contains(currentCCD->getDeviceName())) { shutterfulCCDs.removeOne(currentCCD->getDeviceName()); Options::setShutterfulCCDs(shutterfulCCDs); } if (shutterlessCCDs.contains(currentCCD->getDeviceName())) { shutterlessCCDs.removeOne(currentCCD->getDeviceName()); Options::setShutterlessCCDs(shutterlessCCDs); } // For DSLRs, immediately ask them to enter the values again. if (ISOCombo && ISOCombo->count() > 0) { createDSLRDialog(); } }); KSMessageBox::Instance()->questionYesNo( i18n("Reset %1 configuration to default?", currentCCD->getDeviceName()), i18n("Confirmation"), 30); } void Capture::setCoolerToggled(bool enabled) { coolerOnB->blockSignals(true); coolerOnB->setChecked(enabled); coolerOnB->blockSignals(false); coolerOffB->blockSignals(true); coolerOffB->setChecked(!enabled); coolerOffB->blockSignals(false); appendLogText(enabled ? i18n("Cooler is on") : i18n("Cooler is off")); } void Capture::processCaptureTimeout() { auto restartExposure = [&]() { appendLogText(i18n("Exposure timeout. Restarting exposure...")); currentCCD->setTransformFormat(ISD::CCD::FORMAT_FITS); ISD::CCDChip *targetChip = currentCCD->getChip(useGuideHead ? ISD::CCDChip::GUIDE_CCD : ISD::CCDChip::PRIMARY_CCD); targetChip->abortExposure(); targetChip->capture(exposureIN->value()); captureTimeout.start(exposureIN->value() * 1000 + CAPTURE_TIMEOUT_THRESHOLD); }; m_CaptureTimeoutCounter++; if (m_DeviceRestartCounter >= 3) { m_CaptureTimeoutCounter = 0; m_DeviceRestartCounter = 0; appendLogText(i18n("Exposure timeout. Aborting...")); abort(); return; } if (m_CaptureTimeoutCounter > 1) { QString camera = currentCCD->getDeviceName(); QString fw = currentFilter ? currentFilter->getDeviceName() : ""; emit driverTimedout(camera); QTimer::singleShot(5000, [ &, camera, fw]() { m_DeviceRestartCounter++; reconnectDriver(camera, fw); }); return; } else restartExposure(); } void Capture::setGeneratedPreviewFITS(const QString &previewFITS) { m_GeneratedPreviewFITS = previewFITS; } void Capture::createDSLRDialog() { dslrInfoDialog.reset(new DSLRInfo(this, currentCCD)); connect(dslrInfoDialog.get(), &DSLRInfo::infoChanged, [this]() { addDSLRInfo(QString(currentCCD->getDeviceName()), dslrInfoDialog->sensorMaxWidth, dslrInfoDialog->sensorMaxHeight, dslrInfoDialog->sensorPixelW, dslrInfoDialog->sensorPixelH); }); dslrInfoDialog->show(); emit dslrInfoRequested(currentCCD->getDeviceName()); } void Capture::removeDevice(ISD::GDInterface *device) { device->disconnect(this); if (currentTelescope && currentTelescope->getDeviceName() == device->getDeviceName()) { currentTelescope = nullptr; } else if (currentDome && currentDome->getDeviceName() == device->getDeviceName()) { currentDome = nullptr; } else if (currentRotator && currentRotator->getDeviceName() == device->getDeviceName()) { currentRotator = nullptr; rotatorB->setEnabled(false); } if (CCDs.contains(static_cast(device))) { ISD::CCD *oneCCD = static_cast(device); CCDs.removeAll(oneCCD); CCDCaptureCombo->removeItem(CCDCaptureCombo->findText(device->getDeviceName())); CCDCaptureCombo->removeItem(CCDCaptureCombo->findText(device->getDeviceName() + QString(" Guider"))); if (CCDs.empty()) { currentCCD = nullptr; CCDCaptureCombo->setCurrentIndex(-1); } else CCDCaptureCombo->setCurrentIndex(0); checkCCD(); } if (Filters.contains(static_cast(device))) { Filters.removeOne(static_cast(device)); filterManager->removeDevice(device); FilterDevicesCombo->removeItem(FilterDevicesCombo->findText(device->getDeviceName())); if (Filters.empty()) { currentFilter = nullptr; FilterDevicesCombo->setCurrentIndex(-1); } else FilterDevicesCombo->setCurrentIndex(0); checkFilter(); } } void Capture::setGain(double value) { QMap > customProps = customPropertiesDialog->getCustomProperties(); // Gain is manifested in two forms // Property CCD_GAIN and // Part of CCD_CONTROLS properties. // Therefore, we have to find what the currently camera supports first. if (currentCCD->getProperty("CCD_GAIN")) { QMap ccdGain; ccdGain["GAIN"] = value; customProps["CCD_GAIN"] = ccdGain; } else if (currentCCD->getProperty("CCD_CONTROLS")) { QMap ccdGain; ccdGain["Gain"] = value; customProps["CCD_CONTROLS"] = ccdGain; } customPropertiesDialog->setCustomProperties(customProps); } double Capture::getGain() { if (!GainSpin) return -1; QMap > customProps = customPropertiesDialog->getCustomProperties(); // Gain is manifested in two forms // Property CCD_GAIN and // Part of CCD_CONTROLS properties. // Therefore, we have to find what the currently camera supports first. if (currentCCD->getProperty("CCD_GAIN")) { return customProps["CCD_GAIN"].value("GAIN", -1); } else if (currentCCD->getProperty("CCD_CONTROLS")) { return customProps["CCD_CONTROLS"].value("Gain", -1); } return -1; } double Capture::getEstimatedDownloadTime() { double total = 0; foreach(double dlTime, downloadTimes) total += dlTime; if(downloadTimes.count() == 0) return 0; else return total / downloadTimes.count(); } void Capture::reconnectDriver(const QString &camera, const QString &filterWheel) { for (auto &oneCamera : CCDs) { if (oneCamera->getDeviceName() == camera) { // Set camera again to the one we restarted CCDCaptureCombo->setCurrentIndex(CCDCaptureCombo->findText(camera)); FilterDevicesCombo->setCurrentIndex(FilterDevicesCombo->findText(filterWheel)); checkCCD(); // restart capture m_CaptureTimeoutCounter = 0; if (activeJob) { activeJob->setActiveChip(targetChip); activeJob->setActiveCCD(currentCCD); activeJob->setActiveFilter(currentFilter); captureImage(); } return; } } QTimer::singleShot(5000, this, [ &, camera, filterWheel]() { reconnectDriver(camera, filterWheel); }); } bool Capture::isGuidingActive() { // In case we are doing non guiding dither, then we are not performing autoguiding. if (Options::ditherNoGuiding()) return false; return (guideState == GUIDE_GUIDING || guideState == GUIDE_CALIBRATING || guideState == GUIDE_CALIBRATION_SUCESS || guideState == GUIDE_REACQUIRE || guideState == GUIDE_DITHERING || guideState == GUIDE_DITHERING_SUCCESS || guideState == GUIDE_DITHERING_ERROR || guideState == GUIDE_DITHERING_SETTLE); } QString Capture::MFStageString(MFStage stage) { switch(stage) { case MF_NONE: return "MF_NONE"; case MF_REQUESTED: return "MF_REQUESTED"; case MF_READY: return "MF_READY"; case MF_INITIATED: return "MF_INITIATED"; case MF_FLIPPING: return "MF_FLIPPING"; case MF_SLEWING: return "MF_SLEWING"; case MF_COMPLETED: return "MF_COMPLETED"; case MF_ALIGNING: return "MF_ALIGNING"; case MF_GUIDING: return "MF_GUIDING"; } return "MFStage unknown."; } void Capture::syncDSLRToTargetChip(const QString &model) { auto pos = std::find_if(DSLRInfos.begin(), DSLRInfos.end(), [model](QMap &oneDSLRInfo) { return (oneDSLRInfo["Model"] == model); }); // Sync Pixel Size if (pos != DSLRInfos.end()) { auto camera = *pos; targetChip->setImageInfo(camera["Width"].toDouble(), camera["Height"].toDouble(), camera["PixelW"].toDouble(), camera["PixelH"].toDouble(), 8); } } +void Capture::editFilterName() +{ + if (!currentFilter || m_CurrentFilterPosition < 1) + return; + + QStringList labels = filterManager->getFilterLabels(); + QDialog filterDialog; + + QFormLayout *formLayout = new QFormLayout(&filterDialog); + QVector newLabelEdits; + + for (uint8_t i = 0; i < labels.count(); i++) + { + QLabel *existingLabel = new QLabel(QString("%1. %2").arg(i + 1).arg(labels[i]), &filterDialog); + QLineEdit *newLabel = new QLineEdit(labels[i], &filterDialog); + newLabelEdits.append(newLabel); + formLayout->addRow(existingLabel, newLabel); + } + + filterDialog.setWindowTitle(currentFilter->getDeviceName()); + filterDialog.setLayout(formLayout); + QDialogButtonBox *buttonBox = new QDialogButtonBox(QDialogButtonBox::Ok | QDialogButtonBox::Cancel, &filterDialog); + connect(buttonBox, &QDialogButtonBox::accepted, &filterDialog, &QDialog::accept); + connect(buttonBox, &QDialogButtonBox::rejected, &filterDialog, &QDialog::reject); + filterDialog.layout()->addWidget(buttonBox); + + if (filterDialog.exec() == QDialog::Accepted) + { + QStringList newLabels; + for (uint8_t i = 0; i < labels.count(); i++) + newLabels << newLabelEdits[i]->text(); + filterManager->setFilterNames(newLabels); + } +} + } diff --git a/kstars/ekos/capture/capture.h b/kstars/ekos/capture/capture.h index 4d681c6cd..867d91083 100644 --- a/kstars/ekos/capture/capture.h +++ b/kstars/ekos/capture/capture.h @@ -1,972 +1,975 @@ /* Ekos Capture tool Copyright (C) 2012 Jasem Mutlaq This application is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version. */ #pragma once #include "ui_capture.h" #include "customproperties.h" #include "oal/filter.h" #include "ekos/ekos.h" #include "ekos/mount/mount.h" #include "indi/indiccd.h" #include "indi/indicap.h" #include "indi/indidome.h" #include "indi/indilightbox.h" #include "indi/inditelescope.h" #include "ekos/auxiliary/filtermanager.h" #include "ekos/scheduler/schedulerjob.h" #include "dslrinfodialog.h" #include #include #include #include class QProgressIndicator; class QTableWidgetItem; class KDirWatch; class RotatorSettings; /** * @namespace Ekos * @short Ekos is an advanced Astrophotography tool for Linux. * It is based on a modular extensible framework to perform common astrophotography tasks. This includes highly accurate GOTOs using astrometry solver, ability to measure and correct polar alignment errors , * auto-focus & auto-guide capabilities, and capture of single or stack of images with filter wheel support.\n * Features: * - Control your telescope, CCD (& DSLRs), filter wheel, focuser, guider, adaptive optics unit, and any INDI-compatible auxiliary device from Ekos. * - Extremely accurate GOTOs using astrometry.net solver (both Online and Offline solvers supported). * - Load & Slew: Load a FITS image, slew to solved coordinates, and center the mount on the exact image coordinates in order to get the same desired frame. * - Measure & Correct Polar Alignment errors using astrometry.net solver. * - Auto and manual focus modes using Half-Flux-Radius (HFR) method. * - Automated unattended meridian flip. Ekos performs post meridian flip alignment, calibration, and guiding to resume the capture session. * - Automatic focus between exposures when a user-configurable HFR limit is exceeded. * - Automatic focus between exposures when the temperature has changed a lot since last focus. * - Auto guiding with support for automatic dithering between exposures and support for Adaptive Optics devices in addition to traditional guiders. * - Powerful sequence queue for batch capture of images with optional prefixes, timestamps, filter wheel selection, and much more! * - Export and import sequence queue sets as Ekos Sequence Queue (.esq) files. * - Center the telescope anywhere in a captured FITS image or any FITS with World Coordinate System (WCS) header. * - Automatic flat field capture, just set the desired ADU and let Ekos does the rest! * - Automatic abort and resumption of exposure tasks if guiding errors exceed a user-configurable value. * - Support for dome slaving. * - Complete integration with KStars Observation Planner and SkyMap * - Integrate with all INDI native devices. * - Powerful scripting capabilities via \ref EkosDBusInterface "DBus." * * The primary class is Ekos::Manager. It handles startup and shutdown of local and remote INDI devices, manages and orchesterates the various Ekos modules, and provides advanced DBus * interface to enable unattended scripting. * * @author Jasem Mutlaq * @version 1.8 */ namespace Ekos { class SequenceJob; /** *@class Capture *@short Captures single or sequence of images from a CCD. * The capture class support capturing single or multiple images from a CCD, it provides a powerful sequence queue with filter wheel selection. Any sequence queue can be saved as Ekos Sequence Queue (.esq). * All image capture operations are saved as Sequence Jobs that encapsulate all the different options in a capture process. The user may select in sequence autofocusing by setting a maximum HFR limit. When the limit * is exceeded, it automatically trigger autofocus operation. The capture process can also be linked with guide module. If guiding deviations exceed a certain threshold, the capture operation aborts until * the guiding deviation resume to acceptable levels and the capture operation is resumed. *@author Jasem Mutlaq *@version 1.4 */ class Capture : public QWidget, public Ui::Capture { Q_OBJECT Q_CLASSINFO("D-Bus Interface", "org.kde.kstars.Ekos.Capture") Q_PROPERTY(Ekos::CaptureState status READ status NOTIFY newStatus) Q_PROPERTY(QString targetName MEMBER m_TargetName) Q_PROPERTY(QString observerName MEMBER m_ObserverName) Q_PROPERTY(QString camera READ camera WRITE setCamera) Q_PROPERTY(QString filterWheel READ filterWheel WRITE setFilterWheel) Q_PROPERTY(QString filter READ filter WRITE setFilter) Q_PROPERTY(bool coolerControl READ hasCoolerControl WRITE setCoolerControl) Q_PROPERTY(QStringList logText READ logText NOTIFY newLog) public: typedef enum { MF_NONE, MF_REQUESTED, MF_READY, MF_INITIATED, MF_FLIPPING, MF_SLEWING, MF_COMPLETED, MF_ALIGNING, MF_GUIDING } MFStage; typedef enum { CAL_NONE, CAL_DUSTCAP_PARKING, CAL_DUSTCAP_PARKED, CAL_LIGHTBOX_ON, CAL_SLEWING, CAL_SLEWING_COMPLETE, CAL_MOUNT_PARKING, CAL_MOUNT_PARKED, CAL_DOME_PARKING, CAL_DOME_PARKED, CAL_PRECAPTURE_COMPLETE, CAL_CALIBRATION, CAL_CALIBRATION_COMPLETE, CAL_CAPTURING, CAL_DUSTCAP_UNPARKING, CAL_DUSTCAP_UNPARKED } CalibrationStage; typedef enum { CAL_CHECK_TASK, CAL_CHECK_CONFIRMATION, } CalibrationCheckType; typedef enum { ADU_LEAST_SQUARES, ADU_POLYNOMIAL } ADUAlgorithm; typedef bool (Capture::*PauseFunctionPointer)(); Capture(); ~Capture(); /** @defgroup CaptureDBusInterface Ekos DBus Interface - Capture Module * Ekos::Capture interface provides advanced scripting capabilities to capture image sequences. */ /*@{*/ /** DBUS interface function. * select the CCD device from the available CCD drivers. * @param device The CCD device name */ Q_SCRIPTABLE bool setCamera(const QString &device); Q_SCRIPTABLE QString camera(); /** DBUS interface function. * select the filter device from the available filter drivers. The filter device can be the same as the CCD driver if the filter functionality was embedded within the driver. * @param device The filter device name */ Q_SCRIPTABLE bool setFilterWheel(const QString &device); Q_SCRIPTABLE QString filterWheel(); /** DBUS interface function. * select the filter name from the available filters in case a filter device is active. * @param filter The filter name */ Q_SCRIPTABLE bool setFilter(const QString &filter); Q_SCRIPTABLE QString filter(); /** DBUS interface function. * Aborts any current jobs and remove all sequence queue jobs. */ Q_SCRIPTABLE Q_NOREPLY void clearSequenceQueue(); /** DBUS interface function. * Returns the overall sequence queue status. If there are no jobs pending, it returns "Invalid". If all jobs are idle, it returns "Idle". If all jobs are complete, it returns "Complete". If one or more jobs are aborted * it returns "Aborted" unless it was temporarily aborted due to guiding deviations, then it would return "Suspended". If one or more jobs have errors, it returns "Error". If any jobs is under progress, returns "Running". */ Q_SCRIPTABLE QString getSequenceQueueStatus(); /** DBUS interface function. * Loads the Ekos Sequence Queue file in the Sequence Queue. Jobs are appended to existing jobs. * @param fileURL full URL of the filename */ Q_SCRIPTABLE bool loadSequenceQueue(const QString &fileURL); /** DBUS interface function. * Enables or disables the maximum guiding deviation and sets its value. * @param enable If true, enable the guiding deviation check, otherwise, disable it. * @param value if enable is true, it sets the maximum guiding deviation in arcsecs. If the value is exceeded, the capture operation is aborted until the value falls below the value threshold. */ Q_SCRIPTABLE Q_NOREPLY void setMaximumGuidingDeviation(bool enable, double value); /** DBUS interface function. * Enables or disables the in sequence focus and sets Half-Flux-Radius (HFR) limit. * @param enable If true, enable the in sequence auto focus check, otherwise, disable it. * @param HFR if enable is true, it sets HFR in pixels. After each exposure, the HFR is re-measured and if it exceeds the specified value, an autofocus operation will be commanded. */ Q_SCRIPTABLE Q_NOREPLY void setInSequenceFocus(bool enable, double HFR); /** DBUS interface function. * Does the CCD has a cooler control (On/Off) ? */ Q_SCRIPTABLE bool hasCoolerControl(); /** DBUS interface function. * Set the CCD cooler ON/OFF * */ Q_SCRIPTABLE bool setCoolerControl(bool enable); /** DBUS interface function. * @return Returns the percentage of completed captures in all active jobs */ Q_SCRIPTABLE double getProgressPercentage(); /** DBUS interface function. * @return Returns the number of jobs in the sequence queue. */ Q_SCRIPTABLE int getJobCount() { return jobs.count(); } /** DBUS interface function. * @return Returns the number of pending uncompleted jobs in the sequence queue. */ Q_SCRIPTABLE int getPendingJobCount(); /** DBUS interface function. * @return Returns ID of current active job if any, or -1 if there are no active jobs. */ Q_SCRIPTABLE int getActiveJobID(); /** DBUS interface function. * @return Returns time left in seconds until active job is estimated to be complete. */ Q_SCRIPTABLE int getActiveJobRemainingTime(); /** DBUS interface function. * @return Returns overall time left in seconds until all jobs are estimated to be complete */ Q_SCRIPTABLE int getOverallRemainingTime(); /** DBUS interface function. * @param id job number. Job IDs start from 0 to N-1. * @return Returns the job state (Idle, In Progress, Error, Aborted, Complete) */ Q_SCRIPTABLE QString getJobState(int id); /** DBUS interface function. * @param id job number. Job IDs start from 0 to N-1. * @return Returns The number of images completed capture in the job. */ Q_SCRIPTABLE int getJobImageProgress(int id); /** DBUS interface function. * @param id job number. Job IDs start from 0 to N-1. * @return Returns the total number of images to capture in the job. */ Q_SCRIPTABLE int getJobImageCount(int id); /** DBUS interface function. * @param id job number. Job IDs start from 0 to N-1. * @return Returns the number of seconds left in an exposure operation. */ Q_SCRIPTABLE double getJobExposureProgress(int id); /** DBUS interface function. * @param id job number. Job IDs start from 0 to N-1. * @return Returns the total requested exposure duration in the job. */ Q_SCRIPTABLE double getJobExposureDuration(int id); /** DBUS interface function. * Clear in-sequence focus settings. It sets the autofocus HFR to zero so that next autofocus value is remembered for the in-sequence focusing. */ Q_SCRIPTABLE Q_NOREPLY void clearAutoFocusHFR(); /** DBUS interface function. * Jobs will NOT be checked for progress against the file system and will be always assumed as new jobs. */ Q_SCRIPTABLE Q_NOREPLY void ignoreSequenceHistory(); /** DBUS interface function. * Set count of already completed frames. This is required when we have identical external jobs * with identical paths, but we need to continue where we left off. For example, if we have 3 identical * jobs, each capturing 5 images. Let's suppose 9 images were captured before. If the count for this signature * is set to 1, then we continue to capture frame #2 even though the number of completed images is already * larger than required count (5). It is mostly used in conjunction with Ekos Scheduler. */ Q_SCRIPTABLE Q_NOREPLY void setCapturedFramesMap(const QString &signature, int count); Q_SCRIPTABLE QStringList logText() { return m_LogText; } Q_SCRIPTABLE Ekos::CaptureState status() { return m_State; } /** @}*/ void addCCD(ISD::GDInterface *newCCD); void addFilter(ISD::GDInterface *newFilter); void setDome(ISD::GDInterface *device) { currentDome = dynamic_cast(device); } void setDustCap(ISD::GDInterface *device) { currentDustCap = dynamic_cast(device); } void setLightBox(ISD::GDInterface *device) { currentLightBox = dynamic_cast(device); } void removeDevice(ISD::GDInterface *device); void addGuideHead(ISD::GDInterface *newCCD); void syncFrameType(ISD::GDInterface *ccd); void setTelescope(ISD::GDInterface *newTelescope); void setRotator(ISD::GDInterface *newRotator); void setFilterManager(const QSharedPointer &manager); void syncTelescopeInfo(); void syncFilterInfo(); // Restart driver void reconnectDriver(const QString &camera, const QString &filterWheel); void clearLog(); QString getLogText() { return m_LogText.join("\n"); } /* Capture */ void updateSequencePrefix(const QString &newPrefix, const QString &dir); /** * @brief getSequence Return the JSON representation of the current sequeue queue * @return Reference to JSON array containing sequence queue jobs. */ const QJsonArray &getSequence() const { return m_SequenceArray; } /** * @brief setSettings Set capture settings * @param settings list of settings */ void setSettings(const QJsonObject &settings); /** * @brief getSettings get current capture settings as a JSON Object * @return settings as JSON object */ QJsonObject getSettings(); /** * @brief addDSLRInfo Save DSLR Info the in the database. If the interactive dialog was open, close it. * @param model Camera name * @param maxW Maximum width in pixels * @param maxH Maximum height in pixels * @param pixelW Pixel horizontal size in microns * @param pixelH Pizel vertical size in microns */ void addDSLRInfo(const QString &model, uint32_t maxW, uint32_t maxH, double pixelW, double pixelH); /** * @brief syncDSLRToTargetChip Syncs INDI driver CCD_INFO property to the DSLR values. * This include Max width, height, and pixel sizes. * @param model Name of camera driver in the DSLR database. */ void syncDSLRToTargetChip(const QString &model); double getEstimatedDownloadTime(); public slots: /** \addtogroup CaptureDBusInterface * @{ */ /* Capture */ /** DBUS interface function. * Starts the sequence queue capture procedure sequentially by starting all jobs that are either Idle or Aborted in order. */ Q_SCRIPTABLE Q_NOREPLY void start(); /** DBUS interface function. * Stop all jobs and set current job status to aborted if abort is set to true, otherwise status is idle until * sequence is resumed or restarted. * @param targetState status of the job after abortion */ Q_SCRIPTABLE Q_NOREPLY void stop(CaptureState targetState = CAPTURE_IDLE); /** DBUS interface function. * Aborts all jobs and mark current state as ABORTED. It simply calls stop(CAPTURE_ABORTED) */ Q_SCRIPTABLE Q_NOREPLY void abort() { stop(CAPTURE_ABORTED); } /** DBUS interface function. * Aborts all jobs and mark current state as SUSPENDED. It simply calls stop(CAPTURE_SUSPENDED) * The only difference between SUSPENDED and ABORTED it that capture module can automatically resume a suspended * state on its own without external trigger once the right conditions are met. When whatever reason caused the module * to go into suspended state ceases to exist, the capture module automatically resumes. On the other hand, ABORTED state * must be started via an external programmatic or user trigger (e.g. click the start button again). */ Q_SCRIPTABLE Q_NOREPLY void suspend() { stop(CAPTURE_SUSPENDED); } /** DBUS interface function. * Toggle video streaming if supported by the device. * @param enabled Set to true to start video streaming, false to stop it if active. */ Q_SCRIPTABLE Q_NOREPLY void toggleVideo(bool enabled); /** DBUS interface function. * @brief pause Pauses the Sequence Queue progress AFTER the current capture is complete. */ Q_SCRIPTABLE Q_NOREPLY void pause(); /** DBUS interface function. * @brief toggleSequence Toggle sequence state depending on its current state. * 1. If paused, then resume sequence. * 2. If idle or completed, then start sequence. * 3. Otherwise, abort current sequence. */ Q_SCRIPTABLE Q_NOREPLY void toggleSequence(); /** @}*/ /** * @brief captureOne Capture one preview image */ void captureOne(); /** * @brief startFraming Like captureOne but repeating. */ void startFraming(); /** * @brief setExposure Set desired exposure value in seconds * @param value exposure values in seconds */ void setExposure(double value) { exposureIN->setValue(value); } /** * @brief seqCount Set required number of images to capture in one sequence job * @param count number of images to capture */ void setCount(uint16_t count) { countIN->setValue(count); } /** * @brief setDelay Set delay between capturing images within a sequence in seconds * @param delay numbers of seconds to wait before starting the next image. */ void setDelay(uint16_t delay) { delayIN->setValue(delay); } /** * @brief setPrefix Set target or prefix name used in constructing the generated file name * @param prefix Leading text of the generated image name. */ void setPrefix(const QString &prefix) { prefixIN->setText(prefix); } /** * @brief setBinning Set binning * @param horBin Horizontal binning * @param verBin Vertical binning */ void setBinning(int horBin, int verBin) { binXIN->setValue(horBin); binYIN->setValue(verBin); } /** * @brief setISO Set index of ISO list. * @param index index of ISO list. */ void setISO(int index) { ISOCombo->setCurrentIndex(index); } /** * @brief captureImage Initiates image capture in the active job. */ void captureImage(); /** * @brief newFITS process new FITS data received from camera. Update status of active job and overall sequence. * @param bp pointer to blob containing FITS data */ void newFITS(IBLOB *bp); /** * @brief checkCCD Refreshes the CCD information in the capture module. * @param CCDNum The CCD index in the CCD combo box to select as the active CCD. */ void checkCCD(int CCDNum = -1); /** * @brief checkFilter Refreshes the filter wheel information in the capture module. * @param filterNum The filter wheel index in the filter device combo box to set as the active filter. */ void checkFilter(int filterNum = -1); /** * @brief processCCDNumber Process number properties arriving from CCD. Currently, only CCD and Guider frames are processed. * @param nvp pointer to number property. */ void processCCDNumber(INumberVectorProperty *nvp); /** * @brief processTelescopeNumber Process number properties arriving from telescope for meridian flip purposes. * @param nvp pointer to number property. */ void processTelescopeNumber(INumberVectorProperty *nvp); /** * @brief addJob Add a new job to the sequence queue given the settings in the GUI. * @param preview True if the job is a preview job, otherwise, it is added as a batch job. * @return True if job is added successfully, false otherwise. */ bool addJob(bool preview = false); /** * @brief removeJob Remove a job sequence from the queue * @param index Row index for job to remove, if left as -1 (default), the currently selected row will be removed. * if no row is selected, the last job shall be removed. */ void removeJob(int index = -1); void removeJobFromQueue(); /** * @brief moveJobUp Move the job in the sequence queue one place up. */ void moveJobUp(); /** * @brief moveJobDown Move the job in the sequence queue one place down. */ void moveJobDown(); /** * @brief setGuideDeviation Set the guiding deviation as measured by the guiding module. Abort capture if deviation exceeds user value. Resume capture if capture was aborted and guiding deviations are below user value. * @param delta_ra Deviation in RA in arcsecs from the selected guide star. * @param delta_dec Deviation in DEC in arcsecs from the selected guide star. */ void setGuideDeviation(double delta_ra, double delta_dec); /** * @brief resumeCapture Resume capture after dither and/or focusing processes are complete. */ bool resumeCapture(); /** * @brief updateCCDTemperature Update CCD temperature in capture module. * @param value Temperature in celcius. */ void updateCCDTemperature(double value); /** * @brief setTemperature Set the target CCD temperature in the GUI settings. */ void setTargetTemperature(double temperature); void setForceTemperature(bool enabled) { temperatureCheck->setChecked(enabled); } /** * @brief prepareActiveJob Reset calibration state machine and prepare capture job actions. */ void prepareActiveJob(); /** * @brief preparePreCaptureActions Check if we need to update filter position or CCD temperature before starting capture process */ void preparePreCaptureActions(); void setFrameType(const QString &type) { frameTypeCombo->setCurrentText(type); } // Logs void appendLogText(const QString &); // Auto Focus void setFocusStatus(Ekos::FocusState state); void setHFR(double newHFR, int) { focusHFR = newHFR; } void setFocusTemperatureDelta(double focusTemperatureDelta); // Return TRUE if we need to run focus/autofocus. Otherwise false if not necessary bool startFocusIfRequired(); // Guide void setGuideStatus(Ekos::GuideState state); // short cut for all guiding states that indicate guiding is active bool isGuidingActive(); // Align void setAlignStatus(Ekos::AlignState state); void setAlignResults(double orientation, double ra, double de, double pixscale); // Update Mount module status void setMountStatus(ISD::Telescope::Status newState); void setGuideChip(ISD::CCDChip *chip); void setGeneratedPreviewFITS(const QString &previewFITS); // Clear Camera Configuration void clearCameraConfiguration(); // Meridian flip void meridianFlipStatusChanged(Mount::MeridianFlipStatus status); private slots: /** * @brief setDirty Set dirty bit to indicate sequence queue file was modified and needs saving. */ void setDirty(); void checkFrameType(int index); void resetFrame(); void setExposureProgress(ISD::CCDChip *tChip, double value, IPState state); void checkSeqBoundary(const QString &path); void saveFITSDirectory(); void setDefaultCCD(QString ccd); void setDefaultFilterWheel(QString filterWheel); void setNewRemoteFile(QString file); // Sequence Queue void loadSequenceQueue(); void saveSequenceQueue(); void saveSequenceQueueAs(); // Jobs void resetJobs(); void selectJob(QModelIndex i); void editJob(QModelIndex i); void resetJobEdit(); void executeJob(); // AutoGuide void checkGuideDeviationTimeout(); // Auto Focus // Timed refocus void startRefocusEveryNTimer() { startRefocusTimer(false); } void restartRefocusEveryNTimer() { startRefocusTimer(true); } int getRefocusEveryNTimerElapsedSec(); // Flat field void openCalibrationDialog(); IPState processPreCaptureCalibrationStage(); bool processPostCaptureCalibrationStage(); void updatePreCaptureCalibrationStatus(); // Frame Type calibration checks IPState checkLightFramePendingTasks(); IPState checkLightFrameAuxiliaryTasks(); IPState checkFlatFramePendingTasks(); IPState checkDarkFramePendingTasks(); // Send image info void sendNewImage(const QString &filename, ISD::CCDChip *myChip); // Capture bool setCaptureComplete(); // post capture script void postScriptFinished(int exitCode, QProcess::ExitStatus status); void setVideoStreamEnabled(bool enabled); // Observer void showObserverDialog(); // Active Job Prepare State void updatePrepareState(Ekos::CaptureState prepareState); // Rotator void updateRotatorNumber(INumberVectorProperty *nvp); // Cooler void setCoolerToggled(bool enabled); /** * @brief registerNewModule Register an Ekos module as it arrives via DBus * and create the appropriate DBus interface to communicate with it. * @param name of module */ void registerNewModule(const QString &name); void setDownloadProgress(); signals: Q_SCRIPTABLE void newLog(const QString &text); Q_SCRIPTABLE void meridianFlipStarted(); Q_SCRIPTABLE void meridianFlipCompleted(); Q_SCRIPTABLE void newStatus(Ekos::CaptureState status); Q_SCRIPTABLE void newSequenceImage(const QString &filename, const QString &previewFITS); void ready(); void checkFocus(double); void resetFocus(); void suspendGuiding(); void resumeGuiding(); void newImage(Ekos::SequenceJob *job); void newExposureProgress(Ekos::SequenceJob *job); void newDownloadProgress(double); void sequenceChanged(const QJsonArray &sequence); void settingsUpdated(const QJsonObject &settings); void newMeridianFlipStatus(Mount::MeridianFlipStatus status); void newMeridianFlipSetup(bool activate, double hours); void dslrInfoRequested(const QString &cameraName); void driverTimedout(const QString &deviceName); private: void setBusy(bool enable); bool resumeSequence(); bool startNextExposure(); // reset = 0 --> Do not reset // reset = 1 --> Full reset // reset = 2 --> Only update limits if needed void updateFrameProperties(int reset = 0); void prepareJob(SequenceJob *job); void syncGUIToJob(SequenceJob *job); bool processJobInfo(XMLEle *root); void processJobCompletion(); bool saveSequenceQueue(const QString &path); void constructPrefix(QString &imagePrefix); double setCurrentADU(double value); void llsq(QVector x, QVector y, double &a, double &b); // Gain // This sets and gets the custom properties target gain // it does not access the ccd gain property void setGain(double value); double getGain(); // DSLR Info void cullToDSLRLimits(); //void syncDriverToDSLRLimits(); bool isModelinDSLRInfo(const QString &model); /* Meridian Flip */ bool checkMeridianFlip(); void checkGuidingAfterFlip(); // check if a pause has been planned bool checkPausing(); // Remaining Time in seconds int getJobRemainingTime(SequenceJob *job); void resetFrameToZero(); /* Refocus */ void startRefocusTimer(bool forced = false); // If exposure timed out, let's handle it. void processCaptureTimeout(); // selection of a job void selectedJobChanged(QModelIndex current, QModelIndex previous); + // Change filter name in INDI + void editFilterName(); + /* Capture */ /** * @brief Determine the overall number of target frames with the same signature. * Assume capturing RGBLRGB, where in each sequence job there is only one frame. * For "L" the result will be 1, for "R" it will be 2 etc. * @param frame signature (typically the filter name) * @return */ int getTotalFramesCount(QString signature); double seqExpose { 0 }; int seqTotalCount; int seqCurrentCount { 0 }; int seqDelay { 0 }; int retries { 0 }; QTimer *seqTimer { nullptr }; QString seqPrefix; int nextSequenceID { 0 }; int seqFileCount { 0 }; bool isBusy { false }; bool m_isLooping { false }; // Capture timeout timer QTimer captureTimeout; uint8_t m_CaptureTimeoutCounter { 0 }; uint8_t m_DeviceRestartCounter { 0 }; bool useGuideHead { false }; bool autoGuideReady { false}; QString m_TargetName; QString m_ObserverName; SequenceJob *activeJob { nullptr }; QList CCDs; ISD::CCDChip *targetChip { nullptr }; ISD::CCDChip *guideChip { nullptr }; ISD::CCDChip *blobChip { nullptr }; QString blobFilename; QString m_GeneratedPreviewFITS; // They're generic GDInterface because they could be either ISD::CCD or ISD::Filter QList Filters; QList jobs; ISD::Telescope *currentTelescope { nullptr }; ISD::CCD *currentCCD { nullptr }; ISD::GDInterface *currentFilter { nullptr }; ISD::GDInterface *currentRotator { nullptr }; ISD::DustCap *currentDustCap { nullptr }; ISD::LightBox *currentLightBox { nullptr }; ISD::Dome *currentDome { nullptr }; QPointer mountInterface { nullptr }; QStringList m_LogText; QUrl m_SequenceURL; bool m_Dirty { false }; bool m_JobUnderEdit { false }; int m_CurrentFilterPosition { -1 }; QProgressIndicator *pi { nullptr }; // Guide Deviation bool m_DeviationDetected { false }; bool m_SpikeDetected { false }; bool m_FilterOverride { false }; QTimer guideDeviationTimer; // Autofocus /** * @brief updateHFRThreshold calculate new HFR threshold based on median value for current selected filter */ void updateHFRThreshold(); bool isInSequenceFocus { false }; bool m_AutoFocusReady { false }; //bool requiredAutoFocusStarted { false }; //bool firstAutoFocus { true }; double focusHFR { 0 }; // HFR value as received from the Ekos focus module QMap> HFRMap; double fileHFR { 0 }; // HFR value as loaded from the sequence file // Refocus in progress because of time forced refocus or temperature change bool isRefocus { false }; // Focus on Temperature change bool isTemperatureDeltaCheckActive { false }; double focusTemperatureDelta { 0 }; // Temperature delta as received from the Ekos focus module // Refocus every N minutes int refocusEveryNMinutesValue { 0 }; // number of minutes between forced refocus QElapsedTimer refocusEveryNTimer; // used to determine when next force refocus should occur // Meridian flip SkyPoint initialMountCoords; bool resumeAlignmentAfterFlip { false }; bool resumeGuidingAfterFlip { false }; MFStage meridianFlipStage { MF_NONE }; QString MFStageString(MFStage stage); // Flat field automation QVector ExpRaw, ADURaw; double targetADU { 0 }; double targetADUTolerance { 1000 }; ADUAlgorithm targetADUAlgorithm { ADU_LEAST_SQUARES}; SkyPoint wallCoord; bool preMountPark { false }; bool preDomePark { false }; FlatFieldDuration flatFieldDuration { DURATION_MANUAL }; FlatFieldSource flatFieldSource { SOURCE_MANUAL }; CalibrationStage calibrationStage { CAL_NONE }; CalibrationCheckType calibrationCheckType { CAL_CHECK_TASK }; bool dustCapLightEnabled { false }; bool lightBoxLightEnabled { false }; bool m_TelescopeCoveredDarkExposure { false }; bool m_TelescopeCoveredFlatExposure { false }; ISD::CCD::UploadMode rememberUploadMode { ISD::CCD::UPLOAD_CLIENT }; QUrl dirPath; // Misc bool ignoreJobProgress { true }; bool suspendGuideOnDownload { false }; QJsonArray m_SequenceArray; // State CaptureState m_State { CAPTURE_IDLE }; FocusState focusState { FOCUS_IDLE }; GuideState guideState { GUIDE_IDLE }; AlignState alignState { ALIGN_IDLE }; FilterState filterManagerState { FILTER_IDLE }; PauseFunctionPointer pauseFunction; // CCD Chip frame settings QMap frameSettings; // Post capture script QProcess postCaptureScript; // Rotator Settings std::unique_ptr rotatorSettings; // How many images to capture before dithering operation is executed? uint8_t ditherCounter { 0 }; uint8_t inSequenceFocusCounter { 0 }; std::unique_ptr customPropertiesDialog; void createDSLRDialog(); std::unique_ptr dslrInfoDialog; // Filter Manager QSharedPointer filterManager; // DSLR Infos QList> DSLRInfos; // Captured Frames Map SchedulerJob::CapturedFramesMap capturedFramesMap; // Execute the meridian flip void setMeridianFlipStage(MFStage stage); void processFlipCompleted(); // Controls QPointer ISOCombo; QPointer GainSpin; double GainSpinSpecialValue; QList downloadTimes; QTime downloadTimer; QTimer downloadProgressTimer; }; } diff --git a/kstars/ekos/capture/capture.ui b/kstars/ekos/capture/capture.ui index d78607cc9..a1211b3ef 100644 --- a/kstars/ekos/capture/capture.ui +++ b/kstars/ekos/capture/capture.ui @@ -1,2183 +1,2248 @@ Capture 0 0 - 643 - 559 + 648 + 569 - + - 2 + 3 - 2 + 3 - 2 + 3 - 2 + 3 - 2 + 3 - + - 2 + 3 true 0 0 16777215 16777215 CCD && Filter Wheel - 2 + 3 - 2 + 3 - 2 + 3 - 2 + 3 - 2 + 3 - - - - - - - - - - Frame: - - - - - - - - - - - - - X: - - - - - - - - - - - - - H: - - - - - - - - 0 - 0 - - - - Horizontal binning - - - 1 - - - 10 - - - 1 - - - - - - - - - - - - - V: - - - - - - - - 0 - 0 - - - - - - - - Number of images to capture - - - - - - Count: - - - - - - - - 0 - 0 - - - - - - - - Custom Properties... + + + + 2 - + + + + false + + + + 0 + 0 + + + + + 0 + 32 + + + + Desired CCD temperature + + + + + + + + 0 + 0 + + + + + 0 + 32 + + + + + + + + Cooler: + + + + + + + false + + + + 32 + 32 + + + + Set CCD temperature + + + + + + + .. + + + + 24 + 24 + + + + + + + + false + + + + 32 + 32 + + + + Filter Settings + + + + + + + + + + .. + + + + 24 + 24 + + + + + + + + 2 + + + + + false + + + + 32 + 32 + + + + Turn cooler on + + + QPushButton:checked +{ +background-color: maroon; +border: 1px outset; +font-weight:bold; +} + + + On + + + true + + + + + + + false + + + + 32 + 32 + + + + Turn cooler off + + + QPushButton:checked +{ +background-color: maroon; +border: 1px outset; +font-weight:bold; +} + + + Off + + + true + + + + + + + <html><head/><body><p>Enforce temperature value before capturing an image</p></body></html> + + + Tº + + + false + + + + + + + + + + + + + + + CCD: + + + + + + + + 0 + 32 + + + + + + + + Filter Wheel + + + + + + FW: + + + + + + + + 32 + 32 + + + + Clear camera configuration + + + + + + + .. + + + + 24 + 24 + + + + + + + + + 0 + 32 + + + + Current CCD temperature + + + + + + + - - - - Image Transfer Format + + + + 2 - - FITS - + + + <b>Capture Settings</b> + + - - Native - + + + + 0 + 0 + + + + Qt::Horizontal + + - - - - - - ISO: - - - Qt::AlignLeading|Qt::AlignLeft|Qt::AlignVCenter - - - - - - - - 0 - 0 - - - - 99 - - - - - - - - 0 - 0 - - - - - - - - Horizontal and Vertical binning - - - - - - Binning: - - - - - - - - 0 - 0 - - - - - 0 - 0 - - - - - 16777215 - 16777215 - - - - Reset CCD frame & size values to default values - - - Reset - - - - 16 - 16 - - - - - - - - - - - - - - Size: - - - - - - - - 0 - 0 - - - + - - + + - + Set the exposure time in seconds for individual images, if applicable - Y: - - - - - - - - 0 - 0 - + Exposure: - - + + 0 0 - - 3600 - - - - - - - Format: + + 3 - - - - - - + + 0.001000000000000 - - + + 3600.000000000000000 - - W: + + 1.000000000000000 - - + + - H: + Filter: - - + + 2 - - - false - + - + 0 0 + + false + + + + + + + false + - 22 - 22 + 24 + 24 - 16777215 - 16777215 + 24 + 24 - Dark & Flat frames automatic calibration options + Edit filter names - Calibration + + + + - 16 - 16 + 24 + 24 - - - - false - - - Rotator Control - - - Rotator - - - - - + + - Set the exposure time in seconds for individual images, if applicable + Number of images to capture - Exposure: - - - - - - - - 0 - 0 - - - - 3 - - - 0.001000000000000 - - - 3600.000000000000000 - - - 1.000000000000000 + Count: - + 0 0 1 100000 1 - + Delay in seconds between consecutive images Delay: - - - - 2 + + + + + 0 + 0 + + + + 3600 + + + + + + + Format: + + + + + + + Image Transfer Format - - - <b>Capture Settings</b> - - + + FITS + - - - - 0 - 0 - - - - Qt::Horizontal - - + + Native + - + + + + + + ISO: + + + Qt::AlignLeading|Qt::AlignLeft|Qt::AlignVCenter + + Type: - - + + + + + 0 + 0 + + + + + + + + Custom Properties... + + + + + - Filter: + Frame: - - - - - 0 - 0 - - + + - Vertical binning + - - 1 + + - - 10 + + Size: - - 1 + + + + + + Horizontal and Vertical binning + + + + + + Binning: - + 2 <b>File Settings</b> 0 0 Qt::Horizontal - + 2 When storing images on remote devices, specify the directory where captured images are saved to. Remote: Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter false /home/pi Post-capture script to be executed after an image is captured. The capture sequence is resumed when the script is executed successfully. Script: true 0 0 Locally Remotely Both Local directory to save sequence images Directory: Target true <p>Prefix is used to generate file names ( e.g. M42_HA_240_secs_001.fits)</p> <ul> <li><strong>Target</strong>: Target Name (e.g. M42)</li> <li><strong>Filter</strong>: When using a filter wheel, the filter name is appended to the file name (e.g. HA)</li> <li><strong>Duration</strong>: Capture duration is appended to the file name (e.g. 240_secs)</li> <li><strong>TS</strong>: ISO 8601 timestamp is appended to the file name (e.g. 2016-10-20T22:42:10)</li> </ul> Prefix: <html><head/><body><p>Select how captured images are uploaded:</p><ol style="margin-top: 0px; margin-bottom: 0px; margin-left: 0px; margin-right: 0px; -qt-list-indent: 1;"><li style=" margin-top:12px; margin-bottom:0px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px;"><span style=" font-weight:600;">Locally</span>: Captured images are saved locally on disk in the directory specified above.</li><li style=" margin-top:0px; margin-bottom:0px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px;"><span style=" font-weight:600;">Remotely</span>: When connecting to a remote device, select this option to save images on the remote device only. No images are uploaded to Ekos.</li><li style=" margin-top:0px; margin-bottom:12px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px;"><span style=" font-weight:600;">Both</span>: Captured images are saved on both the remote computer <span style=" font-weight:600;">and</span> on the local disk as well.</li></ol><p>When selecting <span style=" font-style:italic;">Remotely</span> or <span style=" font-style:italic;">Both</span>, you must specify the remote directory where the remote images are saved to. By default, all captured images are saved <span style=" font-style:italic;">Locally</span>.</p></body></html> Save: 1 Append the active filter slot to the prefix Filter Append the expose duration to the prefix Duration Append time stamp to the prefix TS false 0 0 32 32 32 32 - - + + 2 - - + + false - + 0 0 - 0 - 32 + 22 + 22 + + + + + 16777215 + 16777215 + + + + Dark & Flat frames automatic calibration options + + + Calibration + + + + 16 + 16 - - Desired CCD temperature + + + + + + false + + + Rotator Control + + + Rotator + + + + + + + + + + 0 + 0 + + + + + 0 + 0 + + + + + 16777215 + 16777215 + + + + Reset CCD frame & size values to default values + + + Reset + + + + 16 + 16 + + + + + + + + 2 + + + + + + + + + + + X: - - + + - + 0 0 - - - 0 - 32 - - - - - - - - Cooler: - - - - - false - - - - 32 - 32 - - + + - Set CCD temperature - - - - - .. + + - - - 24 - 24 - + + Y: - - - - false - - - - 32 - 32 - + + + + + 0 + 0 + + + + + + + + + 2 + + + - Filter Settings - - - + - - - .. - - - - 24 - 24 - + + W: - - - - 2 + + + + + 0 + 0 + - - - - false - - - - 32 - 32 - - - - Turn cooler on - - - QPushButton:checked -{ -background-color: maroon; -border: 1px outset; -font-weight:bold; -} - - - On - - - true - - - - - - - false - - - - 32 - 32 - - - - Turn cooler off - - - QPushButton:checked -{ -background-color: maroon; -border: 1px outset; -font-weight:bold; -} - - - Off - - - true - - - - - - - <html><head/><body><p>Enforce temperature value before capturing an image</p></body></html> - - - Tº - - - false - - - - + + 99 + + - - + + - CCD: + H: - - - - - 0 - 32 - + + + + + 0 + 0 + - - + + + + + + 2 + + + - Filter Wheel + - FW: + H: - - - - - 32 - 32 - + + + + + 0 + 0 + - Clear camera configuration + Horizontal binning - + + 1 + + + 10 + + + 1 + + + + + + - - - .. + + - - - 24 - 24 - + + V: - - - - - 0 - 32 - + + + + + 0 + 0 + - Current CCD temperature + Vertical binning - - + + 1 + + + 10 + + + 1 Limit Settings 1 1 1 1 1 Number of images to capture pixels true 3 0.000000000000000 10.000000000000000 0.100000000000000 0.000000000000000 true 3 0.100000000000000 10.000000000000000 0.100000000000000 1.000000000000000 true Refocus every true 2 30.000000000000000 0.500000000000000 true 1 999 60 true Perform autofocusing once Half-Flux-Radius (HFR) value exceeds this limit Autofocus if HFR > true <html><head/><body><p>Perform autofocus when the change in temperature since last focus exceeded this limit</p></body></html> Autofocus if T° Delta > Number of images to capture " true Abort sequence if guiding deviation exceed this value Guiding Deviation < false minutes Qt::Vertical 20 1 °C - + 2 1 0 0 Sequence Queue 1 2 2 2 2 1 0 0 32 32 32 32 queueEditButtonGroup false 0 0 32 32 32 32 queueEditButtonGroup Qt::Horizontal QSizePolicy::Fixed 20 20 false 0 0 32 32 32 32 queueEditButtonGroup false 0 0 32 32 32 32 queueEditButtonGroup false 0 0 32 32 32 32 Reset status of all jobs queueEditButtonGroup 32 32 32 32 Select Observer Qt::Horizontal 40 20 0 0 32 32 32 32 Load capture sequence from file queueEditButtonGroup false 0 0 32 32 32 32 Save capture sequence queueEditButtonGroup false 0 0 32 32 32 32 Save capture sequence as ... queueEditButtonGroup 0 0 Qt::ScrollBarAsNeeded QAbstractItemView::SelectRows 30 80 Status Filter Type Bin Exp ISO/Gain Count 1 32 32 32 32 Capture a preview .. 28 28 true true Start framing (looping) .. 28 28 true 32 32 32 32 Live Video .. 28 28 true Qt::Horizontal QSizePolicy::Expanding 18 21 0 0 32 32 32 32 Start Sequence false 0 0 32 32 32 32 Pause Sequence true true 2 FITS Viewer 2 2 2 2 2 0 0 Perform automatic dark subtraction in preview mode Auto Dark false 2 Apply effect to image after capture to enhance it Effects: 0 0 -- Progress 2 2 2 2 2 1 2 Expose: 50 0 QFrame::Box 0 0 85 0 85 16777215 second left Progress: 50 0 QFrame::Box of Qt::AlignCenter 50 0 QFrame::Box completed Qt::Horizontal QSizePolicy::Expanding 51 20 false NonLinearDoubleSpinBox QDoubleSpinBox
auxiliary/nonlineardoublespinbox.h
CCDCaptureCombo FilterDevicesCombo filterManagerB temperatureCheck setTemperatureB exposureIN FilterPosCombo countIN delayIN frameXIN frameYIN resetFrameB frameWIN frameHIN binXIN binYIN prefixIN filterCheck expDurationCheck ISOCheck postCaptureScriptIN fitsDir selectFITSDirB uploadModeCombo remoteDirIN addToQueueB removeFromQueueB queueUpB queueDownB resetB queueLoadB queueSaveB queueSaveAsB queueTable previewB liveVideoB startB pauseB guideDeviationCheck guideDeviation autofocusCheck HFRPixels darkSubCheck filterCombo false
diff --git a/kstars/indi/indicommon.h b/kstars/indi/indicommon.h index 2d47e213a..23a535a34 100644 --- a/kstars/indi/indicommon.h +++ b/kstars/indi/indicommon.h @@ -1,213 +1,214 @@ /* INDI Common Defs Copyright (C) 2012 Jasem Mutlaq This application is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version. */ #ifndef INDICOMMON_H #define INDICOMMON_H #include #include /*! \page INDI "INDI Overview" \tableofcontents \section Intro Introduction INDI is a simple XML-like communications protocol described for interactive and automated remote control of diverse instrumentation. INDI is small, easy to parse, and stateless. The main key concept in INDI is that devices have the ability to describe themselves. This is accomplished by using XML to describe a generic hierarchy that can represent both canonical and non-canonical devices. All devices may contain one or more properties. A property in the INDI paradigm describes a specific function of the driver. Any property may contain one or more elements. There are four types of INDI properties:
  • Text property: Property to transfer simple text information in ISO-8859-1. The text is not encoded or encrypted on transfer. If the text includes elements that can break XML syntax, a BLOB property should be used to make the transfer.
  • Number property: Property to transfer numeric information with configurable minimum, maximum, and step values. The supported number formats are decimal and sexigesmal. The property includes a GUI hint element in printf style format to enable clients to properly display numeric properties.
  • Switch property: Property to hold a group of options or selections (Represented in GUI by buttons and check boxes).
  • Light property: Property to hold a group of status indicators (Represented in GUI by colored LEDs).
  • BLOB property: BLOB is a Binary Large OBject used to transfer binary data to and from drivers.
For example, all INDI devices share the CONNECTION standard switch property. The CONNECTION property has two elements: CONNECT and DISCONNECT switches. GUI clients parse the generic XML description of properties and builds a GUI representation suitable for direct human interaction. KStars is a fully featured INDI client. INDI Data (i.e. devices and properties) and GUI (how they appear on the screen) are separated. INDI adopts is a server/client architecture where one or more servers communicate with one or more drivers, each driver can communicate with one or more actual hardware devices. Clients connect to the server, obtain a list of devices, and generate GUI to represent the devices for user interaction and control. The creation of GUI happens in run time via introspection. \section Structure Hierarchy The following is a description of some of the various classes that support INDI:
  • DriverManager: Manages local drivers as installed by INDI library. Enables users to start/stop local INDI servers (ServerManager) running one or more drivers (DriverInfo). Also enables connecting to local or remote INDI servers. For both local and remote connection, it creates a ClientManager to handle incoming data from each INDI server started, and creates a GUIManager to render the devices in INDI Control Panel. The user may also choose to only start an INDI server without a client manager and GUI.
  • ClientManager: Manages sending and receiving data from and to an INDI server. The ClientManager sends notifications (signals) when a new device or property is defined, updated, or deleted.
  • GUIManager: Handles creation of GUI interface for devices (INDI_D) and their properties and updates the interface in accord with any data emitted by the associated ClientManager. The GUI manager supports multiple ClientManagers and consolidate all devices from all the ClientManagers into a single INDI Control Panel where each device is created as a tab.
  • INDIListener: Once a ClientManager is created in DriverManager after successfully connecting to an INDI server, it is added to INDIListener where it monitors any new devices and if a new device is detected, it creates an ISD::GenericDevice to hold the data of the device. It also monitors new properties and registers them. If it detects an INDI standard property associated with a particular device family (e.g. Property EQUATORIAL_EOD_COORD is a standard property of a Telescope device), then it extends the ISD::GenericDevice to the particular specialized device type using the Decorator Pattern. All updated properties from INDI server are delegated to their respective devices.
  • ServerManager
  • Manages establishment and shutdown of local INDI servers.
  • DriverInfo
  • : Simple class that holds information on INDI drivers such as name, version, device type..etc.
  • ISD::GDInterface: Abstract class where the ISD::DeviceDecorator and ISD::GenericDevice are derived.
  • ISD::DeviceDecorator: Base class of all specialized decorators such as ISD::Telescope, ISD::Filter, and ISD::CCD devices.
  • ISD::GenericDevice: Base class for all INDI devices. It implements processes shared across all INDI devices such as handling connection/disconnection, setting of configuration..etc.. When a specialized decorator is created (e.g. ISD::Telescope), the decorator is passed an ISD::GenericDevice pointer. Since ISD::Telescope is a child of ISD::DeviceDecorator, it can override some or all the device specific functions as defined in ISD::GDInterface (e.g. ProcessNumber(INumberVectorProperty *nvp)). For any function that is not overridden, the parent ISD::DeviceDecorator will invoke the function in ISD::GenericDevice instead. Moreover, The specialized decorator device can explicitly call DeviceDecorator::ProcessNumber(INumberVectorProperty *nvp) for example to cause the ISD::DeviceDecorator to invoke the same function but as defined in ISD::GenericDevice.
\section Example Example Suppose the user wishes to control an EQMod mount with \e indi_eqmod_telescope driver. From the DriverManager GUI, the user selects the driver and \e starts INDI services. The DriverManager will create a ServerManager instance to run an INDI server with the EQMod driver executable. After establishing the server, the DriverManager will create an instance of ClientManager and set its INDI server address as the host and port of the server created in ServerManager. For local servers, the host name is \e localhost and the default INDI port is 7624. If connection to the INDI server is successful, DriverManager then adds the ClientManager to both INDIListener (to handle data), and GUIManager (to handle GUI). Since the ClientManager emits signals whenever a new, updated, or deleted device/property is received from INDI server, it affects both the data handling part as well as GUI rendering. INDIListener holds a list of all INDI devices in KStars regardless of their origin. Once INDIListener receives a new device from the ClientManager, it creates a new ISD::GenericDevice. At the GUIManager side, it creates INDI Control Panel and adds a new tab with the device name. It also creates a separate tab for each property group received. The user is presented with a basic GUI to set the connection port of EQMod and to connect/disconnect to/from the telescope. If the user clicks connect, the status of the connection property is updated, and INDI_P sends the new switch status (CONNECT=ON) to INDI server via the ClientManager. If the connection is successful at the driver side, it will start defining new properties to cover the complete functionality of the EQMod driver, one of the standard properties is EQUATORIAL_EOD_COORD which will be detected in INDIListener. Upon detection of this key signature property, INDIListener creates a new ISD::Telescope device while passing to it the ISD::GenericDevice instance created earlier. Now suppose an updated Number property arrives from INDI server, the ClientManager emits a signal indicating a number property has a new updated value and INDIListener delegates the INDI Number property to the device, which is now of type ISD::Telescope. The ISD::Telescope overridden the processNumber(INumberVectorProperty *nvp) function in ISD::DeviceDecorator because it wants to handle some telescope specific numbers such as EQUATORIAL_EOD_COORD in order to move the telescope marker on the sky map as it changes. If the received property was indeed EQUATORIAL_EOD_COORD or any property handled by the ISD::Telescope ProcessNumber() function, then there is no further action needed. But what if the property is not processed in ISD::Telescope ProcessNumber() function? In this case, the ProcessNumber() function simply calls DeviceDecorator::ProcessNumber() and it will delgate the call to ISD::GenericDevice ProcessNumber() to process. This is how the Decorator pattern work, the decorator classes implements extended functionality, but the basic class is still responsible for handling all of the basic functions. */ #define INDIVERSION 1.7 /* we support this or less */ typedef enum { PRIMARY_XML, THIRD_PARTY_XML, EM_XML, HOST_SOURCE, CUSTOM_SOURCE, GENERATED_SOURCE } DriverSource; typedef enum { SERVER_CLIENT, SERVER_ONLY } ServerMode; typedef enum { DATA_FITS, DATA_VIDEO, DATA_CCDPREVIEW, DATA_ASCII, DATA_OTHER } INDIDataTypes; typedef enum { LOAD_LAST_CONFIG, SAVE_CONFIG, LOAD_DEFAULT_CONFIG, PURGE_CONFIG } INDIConfig; typedef enum { NO_DIR = 0, RA_INC_DIR, RA_DEC_DIR, DEC_INC_DIR, DEC_DEC_DIR } GuideDirection; /* GUI layout */ #define PROPERTY_LABEL_WIDTH 100 #define PROPERTY_LABEL_HEIGHT 30 #define ELEMENT_LABEL_WIDTH 175 #define ELEMENT_LABEL_HEIGHT 30 #define ELEMENT_READ_WIDTH 175 #define ELEMENT_WRITE_WIDTH 175 #define ELEMENT_FULL_WIDTH 340 #define MIN_SET_WIDTH 50 #define MAX_SET_WIDTH 110 #define MED_INDI_FONT 2 #define MAX_LABEL_LENGTH 20 typedef enum { PG_NONE = 0, PG_TEXT, PG_NUMERIC, PG_BUTTONS, PG_RADIO, PG_MENU, PG_LIGHTS, PG_BLOB } PGui; /* new versions of glibc define TIME_UTC as a macro */ #undef TIME_UTC /* INDI std properties */ enum stdProperties { CONNECTION, DEVICE_PORT, TIME_UTC, TIME_LST, TIME_UTC_OFFSET, GEOGRAPHIC_COORD, /* General */ EQUATORIAL_COORD, EQUATORIAL_EOD_COORD, EQUATORIAL_EOD_COORD_REQUEST, HORIZONTAL_COORD, /* Telescope */ TELESCOPE_ABORT_MOTION, ON_COORD_SET, SOLAR_SYSTEM, TELESCOPE_MOTION_NS, /* Telescope */ TELESCOPE_MOTION_WE, TELESCOPE_PARK, /* Telescope */ CCD_EXPOSURE, CCD_TEMPERATURE_REQUEST, CCD_FRAME, /* CCD */ CCD_FRAME_TYPE, CCD_BINNING, CCD_INFO, CCD_VIDEO_STREAM, /* Video */ FOCUS_SPEED, FOCUS_MOTION, FOCUS_TIMER, /* Focuser */ FILTER_SLOT, /* Filter */ WATCHDOG_HEARTBEAT }; /* Watchdog */ /* Devices families that we explicitly support (i.e. with std properties) */ typedef enum { KSTARS_ADAPTIVE_OPTICS, KSTARS_AGENT, KSTARS_AUXILIARY, KSTARS_CCD, KSTARS_DETECTORS, KSTARS_DOME, KSTARS_FILTER, KSTARS_FOCUSER, KSTARS_ROTATOR, KSTARS_SPECTROGRAPHS, KSTARS_TELESCOPE, KSTARS_WEATHER, KSTARS_UNKNOWN } DeviceFamily; const QMap DeviceFamilyLabels = { {KSTARS_ADAPTIVE_OPTICS, "Adaptive Optics"}, {KSTARS_AGENT, "Agent"}, {KSTARS_AUXILIARY, "Auxiliary"}, {KSTARS_CCD, "CCDs"}, {KSTARS_DETECTORS, "Detectors"}, {KSTARS_DOME, "Domes"}, {KSTARS_FILTER, "Filter Wheels"}, {KSTARS_FOCUSER, "Focusers"}, {KSTARS_ROTATOR, "Rotators"}, {KSTARS_SPECTROGRAPHS, "Spectrographs"}, {KSTARS_TELESCOPE, "Telescopes"}, {KSTARS_WEATHER, "Weather"}, {KSTARS_UNKNOWN, "Unknown"}, }; typedef enum { FRAME_LIGHT, FRAME_BIAS, FRAME_DARK, FRAME_FLAT } CCDFrameType; typedef enum { SINGLE_BIN, DOUBLE_BIN, TRIPLE_BIN, QUADRAPLE_BIN } CCDBinType; typedef enum { INDI_SEND_COORDS, INDI_FIND_TELESCOPE, INDI_CENTER_LOCK, INDI_CENTER_UNLOCK, INDI_CUSTOM_PARKING, INDI_SET_PORT, INDI_CONNECT, INDI_DISCONNECT, INDI_SET_FILTER, + INDI_SET_FILTER_NAMES, INDI_CONFIRM_FILTER, INDI_SET_ROTATOR_TICKS, INDI_SET_ROTATOR_ANGLE } DeviceCommand; typedef enum { SOURCE_MANUAL, SOURCE_FLATCAP, SOURCE_WALL, SOURCE_DAWN_DUSK, SOURCE_DARKCAP } FlatFieldSource; typedef enum { DURATION_MANUAL, DURATION_ADU } FlatFieldDuration; #endif // INDICOMMON_H diff --git a/kstars/indi/indistd.cpp b/kstars/indi/indistd.cpp index 2569946c6..106cdb75a 100644 --- a/kstars/indi/indistd.cpp +++ b/kstars/indi/indistd.cpp @@ -1,1357 +1,1379 @@ /* INDI STD Copyright (C) 2012 Jasem Mutlaq (mutlaqja@ikarustech.com) This application is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version. Handle INDI Standard properties. */ #include "indistd.h" #include "clientmanager.h" #include "driverinfo.h" #include "deviceinfo.h" #include "imageviewer.h" #include "indi_debug.h" #include "kstars.h" #include "kstarsdata.h" #include "Options.h" #include "skymap.h" #include #include namespace ISD { GDSetCommand::GDSetCommand(INDI_PROPERTY_TYPE inPropertyType, const QString &inProperty, const QString &inElement, QVariant qValue, QObject *parent) : QObject(parent) { propType = inPropertyType; indiProperty = inProperty; indiElement = inElement; elementValue = qValue; } GenericDevice::GenericDevice(DeviceInfo &idv) { deviceInfo = &idv; driverInfo = idv.getDriverInfo(); baseDevice = idv.getBaseDevice(); m_Name = baseDevice->getDeviceName(); clientManager = driverInfo->getClientManager(); dType = KSTARS_UNKNOWN; registerDBusType(); } void GenericDevice::registerDBusType() { #ifndef KSTARS_LITE static bool isRegistered = false; if (isRegistered == false) { qRegisterMetaType("ISD::ParkStatus"); qDBusRegisterMetaType(); isRegistered = true; } #endif } const QString &GenericDevice::getDeviceName() const { return m_Name; } void GenericDevice::registerProperty(INDI::Property *prop) { if (!prop->getRegistered()) return; const QString name = prop->getName(); if (properties.contains(name)) return; properties[name] = prop; emit propertyDefined(prop); // In case driver already started if (name == "CONNECTION") { ISwitchVectorProperty *svp = prop->getSwitch(); if (svp == nullptr) return; // Still connecting/disconnecting... if (svp->s == IPS_BUSY) return; ISwitch *conSP = IUFindSwitch(svp, "CONNECT"); if (conSP == nullptr) return; if (svp->s == IPS_OK && conSP->s == ISS_ON) { connected = true; emit Connected(); createDeviceInit(); } } else if (name == "DRIVER_INFO") { ITextVectorProperty *tvp = prop->getText(); if (tvp) { IText *tp = IUFindText(tvp, "DRIVER_INTERFACE"); if (tp) { driverInterface = static_cast(atoi(tp->text)); emit interfaceDefined(); } tp = IUFindText(tvp, "DRIVER_VERSION"); if (tp) { driverVersion = QString(tp->text); } } } else if (name == "SYSTEM_PORTS") { // Check if our current port is set to one of the system ports. This indicates that the port // is not mapped yet to a permenant designation ISwitchVectorProperty *svp = prop->getSwitch(); ITextVectorProperty *port = baseDevice->getText("DEVICE_PORT"); if (svp && port) { for (int i = 0; i < svp->nsp; i++) { if (!strcmp(port->tp[0].text, svp->sp[i].name)) { emit systemPortDetected(); break; } } } } else if (name == "TIME_UTC" && Options::useTimeUpdate() && Options::useKStarsSource()) { ITextVectorProperty *tvp = prop->getText(); if (tvp && tvp->p != IP_RO) updateTime(); } else if (name == "GEOGRAPHIC_COORD" && Options::useGeographicUpdate() && Options::useKStarsSource()) { INumberVectorProperty *nvp = prop->getNumber(); if (nvp && nvp->p != IP_RO) updateLocation(); } else if (name == "WATCHDOG_HEARTBEAT") { INumberVectorProperty *nvp = prop->getNumber(); if (nvp) { if (watchDogTimer == nullptr) { watchDogTimer = new QTimer(this); connect(watchDogTimer, SIGNAL(timeout()), this, SLOT(resetWatchdog())); } if (connected && nvp->np[0].value > 0) { // Send immediately a heart beat clientManager->sendNewNumber(nvp); //watchDogTimer->start(0); } } } } void GenericDevice::removeProperty(const QString &name) { properties.remove(name); emit propertyDeleted(name); } void GenericDevice::processSwitch(ISwitchVectorProperty *svp) { if (!strcmp(svp->name, "CONNECTION")) { ISwitch *conSP = IUFindSwitch(svp, "CONNECT"); if (conSP == nullptr) return; // Still connecting/disconnecting... if (svp->s == IPS_BUSY) return; if (svp->s == IPS_OK && conSP->s == ISS_ON) { connected = true; emit Connected(); createDeviceInit(); if (watchDogTimer != nullptr) { INumberVectorProperty *nvp = baseDevice->getNumber("WATCHDOG_HEARTBEAT"); if (nvp && nvp->np[0].value > 0) { // Send immediately clientManager->sendNewNumber(nvp); //watchDogTimer->start(0); } } } else { connected = false; emit Disconnected(); } } emit switchUpdated(svp); } void GenericDevice::processNumber(INumberVectorProperty *nvp) { QString deviceName = getDeviceName(); // uint32_t interface = getDriverInterface(); // Q_UNUSED(interface); if (!strcmp(nvp->name, "GEOGRAPHIC_COORD") && nvp->s == IPS_OK && ( (Options::useMountSource() && (getDriverInterface() & INDI::BaseDevice::TELESCOPE_INTERFACE)) || (Options::useGPSSource() && (getDriverInterface() & INDI::BaseDevice::GPS_INTERFACE)))) { // Update KStars Location once we receive update from INDI, if the source is set to DEVICE dms lng, lat; double elev = 0; INumber *np = IUFindNumber(nvp, "LONG"); if (!np) return; // INDI Longitude convention is 0 to 360. We need to turn it back into 0 to 180 EAST, 0 to -180 WEST if (np->value < 180) lng.setD(np->value); else lng.setD(np->value - 360.0); np = IUFindNumber(nvp, "LAT"); if (!np) return; lat.setD(np->value); // Double check we have valid values if (lng.Degrees() == 0 && lat.Degrees() == 0) { qCWarning(KSTARS_INDI) << "Ignoring invalid device coordinates."; return; } np = IUFindNumber(nvp, "ELEV"); if (np) elev = np->value; GeoLocation *geo = KStars::Instance()->data()->geo(); std::unique_ptr tempGeo; QString newLocationName; if (getDriverInterface() & INDI::BaseDevice::GPS_INTERFACE) newLocationName = i18n("GPS Location"); else newLocationName = i18n("Mount Location"); if (geo->name() != newLocationName) { double TZ0 = geo->TZ0(); TimeZoneRule *rule = geo->tzrule(); tempGeo.reset(new GeoLocation(lng, lat, newLocationName, "", "", TZ0, rule, elev)); geo = tempGeo.get(); } else { geo->setLong(lng); geo->setLat(lat); } qCInfo(KSTARS_INDI) << "Setting location from device:" << deviceName << "Longitude:" << lng.toDMSString() << "Latitude:" << lat.toDMSString(); KStars::Instance()->data()->setLocation(*geo); } else if (!strcmp(nvp->name, "WATCHDOG_HEARTBEAT")) { if (watchDogTimer == nullptr) { watchDogTimer = new QTimer(this); connect(watchDogTimer, SIGNAL(timeout()), this, SLOT(resetWatchdog())); } if (connected && nvp->np[0].value > 0) { // Reset timer 15 seconds before it is due watchDogTimer->start(nvp->np[0].value * 60 * 1000 - 15 * 1000); } else if (nvp->np[0].value == 0) watchDogTimer->stop(); } emit numberUpdated(nvp); } void GenericDevice::processText(ITextVectorProperty *tvp) { // Update KStars time once we receive update from INDI, if the source is set to DEVICE if (!strcmp(tvp->name, "TIME_UTC") && tvp->s == IPS_OK && ( (Options::useMountSource() && (getDriverInterface() & INDI::BaseDevice::TELESCOPE_INTERFACE)) || (Options::useGPSSource() && (getDriverInterface() & INDI::BaseDevice::GPS_INTERFACE)))) { int d, m, y, min, sec, hour; float utcOffset; QDate indiDate; QTime indiTime; IText *tp = IUFindText(tvp, "UTC"); if (!tp) { qCWarning(KSTARS_INDI) << "UTC property missing from TIME_UTC"; return; } sscanf(tp->text, "%d%*[^0-9]%d%*[^0-9]%dT%d%*[^0-9]%d%*[^0-9]%d", &y, &m, &d, &hour, &min, &sec); indiDate.setDate(y, m, d); indiTime.setHMS(hour, min, sec); KStarsDateTime indiDateTime(QDateTime(indiDate, indiTime, Qt::UTC)); tp = IUFindText(tvp, "OFFSET"); if (!tp) { qCWarning(KSTARS_INDI) << "Offset property missing from TIME_UTC"; return; } sscanf(tp->text, "%f", &utcOffset); qCInfo(KSTARS_INDI) << "Setting UTC time from device:" << getDeviceName() << indiDateTime.toString(); KStars::Instance()->data()->changeDateTime(indiDateTime); KStars::Instance()->data()->syncLST(); GeoLocation *geo = KStars::Instance()->data()->geo(); if (geo->tzrule()) utcOffset -= geo->tzrule()->deltaTZ(); // TZ0 is the timezone WTIHOUT any DST offsets. Above, we take INDI UTC Offset (with DST already included) // and subtract from it the deltaTZ from the current TZ rule. geo->setTZ0(utcOffset); } emit textUpdated(tvp); } void GenericDevice::processLight(ILightVectorProperty *lvp) { emit lightUpdated(lvp); } void GenericDevice::processMessage(int messageID) { emit messageUpdated(messageID); } void GenericDevice::processBLOB(IBLOB *bp) { // Ignore write-only BLOBs since we only receive it for state-change if (bp->bvp->p == IP_WO) return; QFile *data_file = nullptr; INDIDataTypes dataType; if (!strcmp(bp->format, ".ascii")) dataType = DATA_ASCII; else dataType = DATA_OTHER; QString currentDir = Options::fitsDir(); int nr, n = 0; if (currentDir.endsWith('/')) currentDir.truncate(sizeof(currentDir) - 1); QString filename(currentDir + '/'); QString ts = QDateTime::currentDateTime().toString("yyyy-MM-ddThh-mm-ss"); filename += QString("%1_").arg(bp->label) + ts + QString(bp->format).trimmed(); strncpy(BLOBFilename, filename.toLatin1(), MAXINDIFILENAME); bp->aux2 = BLOBFilename; if (dataType == DATA_ASCII) { if (bp->aux0 == nullptr) { bp->aux0 = new int(); QFile *ascii_data_file = new QFile(); ascii_data_file->setFileName(filename); if (!ascii_data_file->open(QIODevice::WriteOnly)) { qCCritical(KSTARS_INDI) << "GenericDevice Error: Unable to open " << ascii_data_file->fileName() << endl; return; } bp->aux1 = ascii_data_file; } data_file = (QFile *)bp->aux1; QDataStream out(data_file); for (nr = 0; nr < (int)bp->size; nr += n) n = out.writeRawData(static_cast(bp->blob) + nr, bp->size - nr); out.writeRawData((const char *)"\n", 1); data_file->flush(); } else { QFile fits_temp_file(filename); if (!fits_temp_file.open(QIODevice::WriteOnly)) { qCCritical(KSTARS_INDI) << "GenericDevice Error: Unable to open " << fits_temp_file.fileName() << endl; return; } QDataStream out(&fits_temp_file); for (nr = 0; nr < (int)bp->size; nr += n) n = out.writeRawData(static_cast(bp->blob) + nr, bp->size - nr); fits_temp_file.flush(); fits_temp_file.close(); QByteArray fmt = QString(bp->format).toLower().remove('.').toUtf8(); if (QImageReader::supportedImageFormats().contains(fmt)) { QUrl url(filename); url.setScheme("file"); ImageViewer *iv = new ImageViewer(url, QString(), KStars::Instance()); if (iv) iv->show(); } } if (dataType == DATA_OTHER) KStars::Instance()->statusBar()->showMessage(i18n("Data file saved to %1", filename), 0); emit BLOBUpdated(bp); } bool GenericDevice::setConfig(INDIConfig tConfig) { ISwitchVectorProperty *svp = baseDevice->getSwitch("CONFIG_PROCESS"); if (svp == nullptr) return false; ISwitch *sp = nullptr; IUResetSwitch(svp); switch (tConfig) { case LOAD_LAST_CONFIG: sp = IUFindSwitch(svp, "CONFIG_LOAD"); if (sp == nullptr) return false; IUResetSwitch(svp); sp->s = ISS_ON; break; case SAVE_CONFIG: sp = IUFindSwitch(svp, "CONFIG_SAVE"); if (sp == nullptr) return false; IUResetSwitch(svp); sp->s = ISS_ON; break; case LOAD_DEFAULT_CONFIG: sp = IUFindSwitch(svp, "CONFIG_DEFAULT"); if (sp == nullptr) return false; IUResetSwitch(svp); sp->s = ISS_ON; break; case PURGE_CONFIG: sp = IUFindSwitch(svp, "CONFIG_PURGE"); if (sp == nullptr) return false; IUResetSwitch(svp); sp->s = ISS_ON; break; } clientManager->sendNewSwitch(svp); return true; } void GenericDevice::createDeviceInit() { if (Options::showINDIMessages()) KStars::Instance()->statusBar()->showMessage(i18n("%1 is online.", m_Name), 0); KStars::Instance()->map()->forceUpdateNow(); } /*********************************************************************************/ /* Update the Driver's Time */ /*********************************************************************************/ void GenericDevice::updateTime() { QString offset, isoTS; offset = QString().setNum(KStars::Instance()->data()->geo()->TZ(), 'g', 2); //QTime newTime( KStars::Instance()->data()->ut().time()); //QDate newDate( KStars::Instance()->data()->ut().date()); //isoTS = QString("%1-%2-%3T%4:%5:%6").arg(newDate.year()).arg(newDate.month()).arg(newDate.day()).arg(newTime.hour()).arg(newTime.minute()).arg(newTime.second()); isoTS = KStars::Instance()->data()->ut().toString(Qt::ISODate).remove('Z'); /* Update Date/Time */ ITextVectorProperty *timeUTC = baseDevice->getText("TIME_UTC"); if (timeUTC) { IText *timeEle = IUFindText(timeUTC, "UTC"); if (timeEle) IUSaveText(timeEle, isoTS.toLatin1().constData()); IText *offsetEle = IUFindText(timeUTC, "OFFSET"); if (offsetEle) IUSaveText(offsetEle, offset.toLatin1().constData()); if (timeEle && offsetEle) clientManager->sendNewText(timeUTC); } } /*********************************************************************************/ /* Update the Driver's Geographical Location */ /*********************************************************************************/ void GenericDevice::updateLocation() { GeoLocation *geo = KStars::Instance()->data()->geo(); double longNP; if (geo->lng()->Degrees() >= 0) longNP = geo->lng()->Degrees(); else longNP = dms(geo->lng()->Degrees() + 360.0).Degrees(); INumberVectorProperty *nvp = baseDevice->getNumber("GEOGRAPHIC_COORD"); if (nvp == nullptr) return; INumber *np = IUFindNumber(nvp, "LONG"); if (np == nullptr) return; np->value = longNP; np = IUFindNumber(nvp, "LAT"); if (np == nullptr) return; np->value = geo->lat()->Degrees(); np = IUFindNumber(nvp, "ELEV"); if (np == nullptr) return; np->value = geo->elevation(); clientManager->sendNewNumber(nvp); } bool GenericDevice::Connect() { return runCommand(INDI_CONNECT, nullptr); } bool GenericDevice::Disconnect() { return runCommand(INDI_DISCONNECT, nullptr); } bool GenericDevice::runCommand(int command, void *ptr) { switch (command) { case INDI_CONNECT: clientManager->connectDevice(m_Name.toLatin1().constData()); break; case INDI_DISCONNECT: clientManager->disconnectDevice(m_Name.toLatin1().constData()); break; case INDI_SET_PORT: { if (ptr == nullptr) return false; ITextVectorProperty *tvp = baseDevice->getText("DEVICE_PORT"); if (tvp == nullptr) return false; IText *tp = IUFindText(tvp, "PORT"); IUSaveText(tp, (static_cast(ptr))->toLatin1().constData()); clientManager->sendNewText(tvp); } break; // We do it here because it could be either CCD or FILTER interfaces, so no need to duplicate code case INDI_SET_FILTER: { if (ptr == nullptr) return false; INumberVectorProperty *nvp = baseDevice->getNumber("FILTER_SLOT"); if (nvp == nullptr) return false; int requestedFilter = *(static_cast(ptr)); if (requestedFilter == nvp->np[0].value) break; nvp->np[0].value = requestedFilter; clientManager->sendNewNumber(nvp); } break; + // We do it here because it could be either CCD or FILTER interfaces, so no need to duplicate code + case INDI_SET_FILTER_NAMES: + { + if (ptr == nullptr) + return false; + + ITextVectorProperty *tvp = baseDevice->getText("FILTER_NAME"); + + if (tvp == nullptr) + return false; + + QStringList *requestedFilters = static_cast(ptr); + + if (requestedFilters->count() != tvp->ntp) + return false; + + for (uint8_t i = 0; i < tvp->ntp; i++) + IUSaveText(&tvp->tp[i], requestedFilters->at(i).toLatin1().constData()); + clientManager->sendNewText(tvp); + } + break; + case INDI_CONFIRM_FILTER: { ISwitchVectorProperty *svp = baseDevice->getSwitch("CONFIRM_FILTER_SET"); if (svp == nullptr) return false; svp->sp[0].s = ISS_ON; clientManager->sendNewSwitch(svp); } break; // We do it here because it could be either FOCUSER or ROTATOR interfaces, so no need to duplicate code case INDI_SET_ROTATOR_ANGLE: { if (ptr == nullptr) return false; INumberVectorProperty *nvp = baseDevice->getNumber("ABS_ROTATOR_ANGLE"); if (nvp == nullptr) return false; double requestedAngle = *(static_cast(ptr)); if (requestedAngle == nvp->np[0].value) break; nvp->np[0].value = requestedAngle; clientManager->sendNewNumber(nvp); } break; // We do it here because it could be either FOCUSER or ROTATOR interfaces, so no need to duplicate code case INDI_SET_ROTATOR_TICKS: { if (ptr == nullptr) return false; INumberVectorProperty *nvp = baseDevice->getNumber("ABS_ROTATOR_POSITION"); if (nvp == nullptr) return false; int32_t requestedTicks = *(static_cast(ptr)); if (requestedTicks == nvp->np[0].value) break; nvp->np[0].value = requestedTicks; clientManager->sendNewNumber(nvp); } break; } return true; } bool GenericDevice::setProperty(QObject *setPropCommand) { GDSetCommand *indiCommand = static_cast(setPropCommand); //qDebug() << "We are trying to set value for property " << indiCommand->indiProperty << " and element" << indiCommand->indiElement << " and value " << indiCommand->elementValue << endl; INDI::Property *pp = baseDevice->getProperty(indiCommand->indiProperty.toLatin1().constData()); if (pp == nullptr) return false; switch (indiCommand->propType) { case INDI_SWITCH: { ISwitchVectorProperty *svp = pp->getSwitch(); if (svp == nullptr) return false; ISwitch *sp = IUFindSwitch(svp, indiCommand->indiElement.toLatin1().constData()); if (sp == nullptr) return false; if (svp->r == ISR_1OFMANY || svp->r == ISR_ATMOST1) IUResetSwitch(svp); sp->s = indiCommand->elementValue.toInt() == 0 ? ISS_OFF : ISS_ON; //qDebug() << "Sending switch " << sp->name << " with status " << ((sp->s == ISS_ON) ? "On" : "Off") << endl; clientManager->sendNewSwitch(svp); return true; } case INDI_NUMBER: { INumberVectorProperty *nvp = pp->getNumber(); if (nvp == nullptr) return false; INumber *np = IUFindNumber(nvp, indiCommand->indiElement.toLatin1().constData()); if (np == nullptr) return false; double value = indiCommand->elementValue.toDouble(); if (value == np->value) return true; np->value = value; //qDebug() << "Sending switch " << sp->name << " with status " << ((sp->s == ISS_ON) ? "On" : "Off") << endl; clientManager->sendNewNumber(nvp); } break; // TODO: Add set property for other types of properties default: break; } return true; } bool GenericDevice::getMinMaxStep(const QString &propName, const QString &elementName, double *min, double *max, double *step) { INumberVectorProperty *nvp = baseDevice->getNumber(propName.toLatin1()); if (nvp == nullptr) return false; INumber *np = IUFindNumber(nvp, elementName.toLatin1()); if (np == nullptr) return false; *min = np->min; *max = np->max; *step = np->step; return true; } IPState GenericDevice::getState(const QString &propName) { return baseDevice->getPropertyState(propName.toLatin1().constData()); } IPerm GenericDevice::getPermission(const QString &propName) { return baseDevice->getPropertyPermission(propName.toLatin1().constData()); } INDI::Property *GenericDevice::getProperty(const QString &propName) { for (auto &oneProp : properties) { if (propName == QString(oneProp->getName())) return oneProp; } return nullptr; } bool GenericDevice::setJSONProperty(const QString &propName, const QJsonArray &propValue) { for (auto &oneProp : properties) { if (propName == QString(oneProp->getName())) { switch (oneProp->getType()) { case INDI_SWITCH: { ISwitchVectorProperty *svp = oneProp->getSwitch(); if (svp->r == ISR_1OFMANY || svp->r == ISR_ATMOST1) IUResetSwitch(svp); for (auto oneElement : propValue) { QJsonObject oneElementObject = oneElement.toObject(); ISwitch *sp = IUFindSwitch(svp, oneElementObject["name"].toString().toLatin1().constData()); if (sp) { sp->s = static_cast(oneElementObject["state"].toInt()); } } clientManager->sendNewSwitch(svp); return true; } case INDI_NUMBER: { INumberVectorProperty *nvp = oneProp->getNumber(); for (auto oneElement : propValue) { QJsonObject oneElementObject = oneElement.toObject(); INumber *np = IUFindNumber(nvp, oneElementObject["name"].toString().toLatin1().constData()); if (np) np->value = oneElementObject["value"].toDouble(); } clientManager->sendNewNumber(nvp); return true; } case INDI_TEXT: { ITextVectorProperty *tvp = oneProp->getText(); for (auto oneElement : propValue) { QJsonObject oneElementObject = oneElement.toObject(); IText *tp = IUFindText(tvp, oneElementObject["name"].toString().toLatin1().constData()); if (tp) IUSaveText(tp, oneElementObject["text"].toString().toLatin1().constData()); } clientManager->sendNewText(tvp); return true; } case INDI_BLOB: // TODO break; default: break; } } } return false; } bool GenericDevice::getJSONProperty(const QString &propName, QJsonObject &propObject, bool compact) { for (auto &oneProp : properties) { if (propName == QString(oneProp->getName())) { switch (oneProp->getType()) { case INDI_SWITCH: { ISwitchVectorProperty *svp = oneProp->getSwitch(); propertyToJson(svp, propObject, compact); return true; } case INDI_NUMBER: { INumberVectorProperty *nvp = oneProp->getNumber(); propertyToJson(nvp, propObject, compact); return true; } case INDI_TEXT: { ITextVectorProperty *tvp = oneProp->getText(); propertyToJson(tvp, propObject, compact); return true; } case INDI_LIGHT: { ILightVectorProperty *lvp = oneProp->getLight(); propertyToJson(lvp, propObject, compact); return true; } case INDI_BLOB: // TODO break; default: break; } } } return false; } bool GenericDevice::getJSONBLOB(const QString &propName, const QString &elementName, QJsonObject &blobObject) { auto prop = std::find_if(properties.begin(), properties.end(), [propName](INDI::Property * oneProperty) { return (QString(oneProperty->getName()) == propName); }); if (prop == properties.end()) return false; auto blobProperty = (*prop)->getBLOB(); IBLOB *oneBLOB = IUFindBLOB(blobProperty, elementName.toLatin1().constData()); if (!oneBLOB) return false; // Now convert to base64 and send back. QByteArray data = QByteArray::fromRawData(static_cast(oneBLOB->blob), oneBLOB->bloblen); QString encoded = data.toBase64(QByteArray::Base64UrlEncoding); blobObject.insert("property", propName); blobObject.insert("element", elementName); blobObject.insert("size", encoded.size()); blobObject.insert("data", encoded); return true; } void GenericDevice::resetWatchdog() { INumberVectorProperty *nvp = baseDevice->getNumber("WATCHDOG_HEARTBEAT"); if (nvp) // Send heartbeat to driver clientManager->sendNewNumber(nvp); } DeviceDecorator::DeviceDecorator(GDInterface * iPtr) { interfacePtr = iPtr; baseDevice = interfacePtr->getBaseDevice(); clientManager = interfacePtr->getDriverInfo()->getClientManager(); connect(iPtr, &GDInterface::Connected, this, &DeviceDecorator::Connected); connect(iPtr, &GDInterface::Disconnected, this, &DeviceDecorator::Disconnected); connect(iPtr, &GDInterface::propertyDefined, this, &DeviceDecorator::propertyDefined); connect(iPtr, &GDInterface::propertyDeleted, this, &DeviceDecorator::propertyDeleted); connect(iPtr, &GDInterface::messageUpdated, this, &DeviceDecorator::messageUpdated); connect(iPtr, &GDInterface::switchUpdated, this, &DeviceDecorator::switchUpdated); connect(iPtr, &GDInterface::numberUpdated, this, &DeviceDecorator::numberUpdated); connect(iPtr, &GDInterface::textUpdated, this, &DeviceDecorator::textUpdated); connect(iPtr, &GDInterface::BLOBUpdated, this, &DeviceDecorator::BLOBUpdated); connect(iPtr, &GDInterface::lightUpdated, this, &DeviceDecorator::lightUpdated); } DeviceDecorator::~DeviceDecorator() { delete (interfacePtr); } bool DeviceDecorator::runCommand(int command, void *ptr) { return interfacePtr->runCommand(command, ptr); } bool DeviceDecorator::setProperty(QObject * setPropCommand) { return interfacePtr->setProperty(setPropCommand); } void DeviceDecorator::processBLOB(IBLOB * bp) { interfacePtr->processBLOB(bp); } void DeviceDecorator::processLight(ILightVectorProperty * lvp) { interfacePtr->processLight(lvp); } void DeviceDecorator::processNumber(INumberVectorProperty * nvp) { interfacePtr->processNumber(nvp); } void DeviceDecorator::processSwitch(ISwitchVectorProperty * svp) { interfacePtr->processSwitch(svp); } void DeviceDecorator::processText(ITextVectorProperty * tvp) { interfacePtr->processText(tvp); } void DeviceDecorator::processMessage(int messageID) { interfacePtr->processMessage(messageID); } void DeviceDecorator::registerProperty(INDI::Property * prop) { interfacePtr->registerProperty(prop); } void DeviceDecorator::removeProperty(const QString &name) { interfacePtr->removeProperty(name); } bool DeviceDecorator::setConfig(INDIConfig tConfig) { return interfacePtr->setConfig(tConfig); } DeviceFamily DeviceDecorator::getType() { return interfacePtr->getType(); } DriverInfo *DeviceDecorator::getDriverInfo() { return interfacePtr->getDriverInfo(); } DeviceInfo *DeviceDecorator::getDeviceInfo() { return interfacePtr->getDeviceInfo(); } const QString &DeviceDecorator::getDeviceName() const { return interfacePtr->getDeviceName(); } INDI::BaseDevice *DeviceDecorator::getBaseDevice() { return interfacePtr->getBaseDevice(); } uint32_t DeviceDecorator::getDriverInterface() { return interfacePtr->getDriverInterface(); } QString DeviceDecorator::getDriverVersion() { return interfacePtr->getDriverVersion(); } const QHash &DeviceDecorator::getProperties() { return interfacePtr->getProperties(); } INDI::Property *DeviceDecorator::getProperty(const QString &propName) { return interfacePtr->getProperty(propName); } bool DeviceDecorator::getJSONProperty(const QString &propName, QJsonObject &propObject, bool compact) { return interfacePtr->getJSONProperty(propName, propObject, compact); } bool DeviceDecorator::getJSONBLOB(const QString &propName, const QString &elementName, QJsonObject &blobObject) { return interfacePtr->getJSONBLOB(propName, elementName, blobObject); } bool DeviceDecorator::setJSONProperty(const QString &propName, const QJsonArray &propValue) { return interfacePtr->setJSONProperty(propName, propValue); } bool DeviceDecorator::isConnected() { return interfacePtr->isConnected(); } bool DeviceDecorator::Connect() { return interfacePtr->Connect(); } bool DeviceDecorator::Disconnect() { return interfacePtr->Disconnect(); } bool DeviceDecorator::getMinMaxStep(const QString &propName, const QString &elementName, double * min, double * max, double * step) { return interfacePtr->getMinMaxStep(propName, elementName, min, max, step); } IPState DeviceDecorator::getState(const QString &propName) { return interfacePtr->getState(propName); } IPerm DeviceDecorator::getPermission(const QString &propName) { return interfacePtr->getPermission(propName); } ST4::ST4(INDI::BaseDevice * bdv, ClientManager * cm) { baseDevice = bdv; clientManager = cm; m_Name = baseDevice->getDeviceName(); } const QString &ST4::getDeviceName() { return m_Name; } void ST4::setDECSwap(bool enable) { swapDEC = enable; } bool ST4::doPulse(GuideDirection ra_dir, int ra_msecs, GuideDirection dec_dir, int dec_msecs) { bool raOK = doPulse(ra_dir, ra_msecs); bool decOK = doPulse(dec_dir, dec_msecs); return (raOK && decOK); } bool ST4::doPulse(GuideDirection dir, int msecs) { INumberVectorProperty *raPulse = baseDevice->getNumber("TELESCOPE_TIMED_GUIDE_WE"); INumberVectorProperty *decPulse = baseDevice->getNumber("TELESCOPE_TIMED_GUIDE_NS"); INumberVectorProperty *npulse = nullptr; INumber *dirPulse = nullptr; if (raPulse == nullptr || decPulse == nullptr) return false; if (dir == RA_INC_DIR || dir == RA_DEC_DIR) raPulse->np[0].value = raPulse->np[1].value = 0; else decPulse->np[0].value = decPulse->np[1].value = 0; switch (dir) { case RA_INC_DIR: dirPulse = IUFindNumber(raPulse, "TIMED_GUIDE_W"); if (dirPulse == nullptr) return false; npulse = raPulse; break; case RA_DEC_DIR: dirPulse = IUFindNumber(raPulse, "TIMED_GUIDE_E"); if (dirPulse == nullptr) return false; npulse = raPulse; break; case DEC_INC_DIR: if (swapDEC) dirPulse = IUFindNumber(decPulse, "TIMED_GUIDE_S"); else dirPulse = IUFindNumber(decPulse, "TIMED_GUIDE_N"); if (dirPulse == nullptr) return false; npulse = decPulse; break; case DEC_DEC_DIR: if (swapDEC) dirPulse = IUFindNumber(decPulse, "TIMED_GUIDE_N"); else dirPulse = IUFindNumber(decPulse, "TIMED_GUIDE_S"); if (dirPulse == nullptr) return false; npulse = decPulse; break; default: return false; } dirPulse->value = msecs; clientManager->sendNewNumber(npulse); //qDebug() << "Sending pulse for " << npulse->name << " in direction " << dirPulse->name << " for " << msecs << " ms " << endl; return true; } void propertyToJson(ISwitchVectorProperty *svp, QJsonObject &propObject, bool compact) { QJsonArray switches; for (int i = 0; i < svp->nsp; i++) { QJsonObject oneSwitch = {{"name", svp->sp[i].name}, {"state", svp->sp[i].s}}; if (!compact) oneSwitch.insert("label", svp->sp[i].label); switches.append(oneSwitch); } propObject = {{"name", svp->name}, {"state", svp->s}, {"switches", switches}}; if (!compact) { propObject.insert("label", svp->label); propObject.insert("group", svp->group); propObject.insert("perm", svp->p); propObject.insert("rule", svp->r); } } void propertyToJson(INumberVectorProperty *nvp, QJsonObject &propObject, bool compact) { QJsonArray numbers; for (int i = 0; i < nvp->nnp; i++) { QJsonObject oneNumber = {{"name", nvp->np[i].name}, {"value", nvp->np[i].value}}; if (!compact) { oneNumber.insert("label", nvp->np[i].label); oneNumber.insert("min", nvp->np[i].min); oneNumber.insert("mix", nvp->np[i].max); oneNumber.insert("step", nvp->np[i].step); } numbers.append(oneNumber); } propObject = {{"name", nvp->name}, {"state", nvp->s}, {"numbers", numbers}}; if (!compact) { propObject.insert("label", nvp->label); propObject.insert("group", nvp->group); propObject.insert("perm", nvp->p); } } void propertyToJson(ITextVectorProperty *tvp, QJsonObject &propObject, bool compact) { QJsonArray Texts; for (int i = 0; i < tvp->ntp; i++) { QJsonObject oneText = {{"name", tvp->tp[i].name}, {"value", tvp->tp[i].text}}; if (!compact) { oneText.insert("label", tvp->tp[i].label); } Texts.append(oneText); } propObject = {{"name", tvp->name}, {"state", tvp->s}, {"texts", Texts}}; if (!compact) { propObject.insert("label", tvp->label); propObject.insert("group", tvp->group); propObject.insert("perm", tvp->p); } } void propertyToJson(ILightVectorProperty *lvp, QJsonObject &propObject, bool compact) { QJsonArray Lights; for (int i = 0; i < lvp->nlp; i++) { QJsonObject oneLight = {{"name", lvp->lp[i].name}, {"state", lvp->lp[i].s}}; if (!compact) { oneLight.insert("label", lvp->lp[i].label); } Lights.append(oneLight); } propObject = {{"name", lvp->name}, {"state", lvp->s}, {"lights", Lights}}; if (!compact) { propObject.insert("label", lvp->label); propObject.insert("group", lvp->group); } } } #ifndef KSTARS_LITE QDBusArgument &operator<<(QDBusArgument &argument, const ISD::ParkStatus &source) { argument.beginStructure(); argument << static_cast(source); argument.endStructure(); return argument; } const QDBusArgument &operator>>(const QDBusArgument &argument, ISD::ParkStatus &dest) { int a; argument.beginStructure(); argument >> a; argument.endStructure(); dest = static_cast(a); return argument; } #endif