diff --git a/kstars/ekos/align/align.cpp b/kstars/ekos/align/align.cpp index c5fb5f879..0de7d763e 100644 --- a/kstars/ekos/align/align.cpp +++ b/kstars/ekos/align/align.cpp @@ -1,6111 +1,6111 @@ /* Ekos Alignment Module Copyright (C) 2013 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 "align.h" #include "alignadaptor.h" #include "alignview.h" #include "flagcomponent.h" #include "fov.h" #include "kstars.h" #include "kstarsdata.h" #include "ksuserdb.h" #include "offlineastrometryparser.h" #include "onlineastrometryparser.h" #include "opsalign.h" #include "opsastrometry.h" #include "opsastrometrycfg.h" #include "opsastrometryindexfiles.h" #include "Options.h" #include "remoteastrometryparser.h" #include "skymap.h" #include "skymapcomposite.h" #include "starobject.h" #include "auxiliary/QProgressIndicator.h" #include "auxiliary/ksmessagebox.h" #include "dialogs/finddialog.h" #include "ekos/manager.h" #include "ekos/auxiliary/darklibrary.h" #include "fitsviewer/fitsdata.h" #include "fitsviewer/fitstab.h" #include "indi/clientmanager.h" #include "indi/driverinfo.h" #include "indi/indifilter.h" #include "profileinfo.h" #include "ksnotification.h" #include #include #include #include #include #define PAH_CUTOFF_FOV 10 // Minimum FOV width in arcminutes for PAH to work #define MAXIMUM_SOLVER_ITERATIONS 10 #define AL_FORMAT_VERSION 1.0 namespace Ekos { // 30 arcminutes RA movement const double Align::RAMotion = 0.5; // Sidereal rate, degrees/s const double Align::SIDRATE = 0.004178; const QMap Align::PAHStages = { {PAH_IDLE, I18N_NOOP("Idle")}, {PAH_FIRST_CAPTURE, I18N_NOOP("First Capture"}), {PAH_FIND_CP, I18N_NOOP("Finding CP"}), {PAH_FIRST_ROTATE, I18N_NOOP("First Rotation"}), {PAH_SECOND_CAPTURE, I18N_NOOP("Second Capture"}), {PAH_SECOND_ROTATE, I18N_NOOP("Second Rotation"}), {PAH_THIRD_CAPTURE, I18N_NOOP("Third Capture"}), {PAH_STAR_SELECT, I18N_NOOP("Select Star"}), {PAH_PRE_REFRESH, I18N_NOOP("Select Refresh"}), {PAH_REFRESH, I18N_NOOP("Refreshing"}), {PAH_ERROR, I18N_NOOP("Error")}, }; Align::Align(ProfileInfo *activeProfile) : m_ActiveProfile(activeProfile) { setupUi(this); qRegisterMetaType("Ekos::AlignState"); qDBusRegisterMetaType(); new AlignAdaptor(this); QDBusConnection::sessionBus().registerObject("/KStars/Ekos/Align", this); dirPath = QDir::homePath(); //loadSlewMode = false; solverFOV.reset(new FOV()); solverFOV->setName(i18n("Solver FOV")); solverFOV->setLockCelestialPole(true); solverFOV->setColor(KStars::Instance()->data()->colorScheme()->colorNamed("SolverFOVColor").name()); sensorFOV.reset(new FOV()); sensorFOV->setLockCelestialPole(true); QAction *a = KStars::Instance()->actionCollection()->action("show_sensor_fov"); if (a) a->setEnabled(true); showFITSViewerB->setIcon( QIcon::fromTheme("kstars_fitsviewer")); showFITSViewerB->setAttribute(Qt::WA_LayoutUsesWidgetRect); connect(showFITSViewerB, &QPushButton::clicked, this, &Ekos::Align::showFITSViewer); toggleFullScreenB->setIcon( QIcon::fromTheme("view-fullscreen")); toggleFullScreenB->setShortcut(Qt::Key_F4); toggleFullScreenB->setAttribute(Qt::WA_LayoutUsesWidgetRect); connect(toggleFullScreenB, &QPushButton::clicked, this, &Ekos::Align::toggleAlignWidgetFullScreen); alignView = new AlignView(alignWidget, FITS_ALIGN); alignView->setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Expanding); alignView->setBaseSize(alignWidget->size()); alignView->createFloatingToolBar(); QVBoxLayout *vlayout = new QVBoxLayout(); vlayout->addWidget(alignView); alignWidget->setLayout(vlayout); connect(solveB, &QPushButton::clicked, this, &Ekos::Align::captureAndSolve); connect(stopB, &QPushButton::clicked, this, &Ekos::Align::abort); connect(measureAltB, &QPushButton::clicked, this, &Ekos::Align::measureAltError); connect(measureAzB, &QPushButton::clicked, this, &Ekos::Align::measureAzError); // Effective FOV Edit connect(FOVOut, &QLineEdit::editingFinished, this, &Align::syncFOV); connect(CCDCaptureCombo, static_cast(&QComboBox::activated), this, &Ekos::Align::setDefaultCCD); connect(CCDCaptureCombo, static_cast(&QComboBox::activated), this, &Ekos::Align::checkCCD); connect(correctAltB, &QPushButton::clicked, this, &Ekos::Align::correctAltError); connect(correctAzB, &QPushButton::clicked, this, &Ekos::Align::correctAzError); connect(loadSlewB, &QPushButton::clicked, [&]() { loadAndSlew(); }); FilterDevicesCombo->addItem("--"); connect(FilterDevicesCombo, static_cast(&QComboBox::activated), [ = ](const QString & text) { syncSettings(); Options::setDefaultAlignFW(text); }); connect(FilterDevicesCombo, static_cast(&QComboBox::activated), this, &Ekos::Align::checkFilter); connect(FilterPosCombo, static_cast(&QComboBox::activated), [ = ](int index) { syncSettings(); Options::setLockAlignFilterIndex(index); } ); connect(PAHSlewRateCombo, static_cast(&QComboBox::activated), [&](int index) { Options::setPAHMountSpeedIndex(index); }); gotoModeButtonGroup->setId(syncR, GOTO_SYNC); gotoModeButtonGroup->setId(slewR, GOTO_SLEW); gotoModeButtonGroup->setId(nothingR, GOTO_NOTHING); connect(gotoModeButtonGroup, static_cast(&QButtonGroup::buttonClicked), this, [ = ](int id) { this->currentGotoMode = static_cast(id); }); m_CaptureTimer.setSingleShot(true); m_CaptureTimer.setInterval(10000); connect(&m_CaptureTimer, &QTimer::timeout, [&]() { if (m_CaptureTimeoutCounter++ > 3) { appendLogText(i18n("Capture timed out.")); abort(); } else { ISD::CCDChip *targetChip = currentCCD->getChip(useGuideHead ? ISD::CCDChip::GUIDE_CCD : ISD::CCDChip::PRIMARY_CCD); if (targetChip->isCapturing()) { targetChip->abortExposure(); m_CaptureTimer.start( m_CaptureTimer.interval() * 2); } else captureAndSolve(); } }); m_AlignTimer.setSingleShot(true); m_AlignTimer.setInterval(Options::astrometryTimeout() * 1000); connect(&m_AlignTimer, &QTimer::timeout, this, &Ekos::Align::checkAlignmentTimeout); currentGotoMode = static_cast(Options::solverGotoOption()); gotoModeButtonGroup->button(currentGotoMode)->setChecked(true); editOptionsB->setIcon(QIcon::fromTheme("document-edit")); editOptionsB->setAttribute(Qt::WA_LayoutUsesWidgetRect); KConfigDialog *dialog = new KConfigDialog(this, "alignsettings", Options::self()); #ifdef Q_OS_OSX dialog->setWindowFlags(Qt::Tool | Qt::WindowStaysOnTopHint); #endif opsAlign = new OpsAlign(this); connect(opsAlign, &OpsAlign::settingsUpdated, this, &Ekos::Align::refreshAlignOptions); KPageWidgetItem *page = dialog->addPage(opsAlign, i18n("Astrometry.net")); page->setIcon(QIcon(":/icons/astrometry.svg")); opsAstrometry = new OpsAstrometry(this); page = dialog->addPage(opsAstrometry, i18n("Solver Options")); page->setIcon(QIcon::fromTheme("configure")); #ifndef Q_OS_WIN opsAstrometryCfg = new OpsAstrometryCfg(this); page = dialog->addPage(opsAstrometryCfg, i18n("Astrometry.cfg")); page->setIcon(QIcon::fromTheme("document-edit")); opsAstrometryIndexFiles = new OpsAstrometryIndexFiles(this); page = dialog->addPage(opsAstrometryIndexFiles, i18n("Index Files")); page->setIcon(QIcon::fromTheme("map-flat")); #endif connect(editOptionsB, &QPushButton::clicked, dialog, &QDialog::show); appendLogText(i18n("Idle.")); pi.reset(new QProgressIndicator(this)); stopLayout->addWidget(pi.get()); exposureIN->setValue(Options::alignExposure()); connect(exposureIN, static_cast(&QDoubleSpinBox::valueChanged), [&]() { syncSettings(); }); altStage = ALT_INIT; azStage = AZ_INIT; rememberSolverWCS = Options::astrometrySolverWCS(); rememberAutoWCS = Options::autoWCS(); // Online/Offline/Remote solver check solverTypeGroup->setId(onlineSolverR, SOLVER_ONLINE); solverTypeGroup->setId(offlineSolverR, SOLVER_OFFLINE); solverTypeGroup->setId(remoteSolverR, SOLVER_REMOTE); #ifdef Q_OS_WIN offlineSolverR->setEnabled(false); offlineSolverR->setToolTip( i18n("Offline solver is not supported under Windows. Please use either the Online or Remote solvers.")); #endif solverTypeGroup->button(Options::solverType())->setChecked(true); connect(solverTypeGroup, static_cast(&QButtonGroup::buttonClicked), this, &Align::setSolverType); switch (solverTypeGroup->checkedId()) { case SOLVER_ONLINE: onlineParser.reset(new Ekos::OnlineAstrometryParser()); parser = onlineParser.get(); break; case SOLVER_OFFLINE: offlineParser.reset(new OfflineAstrometryParser()); parser = offlineParser.get(); break; case SOLVER_REMOTE: remoteParser.reset(new RemoteAstrometryParser()); parser = remoteParser.get(); break; } parser->setAlign(this); if (parser->init() == false) setEnabled(false); else { connect(parser, &Ekos::AstrometryParser::solverFinished, this, &Ekos::Align::solverFinished, Qt::UniqueConnection); connect(parser, &Ekos::AstrometryParser::solverFailed, this, &Ekos::Align::solverFailed, Qt::UniqueConnection); } //solverOptions->setText(Options::solverOptions()); // Which telescope info to use for FOV calculations //kcfg_solverOTA->setChecked(Options::solverOTA()); //guideScopeCCDs = Options::guideScopeCCDs(); FOVScopeCombo->setCurrentIndex(Options::solverScopeType()); connect(FOVScopeCombo, static_cast(&QComboBox::currentIndexChanged), this, &Ekos::Align::updateTelescopeType); //connect(FOVScopeCombo, SIGNAL(currentIndexChanged(int)), this, SIGNAL(newFOVTelescopeType(int))); accuracySpin->setValue(Options::solverAccuracyThreshold()); alignDarkFrameCheck->setChecked(Options::alignDarkFrame()); delaySpin->setValue(Options::settlingTime()); connect(delaySpin, &QSpinBox::editingFinished, this, &Ekos::Align::saveSettleTime); connect(binningCombo, static_cast(&QComboBox::currentIndexChanged), this, &Ekos::Align::setBinningIndex); // PAH Connections connect(this, &Align::PAHEnabled, [&](bool enabled) { PAHStartB->setEnabled(enabled); directionLabel->setEnabled(enabled); PAHDirectionCombo->setEnabled(enabled); PAHRotationSpin->setEnabled(enabled); PAHSlewRateCombo->setEnabled(enabled); PAHManual->setEnabled(enabled); }); connect(PAHStartB, &QPushButton::clicked, this, &Ekos::Align::startPAHProcess); // PAH StopB is just a shortcut for the regular stop connect(PAHStopB, &QPushButton::clicked, this, &Align::stopPAHProcess); connect(PAHCorrectionsNextB, &QPushButton::clicked, this, &Ekos::Align::setPAHCorrectionSelectionComplete); connect(PAHRefreshB, &QPushButton::clicked, this, &Ekos::Align::startPAHRefreshProcess); connect(PAHDoneB, &QPushButton::clicked, this, &Ekos::Align::setPAHRefreshComplete); // done buttons for manual slewing during polar alignment: connect(PAHfirstDone, &QPushButton::clicked, this, &Ekos::Align::setPAHSlewDone); connect(PAHsecondDone, &QPushButton::clicked, this, &Ekos::Align::setPAHSlewDone); if (solverOptions->text().contains("no-fits2fits")) appendLogText(i18n( "Warning: If using astrometry.net v0.68 or above, remove the --no-fits2fits from the astrometry options.")); hemisphere = KStarsData::Instance()->geo()->lat()->Degrees() > 0 ? NORTH_HEMISPHERE : SOUTH_HEMISPHERE; double accuracyRadius = accuracySpin->value(); alignPlot->setBackground(QBrush(Qt::black)); alignPlot->setSelectionTolerance(10); alignPlot->xAxis->setBasePen(QPen(Qt::white, 1)); alignPlot->yAxis->setBasePen(QPen(Qt::white, 1)); alignPlot->xAxis->setTickPen(QPen(Qt::white, 1)); alignPlot->yAxis->setTickPen(QPen(Qt::white, 1)); alignPlot->xAxis->setSubTickPen(QPen(Qt::white, 1)); alignPlot->yAxis->setSubTickPen(QPen(Qt::white, 1)); alignPlot->xAxis->setTickLabelColor(Qt::white); alignPlot->yAxis->setTickLabelColor(Qt::white); alignPlot->xAxis->setLabelColor(Qt::white); alignPlot->yAxis->setLabelColor(Qt::white); alignPlot->xAxis->setLabelFont(QFont(font().family(), 10)); alignPlot->yAxis->setLabelFont(QFont(font().family(), 10)); alignPlot->xAxis->setLabelPadding(2); alignPlot->yAxis->setLabelPadding(2); alignPlot->xAxis->grid()->setPen(QPen(QColor(140, 140, 140), 1, Qt::DotLine)); alignPlot->yAxis->grid()->setPen(QPen(QColor(140, 140, 140), 1, Qt::DotLine)); alignPlot->xAxis->grid()->setSubGridPen(QPen(QColor(80, 80, 80), 1, Qt::DotLine)); alignPlot->yAxis->grid()->setSubGridPen(QPen(QColor(80, 80, 80), 1, Qt::DotLine)); alignPlot->xAxis->grid()->setZeroLinePen(QPen(Qt::yellow)); alignPlot->yAxis->grid()->setZeroLinePen(QPen(Qt::yellow)); alignPlot->xAxis->setLabel(i18n("dRA (arcsec)")); alignPlot->yAxis->setLabel(i18n("dDE (arcsec)")); alignPlot->xAxis->setRange(-accuracyRadius * 3, accuracyRadius * 3); alignPlot->yAxis->setRange(-accuracyRadius * 3, accuracyRadius * 3); alignPlot->setInteractions(QCP::iRangeZoom); alignPlot->setInteraction(QCP::iRangeDrag, true); alignPlot->addGraph(); alignPlot->graph(0)->setLineStyle(QCPGraph::lsNone); alignPlot->graph(0)->setScatterStyle(QCPScatterStyle(QCPScatterStyle::ssDisc, Qt::white, 15)); buildTarget(); connect(alignPlot, &QCustomPlot::mouseMove, this, &Ekos::Align::handlePointTooltip); connect(rightLayout, &QSplitter::splitterMoved, this, &Ekos::Align::handleVerticalPlotSizeChange); connect(alignSplitter, &QSplitter::splitterMoved, this, &Ekos::Align::handleHorizontalPlotSizeChange); connect(accuracySpin, static_cast(&QSpinBox::valueChanged), this, &Ekos::Align::buildTarget); alignPlot->resize(190, 190); alignPlot->replot(); solutionTable->setColumnWidth(0, 70); solutionTable->setColumnWidth(1, 75); solutionTable->setColumnWidth(2, 80); solutionTable->setColumnWidth(3, 30); solutionTable->setColumnWidth(4, 100); solutionTable->setColumnWidth(5, 100); clearAllSolutionsB->setIcon( QIcon::fromTheme("application-exit")); clearAllSolutionsB->setAttribute(Qt::WA_LayoutUsesWidgetRect); removeSolutionB->setIcon(QIcon::fromTheme("list-remove")); removeSolutionB->setAttribute(Qt::WA_LayoutUsesWidgetRect); exportSolutionsCSV->setIcon( QIcon::fromTheme("document-save-as")); exportSolutionsCSV->setAttribute(Qt::WA_LayoutUsesWidgetRect); autoScaleGraphB->setIcon(QIcon::fromTheme("zoom-fit-best")); autoScaleGraphB->setAttribute(Qt::WA_LayoutUsesWidgetRect); mountModel.setupUi(&mountModelDialog); mountModelDialog.setWindowTitle("Mount Model Tool"); mountModelDialog.setWindowFlags(Qt::Tool | Qt::WindowStaysOnTopHint); mountModel.alignTable->setColumnWidth(0, 70); mountModel.alignTable->setColumnWidth(1, 75); mountModel.alignTable->setColumnWidth(2, 130); mountModel.alignTable->setColumnWidth(3, 30); mountModel.wizardAlignB->setIcon( QIcon::fromTheme("tools-wizard")); mountModel.wizardAlignB->setAttribute(Qt::WA_LayoutUsesWidgetRect); mountModel.clearAllAlignB->setIcon( QIcon::fromTheme("application-exit")); mountModel.clearAllAlignB->setAttribute(Qt::WA_LayoutUsesWidgetRect); mountModel.removeAlignB->setIcon(QIcon::fromTheme("list-remove")); mountModel.removeAlignB->setAttribute(Qt::WA_LayoutUsesWidgetRect); mountModel.addAlignB->setIcon(QIcon::fromTheme("list-add")); mountModel.addAlignB->setAttribute(Qt::WA_LayoutUsesWidgetRect); mountModel.findAlignB->setIcon(QIcon::fromTheme("edit-find")); mountModel.findAlignB->setAttribute(Qt::WA_LayoutUsesWidgetRect); mountModel.alignTable->verticalHeader()->setDragDropOverwriteMode(false); mountModel.alignTable->verticalHeader()->setSectionsMovable(true); mountModel.alignTable->verticalHeader()->setDragEnabled(true); mountModel.alignTable->verticalHeader()->setDragDropMode(QAbstractItemView::InternalMove); connect(mountModel.alignTable->verticalHeader(), SIGNAL(sectionMoved(int, int, int)), this, SLOT(moveAlignPoint(int, int, int))); mountModel.loadAlignB->setIcon( QIcon::fromTheme("document-open")); mountModel.loadAlignB->setAttribute(Qt::WA_LayoutUsesWidgetRect); mountModel.saveAsAlignB->setIcon( QIcon::fromTheme("document-save-as")); mountModel.saveAsAlignB->setAttribute(Qt::WA_LayoutUsesWidgetRect); mountModel.saveAlignB->setIcon( QIcon::fromTheme("document-save")); mountModel.saveAlignB->setAttribute(Qt::WA_LayoutUsesWidgetRect); mountModel.previewB->setIcon(QIcon::fromTheme("kstars_grid")); mountModel.previewB->setAttribute(Qt::WA_LayoutUsesWidgetRect); mountModel.previewB->setCheckable(true); mountModel.sortAlignB->setIcon(QIcon::fromTheme("svn-update")); mountModel.sortAlignB->setAttribute(Qt::WA_LayoutUsesWidgetRect); mountModel.stopAlignB->setIcon( QIcon::fromTheme("media-playback-stop")); mountModel.stopAlignB->setAttribute(Qt::WA_LayoutUsesWidgetRect); mountModel.startAlignB->setIcon( QIcon::fromTheme("media-playback-start")); mountModel.startAlignB->setAttribute(Qt::WA_LayoutUsesWidgetRect); connect(clearAllSolutionsB, &QPushButton::clicked, this, &Ekos::Align::slotClearAllSolutionPoints); connect(removeSolutionB, &QPushButton::clicked, this, &Ekos::Align::slotRemoveSolutionPoint); connect(exportSolutionsCSV, &QPushButton::clicked, this, &Ekos::Align::exportSolutionPoints); connect(autoScaleGraphB, &QPushButton::clicked, this, &Ekos::Align::slotAutoScaleGraph); connect(mountModelB, &QPushButton::clicked, this, &Ekos::Align::slotMountModel); connect(solutionTable, &QTableWidget::cellClicked, this, &Ekos::Align::selectSolutionTableRow); connect(mountModel.wizardAlignB, &QPushButton::clicked, this, &Ekos::Align::slotWizardAlignmentPoints); connect(mountModel.alignTypeBox, static_cast(&QComboBox::currentIndexChanged), this, &Ekos::Align::alignTypeChanged); connect(mountModel.starListBox, static_cast(&QComboBox::currentIndexChanged), this, &Ekos::Align::slotStarSelected); connect(mountModel.greekStarListBox, static_cast(&QComboBox::currentIndexChanged), this, &Ekos::Align::slotStarSelected); connect(mountModel.loadAlignB, &QPushButton::clicked, this, &Ekos::Align::slotLoadAlignmentPoints); connect(mountModel.saveAsAlignB, &QPushButton::clicked, this, &Ekos::Align::slotSaveAsAlignmentPoints); connect(mountModel.saveAlignB, &QPushButton::clicked, this, &Ekos::Align::slotSaveAlignmentPoints); connect(mountModel.clearAllAlignB, &QPushButton::clicked, this, &Ekos::Align::slotClearAllAlignPoints); connect(mountModel.removeAlignB, &QPushButton::clicked, this, &Ekos::Align::slotRemoveAlignPoint); connect(mountModel.addAlignB, &QPushButton::clicked, this, &Ekos::Align::slotAddAlignPoint); connect(mountModel.findAlignB, &QPushButton::clicked, this, &Ekos::Align::slotFindAlignObject); connect(mountModel.sortAlignB, &QPushButton::clicked, this, &Ekos::Align::slotSortAlignmentPoints); connect(mountModel.previewB, &QPushButton::clicked, this, &Ekos::Align::togglePreviewAlignPoints); connect(mountModel.stopAlignB, &QPushButton::clicked, this, &Ekos::Align::resetAlignmentProcedure); connect(mountModel.startAlignB, &QPushButton::clicked, this, &Ekos::Align::startStopAlignmentProcedure); //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); } Align::~Align() { if (alignWidget->parent() == nullptr) toggleAlignWidgetFullScreen(); // Remove temporary FITS files left before by the solver QDir dir(QDir::tempPath()); dir.setNameFilters(QStringList() << "fits*" << "tmp.*"); dir.setFilter(QDir::Files); for (auto &dirFile : dir.entryList()) dir.remove(dirFile); } void Align::selectSolutionTableRow(int row, int column) { Q_UNUSED(column); solutionTable->selectRow(row); for (int i = 0; i < alignPlot->itemCount(); i++) { QCPAbstractItem *abstractItem = alignPlot->item(i); if (abstractItem) { QCPItemText *item = qobject_cast(abstractItem); if (item) { if (i == row) { item->setColor(Qt::black); item->setBrush(Qt::yellow); } else { item->setColor(Qt::red); item->setBrush(Qt::white); } } } } alignPlot->replot(); } void Align::handleHorizontalPlotSizeChange() { alignPlot->xAxis->setScaleRatio(alignPlot->yAxis, 1.0); alignPlot->replot(); } void Align::handleVerticalPlotSizeChange() { alignPlot->yAxis->setScaleRatio(alignPlot->xAxis, 1.0); alignPlot->replot(); } void Align::resizeEvent(QResizeEvent *event) { if (event->oldSize().width() != -1) { if (event->oldSize().width() != size().width()) handleHorizontalPlotSizeChange(); else if (event->oldSize().height() != size().height()) handleVerticalPlotSizeChange(); } else { QTimer::singleShot(10, this, &Ekos::Align::handleHorizontalPlotSizeChange); } } void Align::handlePointTooltip(QMouseEvent *event) { QCPAbstractItem *item = alignPlot->itemAt(event->localPos()); if (item) { QCPItemText *label = qobject_cast(item); if (label) { QString labelText = label->text(); int point = labelText.toInt() - 1; if (point < 0) return; QToolTip::showText(event->globalPos(), tr("" "" "" "" "" "" "" "" "" "" "" "" "" "" "" "" "
Object %L1: %L2
RA:%L3
DE:%L4
dRA:%L5
dDE:%L6
") .arg(point + 1) .arg(solutionTable->item(point, 2)->text(), solutionTable->item(point, 0)->text(), solutionTable->item(point, 1)->text(), solutionTable->item(point, 4)->text(), solutionTable->item(point, 5)->text()), alignPlot, alignPlot->rect()); } } } void Align::buildTarget() { double accuracyRadius = accuracySpin->value(); if (centralTarget) { concentricRings->data()->clear(); redTarget->data()->clear(); yellowTarget->data()->clear(); centralTarget->data()->clear(); } else { concentricRings = new QCPCurve(alignPlot->xAxis, alignPlot->yAxis); redTarget = new QCPCurve(alignPlot->xAxis, alignPlot->yAxis); yellowTarget = new QCPCurve(alignPlot->xAxis, alignPlot->yAxis); centralTarget = new QCPCurve(alignPlot->xAxis, alignPlot->yAxis); } const int pointCount = 200; QVector circleRings( pointCount * (5)); //Have to multiply by the number of rings, Rings at : 25%, 50%, 75%, 125%, 175% QVector circleCentral(pointCount); QVector circleYellow(pointCount); QVector circleRed(pointCount); int circleRingPt = 0; for (int i = 0; i < pointCount; i++) { double theta = i / static_cast(pointCount) * 2 * M_PI; for (double ring = 1; ring < 8; ring++) { if (ring != 4 && ring != 6) { if (i % (9 - static_cast(ring)) == 0) //This causes fewer points to draw on the inner circles. { circleRings[circleRingPt] = QCPCurveData(circleRingPt, accuracyRadius * ring * 0.25 * qCos(theta), accuracyRadius * ring * 0.25 * qSin(theta)); circleRingPt++; } } } circleCentral[i] = QCPCurveData(i, accuracyRadius * qCos(theta), accuracyRadius * qSin(theta)); circleYellow[i] = QCPCurveData(i, accuracyRadius * 1.5 * qCos(theta), accuracyRadius * 1.5 * qSin(theta)); circleRed[i] = QCPCurveData(i, accuracyRadius * 2 * qCos(theta), accuracyRadius * 2 * qSin(theta)); } concentricRings->setLineStyle(QCPCurve::lsNone); concentricRings->setScatterSkip(0); concentricRings->setScatterStyle(QCPScatterStyle(QCPScatterStyle::ssDisc, QColor(255, 255, 255, 150), 1)); concentricRings->data()->set(circleRings, true); redTarget->data()->set(circleRed, true); yellowTarget->data()->set(circleYellow, true); centralTarget->data()->set(circleCentral, true); concentricRings->setPen(QPen(Qt::white)); redTarget->setPen(QPen(Qt::red)); yellowTarget->setPen(QPen(Qt::yellow)); centralTarget->setPen(QPen(Qt::green)); concentricRings->setBrush(Qt::NoBrush); redTarget->setBrush(QBrush(QColor(255, 0, 0, 50))); yellowTarget->setBrush( QBrush(QColor(0, 255, 0, 50))); //Note this is actually yellow. It is green on top of red with equal opacity. centralTarget->setBrush(QBrush(QColor(0, 255, 0, 50))); if (alignPlot->size().width() > 0) alignPlot->replot(); } void Align::slotAutoScaleGraph() { double accuracyRadius = accuracySpin->value(); alignPlot->xAxis->setRange(-accuracyRadius * 3, accuracyRadius * 3); alignPlot->yAxis->setRange(-accuracyRadius * 3, accuracyRadius * 3); alignPlot->xAxis->setScaleRatio(alignPlot->yAxis, 1.0); alignPlot->replot(); } void Align::slotWizardAlignmentPoints() { int points = mountModel.alignPtNum->value(); if (points < 2) //The minimum is 2 because the wizard calculations require the calculation of an angle between points. return; //It should not be less than 2 because the minimum in the spin box is 2. int minAlt = mountModel.minAltBox->value(); KStarsData *data = KStarsData::Instance(); GeoLocation *geo = data->geo(); double lat = geo->lat()->Degrees(); if (mountModel.alignTypeBox->currentIndex() == OBJECT_FIXED_DEC) { double decAngle = mountModel.alignDec->value(); //Dec that never rises. if (lat > 0) { if (decAngle < lat - 90 + minAlt) //Min altitude possible at minAlt deg above horizon { KSNotification::sorry(i18n("DEC is below the altitude limit")); return; } } else { if (decAngle > lat + 90 - minAlt) //Max altitude possible at minAlt deg above horizon { KSNotification::sorry(i18n("DEC is below the altitude limit")); return; } } } //If there are less than 6 points, keep them all in the same DEC, //any more, set the num per row to be the sqrt of the points to evenly distribute in RA and DEC int numRAperDEC = 5; if (points > 5) numRAperDEC = qSqrt(points); //These calculations rely on modulus and int division counting beginning at 0, but the #s start at 1. int decPoints = (points - 1) / numRAperDEC + 1; int lastSetRAPoints = (points - 1) % numRAperDEC + 1; double decIncrement = -1; double initDEC = -1; SkyPoint spTest; if (mountModel.alignTypeBox->currentIndex() == OBJECT_FIXED_DEC) { decPoints = 1; initDEC = mountModel.alignDec->value(); decIncrement = 0; } else if (decPoints == 1) { decIncrement = 0; spTest.setAlt( minAlt); //The goal here is to get the point exactly West at the minAlt so that we can use that DEC spTest.setAz(270); spTest.HorizontalToEquatorial(KStars::Instance()->data()->lst(), KStars::Instance()->data()->geo()->lat()); initDEC = spTest.dec().Degrees(); } else { spTest.setAlt( minAlt + 10); //We don't want to be right at the minAlt because there would be only 1 point on the dec circle above the alt. spTest.setAz(180); spTest.HorizontalToEquatorial(KStars::Instance()->data()->lst(), KStars::Instance()->data()->geo()->lat()); initDEC = spTest.dec().Degrees(); if (lat > 0) decIncrement = (80 - initDEC) / (decPoints); //Don't quite want to reach NCP else decIncrement = (initDEC - 80) / (decPoints); //Don't quite want to reach SCP } for (int d = 0; d < decPoints; d++) { double initRA = -1; double raPoints = -1; double raIncrement = -1; double dec; if (lat > 0) dec = initDEC + d * decIncrement; else dec = initDEC - d * decIncrement; if (mountModel.alignTypeBox->currentIndex() == OBJECT_FIXED_DEC) { raPoints = points; } else if (d == decPoints - 1) { raPoints = lastSetRAPoints; } else { raPoints = numRAperDEC; } //This computes both the initRA and the raIncrement. calculateAngleForRALine(raIncrement, initRA, dec, lat, raPoints, minAlt); if (raIncrement == -1 || decIncrement == -1) { KSNotification::sorry(i18n("Point calculation error.")); return; } for (int i = 0; i < raPoints; i++) { double ra = initRA + i * raIncrement; const SkyObject *original = getWizardAlignObject(ra, dec); QString ra_report, dec_report, name; if (original) { SkyObject *o = original->clone(); o->updateCoords(data->updateNum(), true, data->geo()->lat(), data->lst(), false); getFormattedCoords(o->ra0().Hours(), o->dec0().Degrees(), ra_report, dec_report); name = o->longname(); } else { getFormattedCoords(dms(ra).Hours(), dec, ra_report, dec_report); name = i18n("Sky Point"); } int currentRow = mountModel.alignTable->rowCount(); mountModel.alignTable->insertRow(currentRow); QTableWidgetItem *RAReport = new QTableWidgetItem(); RAReport->setText(ra_report); RAReport->setTextAlignment(Qt::AlignHCenter); mountModel.alignTable->setItem(currentRow, 0, RAReport); QTableWidgetItem *DECReport = new QTableWidgetItem(); DECReport->setText(dec_report); DECReport->setTextAlignment(Qt::AlignHCenter); mountModel.alignTable->setItem(currentRow, 1, DECReport); QTableWidgetItem *ObjNameReport = new QTableWidgetItem(); ObjNameReport->setText(name); ObjNameReport->setTextAlignment(Qt::AlignHCenter); mountModel.alignTable->setItem(currentRow, 2, ObjNameReport); QTableWidgetItem *disabledBox = new QTableWidgetItem(); disabledBox->setFlags(Qt::ItemIsSelectable); mountModel.alignTable->setItem(currentRow, 3, disabledBox); } } if (previewShowing) updatePreviewAlignPoints(); } void Align::calculateAngleForRALine(double &raIncrement, double &initRA, double initDEC, double lat, double raPoints, double minAlt) { SkyPoint spEast; SkyPoint spWest; //Circumpolar dec if (fabs(initDEC) > (90 - fabs(lat) + minAlt)) { if (raPoints > 1) raIncrement = 360 / (raPoints - 1); else raIncrement = 0; initRA = 0; } else { dms AZEast, AZWest; calculateAZPointsForDEC(dms(initDEC), dms(minAlt), AZEast, AZWest); spEast.setAlt(minAlt); spEast.setAz(AZEast.Degrees()); spEast.HorizontalToEquatorial(KStars::Instance()->data()->lst(), KStars::Instance()->data()->geo()->lat()); spWest.setAlt(minAlt); spWest.setAz(AZWest.Degrees()); spWest.HorizontalToEquatorial(KStars::Instance()->data()->lst(), KStars::Instance()->data()->geo()->lat()); dms angleSep = spEast.ra().deltaAngle(spWest.ra()); //dms angleSep; // if (spEast.ra().Degrees() > spWest.ra().Degrees()) // angleSep = spEast.ra() - spWest.ra(); // else // angleSep = spEast.ra() + dms(360) - spWest.ra(); initRA = spWest.ra().Degrees(); if (raPoints > 1) raIncrement = fabs(angleSep.Degrees() / (raPoints - 1)); else raIncrement = 0; } } void Align::calculateAZPointsForDEC(dms dec, dms alt, dms &AZEast, dms &AZWest) { KStarsData *data = KStarsData::Instance(); GeoLocation *geo = data->geo(); double AZRad; double sindec, cosdec, sinlat, coslat; double sinAlt, cosAlt; geo->lat()->SinCos(sinlat, coslat); dec.SinCos(sindec, cosdec); alt.SinCos(sinAlt, cosAlt); double arg = (sindec - sinlat * sinAlt) / (coslat * cosAlt); AZRad = acos(arg); AZEast.setRadians(AZRad); AZWest.setRadians(2.0 * dms::PI - AZRad); } const SkyObject *Align::getWizardAlignObject(double ra, double dec) { double maxSearch = 5.0; switch (mountModel.alignTypeBox->currentIndex()) { case OBJECT_ANY_OBJECT: return KStarsData::Instance()->skyComposite()->objectNearest(new SkyPoint(dms(ra), dms(dec)), maxSearch); case OBJECT_FIXED_DEC: case OBJECT_FIXED_GRID: return nullptr; case OBJECT_ANY_STAR: return KStarsData::Instance()->skyComposite()->starNearest(new SkyPoint(dms(ra), dms(dec)), maxSearch); } // if (mountModel.alignTypeBox->currentText() == "Any Object") // return KStarsData::Instance()->skyComposite()->objectNearest(new SkyPoint(dms(ra), dms(dec)), maxSearch); // else if (mountModel.alignTypeBox->currentText() == "Fixed DEC" || // mountModel.alignTypeBox->currentText() == "Fixed Grid") // return nullptr; // else if (mountModel.alignTypeBox->currentText() == "Any Stars") // return KStarsData::Instance()->skyComposite()->starNearest(new SkyPoint(dms(ra), dms(dec)), maxSearch); //If they want named stars, then try to search for and return the closest Align Star to the requested location dms bestDiff = dms(360); double index = -1; for (int i = 0; i < alignStars.size(); i++) { const StarObject *star = alignStars.value(i); if (star) { if (star->hasName()) { SkyPoint thisPt(ra / 15.0, dec); dms thisDiff = thisPt.angularDistanceTo(star); if (thisDiff.Degrees() < bestDiff.Degrees()) { index = i; bestDiff = thisDiff; } } } } if (index == -1) return KStarsData::Instance()->skyComposite()->starNearest(new SkyPoint(dms(ra), dms(dec)), maxSearch); return alignStars.value(index); } void Align::alignTypeChanged(int alignType) { if (alignType == OBJECT_FIXED_DEC) mountModel.alignDec->setEnabled(true); else mountModel.alignDec->setEnabled(false); } void Align::slotStarSelected(const QString selectedStar) { for (int i = 0; i < alignStars.size(); i++) { const StarObject *star = alignStars.value(i); if (star) { if (star->name() == selectedStar || star->gname().simplified() == selectedStar) { int currentRow = mountModel.alignTable->rowCount(); mountModel.alignTable->insertRow(currentRow); QString ra_report, dec_report; getFormattedCoords(star->ra0().Hours(), star->dec0().Degrees(), ra_report, dec_report); QTableWidgetItem *RAReport = new QTableWidgetItem(); RAReport->setText(ra_report); RAReport->setTextAlignment(Qt::AlignHCenter); mountModel.alignTable->setItem(currentRow, 0, RAReport); QTableWidgetItem *DECReport = new QTableWidgetItem(); DECReport->setText(dec_report); DECReport->setTextAlignment(Qt::AlignHCenter); mountModel.alignTable->setItem(currentRow, 1, DECReport); QTableWidgetItem *ObjNameReport = new QTableWidgetItem(); ObjNameReport->setText(star->longname()); ObjNameReport->setTextAlignment(Qt::AlignHCenter); mountModel.alignTable->setItem(currentRow, 2, ObjNameReport); QTableWidgetItem *disabledBox = new QTableWidgetItem(); disabledBox->setFlags(Qt::ItemIsSelectable); mountModel.alignTable->setItem(currentRow, 3, disabledBox); mountModel.starListBox->setCurrentIndex(0); mountModel.greekStarListBox->setCurrentIndex(0); return; } } } if (previewShowing) updatePreviewAlignPoints(); } void Align::generateAlignStarList() { alignStars.clear(); mountModel.starListBox->clear(); mountModel.greekStarListBox->clear(); KStarsData *data = KStarsData::Instance(); QVector> listStars; listStars.append(data->skyComposite()->objectLists(SkyObject::STAR)); for (int i = 0; i < listStars.size(); i++) { QPair pair = listStars.value(i); const StarObject *star = dynamic_cast(pair.second); if (star) { StarObject *alignStar = star->clone(); alignStar->updateCoords(data->updateNum(), true, data->geo()->lat(), data->lst(), false); alignStars.append(alignStar); } } QStringList boxNames; QStringList greekBoxNames; for (int i = 0; i < alignStars.size(); i++) { const StarObject *star = alignStars.value(i); if (star) { if (!isVisible(star)) { alignStars.remove(i); i--; } else { if (star->hasLatinName()) boxNames << star->name(); else { if (!star->gname().isEmpty()) greekBoxNames << star->gname().simplified(); } } } } boxNames.sort(Qt::CaseInsensitive); boxNames.removeDuplicates(); greekBoxNames.removeDuplicates(); qSort(greekBoxNames.begin(), greekBoxNames.end(), [](const QString & a, const QString & b) { QStringList aParts = a.split(' '); QStringList bParts = b.split(' '); if (aParts.length() < 2 || bParts.length() < 2) return a < b; //This should not happen, they should all have 2 words in the string. if (aParts[1] == bParts[1]) { return aParts[0] < bParts[0]; //This compares the greek letter when the constellation is the same } else return aParts[1] < bParts[1]; //This compares the constellation names }); mountModel.starListBox->addItem("Select one:"); mountModel.greekStarListBox->addItem("Select one:"); for (int i = 0; i < boxNames.size(); i++) mountModel.starListBox->addItem(boxNames.at(i)); for (int i = 0; i < greekBoxNames.size(); i++) mountModel.greekStarListBox->addItem(greekBoxNames.at(i)); } bool Align::isVisible(const SkyObject *so) { return (getAltitude(so) > 30); } double Align::getAltitude(const SkyObject *so) { KStarsData *data = KStarsData::Instance(); SkyPoint sp = so->recomputeCoords(data->ut(), data->geo()); //check altitude of object at this time. sp.EquatorialToHorizontal(data->lst(), data->geo()->lat()); return sp.alt().Degrees(); } void Align::togglePreviewAlignPoints() { previewShowing = !previewShowing; mountModel.previewB->setChecked(previewShowing); updatePreviewAlignPoints(); } void Align::updatePreviewAlignPoints() { FlagComponent *flags = KStarsData::Instance()->skyComposite()->flags(); for (int i = 0; i < flags->size(); i++) { if (flags->label(i).startsWith(QLatin1String("Align"))) { flags->remove(i); i--; } } if (previewShowing) { for (int i = 0; i < mountModel.alignTable->rowCount(); i++) { QTableWidgetItem *raCell = mountModel.alignTable->item(i, 0); QTableWidgetItem *deCell = mountModel.alignTable->item(i, 1); QTableWidgetItem *objNameCell = mountModel.alignTable->item(i, 2); if (raCell && deCell && objNameCell) { QString raString = raCell->text(); QString deString = deCell->text(); dms raDMS = dms::fromString(raString, false); dms decDMS = dms::fromString(deString, true); QString objString = objNameCell->text(); SkyPoint flagPoint(raDMS, decDMS); flags->add(flagPoint, "J2000", "Default", "Align " + QString::number(i + 1) + ' ' + objString, "white"); } } } KStars::Instance()->map()->forceUpdate(true); } void Align::slotLoadAlignmentPoints() { QUrl fileURL = QFileDialog::getOpenFileUrl(&mountModelDialog, i18n("Open Ekos Alignment List"), alignURLPath, "Ekos AlignmentList (*.eal)"); if (fileURL.isEmpty()) return; if (fileURL.isValid() == false) { QString message = i18n("Invalid URL: %1", fileURL.toLocalFile()); KSNotification::sorry(message, i18n("Invalid URL")); return; } alignURLPath = QUrl(fileURL.url(QUrl::RemoveFilename)); loadAlignmentPoints(fileURL.toLocalFile()); if (previewShowing) updatePreviewAlignPoints(); } bool Align::loadAlignmentPoints(const QString &fileURL) { QFile sFile; sFile.setFileName(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; } mountModel.alignTable->setRowCount(0); LilXML *xmlParser = newLilXML(); char errmsg[MAXRBUF]; XMLEle *root = nullptr; char c; while (sFile.getChar(&c)) { root = readXMLEle(xmlParser, c, errmsg); if (root) { double sqVersion = atof(findXMLAttValu(root, "version")); if (sqVersion < AL_FORMAT_VERSION) { appendLogText(i18n("Deprecated sequence file format version %1. Please construct a new sequence file.", sqVersion)); return false; } XMLEle *ep = nullptr; XMLEle *subEP = nullptr; int currentRow = 0; for (ep = nextXMLEle(root, 1); ep != nullptr; ep = nextXMLEle(root, 0)) { if (!strcmp(tagXMLEle(ep), "AlignmentPoint")) { mountModel.alignTable->insertRow(currentRow); subEP = findXMLEle(ep, "RA"); if (subEP) { QTableWidgetItem *RAReport = new QTableWidgetItem(); RAReport->setText(pcdataXMLEle(subEP)); RAReport->setTextAlignment(Qt::AlignHCenter); mountModel.alignTable->setItem(currentRow, 0, RAReport); } else return false; subEP = findXMLEle(ep, "DE"); if (subEP) { QTableWidgetItem *DEReport = new QTableWidgetItem(); DEReport->setText(pcdataXMLEle(subEP)); DEReport->setTextAlignment(Qt::AlignHCenter); mountModel.alignTable->setItem(currentRow, 1, DEReport); } else return false; subEP = findXMLEle(ep, "NAME"); if (subEP) { QTableWidgetItem *ObjReport = new QTableWidgetItem(); ObjReport->setText(pcdataXMLEle(subEP)); ObjReport->setTextAlignment(Qt::AlignHCenter); mountModel.alignTable->setItem(currentRow, 2, ObjReport); } else return false; } currentRow++; } return true; } } return false; } void Align::slotSaveAsAlignmentPoints() { alignURL.clear(); slotSaveAlignmentPoints(); } void Align::slotSaveAlignmentPoints() { QUrl backupCurrent = alignURL; if (alignURL.toLocalFile().startsWith(QLatin1String("/tmp/")) || alignURL.toLocalFile().contains("/Temp")) alignURL.clear(); if (alignURL.isEmpty()) { alignURL = QFileDialog::getSaveFileUrl(&mountModelDialog, i18n("Save Ekos Alignment List"), alignURLPath, "Ekos Alignment List (*.eal)"); // if user presses cancel if (alignURL.isEmpty()) { alignURL = backupCurrent; return; } alignURLPath = QUrl(alignURL.url(QUrl::RemoveFilename)); if (alignURL.toLocalFile().endsWith(QLatin1String(".eal")) == false) alignURL.setPath(alignURL.toLocalFile() + ".eal"); if (QFile::exists(alignURL.toLocalFile())) { int r = KMessageBox::warningContinueCancel(nullptr, i18n("A file named \"%1\" already exists. " "Overwrite it?", alignURL.fileName()), i18n("Overwrite File?"), KStandardGuiItem::overwrite()); if (r == KMessageBox::Cancel) return; } } if (alignURL.isValid()) { if ((saveAlignmentPoints(alignURL.toLocalFile())) == false) { KSNotification::error(i18n("Failed to save alignment list"), i18n("Save")); return; } } else { QString message = i18n("Invalid URL: %1", alignURL.url()); KSNotification::sorry(message, i18n("Invalid URL")); } } bool Align::saveAlignmentPoints(const QString &path) { QFile file; 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); outstream << "" << endl; outstream << "" << endl; for (int i = 0; i < mountModel.alignTable->rowCount(); i++) { QTableWidgetItem *raCell = mountModel.alignTable->item(i, 0); QTableWidgetItem *deCell = mountModel.alignTable->item(i, 1); QTableWidgetItem *objNameCell = mountModel.alignTable->item(i, 2); if (!raCell || !deCell || !objNameCell) return false; QString raString = raCell->text(); QString deString = deCell->text(); QString objString = objNameCell->text(); outstream << "" << endl; outstream << "" << raString << "" << endl; outstream << "" << deString << "" << endl; outstream << "" << objString << "" << endl; outstream << "" << endl; } outstream << "" << endl; appendLogText(i18n("Alignment List saved to %1", path)); file.close(); return true; } void Align::slotSortAlignmentPoints() { int firstAlignmentPt = findClosestAlignmentPointToTelescope(); if (firstAlignmentPt != -1) { swapAlignPoints(firstAlignmentPt, 0); } for (int i = 0; i < mountModel.alignTable->rowCount() - 1; i++) { int nextAlignmentPoint = findNextAlignmentPointAfter(i); if (nextAlignmentPoint != -1) { swapAlignPoints(nextAlignmentPoint, i + 1); } } if (previewShowing) updatePreviewAlignPoints(); } int Align::findClosestAlignmentPointToTelescope() { dms bestDiff = dms(360); double index = -1; for (int i = 0; i < mountModel.alignTable->rowCount(); i++) { QTableWidgetItem *raCell = mountModel.alignTable->item(i, 0); QTableWidgetItem *deCell = mountModel.alignTable->item(i, 1); if (raCell && deCell) { dms raDMS = dms::fromString(raCell->text(), false); dms deDMS = dms::fromString(deCell->text(), true); dms thisDiff = telescopeCoord.angularDistanceTo(new SkyPoint(raDMS, deDMS)); if (thisDiff.Degrees() < bestDiff.Degrees()) { index = i; bestDiff = thisDiff; } } } return index; } int Align::findNextAlignmentPointAfter(int currentSpot) { QTableWidgetItem *currentRACell = mountModel.alignTable->item(currentSpot, 0); QTableWidgetItem *currentDECell = mountModel.alignTable->item(currentSpot, 1); if (currentRACell && currentDECell) { dms thisRADMS = dms::fromString(currentRACell->text(), false); dms thisDEDMS = dms::fromString(currentDECell->text(), true); SkyPoint thisPt(thisRADMS, thisDEDMS); dms bestDiff = dms(360); double index = -1; for (int i = currentSpot + 1; i < mountModel.alignTable->rowCount(); i++) { QTableWidgetItem *raCell = mountModel.alignTable->item(i, 0); QTableWidgetItem *deCell = mountModel.alignTable->item(i, 1); if (raCell && deCell) { dms raDMS = dms::fromString(raCell->text(), false); dms deDMS = dms::fromString(deCell->text(), true); SkyPoint point(raDMS, deDMS); dms thisDiff = thisPt.angularDistanceTo(&point); if (thisDiff.Degrees() < bestDiff.Degrees()) { index = i; bestDiff = thisDiff; } } } return index; } else return -1; } void Align::exportSolutionPoints() { if (solutionTable->rowCount() == 0) return; QUrl exportFile = QFileDialog::getSaveFileUrl(KStars::Instance(), i18n("Export Solution Points"), alignURLPath, "CSV File (*.csv)"); if (exportFile.isEmpty()) // if user presses cancel return; if (exportFile.toLocalFile().endsWith(QLatin1String(".csv")) == false) exportFile.setPath(exportFile.toLocalFile() + ".csv"); QString path = exportFile.toLocalFile(); if (QFile::exists(path)) { int r = KMessageBox::warningContinueCancel(nullptr, i18n("A file named \"%1\" already exists. " "Overwrite it?", exportFile.fileName()), i18n("Overwrite File?"), KStandardGuiItem::overwrite()); if (r == KMessageBox::Cancel) return; } if (!exportFile.isValid()) { QString message = i18n("Invalid URL: %1", exportFile.url()); KSNotification::sorry(message, i18n("Invalid URL")); return; } QFile file; 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; } QTextStream outstream(&file); QString epoch = QString::number(KStarsDateTime::currentDateTime().epoch()); outstream << "RA (J" << epoch << "),DE (J" << epoch << "),RA (degrees),DE (degrees),Name,RA Error (arcsec),DE Error (arcsec)" << endl; for (int i = 0; i < solutionTable->rowCount(); i++) { QTableWidgetItem *raCell = solutionTable->item(i, 0); QTableWidgetItem *deCell = solutionTable->item(i, 1); QTableWidgetItem *objNameCell = solutionTable->item(i, 2); QTableWidgetItem *raErrorCell = solutionTable->item(i, 4); QTableWidgetItem *deErrorCell = solutionTable->item(i, 5); if (!raCell || !deCell || !objNameCell || !raErrorCell || !deErrorCell) { KSNotification::sorry(i18n("Error in table structure.")); return; } dms raDMS = dms::fromString(raCell->text(), false); dms deDMS = dms::fromString(deCell->text(), true); outstream << raDMS.toHMSString() << ',' << deDMS.toDMSString() << ',' << raDMS.Degrees() << ',' << deDMS.Degrees() << ',' << objNameCell->text() << ',' << raErrorCell->text().remove('\"') << ',' << deErrorCell->text().remove('\"') << endl; } appendLogText(i18n("Solution Points Saved as: %1", path)); file.close(); } void Align::slotClearAllSolutionPoints() { if (solutionTable->rowCount() == 0) return; // if (Options::autonomousMode() || KMessageBox::questionYesNo( // KStars::Instance(), i18n("Are you sure you want to clear all of the solution points?"), // i18n("Clear Solution Points"), KStandardGuiItem::yes(), KStandardGuiItem::no()) == KMessageBox::Yes) // { // solutionTable->setRowCount(0); // alignPlot->graph(0)->data()->clear(); // alignPlot->clearItems(); // buildTarget(); // slotAutoScaleGraph(); // } connect(KSMessageBox::Instance(), &KSMessageBox::accepted, this, [this]() { //QObject::disconnect(KSMessageBox::Instance(), &KSMessageBox::accepted, this, nullptr); KSMessageBox::Instance()->disconnect(this); solutionTable->setRowCount(0); alignPlot->graph(0)->data()->clear(); alignPlot->clearItems(); buildTarget(); slotAutoScaleGraph(); }); KSMessageBox::Instance()->questionYesNo(i18n("Are you sure you want to clear all of the solution points?"), i18n("Clear Solution Points"), 60); } void Align::slotClearAllAlignPoints() { if (mountModel.alignTable->rowCount() == 0) return; if (KMessageBox::questionYesNo(&mountModelDialog, i18n("Are you sure you want to clear all the alignment points?"), i18n("Clear Align Points")) == KMessageBox::Yes) mountModel.alignTable->setRowCount(0); if (previewShowing) updatePreviewAlignPoints(); } void Align::slotRemoveSolutionPoint() { QCPAbstractItem *abstractItem = alignPlot->item(solutionTable->currentRow()); if (abstractItem) { QCPItemText *item = qobject_cast(abstractItem); if (item) { double point = item->position->key(); alignPlot->graph(0)->data()->remove(point); } } alignPlot->removeItem(solutionTable->currentRow()); for (int i = 0; i < alignPlot->itemCount(); i++) { QCPAbstractItem *abstractItem = alignPlot->item(i); if (abstractItem) { QCPItemText *item = qobject_cast(abstractItem); if (item) item->setText(QString::number(i + 1)); } } solutionTable->removeRow(solutionTable->currentRow()); alignPlot->replot(); } void Align::slotRemoveAlignPoint() { mountModel.alignTable->removeRow(mountModel.alignTable->currentRow()); if (previewShowing) updatePreviewAlignPoints(); } void Align::moveAlignPoint(int logicalIndex, int oldVisualIndex, int newVisualIndex) { Q_UNUSED(logicalIndex); for (int i = 0; i < mountModel.alignTable->columnCount(); i++) { QTableWidgetItem *oldItem = mountModel.alignTable->takeItem(oldVisualIndex, i); QTableWidgetItem *newItem = mountModel.alignTable->takeItem(newVisualIndex, i); mountModel.alignTable->setItem(newVisualIndex, i, oldItem); mountModel.alignTable->setItem(oldVisualIndex, i, newItem); } mountModel.alignTable->verticalHeader()->blockSignals(true); mountModel.alignTable->verticalHeader()->moveSection(newVisualIndex, oldVisualIndex); mountModel.alignTable->verticalHeader()->blockSignals(false); if (previewShowing) updatePreviewAlignPoints(); } void Align::swapAlignPoints(int firstPt, int secondPt) { for (int i = 0; i < mountModel.alignTable->columnCount(); i++) { QTableWidgetItem *firstPtItem = mountModel.alignTable->takeItem(firstPt, i); QTableWidgetItem *secondPtItem = mountModel.alignTable->takeItem(secondPt, i); mountModel.alignTable->setItem(firstPt, i, secondPtItem); mountModel.alignTable->setItem(secondPt, i, firstPtItem); } } void Align::slotMountModel() { generateAlignStarList(); SkyPoint spWest; spWest.setAlt(30); spWest.setAz(270); spWest.HorizontalToEquatorial(KStars::Instance()->data()->lst(), KStars::Instance()->data()->geo()->lat()); mountModel.alignDec->setValue(static_cast(spWest.dec().Degrees())); mountModelDialog.show(); } void Align::slotAddAlignPoint() { int currentRow = mountModel.alignTable->rowCount(); mountModel.alignTable->insertRow(currentRow); QTableWidgetItem *disabledBox = new QTableWidgetItem(); disabledBox->setFlags(Qt::ItemIsSelectable); mountModel.alignTable->setItem(currentRow, 3, disabledBox); } void Align::slotFindAlignObject() { KStarsData *data = KStarsData::Instance(); if (FindDialog::Instance()->exec() == QDialog::Accepted) { SkyObject *object = FindDialog::Instance()->targetObject(); if (object != nullptr) { SkyObject *o = object->clone(); o->updateCoords(data->updateNum(), true, data->geo()->lat(), data->lst(), false); int currentRow = mountModel.alignTable->rowCount(); mountModel.alignTable->insertRow(currentRow); QString ra_report, dec_report; getFormattedCoords(o->ra0().Hours(), o->dec0().Degrees(), ra_report, dec_report); QTableWidgetItem *RAReport = new QTableWidgetItem(); RAReport->setText(ra_report); RAReport->setTextAlignment(Qt::AlignHCenter); mountModel.alignTable->setItem(currentRow, 0, RAReport); QTableWidgetItem *DECReport = new QTableWidgetItem(); DECReport->setText(dec_report); DECReport->setTextAlignment(Qt::AlignHCenter); mountModel.alignTable->setItem(currentRow, 1, DECReport); QTableWidgetItem *ObjNameReport = new QTableWidgetItem(); ObjNameReport->setText(o->longname()); ObjNameReport->setTextAlignment(Qt::AlignHCenter); mountModel.alignTable->setItem(currentRow, 2, ObjNameReport); QTableWidgetItem *disabledBox = new QTableWidgetItem(); disabledBox->setFlags(Qt::ItemIsSelectable); mountModel.alignTable->setItem(currentRow, 3, disabledBox); } } if (previewShowing) updatePreviewAlignPoints(); } void Align::resetAlignmentProcedure() { mountModel.alignTable->setCellWidget(currentAlignmentPoint, 3, new QWidget()); QTableWidgetItem *statusReport = new QTableWidgetItem(); statusReport->setFlags(Qt::ItemIsSelectable); statusReport->setIcon(QIcon(":/icons/AlignWarning.svg")); mountModel.alignTable->setItem(currentAlignmentPoint, 3, statusReport); appendLogText(i18n("The Mount Model Tool is Reset.")); mountModel.startAlignB->setIcon( QIcon::fromTheme("media-playback-start")); mountModelRunning = false; currentAlignmentPoint = 0; abort(); } bool Align::alignmentPointsAreBad() { for (int i = 0; i < mountModel.alignTable->rowCount(); i++) { QTableWidgetItem *raCell = mountModel.alignTable->item(i, 0); if (!raCell) return true; QString raString = raCell->text(); if (dms().setFromString(raString, false) == false) return true; QTableWidgetItem *decCell = mountModel.alignTable->item(i, 1); if (!decCell) return true; QString decString = decCell->text(); if (dms().setFromString(decString, true) == false) return true; } return false; } void Align::startStopAlignmentProcedure() { if (!mountModelRunning) { if (mountModel.alignTable->rowCount() > 0) { if (alignmentPointsAreBad()) { KSNotification::error(i18n("Please Check the Alignment Points.")); return; } if (currentGotoMode == GOTO_NOTHING) { int r = KMessageBox::warningContinueCancel( nullptr, i18n("In the Align Module, \"Nothing\" is Selected for the Solver Action. This means that the " "mount model tool will not sync/align your mount but will only report the pointing model " "errors. Do you wish to continue?"), i18n("Pointing Model Report Only?"), KStandardGuiItem::cont(), KStandardGuiItem::cancel(), "nothing_selected_warning"); if (r == KMessageBox::Cancel) return; } if (currentAlignmentPoint == 0) { for (int row = 0; row < mountModel.alignTable->rowCount(); row++) { QTableWidgetItem *statusReport = new QTableWidgetItem(); statusReport->setIcon(QIcon()); mountModel.alignTable->setItem(row, 3, statusReport); } } mountModel.startAlignB->setIcon( QIcon::fromTheme("media-playback-pause")); mountModelRunning = true; appendLogText(i18n("The Mount Model Tool is Starting.")); startAlignmentPoint(); } } else { mountModel.startAlignB->setIcon( QIcon::fromTheme("media-playback-start")); mountModel.alignTable->setCellWidget(currentAlignmentPoint, 3, new QWidget()); appendLogText(i18n("The Mount Model Tool is Paused.")); abort(); mountModelRunning = false; QTableWidgetItem *statusReport = new QTableWidgetItem(); statusReport->setFlags(Qt::ItemIsSelectable); statusReport->setIcon(QIcon(":/icons/AlignWarning.svg")); mountModel.alignTable->setItem(currentAlignmentPoint, 3, statusReport); } } void Align::startAlignmentPoint() { if (mountModelRunning && currentAlignmentPoint >= 0 && currentAlignmentPoint < mountModel.alignTable->rowCount()) { QTableWidgetItem *raCell = mountModel.alignTable->item(currentAlignmentPoint, 0); QString raString = raCell->text(); dms raDMS = dms::fromString(raString, false); double ra = raDMS.Hours(); QTableWidgetItem *decCell = mountModel.alignTable->item(currentAlignmentPoint, 1); QString decString = decCell->text(); dms decDMS = dms::fromString(decString, true); double dec = decDMS.Degrees(); QProgressIndicator *alignIndicator = new QProgressIndicator(this); mountModel.alignTable->setCellWidget(currentAlignmentPoint, 3, alignIndicator); alignIndicator->startAnimation(); targetCoord.setRA0(ra); targetCoord.setDec0(dec); targetCoord.updateCoordsNow(KStarsData::Instance()->updateNum()); Slew(); } } void Align::finishAlignmentPoint(bool solverSucceeded) { if (mountModelRunning && currentAlignmentPoint >= 0 && currentAlignmentPoint < mountModel.alignTable->rowCount()) { mountModel.alignTable->setCellWidget(currentAlignmentPoint, 3, new QWidget()); QTableWidgetItem *statusReport = new QTableWidgetItem(); statusReport->setFlags(Qt::ItemIsSelectable); if (solverSucceeded) statusReport->setIcon(QIcon(":/icons/AlignSuccess.svg")); else statusReport->setIcon(QIcon(":/icons/AlignFailure.svg")); mountModel.alignTable->setItem(currentAlignmentPoint, 3, statusReport); currentAlignmentPoint++; if (currentAlignmentPoint < mountModel.alignTable->rowCount()) { startAlignmentPoint(); } else { mountModelRunning = false; mountModel.startAlignB->setIcon( QIcon::fromTheme("media-playback-start")); appendLogText(i18n("The Mount Model Tool is Finished.")); currentAlignmentPoint = 0; } } } bool Align::isParserOK() { bool rc = parser->init(); if (rc) { connect(parser, &AstrometryParser::solverFinished, this, &Ekos::Align::solverFinished, Qt::UniqueConnection); connect(parser, &AstrometryParser::solverFailed, this, &Ekos::Align::solverFailed, Qt::UniqueConnection); } return rc; } void Align::checkAlignmentTimeout() { if (loadSlewState != IPS_IDLE || ++solverIterations == MAXIMUM_SOLVER_ITERATIONS) abort(); else if (loadSlewState == IPS_IDLE) { appendLogText(i18n("Solver timed out.")); parser->stopSolver(); captureAndSolve(); } // TODO must also account for loadAndSlew. Retain file name } void Align::setSolverType(int type) { if (sender() == nullptr && type >= 0 && type <= 2) solverTypeGroup->button(type)->setChecked(true); syncSettings(); Options::setSolverType(type); switch (type) { case SOLVER_ONLINE: loadSlewB->setEnabled(true); if (onlineParser.get() != nullptr) { parser = onlineParser.get(); return; } onlineParser.reset(new Ekos::OnlineAstrometryParser()); parser = onlineParser.get(); break; case SOLVER_OFFLINE: loadSlewB->setEnabled(true); if (offlineParser.get() != nullptr) { parser = offlineParser.get(); return; } offlineParser.reset(new Ekos::OfflineAstrometryParser()); parser = offlineParser.get(); break; case SOLVER_REMOTE: loadSlewB->setEnabled(true); if (remoteParser.get() != nullptr && remoteParserDevice != nullptr) { parser = remoteParser.get(); (dynamic_cast(parser))->setAstrometryDevice(remoteParserDevice); return; } remoteParser.reset(new Ekos::RemoteAstrometryParser()); parser = remoteParser.get(); (dynamic_cast(parser))->setAstrometryDevice(remoteParserDevice); if (currentCCD) (dynamic_cast(parser))->setCCD(currentCCD->getDeviceName()); break; } parser->setAlign(this); if (parser->init()) { connect(parser, &AstrometryParser::solverFinished, this, &Ekos::Align::solverFinished, Qt::UniqueConnection); connect(parser, &AstrometryParser::solverFailed, this, &Ekos::Align::solverFailed, Qt::UniqueConnection); } else parser->disconnect(); } bool Align::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 Align::camera() { if (currentCCD) return currentCCD->getDeviceName(); return QString(); } void Align::setDefaultCCD(QString ccd) { syncSettings(); Options::setDefaultAlignCCD(ccd); } void Align::checkCCD(int ccdNum) { if (ccdNum == -1 || ccdNum >= CCDs.count()) { ccdNum = CCDCaptureCombo->currentIndex(); if (ccdNum == -1) return; } currentCCD = CCDs.at(ccdNum); ISD::CCDChip *targetChip = currentCCD->getChip(ISD::CCDChip::PRIMARY_CCD); if (targetChip && targetChip->isCapturing()) return; if (solverTypeGroup->checkedId() == SOLVER_REMOTE && remoteParser.get() != nullptr) (dynamic_cast(remoteParser.get()))->setCCD(currentCCD->getDeviceName()); syncCCDInfo(); /* FOVScopeCombo->blockSignals(true); ISD::CCD::TelescopeType type = currentCCD->getTelescopeType(); FOVScopeCombo->setCurrentIndex(type == ISD::CCD::TELESCOPE_UNKNOWN ? 0 : type); FOVScopeCombo->blockSignals(false); */ syncTelescopeInfo(); } void Align::addCCD(ISD::GDInterface *newCCD) { if (CCDs.contains(static_cast(newCCD))) { syncCCDInfo(); return; } CCDs.append(static_cast(newCCD)); CCDCaptureCombo->addItem(newCCD->getDeviceName()); checkCCD(); syncSettings(); } void Align::setTelescope(ISD::GDInterface *newTelescope) { currentTelescope = static_cast(newTelescope); currentTelescope->disconnect(this); connect(currentTelescope, &ISD::GDInterface::numberUpdated, this, &Ekos::Align::processNumber, Qt::UniqueConnection); connect(currentTelescope, &ISD::GDInterface::Disconnected, this, [this]() { m_isRateSynced = false; }); if (m_isRateSynced == false) { PAHSlewRateCombo->blockSignals(true); PAHSlewRateCombo->clear(); PAHSlewRateCombo->addItems(currentTelescope->slewRates()); if (Options::pAHMountSpeedIndex() >= 0) PAHSlewRateCombo->setCurrentIndex(Options::pAHMountSpeedIndex()); else PAHSlewRateCombo->setCurrentIndex(currentTelescope->getSlewRate()); PAHSlewRateCombo->blockSignals(false); m_isRateSynced = !currentTelescope->slewRates().empty(); } syncTelescopeInfo(); } void Align::setDome(ISD::GDInterface *newDome) { currentDome = static_cast(newDome); connect(currentDome, &ISD::GDInterface::switchUpdated, this, &Ekos::Align::processSwitch, Qt::UniqueConnection); } void Align::removeDevice(ISD::GDInterface *device) { device->disconnect(this); if (currentTelescope && !strcmp(currentTelescope->getDeviceName(), device->getDeviceName())) { currentTelescope = nullptr; m_isRateSynced = false; } else if (currentDome && !strcmp(currentDome->getDeviceName(), device->getDeviceName())) { currentDome = nullptr; } else if (currentRotator && !strcmp(currentRotator->getDeviceName(), device->getDeviceName())) { currentRotator = nullptr; } if (CCDs.contains(static_cast(device))) { CCDs.removeAll(static_cast(device)); CCDCaptureCombo->removeItem(CCDCaptureCombo->findText(device->getDeviceName())); CCDCaptureCombo->removeItem(CCDCaptureCombo->findText(device->getDeviceName() + QString(" Guider"))); if (CCDs.empty()) currentCCD = nullptr; checkCCD(); } if (Filters.contains(static_cast(device))) { Filters.removeAll(static_cast(device)); filterManager->removeDevice(device); FilterDevicesCombo->removeItem(FilterDevicesCombo->findText(device->getDeviceName())); if (Filters.empty()) currentFilter = nullptr; checkFilter(); } } bool Align::syncTelescopeInfo() { if (currentTelescope == nullptr || currentTelescope->isConnected() == false) return false; canSync = currentTelescope->canSync(); if (canSync == false && syncR->isEnabled()) { slewR->setChecked(true); appendLogText(i18n("Mount does not support syncing.")); } syncR->setEnabled(canSync); INumberVectorProperty *nvp = currentTelescope->getBaseDevice()->getNumber("TELESCOPE_INFO"); if (nvp) { INumber *np = IUFindNumber(nvp, "TELESCOPE_APERTURE"); if (np && np->value > 0) primaryAperture = np->value; np = IUFindNumber(nvp, "GUIDER_APERTURE"); if (np && np->value > 0) guideAperture = np->value; aperture = primaryAperture; //if (currentCCD && currentCCD->getTelescopeType() == ISD::CCD::TELESCOPE_GUIDE) if (FOVScopeCombo->currentIndex() == ISD::CCD::TELESCOPE_GUIDE) aperture = guideAperture; np = IUFindNumber(nvp, "TELESCOPE_FOCAL_LENGTH"); if (np && np->value > 0) primaryFL = np->value; np = IUFindNumber(nvp, "GUIDER_FOCAL_LENGTH"); if (np && np->value > 0) guideFL = np->value; focal_length = primaryFL; //if (currentCCD && currentCCD->getTelescopeType() == ISD::CCD::TELESCOPE_GUIDE) if (FOVScopeCombo->currentIndex() == ISD::CCD::TELESCOPE_GUIDE) focal_length = guideFL; } if (focal_length == -1 || aperture == -1) return false; if (ccd_hor_pixel != -1 && ccd_ver_pixel != -1 && focal_length != -1 && aperture != -1) { FOVScopeCombo->setItemData( ISD::CCD::TELESCOPE_PRIMARY, i18nc("F-Number, Focal Length, Aperture", "F%1 Focal Length: %2 mm Aperture: %3 mm2", QString::number(primaryFL / primaryAperture, 'f', 1), QString::number(primaryFL, 'f', 2), QString::number(primaryAperture, 'f', 2)), Qt::ToolTipRole); FOVScopeCombo->setItemData( ISD::CCD::TELESCOPE_GUIDE, i18nc("F-Number, Focal Length, Aperture", "F%1 Focal Length: %2 mm Aperture: %3 mm2", QString::number(guideFL / guideAperture, 'f', 1), QString::number(guideFL, 'f', 2), QString::number(guideAperture, 'f', 2)), Qt::ToolTipRole); calculateFOV(); generateArgs(); return true; } return false; } void Align::setTelescopeInfo(double primaryFocalLength, double primaryAperture, double guideFocalLength, double guideAperture) { if (primaryFocalLength > 0) primaryFL = primaryFocalLength; if (guideFocalLength > 0) guideFL = guideFocalLength; if (primaryAperture > 0) this->primaryAperture = primaryAperture; if (guideAperture > 0) this->guideAperture = guideAperture; focal_length = primaryFL; if (currentCCD && currentCCD->getTelescopeType() == ISD::CCD::TELESCOPE_GUIDE) focal_length = guideFL; aperture = primaryAperture; if (currentCCD && currentCCD->getTelescopeType() == ISD::CCD::TELESCOPE_GUIDE) aperture = guideAperture; syncTelescopeInfo(); } void Align::syncCCDInfo() { INumberVectorProperty *nvp = nullptr; if (currentCCD == nullptr) return; if (useGuideHead) nvp = currentCCD->getBaseDevice()->getNumber("GUIDER_INFO"); else nvp = currentCCD->getBaseDevice()->getNumber("CCD_INFO"); if (nvp) { INumber *np = IUFindNumber(nvp, "CCD_PIXEL_SIZE_X"); if (np && np->value > 0) ccd_hor_pixel = ccd_ver_pixel = np->value; np = IUFindNumber(nvp, "CCD_PIXEL_SIZE_Y"); if (np && np->value > 0) ccd_ver_pixel = np->value; np = IUFindNumber(nvp, "CCD_PIXEL_SIZE_Y"); if (np && np->value > 0) ccd_ver_pixel = np->value; } ISD::CCDChip *targetChip = currentCCD->getChip(useGuideHead ? ISD::CCDChip::GUIDE_CCD : ISD::CCDChip::PRIMARY_CCD); ISwitchVectorProperty *svp = currentCCD->getBaseDevice()->getSwitch("WCS_CONTROL"); if (svp) setWCSEnabled(Options::astrometrySolverWCS()); targetChip->setImageView(alignView, FITS_ALIGN); targetChip->getFrameMinMax(nullptr, nullptr, nullptr, nullptr, nullptr, &ccd_width, nullptr, &ccd_height); //targetChip->getFrame(&x,&y,&ccd_width,&ccd_height); binningCombo->setEnabled(targetChip->canBin()); if (targetChip->canBin()) { binningCombo->blockSignals(true); int binx = 1, biny = 1; targetChip->getMaxBin(&binx, &biny); binningCombo->clear(); for (int i = 0; i < binx; i++) binningCombo->addItem(QString("%1x%2").arg(i + 1).arg(i + 1)); // By default, set to maximum binning since the solver behaves better this way // solverBinningIndex is set by default to 4, but as soon as the user changes the binning, it changes // to whatever value the user selected. if (Options::solverBinningIndex() == 4 && binningCombo->count() <= 4) { binningCombo->setCurrentIndex(binningCombo->count() - 1); Options::setSolverBinningIndex(binningCombo->count() - 1); } else binningCombo->setCurrentIndex(Options::solverBinningIndex()); binningCombo->blockSignals(false); } if (ccd_hor_pixel == -1 || ccd_ver_pixel == -1) return; if (ccd_hor_pixel != -1 && ccd_ver_pixel != -1 && focal_length != -1 && aperture != -1) { calculateFOV(); generateArgs(); } } void Align::getFOVScale(double &fov_w, double &fov_h, double &fov_scale) { fov_w = fov_x; fov_h = fov_y; fov_scale = fov_pixscale; } QList Align::fov() { QList result; result << fov_x << fov_y << fov_pixscale; return result; } void Align::getCalculatedFOVScale(double &fov_w, double &fov_h, double &fov_scale) { // FOV in arcsecs fov_w = 206264.8062470963552 * ccd_width * ccd_hor_pixel / 1000.0 / focal_length; fov_h = 206264.8062470963552 * ccd_height * ccd_ver_pixel / 1000.0 / focal_length; // Pix Scale fov_scale = (fov_w * (Options::solverBinningIndex() + 1)) / ccd_width; // FOV in arcmins fov_w /= 60.0; fov_h /= 60.0; } void Align::calculateFOV() { // Calculate FOV // FOV in arcsecs fov_x = 206264.8062470963552 * ccd_width * ccd_hor_pixel / 1000.0 / focal_length; fov_y = 206264.8062470963552 * ccd_height * ccd_ver_pixel / 1000.0 / focal_length; // Pix Scale fov_pixscale = (fov_x * (Options::solverBinningIndex() + 1)) / ccd_width; // FOV in arcmins fov_x /= 60.0; fov_y /= 60.0; QString calculatedFOV = (QString("%1' x %2'").arg(fov_x, 0, 'g', 3).arg(fov_y, 0, 'g', 3)); // JM 2018-04-20 Above calculations are for RAW FOV. Starting from 2.9.5, we are using EFFECTIVE FOV // Which is the real FOV as measured from the plate solution. The effective FOVs are stored in the database and are unique // per profile/pixel_size/focal_length combinations. It defaults to 0' x 0' and gets updated after the first successful solver is complete. getEffectiveFOV(); if (fov_x == 0) { //FOVOut->setReadOnly(false); FOVOut->setToolTip(i18n("

Effective field of view size in arcminutes.

Please capture and solve once to measure the effective FOV or enter the values manually.

Calculated FOV: %1

", calculatedFOV)); } else { FOVOut->setToolTip(i18n("

Effective field of view size in arcminutes.

")); //FOVOut->setReadOnly(true); } solverFOV->setSize(fov_x, fov_y); sensorFOV->setSize(fov_x, fov_y); if (currentCCD) sensorFOV->setName(currentCCD->getDeviceName()); FOVOut->setText(QString("%1' x %2'").arg(fov_x, 0, 'g', 3).arg(fov_y, 0, 'g', 3)); if (((fov_x + fov_y) / 2.0) > PAH_CUTOFF_FOV) { if (isPAHReady == false) { PAHWidgets->setEnabled(true); isPAHReady = true; emit PAHEnabled(true); PAHWidgets->setToolTip(QString()); FOVDisabledLabel->hide(); } } else if (PAHWidgets->isEnabled()) { PAHWidgets->setEnabled(false); isPAHReady = false; emit PAHEnabled(false); PAHWidgets->setToolTip(i18n( "

Polar Alignment Helper tool requires the following:

1. German Equatorial Mount

2. FOV >" " 0.5 degrees

For small FOVs, use the Legacy Polar Alignment Tool.

")); FOVDisabledLabel->show(); } if (opsAstrometry->kcfg_AstrometryUseImageScale->isChecked()) { int unitType = opsAstrometry->kcfg_AstrometryImageScaleUnits->currentIndex(); // Degrees if (unitType == 0) { double fov_low = qMin(fov_x / 60, fov_y / 60); double fov_high = qMax(fov_x / 60, fov_y / 60); opsAstrometry->kcfg_AstrometryImageScaleLow->setValue(fov_low); opsAstrometry->kcfg_AstrometryImageScaleHigh->setValue(fov_high); Options::setAstrometryImageScaleLow(fov_low); Options::setAstrometryImageScaleHigh(fov_high); } // Arcmins else if (unitType == 1) { double fov_low = qMin(fov_x, fov_y); double fov_high = qMax(fov_x, fov_y); opsAstrometry->kcfg_AstrometryImageScaleLow->setValue(fov_low); opsAstrometry->kcfg_AstrometryImageScaleHigh->setValue(fov_high); Options::setAstrometryImageScaleLow(fov_low); Options::setAstrometryImageScaleHigh(fov_high); } // Arcsec per pixel else { opsAstrometry->kcfg_AstrometryImageScaleLow->setValue(fov_pixscale * 0.9); opsAstrometry->kcfg_AstrometryImageScaleHigh->setValue(fov_pixscale * 1.1); // 10% boundary Options::setAstrometryImageScaleLow(fov_pixscale * 0.9); Options::setAstrometryImageScaleHigh(fov_pixscale * 1.1); } } } QStringList Align::generateOptions(const QVariantMap &optionsMap) { // -O overwrite // -3 Expected RA // -4 Expected DEC // -5 Radius (deg) // -L lower scale of image in arcminutes // -H upper scale of image in arcminutes // -u aw set scale to be in arcminutes // -W solution.wcs name of solution file // apog1.jpg name of target file to analyze //solve-field -O -3 06:40:51 -4 +09:49:53 -5 1 -L 40 -H 100 -u aw -W solution.wcs apod1.jpg QStringList solver_args; // Start with always-used arguments solver_args << "-O" << "--no-plots"; // Now go over boolean options // noverify if (optionsMap.contains("noverify")) solver_args << "--no-verify"; // noresort if (optionsMap.contains("resort")) solver_args << "--resort"; // fits2fits if (optionsMap.contains("nofits2fits")) solver_args << "--no-fits2fits"; // downsample if (optionsMap.contains("downsample")) solver_args << "--downsample" << QString::number(optionsMap.value("downsample", 2).toInt()); // image scale low if (optionsMap.contains("scaleL")) solver_args << "-L" << QString::number(optionsMap.value("scaleL").toDouble()); // image scale high if (optionsMap.contains("scaleH")) solver_args << "-H" << QString::number(optionsMap.value("scaleH").toDouble()); // image scale units if (optionsMap.contains("scaleUnits")) solver_args << "-u" << optionsMap.value("scaleUnits").toString(); // RA if (optionsMap.contains("ra")) solver_args << "-3" << QString::number(optionsMap.value("ra").toDouble()); // DE if (optionsMap.contains("de")) solver_args << "-4" << QString::number(optionsMap.value("de").toDouble()); // Radius if (optionsMap.contains("radius")) solver_args << "-5" << QString::number(optionsMap.value("radius").toDouble()); // Custom if (optionsMap.contains("custom")) solver_args << optionsMap.value("custom").toString(); return solver_args; } //This will generate the high and low scale of the imager field size based on the stated units. void Align::generateFOVBounds(double fov_h, double fov_v, QString &fov_low, QString &fov_high) { double fov_lower, fov_upper; // let's stretch the boundaries by 5% fov_lower = ((fov_h < fov_v) ? (fov_h * 0.95) : (fov_v * 0.95)); fov_upper = ((fov_h > fov_v) ? (fov_h * 1.05) : (fov_v * 1.05)); //No need to do anything if they are aw, since that is the default fov_low = QString::number(fov_lower); fov_high = QString::number(fov_upper); } void Align::generateArgs() { // -O overwrite // -3 Expected RA // -4 Expected DEC // -5 Radius (deg) // -L lower scale of image in arcminutes // -H upper scale of image in arcminutes // -u aw set scale to be in arcminutes // -W solution.wcs name of solution file // apog1.jpg name of target file to analyze //solve-field -O -3 06:40:51 -4 +09:49:53 -5 1 -L 40 -H 100 -u aw -W solution.wcs apod1.jpg QVariantMap optionsMap; if (Options::astrometryUseNoVerify()) optionsMap["noverify"] = true; if (Options::astrometryUseResort()) optionsMap["resort"] = true; if (Options::astrometryUseNoFITS2FITS()) optionsMap["nofits2fits"] = true; if (Options::astrometryUseDownsample()) { if (Options::astrometryAutoDownsample() && ccd_width && ccd_height) { uint8_t bin = qMax(Options::solverBinningIndex() + 1, 1u); uint16_t w = ccd_width / bin; optionsMap["downsample"] = getSolverDownsample(w); } else optionsMap["downsample"] = Options::astrometryDownsample(); } if (Options::astrometryUseImageScale() && fov_x > 0 && fov_y > 0) { QString units = ImageScales[Options::astrometryImageScaleUnits()]; if (Options::astrometryAutoUpdateImageScale()) { QString fov_low, fov_high; double fov_w = fov_x; double fov_h = fov_y; if (units == "dw") { fov_w /= 60; fov_h /= 60; } else if (units == "app") { fov_w = fov_pixscale; fov_h = fov_pixscale; } generateFOVBounds(fov_w, fov_h, fov_low, fov_high); optionsMap["scaleL"] = fov_low; optionsMap["scaleH"] = fov_high; optionsMap["scaleUnits"] = units; } else { optionsMap["scaleL"] = Options::astrometryImageScaleLow(); optionsMap["scaleH"] = Options::astrometryImageScaleHigh(); optionsMap["scaleUnits"] = units; } } if (Options::astrometryUsePosition() && currentTelescope != nullptr) { double ra = 0, dec = 0; currentTelescope->getEqCoords(&ra, &dec); optionsMap["ra"] = ra * 15.0; optionsMap["de"] = dec; optionsMap["radius"] = Options::astrometryRadius(); } if (Options::astrometryCustomOptions().isEmpty() == false) optionsMap["custom"] = Options::astrometryCustomOptions(); QStringList solverArgs = generateOptions(optionsMap); QString options = solverArgs.join(" "); solverOptions->setText(options); solverOptions->setToolTip(options); } bool Align::captureAndSolve() { m_AlignTimer.stop(); m_CaptureTimer.stop(); #ifdef Q_OS_OSX if(solverTypeGroup->checkedId() == SOLVER_OFFLINE) { if(Options::useDefaultPython()) { if( !opsAlign->astropyInstalled() || !opsAlign->pythonInstalled() ) { KSNotification::error(i18n("Astrometry.net uses python3 and the astropy package for plate solving images offline. These were not detected on your system. Please go into the Align Options and either click the setup button to install them or uncheck the default button and enter the path to python3 on your system and manually install astropy.")); return false; } } } #endif if (currentCCD == nullptr) return false; if (currentCCD->isConnected() == false) { appendLogText(i18n("Error: lost connection to CCD.")); KSNotification::event(QLatin1String("AlignFailed"), i18n("Astrometry alignment failed"), KSNotification::EVENT_ALERT); return false; } if (currentCCD->isBLOBEnabled() == false) { currentCCD->setBLOBEnabled(true); } // If CCD Telescope Type does not match desired scope type, change it // but remember current value so that it can be reset once capture is complete or is aborted. if (currentCCD->getTelescopeType() != FOVScopeCombo->currentIndex()) { rememberTelescopeType = currentCCD->getTelescopeType(); currentCCD->setTelescopeType(static_cast(FOVScopeCombo->currentIndex())); } if (parser->init() == false) return false; if (focal_length == -1 || aperture == -1) { KSNotification::error(i18n("Telescope aperture and focal length are missing. Please check your driver settings and try again.")); return false; } if (ccd_hor_pixel == -1 || ccd_ver_pixel == -1) { KSNotification::error(i18n("CCD pixel size is missing. Please check your driver settings and try again.")); return false; } if (currentFilter != nullptr) { if (currentFilter->isConnected() == false) { appendLogText(i18n("Error: lost connection to filter wheel.")); return false; } int targetPosition = FilterPosCombo->currentIndex() + 1; if (targetPosition > 0 && targetPosition != currentFilterPosition) { filterPositionPending = true; filterManager->setFilterPosition(targetPosition); state = ALIGN_PROGRESS; return true; } } if (currentCCD->getDriverInfo()->getClientManager()->getBLOBMode(currentCCD->getDeviceName(), "CCD1") == B_NEVER) { if (KMessageBox::questionYesNo( nullptr, i18n("Image transfer is disabled for this camera. Would you like to enable it?")) == KMessageBox::Yes) { currentCCD->getDriverInfo()->getClientManager()->setBLOBMode(B_ONLY, currentCCD->getDeviceName(), "CCD1"); currentCCD->getDriverInfo()->getClientManager()->setBLOBMode(B_ONLY, currentCCD->getDeviceName(), "CCD2"); } else { return false; } } double seqExpose = exposureIN->value(); ISD::CCDChip *targetChip = currentCCD->getChip(useGuideHead ? ISD::CCDChip::GUIDE_CCD : ISD::CCDChip::PRIMARY_CCD); if (focusState >= FOCUS_PROGRESS) { appendLogText(i18n("Cannot capture while focus module is busy. Retrying in 10 seconds...")); m_CaptureTimer.start(); return false; } if (targetChip->isCapturing()) { appendLogText(i18n("Cannot capture while CCD exposure is in progress. Retrying in 10 seconds...")); m_CaptureTimer.start(); return false; } alignView->setBaseSize(alignWidget->size()); connect(currentCCD, &ISD::CCD::BLOBUpdated, this, &Ekos::Align::newFITS); connect(currentCCD, &ISD::CCD::newExposureValue, this, &Ekos::Align::checkCCDExposureProgress); // In case of remote solver, check if we need to update active CCD if (solverTypeGroup->checkedId() == SOLVER_REMOTE && remoteParser.get() != nullptr) { // Update ACTIVE_CCD of the remote astrometry driver so it listens to BLOB emitted by the CCD ITextVectorProperty *activeDevices = remoteParserDevice->getBaseDevice()->getText("ACTIVE_DEVICES"); if (activeDevices) { IText *activeCCD = IUFindText(activeDevices, "ACTIVE_CCD"); if (QString(activeCCD->text) != CCDCaptureCombo->currentText()) { IUSaveText(activeCCD, CCDCaptureCombo->currentText().toLatin1().data()); remoteParserDevice->getDriverInfo()->getClientManager()->sendNewText(activeDevices); } } // Enable remote parse dynamic_cast(remoteParser.get())->setEnabled(true); QString options = solverOptions->text().simplified(); QStringList solverArgs = options.split(' '); dynamic_cast(remoteParser.get())->sendArgs(solverArgs); // If mount model was reset, we do not update targetCoord // since the RA/DE is now different immediately after the reset // so we still try to lock for the coordinates before the reset. if (solverIterations == 0 && mountModelReset == false) { double ra, dec; currentTelescope->getEqCoords(&ra, &dec); targetCoord.setRA(ra); targetCoord.setDec(dec); } mountModelReset = false; solverTimer.start(); } //else //{ if (currentCCD->getUploadMode() == ISD::CCD::UPLOAD_LOCAL) { rememberUploadMode = ISD::CCD::UPLOAD_LOCAL; currentCCD->setUploadMode(ISD::CCD::UPLOAD_CLIENT); } rememberCCDExposureLooping = currentCCD->isLooping(); if (rememberCCDExposureLooping) currentCCD->setExposureLoopingEnabled(false); // Remove temporary FITS files left before by the solver QDir dir(QDir::tempPath()); dir.setNameFilters(QStringList() << "fits*" << "tmp.*"); dir.setFilter(QDir::Files); for (auto &dirFile : dir.entryList()) dir.remove(dirFile); //} currentCCD->setTransformFormat(ISD::CCD::FORMAT_FITS); targetChip->resetFrame(); targetChip->setBatchMode(false); targetChip->setCaptureMode(FITS_ALIGN); targetChip->setFrameType(FRAME_LIGHT); int bin = Options::solverBinningIndex() + 1; targetChip->setBinning(bin, bin); // In case we're in refresh phase of the polar alignment helper then we use capture value from there if (pahStage == PAH_REFRESH) targetChip->capture(PAHExposure->value()); else targetChip->capture(seqExpose); Options::setAlignExposure(seqExpose); solveB->setEnabled(false); stopB->setEnabled(true); pi->startAnimation(); differentialSlewingActivated = false; state = ALIGN_PROGRESS; emit newStatus(state); // If we're just refreshing, then we're done if (pahStage == PAH_REFRESH) return true; appendLogText(i18n("Capturing image...")); //This block of code will create the row in the solution table and populate RA, DE, and object name. //It also starts the progress indicator. double ra, dec; currentTelescope->getEqCoords(&ra, &dec); if (loadSlewState == IPS_IDLE) { int currentRow = solutionTable->rowCount(); solutionTable->insertRow(currentRow); for (int i = 4; i < 6; i++) { QTableWidgetItem *disabledBox = new QTableWidgetItem(); disabledBox->setFlags(Qt::ItemIsSelectable); solutionTable->setItem(currentRow, i, disabledBox); } QTableWidgetItem *RAReport = new QTableWidgetItem(); RAReport->setText(ScopeRAOut->text()); RAReport->setTextAlignment(Qt::AlignHCenter); RAReport->setFlags(Qt::ItemIsSelectable); solutionTable->setItem(currentRow, 0, RAReport); QTableWidgetItem *DECReport = new QTableWidgetItem(); DECReport->setText(ScopeDecOut->text()); DECReport->setTextAlignment(Qt::AlignHCenter); DECReport->setFlags(Qt::ItemIsSelectable); solutionTable->setItem(currentRow, 1, DECReport); double maxrad = 1.0; SkyObject *so = KStarsData::Instance()->skyComposite()->objectNearest(new SkyPoint(dms(ra * 15), dms(dec)), maxrad); QString name; if (so) { name = so->longname(); } else { name = "None"; } QTableWidgetItem *ObjNameReport = new QTableWidgetItem(); ObjNameReport->setText(name); ObjNameReport->setTextAlignment(Qt::AlignHCenter); ObjNameReport->setFlags(Qt::ItemIsSelectable); solutionTable->setItem(currentRow, 2, ObjNameReport); #ifdef Q_OS_OSX repaint(); //This is a band-aid for a bug in QT 5.10.0 #endif QProgressIndicator *alignIndicator = new QProgressIndicator(this); solutionTable->setCellWidget(currentRow, 3, alignIndicator); alignIndicator->startAnimation(); #ifdef Q_OS_OSX repaint(); //This is a band-aid for a bug in QT 5.10.0 #endif } return true; } void Align::newFITS(IBLOB *bp) { // Ignore guide head if there is any. if (!strcmp(bp->name, "CCD2")) return; disconnect(currentCCD, &ISD::CCD::BLOBUpdated, this, &Ekos::Align::newFITS); disconnect(currentCCD, &ISD::CCD::newExposureValue, this, &Ekos::Align::checkCCDExposureProgress); blobType = *(static_cast(bp->aux1)); blobFileName = QString(static_cast(bp->aux2)); // If it's Refresh, we're done if (pahStage == PAH_REFRESH) { setCaptureComplete(); return; } appendLogText(i18n("Image received.")); if (solverTypeGroup->checkedId() != SOLVER_REMOTE) { if (blobType == ISD::CCD::BLOB_FITS) { ISD::CCDChip *targetChip = currentCCD->getChip(useGuideHead ? ISD::CCDChip::GUIDE_CCD : ISD::CCDChip::PRIMARY_CCD); if (alignDarkFrameCheck->isChecked()) { int x, y, w, h, binx = 1, biny = 1; targetChip->getFrame(&x, &y, &w, &h); targetChip->getBinning(&binx, &biny); uint16_t offsetX = x / binx; uint16_t offsetY = y / biny; FITSData *darkData = DarkLibrary::Instance()->getDarkFrame(targetChip, exposureIN->value()); connect(DarkLibrary::Instance(), &DarkLibrary::darkFrameCompleted, this, [&](bool completed) { DarkLibrary::Instance()->disconnect(this); alignDarkFrameCheck->setChecked(completed); if (completed) setCaptureComplete(); else abort(); }); connect(DarkLibrary::Instance(), &DarkLibrary::newLog, this, &Ekos::Align::appendLogText); if (darkData) DarkLibrary::Instance()->subtract(darkData, alignView, FITS_NONE, offsetX, offsetY); else { DarkLibrary::Instance()->captureAndSubtract(targetChip, alignView, exposureIN->value(), offsetX, offsetY); } return; } } setCaptureComplete(); } } void Align::setCaptureComplete() { DarkLibrary::Instance()->disconnect(this); if (pahStage == PAH_REFRESH) { newFrame(alignView); captureAndSolve(); return; } emit newImage(alignView); if (solverTypeGroup->checkedId() == SOLVER_ONLINE && Options::astrometryUseJPEG()) { ISD::CCDChip *targetChip = currentCCD->getChip(useGuideHead ? ISD::CCDChip::GUIDE_CCD : ISD::CCDChip::PRIMARY_CCD); if (targetChip) { QString jpegFile = blobFileName + ".jpg"; bool rc = alignView->getDisplayImage().save(jpegFile, "JPG"); if (rc) blobFileName = jpegFile; } } if (getSolverFOV()) getSolverFOV()->setImage(alignView->getDisplayImage()); startSolving(blobFileName); } void Align::setSolverAction(int mode) { gotoModeButtonGroup->button(mode)->setChecked(true); currentGotoMode = static_cast(mode); } void Align::startSolving(const QString &filename, bool isGenerated) { QStringList solverArgs; QString options = solverOptions->text().simplified(); if (isGenerated) { solverArgs = options.split(' '); // Replace RA and DE with LST & 90/-90 pole if (pahStage == PAH_FIRST_CAPTURE) { for (int i = 0; i < solverArgs.count(); i++) { // RA if (solverArgs[i] == "-3") solverArgs[i + 1] = QString::number(KStarsData::Instance()->lst()->Degrees()); // DE. +90 for Northern hemisphere. -90 for southern hemisphere else if (solverArgs[i] == "-4") solverArgs[i + 1] = QString::number(hemisphere == NORTH_HEMISPHERE ? 90 : -90); } } } else if (filename.endsWith(QLatin1String("fits")) || filename.endsWith(QLatin1String("fit"))) { solverArgs = getSolverOptionsFromFITS(filename); appendLogText(i18n("Using solver options: %1", solverArgs.join(' '))); } else { KGuiItem blindItem(i18n("Blind solver"), QString(), i18n("Blind solver takes a very long time to solve but can reliably solve any image any " "where in the sky given enough time.")); KGuiItem existingItem(i18n("Use existing settings"), QString(), i18n("Mount must be pointing close to the target location and current field of view must " "match the image's field of view.")); int rc = KMessageBox::questionYesNoCancel(nullptr, i18n("No metadata is available in this image. Do you want to use the " "blind solver or the existing solver settings?"), i18n("Astrometry solver"), blindItem, existingItem, KStandardGuiItem::cancel(), "blind_solver_or_existing_solver_option"); if (rc == KMessageBox::Yes) { QVariantMap optionsMap; if (Options::astrometryUseNoVerify()) optionsMap["noverify"] = true; if (Options::astrometryUseResort()) optionsMap["resort"] = true; if (Options::astrometryUseNoFITS2FITS()) optionsMap["nofits2fits"] = true; if (Options::astrometryUseDownsample()) optionsMap["downsample"] = Options::astrometryDownsample(); solverArgs = generateOptions(optionsMap); } else if (rc == KMessageBox::No) solverArgs = options.split(' '); else { abort(); return; } } if (solverIterations == 0 && mountModelReset == false) { double ra, dec; currentTelescope->getEqCoords(&ra, &dec); targetCoord.setRA(ra); targetCoord.setDec(dec); } mountModelReset = false; //Options::setSolverOptions(solverOptions->text()); //Options::setGuideScopeCCDs(guideScopeCCDs); Options::setSolverAccuracyThreshold(accuracySpin->value()); Options::setAlignDarkFrame(alignDarkFrameCheck->isChecked()); Options::setSolverGotoOption(currentGotoMode); //m_isSolverComplete = false; //m_isSolverSuccessful = false; if (fov_x > 0) parser->verifyIndexFiles(fov_x, fov_y); solverTimer.start(); m_AlignTimer.start(); if (currentGotoMode == GOTO_SLEW) appendLogText(i18n("Solver iteration #%1", solverIterations + 1)); state = ALIGN_PROGRESS; emit newStatus(state); parser->startSovler(filename, solverArgs, isGenerated); } void Align::solverFinished(double orientation, double ra, double dec, double pixscale) { pi->stopAnimation(); stopB->setEnabled(false); solveB->setEnabled(true); sOrientation = orientation; sRA = ra; sDEC = dec; // Reset Telescope Type to remembered value if (rememberTelescopeType != ISD::CCD::TELESCOPE_UNKNOWN) { currentCCD->setTelescopeType(rememberTelescopeType); rememberTelescopeType = ISD::CCD::TELESCOPE_UNKNOWN; } m_AlignTimer.stop(); if (solverTypeGroup->checkedId() == SOLVER_REMOTE && remoteParser.get() != nullptr) { // Disable remote parse dynamic_cast(remoteParser.get())->setEnabled(false); } int binx, biny; ISD::CCDChip *targetChip = currentCCD->getChip(useGuideHead ? ISD::CCDChip::GUIDE_CCD : ISD::CCDChip::PRIMARY_CCD); targetChip->getBinning(&binx, &biny); if (Options::alignmentLogging()) appendLogText(i18n("Solver RA (%1) DEC (%2) Orientation (%3) Pixel Scale (%4)", QString::number(ra, 'g', 5), QString::number(dec, 'g', 5), QString::number(orientation, 'g', 5), QString::number(pixscale, 'g', 5))); #if 0 if (pixscale > 0 && loadSlewState == IPS_IDLE) { double solver_focal_length = (206.264 * ccd_hor_pixel) / pixscale * binx; if (fabs(focal_length - solver_focal_length) > 1) appendLogText(i18n("Current focal length is %1 mm while computed focal length from the solver is %2 mm. " "Please update the mount focal length to obtain accurate results.", QString::number(focal_length, 'g', 5), QString::number(solver_focal_length, 'g', 5))); } #endif if (fov_x == 0 && pixscale > 0) { double newFOVW = ccd_width * pixscale / binx / 60.0; double newFOVH = ccd_height * pixscale / biny / 60.0; saveNewEffectiveFOV(newFOVW, newFOVH); } alignCoord.setRA0(ra / 15.0); alignCoord.setDec0(dec); RotOut->setText(QString::number(orientation, 'g', 5)); // Convert to JNow alignCoord.apparentCoord(static_cast(J2000), KStars::Instance()->data()->ut().djd()); // Get horizontal coords alignCoord.EquatorialToHorizontal(KStarsData::Instance()->lst(), KStarsData::Instance()->geo()->lat()); // double raDiff = (alignCoord.ra().Degrees() - targetCoord.ra().Degrees()) * 3600; // double deDiff = (alignCoord.dec().Degrees() - targetCoord.dec().Degrees()) * 3600; double raDiff = (alignCoord.ra().deltaAngle(targetCoord.ra())).Degrees() * 3600; double deDiff = (alignCoord.dec().deltaAngle(targetCoord.dec())).Degrees() * 3600; dms RADiff(fabs(raDiff) / 3600.0), DEDiff(deDiff / 3600.0); dRAOut->setText(QString("%1%2").arg((raDiff > 0 ? "+" : "-"), RADiff.toHMSString())); dDEOut->setText(DEDiff.toDMSString(true)); pixScaleOut->setText(QString::number(pixscale, 'f', 2)); //emit newSolutionDeviation(raDiff, deDiff); targetDiff = sqrt(raDiff * raDiff + deDiff * deDiff); // Because astrometry reads image upside-down (bottom to top), the orientation is rotated 180 degrees when compared to PA // PA = Orientation + 180 double solverPA = orientation + 180; // Limit PA to -180 to +180 if (solverPA > 180) solverPA -= 360; if (solverPA < -180) solverPA += 360; solverFOV->setCenter(alignCoord); solverFOV->setPA(solverPA); solverFOV->setImageDisplay(Options::astrometrySolverOverlay()); sensorFOV->setPA(solverPA); QString ra_dms, dec_dms; getFormattedCoords(alignCoord.ra().Hours(), alignCoord.dec().Degrees(), ra_dms, dec_dms); SolverRAOut->setText(ra_dms); SolverDecOut->setText(dec_dms); //This block of code will write the result into the solution table and plot it on the graph. int currentRow = solutionTable->rowCount() - 1; if (loadSlewState == IPS_IDLE) { QTableWidgetItem *dRAReport = new QTableWidgetItem(); if (dRAReport) { dRAReport->setText(QString::number(raDiff, 'f', 3) + "\""); dRAReport->setTextAlignment(Qt::AlignHCenter); dRAReport->setFlags(Qt::ItemIsSelectable); solutionTable->setItem(currentRow, 4, dRAReport); } QTableWidgetItem *dDECReport = new QTableWidgetItem(); if (dDECReport) { dDECReport->setText(QString::number(deDiff, 'f', 3) + "\""); dDECReport->setTextAlignment(Qt::AlignHCenter); dDECReport->setFlags(Qt::ItemIsSelectable); solutionTable->setItem(currentRow, 5, dDECReport); } double raPlot = raDiff; double decPlot = deDiff; alignPlot->graph(0)->addData(raPlot, decPlot); QCPItemText *textLabel = new QCPItemText(alignPlot); textLabel->setPositionAlignment(Qt::AlignVCenter | Qt::AlignHCenter); textLabel->position->setType(QCPItemPosition::ptPlotCoords); textLabel->position->setCoords(raPlot, decPlot); textLabel->setColor(Qt::red); textLabel->setPadding(QMargins(0, 0, 0, 0)); textLabel->setBrush(Qt::white); //textLabel->setBrush(Qt::NoBrush); textLabel->setPen(Qt::NoPen); textLabel->setText(' ' + QString::number(solutionTable->rowCount()) + ' '); textLabel->setFont(QFont(font().family(), 8)); if (!alignPlot->xAxis->range().contains(raDiff)) { alignPlot->graph(0)->rescaleKeyAxis(true); alignPlot->yAxis->setScaleRatio(alignPlot->xAxis, 1.0); } if (!alignPlot->yAxis->range().contains(deDiff)) { alignPlot->graph(0)->rescaleValueAxis(true); alignPlot->xAxis->setScaleRatio(alignPlot->yAxis, 1.0); } alignPlot->replot(); } if (Options::astrometrySolverWCS()) { INumberVectorProperty *ccdRotation = currentCCD->getBaseDevice()->getNumber("CCD_ROTATION"); if (ccdRotation) { INumber *rotation = IUFindNumber(ccdRotation, "CCD_ROTATION_VALUE"); if (rotation) { ClientManager *clientManager = currentCCD->getDriverInfo()->getClientManager(); rotation->value = orientation; clientManager->sendNewNumber(ccdRotation); if (m_wcsSynced == false) { appendLogText( i18n("WCS information updated. Images captured from this point forward shall have valid WCS.")); // Just send telescope info in case the CCD driver did not pick up before. INumberVectorProperty *telescopeInfo = currentTelescope->getBaseDevice()->getNumber("TELESCOPE_INFO"); if (telescopeInfo) clientManager->sendNewNumber(telescopeInfo); m_wcsSynced = true; } } } } m_CaptureErrorCounter = 0; m_SlewErrorCounter = 0; m_CaptureTimeoutCounter = 0; appendLogText(i18n("Solution coordinates: RA (%1) DEC (%2) Telescope Coordinates: RA (%3) DEC (%4)", alignCoord.ra().toHMSString(), alignCoord.dec().toDMSString(), telescopeCoord.ra().toHMSString(), telescopeCoord.dec().toDMSString())); if (loadSlewState == IPS_IDLE && currentGotoMode == GOTO_SLEW) { dms diffDeg(targetDiff / 3600.0); appendLogText(i18n("Target is within %1 degrees of solution coordinates.", diffDeg.toDMSString())); } if (rememberUploadMode != currentCCD->getUploadMode()) currentCCD->setUploadMode(rememberUploadMode); if (rememberCCDExposureLooping) currentCCD->setExposureLoopingEnabled(true); //if (syncR->isChecked() || nothingR->isChecked() || targetDiff <= accuracySpin->value()) // CONTINUE HERE //This block of code along with some sections in the switch below will set the status report in the solution table for this item. std::unique_ptr statusReport(new QTableWidgetItem()); if (loadSlewState == IPS_IDLE) { solutionTable->setCellWidget(currentRow, 3, new QWidget()); statusReport->setFlags(Qt::ItemIsSelectable); } // Update Rotator offsets if (currentRotator != nullptr) { // When Load&Slew image is solved, we check if we need to rotate the rotator to match the position angle of the image if (loadSlewState == IPS_BUSY && Options::astrometryUseRotator()) { loadSlewTargetPA = solverPA; qCDebug(KSTARS_EKOS_ALIGN) << "loaSlewTargetPA:" << loadSlewTargetPA; } else { INumberVectorProperty *absAngle = currentRotator->getBaseDevice()->getNumber("ABS_ROTATOR_ANGLE"); if (absAngle) { // PA = RawAngle * Multiplier + Offset currentRotatorPA = solverPA; double rawAngle = absAngle->np[0].value; double offset = solverPA - (rawAngle * Options::pAMultiplier()); qCDebug(KSTARS_EKOS_ALIGN) << "Raw Rotator Angle:" << rawAngle << "Rotator PA:" << currentRotatorPA << "Rotator Offset:" << offset; Options::setPAOffset(offset); } if (absAngle && std::isnan(loadSlewTargetPA) == false && fabs(currentRotatorPA - loadSlewTargetPA) * 60 > Options::astrometryRotatorThreshold()) { double rawAngle = (loadSlewTargetPA - Options::pAOffset()) / Options::pAMultiplier(); if (rawAngle < 0) rawAngle += 360; else if (rawAngle > 360) rawAngle -= 360; absAngle->np[0].value = rawAngle; ClientManager *clientManager = currentRotator->getDriverInfo()->getClientManager(); clientManager->sendNewNumber(absAngle); appendLogText(i18n("Setting position angle to %1 degrees E of N...", loadSlewTargetPA)); return; } } } emit newSolverResults(orientation, ra, dec, pixscale); QJsonObject solution = { {"ra", SolverRAOut->text()}, {"de", SolverDecOut->text()}, {"dRA", dRAOut->text()}, {"dDE", dDEOut->text()}, {"pix", pixscale}, {"rot", orientation}, {"fov", FOVOut->text()}, }; emit newSolution(solution.toVariantMap()); switch (currentGotoMode) { case GOTO_SYNC: executeGOTO(); if (loadSlewState == IPS_IDLE) { statusReport->setIcon(QIcon(":/icons/AlignSuccess.svg")); solutionTable->setItem(currentRow, 3, statusReport.release()); } return; case GOTO_SLEW: if (loadSlewState == IPS_BUSY || targetDiff > static_cast(accuracySpin->value())) { if (loadSlewState == IPS_IDLE && ++solverIterations == MAXIMUM_SOLVER_ITERATIONS) { appendLogText(i18n("Maximum number of iterations reached. Solver failed.")); if (loadSlewState == IPS_IDLE) { statusReport->setIcon(QIcon(":/icons/AlignFailure.svg")); solutionTable->setItem(currentRow, 3, statusReport.release()); } solverFailed(); if (mountModelRunning) finishAlignmentPoint(false); return; } targetAccuracyNotMet = true; if (loadSlewState == IPS_IDLE) { statusReport->setIcon(QIcon(":/icons/AlignWarning.svg")); solutionTable->setItem(currentRow, 3, statusReport.release()); } executeGOTO(); return; } if (loadSlewState == IPS_IDLE) { statusReport->setIcon(QIcon(":/icons/AlignSuccess.svg")); solutionTable->setItem(currentRow, 3, statusReport.release()); } appendLogText(i18n("Target is within acceptable range. Astrometric solver is successful.")); if (mountModelRunning) { finishAlignmentPoint(true); if (mountModelRunning) return; } break; case GOTO_NOTHING: if (loadSlewState == IPS_IDLE) { statusReport->setIcon(QIcon(":/icons/AlignSuccess.svg")); solutionTable->setItem(currentRow, 3, statusReport.release()); } if (mountModelRunning) { finishAlignmentPoint(true); if (mountModelRunning) return; } break; } KSNotification::event(QLatin1String("AlignSuccessful"), i18n("Astrometry alignment completed successfully")); state = ALIGN_COMPLETE; emit newStatus(state); solverIterations = 0; if (pahStage != PAH_IDLE) processPAHStage(orientation, ra, dec, pixscale); else if (azStage > AZ_INIT || altStage > ALT_INIT) executePolarAlign(); else { solveB->setEnabled(true); loadSlewB->setEnabled(true); } } void Align::solverFailed() { KSNotification::event(QLatin1String("AlignFailed"), i18n("Astrometry alignment failed with errors"), KSNotification::EVENT_ALERT); pi->stopAnimation(); stopB->setEnabled(false); solveB->setEnabled(true); m_AlignTimer.stop(); azStage = AZ_INIT; altStage = ALT_INIT; //loadSlewMode = false; loadSlewState = IPS_IDLE; solverIterations = 0; m_CaptureErrorCounter = 0; m_CaptureTimeoutCounter = 0; m_SlewErrorCounter = 0; //emit solverComplete(false); state = ALIGN_FAILED; emit newStatus(state); int currentRow = solutionTable->rowCount() - 1; solutionTable->setCellWidget(currentRow, 3, new QWidget()); QTableWidgetItem *statusReport = new QTableWidgetItem(); statusReport->setIcon(QIcon(":/icons/AlignFailure.svg")); statusReport->setFlags(Qt::ItemIsSelectable); solutionTable->setItem(currentRow, 3, statusReport); } void Align::abort() { parser->stopSolver(); pi->stopAnimation(); stopB->setEnabled(false); solveB->setEnabled(true); loadSlewB->setEnabled(true); // Reset Telescope Type to remembered value if (rememberTelescopeType != ISD::CCD::TELESCOPE_UNKNOWN) { currentCCD->setTelescopeType(rememberTelescopeType); rememberTelescopeType = ISD::CCD::TELESCOPE_UNKNOWN; } azStage = AZ_INIT; altStage = ALT_INIT; //loadSlewMode = false; loadSlewState = IPS_IDLE; solverIterations = 0; m_CaptureErrorCounter = 0; m_CaptureTimeoutCounter = 0; m_SlewErrorCounter = 0; m_AlignTimer.stop(); //currentCCD->disconnect(this); disconnect(currentCCD, &ISD::CCD::BLOBUpdated, this, &Ekos::Align::newFITS); disconnect(currentCCD, &ISD::CCD::newExposureValue, this, &Ekos::Align::checkCCDExposureProgress); if (rememberUploadMode != currentCCD->getUploadMode()) currentCCD->setUploadMode(rememberUploadMode); if (rememberCCDExposureLooping) currentCCD->setExposureLoopingEnabled(true); ISD::CCDChip *targetChip = currentCCD->getChip(useGuideHead ? ISD::CCDChip::GUIDE_CCD : ISD::CCDChip::PRIMARY_CCD); // If capture is still in progress, let's stop that. if (pahStage == PAH_REFRESH) { if (targetChip->isCapturing()) targetChip->abortExposure(); appendLogText(i18n("Refresh is complete.")); } else { if (targetChip->isCapturing()) { targetChip->abortExposure(); appendLogText(i18n("Capture aborted.")); } else { int elapsed = static_cast(round(solverTimer.elapsed() / 1000.0)); appendLogText(i18np("Solver aborted after %1 second.", "Solver aborted after %1 seconds", elapsed)); } } state = ALIGN_ABORTED; emit newStatus(state); int currentRow = solutionTable->rowCount() - 1; solutionTable->setCellWidget(currentRow, 3, new QWidget()); QTableWidgetItem *statusReport = new QTableWidgetItem(); statusReport->setIcon(QIcon(":/icons/AlignFailure.svg")); statusReport->setFlags(Qt::ItemIsSelectable); solutionTable->setItem(currentRow, 3, statusReport); } QList Align::getSolutionResult() { QList result; result << sOrientation << sRA << sDEC; return result; } void Align::appendLogText(const QString &text) { m_LogText.insert(0, i18nc("log entry; %1 is the date, %2 is the text", "%1 %2", - QDateTime::currentDateTime().toString("yyyy-MM-ddThh:mm:ss"), text)); + KStarsData::Instance()->lt().toString("yyyy-MM-ddThh:mm:ss"), text)); qCInfo(KSTARS_EKOS_ALIGN) << text; emit newLog(text); } void Align::clearLog() { m_LogText.clear(); emit newLog(QString()); } void Align::processSwitch(ISwitchVectorProperty *svp) { if (!strcmp(svp->name, "DOME_MOTION")) { // If dome is not ready and state is now if (domeReady == false && svp->s == IPS_OK) { domeReady = true; // trigger process number for mount so that it proceeds with normal workflow since // it was stopped by dome not being ready INumberVectorProperty *nvp = nullptr; if (currentTelescope->isJ2000()) nvp = currentTelescope->getBaseDevice()->getNumber("EQUATORIAL_COORD"); else nvp = currentTelescope->getBaseDevice()->getNumber("EQUATORIAL_EOD_COORD"); if (nvp) processNumber(nvp); } } } void Align::processNumber(INumberVectorProperty *nvp) { if (!strcmp(nvp->name, "EQUATORIAL_EOD_COORD") || !strcmp(nvp->name, "EQUATORIAL_COORD")) { QString ra_dms, dec_dms; if (!strcmp(nvp->name, "EQUATORIAL_COORD")) { telescopeCoord.setRA0(nvp->np[0].value); telescopeCoord.setDec0(nvp->np[1].value); // Get JNow as well telescopeCoord.apparentCoord(static_cast(J2000), KStars::Instance()->data()->ut().djd()); } else { telescopeCoord.setRA(nvp->np[0].value); telescopeCoord.setDec(nvp->np[1].value); } getFormattedCoords(telescopeCoord.ra().Hours(), telescopeCoord.dec().Degrees(), ra_dms, dec_dms); telescopeCoord.EquatorialToHorizontal(KStarsData::Instance()->lst(), KStarsData::Instance()->geo()->lat()); ScopeRAOut->setText(ra_dms); ScopeDecOut->setText(dec_dms); // qCDebug(KSTARS_EKOS_ALIGN) << "## RA" << ra_dms << "DE" << dec_dms // << "state:" << pstateStr(nvp->s) << "slewStarted?" << m_wasSlewStarted; switch (nvp->s) { // Idle --> Mount not tracking or slewing case IPS_IDLE: m_wasSlewStarted = false; //qCDebug(KSTARS_EKOS_ALIGN) << "## IPS_IDLE --> setting slewStarted to FALSE"; break; // Ok --> Mount Tracking. If m_wasSlewStarted is true // then it just finished slewing case IPS_OK: { // Update the boxes as the mount just finished slewing if (m_wasSlewStarted && Options::astrometryAutoUpdatePosition()) { //qCDebug(KSTARS_EKOS_ALIGN) << "## IPS_OK --> Auto Update Position..."; opsAstrometry->estRA->setText(ra_dms); opsAstrometry->estDec->setText(dec_dms); Options::setAstrometryPositionRA(nvp->np[0].value * 15); Options::setAstrometryPositionDE(nvp->np[1].value); generateArgs(); } // If dome is syncing, wait until it stops if (currentDome && currentDome->isMoving()) { domeReady = false; return; } // If we are looking for celestial pole if (m_wasSlewStarted && pahStage == PAH_FIND_CP) { //qCDebug(KSTARS_EKOS_ALIGN) << "## PAH_FIND_CP--> setting slewStarted to FALSE"; m_wasSlewStarted = false; appendLogText(i18n("Mount completed slewing near celestial pole. Capture again to verify.")); setSolverAction(GOTO_NOTHING); pahStage = PAH_FIRST_CAPTURE; emit newPAHStage(pahStage); return; } // if (m_wasSlewStarted && pahStage == PAH_FIRST_ROTATE) // { // m_wasSlewStarted = false; // appendLogText(i18n("Mount first rotation is complete.")); // pahStage = PAH_SECOND_CAPTURE; // emit newPAHStage(pahStage); // PAHWidgets->setCurrentWidget(PAHSecondCapturePage); // emit newPAHMessage(secondCaptureText->text()); // if (delaySpin->value() >= DELAY_THRESHOLD_NOTIFY) // appendLogText(i18n("Settling...")); // QTimer::singleShot(delaySpin->value(), this, &Ekos::Align::captureAndSolve); // return; // } // else if (m_wasSlewStarted && pahStage == PAH_SECOND_ROTATE) // { // m_wasSlewStarted = false; // appendLogText(i18n("Mount second rotation is complete.")); // pahStage = PAH_THIRD_CAPTURE; // emit newPAHStage(pahStage); // PAHWidgets->setCurrentWidget(PAHThirdCapturePage); // emit newPAHMessage(thirdCaptureText->text()); // if (delaySpin->value() >= DELAY_THRESHOLD_NOTIFY) // appendLogText(i18n("Settling...")); // QTimer::singleShot(delaySpin->value(), this, &Ekos::Align::captureAndSolve); // return; // } switch (state) { case ALIGN_PROGRESS: break; case ALIGN_SYNCING: { m_wasSlewStarted = false; //qCDebug(KSTARS_EKOS_ALIGN) << "## ALIGN_SYNCING --> setting slewStarted to FALSE"; if (currentGotoMode == GOTO_SLEW) { Slew(); return; } else { appendLogText(i18n("Mount is synced to solution coordinates. Astrometric solver is successful.")); KSNotification::event(QLatin1String("AlignSuccessful"), i18n("Astrometry alignment completed successfully")); state = ALIGN_COMPLETE; emit newStatus(state); solverIterations = 0; if (mountModelRunning) finishAlignmentPoint(true); } } break; case ALIGN_SLEWING: // If mount has not started slewing yet, then skip //qCDebug(KSTARS_EKOS_ALIGN) << "## Mount has not started slewing yet..."; if (m_wasSlewStarted == false) break; //qCDebug(KSTARS_EKOS_ALIGN) << "## ALIGN_SLEWING --> setting slewStarted to FALSE"; m_wasSlewStarted = false; if (loadSlewState == IPS_BUSY) { loadSlewState = IPS_IDLE; qCDebug(KSTARS_EKOS_ALIGN) << "loadSlewState is IDLE."; state = ALIGN_PROGRESS; emit newStatus(state); if (delaySpin->value() >= DELAY_THRESHOLD_NOTIFY) appendLogText(i18n("Settling...")); QTimer::singleShot(delaySpin->value(), this, &Ekos::Align::captureAndSolve); return; } else if (differentialSlewingActivated) { appendLogText(i18n("Differential slewing complete. Astrometric solver is successful.")); KSNotification::event(QLatin1String("AlignSuccessful"), i18n("Astrometry alignment completed successfully")); state = ALIGN_COMPLETE; emit newStatus(state); solverIterations = 0; if (mountModelRunning) finishAlignmentPoint(true); } else if (currentGotoMode == GOTO_SLEW || mountModelRunning) { if (targetAccuracyNotMet) appendLogText(i18n("Slew complete. Target accuracy is not met, running solver again...")); else appendLogText(i18n("Slew complete. Solving Alignment Point. . .")); targetAccuracyNotMet = false; state = ALIGN_PROGRESS; emit newStatus(state); if (delaySpin->value() >= DELAY_THRESHOLD_NOTIFY) appendLogText(i18n("Settling...")); QTimer::singleShot(delaySpin->value(), this, &Ekos::Align::captureAndSolve); return; } break; default: { //qCDebug(KSTARS_EKOS_ALIGN) << "## Align State " << state << "--> setting slewStarted to FALSE"; m_wasSlewStarted = false; } break; } } break; // Busy --> Mount Slewing or Moving (NSWE buttons) case IPS_BUSY: { //qCDebug(KSTARS_EKOS_ALIGN) << "## IPS_BUSY --> setting slewStarted to TRUE"; m_wasSlewStarted = true; } break; // Alert --> Mount has problem moving or communicating. case IPS_ALERT: { //qCDebug(KSTARS_EKOS_ALIGN) << "## IPS_ALERT --> setting slewStarted to FALSE"; m_wasSlewStarted = false; if (state == ALIGN_SYNCING || state == ALIGN_SLEWING) { if (state == ALIGN_SYNCING) appendLogText(i18n("Syncing failed.")); else appendLogText(i18n("Slewing failed.")); if (++m_SlewErrorCounter == 3) { abort(); return; } else { if (currentGotoMode == GOTO_SLEW) Slew(); else Sync(); } } return; } } if (pahStage == PAH_FIRST_ROTATE) { // only wait for telescope to slew to new position if manual slewing is switched off if(!PAHManual->isChecked()) { double deltaAngle = fabs(telescopeCoord.ra().deltaAngle(targetPAH.ra()).Degrees()); qCDebug(KSTARS_EKOS_ALIGN) << "First mount rotation remaining degrees:" << deltaAngle; if (deltaAngle <= PAH_ROTATION_THRESHOLD) { currentTelescope->StopWE(); appendLogText(i18n("Mount first rotation is complete.")); pahStage = PAH_SECOND_CAPTURE; emit newPAHStage(pahStage); PAHWidgets->setCurrentWidget(PAHSecondCapturePage); emit newPAHMessage(secondCaptureText->text()); if (delaySpin->value() >= DELAY_THRESHOLD_NOTIFY) appendLogText(i18n("Settling...")); QTimer::singleShot(delaySpin->value(), this, &Ekos::Align::captureAndSolve); } // If for some reason we didn't stop, let's stop if we get too far else if (deltaAngle > PAHRotationSpin->value() * 1.25) { currentTelescope->Abort(); appendLogText(i18n("Mount aborted. Please restart the process and reduce the speed.")); stopPAHProcess(); } return; } // endif not manual slew } else if (pahStage == PAH_SECOND_ROTATE) { // only wait for telescope to slew to new position if manual slewing is switched off if(!PAHManual->isChecked()) { double deltaAngle = fabs(telescopeCoord.ra().deltaAngle(targetPAH.ra()).Degrees()); qCDebug(KSTARS_EKOS_ALIGN) << "Second mount rotation remaining degrees:" << deltaAngle; if (deltaAngle <= PAH_ROTATION_THRESHOLD) { currentTelescope->StopWE(); appendLogText(i18n("Mount second rotation is complete.")); pahStage = PAH_THIRD_CAPTURE; emit newPAHStage(pahStage); PAHWidgets->setCurrentWidget(PAHThirdCapturePage); emit newPAHMessage(thirdCaptureText->text()); if (delaySpin->value() >= DELAY_THRESHOLD_NOTIFY) appendLogText(i18n("Settling...")); QTimer::singleShot(delaySpin->value(), this, &Ekos::Align::captureAndSolve); } // If for some reason we didn't stop, let's stop if we get too far else if (deltaAngle > PAHRotationSpin->value() * 1.25) { currentTelescope->Abort(); appendLogText(i18n("Mount aborted. Please restart the process and reduce the speed.")); stopPAHProcess(); } return; } // endif not manual slew } switch (azStage) { case AZ_SYNCING: if (currentTelescope->isSlewing()) azStage = AZ_SLEWING; break; case AZ_SLEWING: if (currentTelescope->isSlewing() == false) { azStage = AZ_SECOND_TARGET; measureAzError(); } break; case AZ_CORRECTING: if (currentTelescope->isSlewing() == false) { appendLogText(i18n( "Slew complete. Please adjust azimuth knob until the target is in the center of the view.")); azStage = AZ_INIT; } break; default: break; } switch (altStage) { case ALT_SYNCING: if (currentTelescope->isSlewing()) altStage = ALT_SLEWING; break; case ALT_SLEWING: if (currentTelescope->isSlewing() == false) { altStage = ALT_SECOND_TARGET; measureAltError(); } break; case ALT_CORRECTING: if (currentTelescope->isSlewing() == false) { appendLogText(i18n( "Slew complete. Please adjust altitude knob until the target is in the center of the view.")); altStage = ALT_INIT; } break; default: break; } } else if (!strcmp(nvp->name, "ABS_ROTATOR_ANGLE")) { // PA = RawAngle * Multiplier + Offset currentRotatorPA = (nvp->np[0].value * Options::pAMultiplier()) + Options::pAOffset(); if (currentRotatorPA > 180) currentRotatorPA -= 360; if (currentRotatorPA < -180) currentRotatorPA += 360; if (std::isnan(loadSlewTargetPA) == false && fabs(currentRotatorPA - loadSlewTargetPA) * 60 <= Options::astrometryRotatorThreshold()) { appendLogText(i18n("Rotator reached target position angle.")); targetAccuracyNotMet = true; loadSlewTargetPA = std::numeric_limits::quiet_NaN(); QTimer::singleShot(Options::settlingTime(), this, &Ekos::Align::executeGOTO); } } // N.B. Ekos::Manager already manages TELESCOPE_INFO, why here again? //if (!strcmp(coord->name, "TELESCOPE_INFO")) //syncTelescopeInfo(); } void Align::executeGOTO() { if (loadSlewState == IPS_BUSY) { //if (loadSlewIterations == loadSlewIterationsSpin->value()) //loadSlewCoord = alignCoord; //targetCoord = loadSlewCoord; targetCoord = alignCoord; SlewToTarget(); } else if (currentGotoMode == GOTO_SYNC) Sync(); else if (currentGotoMode == GOTO_SLEW) SlewToTarget(); } void Align::Sync() { state = ALIGN_SYNCING; if (currentTelescope->Sync(&alignCoord)) { emit newStatus(state); appendLogText( i18n("Syncing to RA (%1) DEC (%2)", alignCoord.ra().toHMSString(), alignCoord.dec().toDMSString())); } else { state = ALIGN_IDLE; emit newStatus(state); appendLogText(i18n("Syncing failed.")); } } void Align::Slew() { state = ALIGN_SLEWING; emit newStatus(state); //qCDebug(KSTARS_EKOS_ALIGN) << "## Before SLEW command: wasSlewStarted -->" << m_wasSlewStarted; //m_wasSlewStarted = currentTelescope->Slew(&targetCoord); //qCDebug(KSTARS_EKOS_ALIGN) << "## After SLEW command: wasSlewStarted -->" << m_wasSlewStarted; // JM 2019-08-23: Do not assume that slew was started immediately. Wait until IPS_BUSY state is triggered // from Goto currentTelescope->Slew(&targetCoord); appendLogText(i18n("Slewing to target coordinates: RA (%1) DEC (%2).", targetCoord.ra().toHMSString(), targetCoord.dec().toDMSString())); } void Align::SlewToTarget() { if (canSync && loadSlewState == IPS_IDLE) { // 2018-01-24 JM: This is ugly. Maybe use DBus? Signal/Slots? Ekos Manager usage like this should be avoided if (KStars::Instance()->ekosManager() && !KStars::Instance()->ekosManager()->getCurrentJobName().isEmpty()) { KSNotification::event(QLatin1String("EkosSchedulerTelescopeSynced"), i18n("Ekos job (%1) - Telescope synced", KStars::Instance()->ekosManager()->getCurrentJobName())); } // Do we perform a regular sync or use differential slewing? if (Options::astrometryDifferentialSlewing()) { dms raDiff = alignCoord.ra().deltaAngle(targetCoord.ra()); dms deDiff = alignCoord.dec().deltaAngle(targetCoord.dec()); targetCoord.setRA(targetCoord.ra() - raDiff); targetCoord.setDec(targetCoord.dec() - deDiff); differentialSlewingActivated = true; qCDebug(KSTARS_EKOS_ALIGN) << "Using differential slewing..."; Slew(); } else Sync(); return; } Slew(); } void Align::executePolarAlign() { appendLogText(i18n("Processing solution for polar alignment...")); switch (azStage) { case AZ_FIRST_TARGET: case AZ_FINISHED: measureAzError(); break; default: break; } switch (altStage) { case ALT_FIRST_TARGET: case ALT_FINISHED: measureAltError(); break; default: break; } } void Align::measureAzError() { static double initRA = 0, initDEC = 0, finalRA = 0, finalDEC = 0, initAz = 0; if (pahStage != PAH_IDLE && (KMessageBox::warningContinueCancel(KStars::Instance(), i18n("Polar Alignment Helper is still active. Do you want to continue " "using legacy polar alignment tool?")) != KMessageBox::Continue)) return; pahStage = PAH_IDLE; emit newPAHStage(pahStage); qCDebug(KSTARS_EKOS_ALIGN) << "Polar Measuring Azimuth Error..."; switch (azStage) { case AZ_INIT: // Display message box confirming user point scope near meridian and south // N.B. This action cannot be automated. if (KMessageBox::warningContinueCancel( nullptr, hemisphere == NORTH_HEMISPHERE ? i18n("Point the telescope at the southern meridian. Press Continue when ready.") : i18n("Point the telescope at the northern meridian. Press Continue when ready."), i18n("Polar Alignment Measurement"), KStandardGuiItem::cont(), KStandardGuiItem::cancel(), "ekos_measure_az_error") != KMessageBox::Continue) return; appendLogText(i18n("Solving first frame near the meridian.")); azStage = AZ_FIRST_TARGET; captureAndSolve(); break; case AZ_FIRST_TARGET: // start solving there, find RA/DEC initRA = alignCoord.ra().Degrees(); initDEC = alignCoord.dec().Degrees(); initAz = alignCoord.az().Degrees(); qCDebug(KSTARS_EKOS_ALIGN) << "Polar initRA " << alignCoord.ra().toHMSString() << " initDEC " << alignCoord.dec().toDMSString() << " initlAz " << alignCoord.az().toDMSString() << " initAlt " << alignCoord.alt().toDMSString(); // Now move 30 arcminutes in RA if (canSync) { azStage = AZ_SYNCING; currentTelescope->Sync(initRA / 15.0, initDEC); currentTelescope->Slew((initRA - RAMotion) / 15.0, initDEC); } // If telescope doesn't sync, we slew relative to its current coordinates else { azStage = AZ_SLEWING; currentTelescope->Slew(telescopeCoord.ra().Hours() - RAMotion / 15.0, telescopeCoord.dec().Degrees()); } appendLogText(i18n("Slewing 30 arcminutes in RA...")); break; case AZ_SECOND_TARGET: // We reached second target now // Let now solver for RA/DEC appendLogText(i18n("Solving second frame near the meridian.")); azStage = AZ_FINISHED; captureAndSolve(); break; case AZ_FINISHED: // Measure deviation in DEC // Call function to report error // set stage to AZ_FIRST_TARGET again appendLogText(i18n("Calculating azimuth alignment error...")); finalRA = alignCoord.ra().Degrees(); finalDEC = alignCoord.dec().Degrees(); qCDebug(KSTARS_EKOS_ALIGN) << "Polar finalRA " << alignCoord.ra().toHMSString() << " finalDEC " << alignCoord.dec().toDMSString() << " finalAz " << alignCoord.az().toDMSString() << " finalAlt " << alignCoord.alt().toDMSString(); // Slew back to original position if (canSync) currentTelescope->Slew(initRA / 15.0, initDEC); else { currentTelescope->Slew(telescopeCoord.ra().Hours() + RAMotion / 15.0, telescopeCoord.dec().Degrees()); } appendLogText(i18n("Slewing back to original position...")); calculatePolarError(initRA, initDEC, finalRA, finalDEC, initAz); azStage = AZ_INIT; break; default: break; } } void Align::measureAltError() { static double initRA = 0, initDEC = 0, finalRA = 0, finalDEC = 0, initAz = 0; if (pahStage != PAH_IDLE && (KMessageBox::warningContinueCancel(KStars::Instance(), i18n("Polar Alignment Helper is still active. Do you want to continue " "using legacy polar alignment tool?")) != KMessageBox::Continue)) return; pahStage = PAH_IDLE; emit newPAHStage(pahStage); qCDebug(KSTARS_EKOS_ALIGN) << "Polar Measuring Altitude Error..."; switch (altStage) { case ALT_INIT: // Display message box confirming user point scope near meridian and south // N.B. This action cannot be automated. if (KMessageBox::warningContinueCancel(nullptr, i18n("Point the telescope to the eastern or western horizon with a " "minimum altitude of 20 degrees. Press continue when ready."), i18n("Polar Alignment Measurement"), KStandardGuiItem::cont(), KStandardGuiItem::cancel(), "ekos_measure_alt_error") != KMessageBox::Continue) return; appendLogText(i18n("Solving first frame.")); altStage = ALT_FIRST_TARGET; if (delaySpin->value() >= DELAY_THRESHOLD_NOTIFY) appendLogText(i18n("Settling...")); QTimer::singleShot(delaySpin->value(), this, &Ekos::Align::captureAndSolve); break; case ALT_FIRST_TARGET: // start solving there, find RA/DEC initRA = alignCoord.ra().Degrees(); initDEC = alignCoord.dec().Degrees(); initAz = alignCoord.az().Degrees(); qCDebug(KSTARS_EKOS_ALIGN) << "Polar initRA " << alignCoord.ra().toHMSString() << " initDEC " << alignCoord.dec().toDMSString() << " initlAz " << alignCoord.az().toDMSString() << " initAlt " << alignCoord.alt().toDMSString(); // Now move 30 arcminutes in RA if (canSync) { altStage = ALT_SYNCING; currentTelescope->Sync(initRA / 15.0, initDEC); currentTelescope->Slew((initRA - RAMotion) / 15.0, initDEC); } // If telescope doesn't sync, we slew relative to its current coordinates else { altStage = ALT_SLEWING; currentTelescope->Slew(telescopeCoord.ra().Hours() - RAMotion / 15.0, telescopeCoord.dec().Degrees()); } appendLogText(i18n("Slewing 30 arcminutes in RA...")); break; case ALT_SECOND_TARGET: // We reached second target now // Let now solver for RA/DEC appendLogText(i18n("Solving second frame.")); altStage = ALT_FINISHED; if (delaySpin->value() >= DELAY_THRESHOLD_NOTIFY) appendLogText(i18n("Settling...")); QTimer::singleShot(delaySpin->value(), this, &Ekos::Align::captureAndSolve); break; case ALT_FINISHED: // Measure deviation in DEC // Call function to report error appendLogText(i18n("Calculating altitude alignment error...")); finalRA = alignCoord.ra().Degrees(); finalDEC = alignCoord.dec().Degrees(); qCDebug(KSTARS_EKOS_ALIGN) << "Polar finalRA " << alignCoord.ra().toHMSString() << " finalDEC " << alignCoord.dec().toDMSString() << " finalAz " << alignCoord.az().toDMSString() << " finalAlt " << alignCoord.alt().toDMSString(); // Slew back to original position if (canSync) currentTelescope->Slew(initRA / 15.0, initDEC); // If telescope doesn't sync, we slew relative to its current coordinates else { currentTelescope->Slew(telescopeCoord.ra().Hours() + RAMotion / 15.0, telescopeCoord.dec().Degrees()); } appendLogText(i18n("Slewing back to original position...")); calculatePolarError(initRA, initDEC, finalRA, finalDEC, initAz); altStage = ALT_INIT; break; default: break; } } void Align::calculatePolarError(double initRA, double initDEC, double finalRA, double finalDEC, double initAz) { double raMotion = finalRA - initRA; decDeviation = finalDEC - initDEC; // East/West of meridian int horizon = (initAz > 0 && initAz <= 180) ? 0 : 1; // How much time passed siderrally form initRA to finalRA? //double RATime = fabs(raMotion / SIDRATE) / 60.0; // 2016-03-30: Diff in RA is sufficient for time difference // raMotion in degrees. RATime in minutes. double RATime = fabs(raMotion) * 60.0; // Equation by Frank Berret (Measuring Polar Axis Alignment Error, page 4) // In degrees double deviation = (3.81 * (decDeviation * 3600)) / (RATime * cos(initDEC * dms::DegToRad)) / 60.0; dms devDMS(fabs(deviation)); KLocalizedString deviationDirection; switch (hemisphere) { // Northern hemisphere case NORTH_HEMISPHERE: if (azStage == AZ_FINISHED) { if (decDeviation > 0) deviationDirection = ki18n("%1 too far east"); else deviationDirection = ki18n("%1 too far west"); } else if (altStage == ALT_FINISHED) { switch (horizon) { // East case 0: if (decDeviation > 0) deviationDirection = ki18n("%1 too far high"); else deviationDirection = ki18n("%1 too far low"); break; // West case 1: if (decDeviation > 0) deviationDirection = ki18n("%1 too far low"); else deviationDirection = ki18n("%1 too far high"); break; default: break; } } break; // Southern hemisphere case SOUTH_HEMISPHERE: if (azStage == AZ_FINISHED) { if (decDeviation > 0) deviationDirection = ki18n("%1 too far west"); else deviationDirection = ki18n("%1 too far east"); } else if (altStage == ALT_FINISHED) { switch (horizon) { // East case 0: if (decDeviation > 0) deviationDirection = ki18n("%1 too far low"); else deviationDirection = ki18n("%1 too far high"); break; // West case 1: if (decDeviation > 0) deviationDirection = ki18n("%1 too far high"); else deviationDirection = ki18n("%1 too far low"); break; default: break; } } break; } qCDebug(KSTARS_EKOS_ALIGN) << "Polar Hemisphere is " << ((hemisphere == NORTH_HEMISPHERE) ? "North" : "South") << " --- initAz " << initAz; qCDebug(KSTARS_EKOS_ALIGN) << "Polar initRA " << initRA << " initDEC " << initDEC << " finalRA " << finalRA << " finalDEC " << finalDEC; qCDebug(KSTARS_EKOS_ALIGN) << "Polar decDeviation " << decDeviation * 3600 << " arcsec " << " RATime " << RATime << " minutes"; qCDebug(KSTARS_EKOS_ALIGN) << "Polar Raw Deviation " << deviation << " degrees."; if (azStage == AZ_FINISHED) { azError->setText(deviationDirection.subs(QString("%1").arg(devDMS.toDMSString())).toString()); //azError->setText(deviationDirection.subs(QString("%1")azDMS.toDMSString()); azDeviation = deviation * (decDeviation > 0 ? 1 : -1); qCDebug(KSTARS_EKOS_ALIGN) << "Polar Azimuth Deviation " << azDeviation << " degrees."; correctAzB->setEnabled(true); } if (altStage == ALT_FINISHED) { //altError->setText(deviationDirection.subs(QString("%1").arg(fabs(deviation), 0, 'g', 3)).toString()); altError->setText(deviationDirection.subs(QString("%1").arg(devDMS.toDMSString())).toString()); altDeviation = deviation * (decDeviation > 0 ? 1 : -1); qCDebug(KSTARS_EKOS_ALIGN) << "Polar Altitude Deviation " << altDeviation << " degrees."; correctAltB->setEnabled(true); } } void Align::correctAltError() { double newRA, newDEC; SkyPoint currentCoord(telescopeCoord); dms targetLat; qCDebug(KSTARS_EKOS_ALIGN) << "Polar Correcting Altitude Error..."; qCDebug(KSTARS_EKOS_ALIGN) << "Polar Current Mount RA " << currentCoord.ra().toHMSString() << " DEC " << currentCoord.dec().toDMSString() << "Az " << currentCoord.az().toDMSString() << " Alt " << currentCoord.alt().toDMSString(); // An error in polar alignment altitude reflects a deviation in the latitude of the mount from actual latitude of the site // Calculating the latitude accounting for the altitude deviation. This is the latitude at which the altitude deviation should be zero. targetLat.setD(KStars::Instance()->data()->geo()->lat()->Degrees() + altDeviation); // Calculate the Az/Alt of the mount if it were located at the corrected latitude currentCoord.EquatorialToHorizontal(KStars::Instance()->data()->lst(), &targetLat); // Convert corrected Az/Alt to RA/DEC given the local sideral time and current (not corrected) latitude currentCoord.HorizontalToEquatorial(KStars::Instance()->data()->lst(), KStars::Instance()->data()->geo()->lat()); // New RA/DEC should reflect the position in the sky at which the polar alignment altitude error is minimal. newRA = currentCoord.ra().Hours(); newDEC = currentCoord.dec().Degrees(); altStage = ALT_CORRECTING; qCDebug(KSTARS_EKOS_ALIGN) << "Polar Target Latitude = Latitude " << KStars::Instance()->data()->geo()->lat()->Degrees() << " + Altitude Deviation " << altDeviation << " = " << targetLat.Degrees(); qCDebug(KSTARS_EKOS_ALIGN) << "Polar Slewing to calibration position..."; currentTelescope->Slew(newRA, newDEC); appendLogText(i18n("Slewing to calibration position, please wait until telescope completes slewing.")); } void Align::correctAzError() { double newRA, newDEC, currentAlt, currentAz; SkyPoint currentCoord(telescopeCoord); qCDebug(KSTARS_EKOS_ALIGN) << "Polar Correcting Azimuth Error..."; qCDebug(KSTARS_EKOS_ALIGN) << "Polar Current Mount RA " << currentCoord.ra().toHMSString() << " DEC " << currentCoord.dec().toDMSString() << "Az " << currentCoord.az().toDMSString() << " Alt " << currentCoord.alt().toDMSString(); qCDebug(KSTARS_EKOS_ALIGN) << "Polar Target Azimuth = Current Azimuth " << currentCoord.az().Degrees() << " + Azimuth Deviation " << azDeviation << " = " << currentCoord.az().Degrees() + azDeviation; // Get current horizontal coordinates of the mount currentCoord.EquatorialToHorizontal(KStars::Instance()->data()->lst(), KStars::Instance()->data()->geo()->lat()); // Keep Altitude as it is and change Azimuth to account for the azimuth deviation // The new sky position should be where the polar alignment azimuth error is minimal currentAlt = currentCoord.alt().Degrees(); currentAz = currentCoord.az().Degrees() + azDeviation; // Update current Alt and Azimuth to new values currentCoord.setAlt(currentAlt); currentCoord.setAz(currentAz); // Convert Alt/Az back to equatorial coordinates currentCoord.HorizontalToEquatorial(KStars::Instance()->data()->lst(), KStars::Instance()->data()->geo()->lat()); // Get new RA and DEC newRA = currentCoord.ra().Hours(); newDEC = currentCoord.dec().Degrees(); azStage = AZ_CORRECTING; qCDebug(KSTARS_EKOS_ALIGN) << "Polar Slewing to calibration position..."; currentTelescope->Slew(newRA, newDEC); appendLogText(i18n("Slewing to calibration position, please wait until telescope completes slewing.")); } void Align::getFormattedCoords(double ra, double dec, QString &ra_str, QString &dec_str) { dms ra_s, dec_s; ra_s.setH(ra); dec_s.setD(dec); ra_str = QString("%1:%2:%3") .arg(ra_s.hour(), 2, 10, QChar('0')) .arg(ra_s.minute(), 2, 10, QChar('0')) .arg(ra_s.second(), 2, 10, QChar('0')); if (dec_s.Degrees() < 0) dec_str = QString("-%1:%2:%3") .arg(abs(dec_s.degree()), 2, 10, QChar('0')) .arg(abs(dec_s.arcmin()), 2, 10, QChar('0')) .arg(dec_s.arcsec(), 2, 10, QChar('0')); else dec_str = QString("%1:%2:%3") .arg(dec_s.degree(), 2, 10, QChar('0')) .arg(dec_s.arcmin(), 2, 10, QChar('0')) .arg(dec_s.arcsec(), 2, 10, QChar('0')); } bool Align::loadAndSlew(QString fileURL) { /*if (solverTypeGroup->checkedId() == SOLVER_REMOTE) { appendLogText(i18n("Load and Slew is not supported in remote solver mode.")); loadSlewB->setEnabled(false); return; }*/ #ifdef Q_OS_OSX if(solverTypeGroup->checkedId() == SOLVER_OFFLINE) { if(Options::useDefaultPython()) { if( !opsAlign->astropyInstalled() || !opsAlign->pythonInstalled() ) { KSNotification::error(i18n("Astrometry.net uses python3 and the astropy package for plate solving images offline. These were not detected on your system. Please go into the Align Options and either click the setup button to install them or uncheck the default button and enter the path to python3 on your system and manually install astropy.")); return false; } } } #endif if (fileURL.isEmpty()) fileURL = QFileDialog::getOpenFileName(KStars::Instance(), i18n("Load Image"), dirPath, "Images (*.fits *.fit *.jpg *.jpeg)"); if (fileURL.isEmpty()) return false; QFileInfo fileInfo(fileURL); dirPath = fileInfo.absolutePath(); differentialSlewingActivated = false; loadSlewState = IPS_BUSY; stopPAHProcess(); slewR->setChecked(true); currentGotoMode = GOTO_SLEW; solveB->setEnabled(false); stopB->setEnabled(true); pi->startAnimation(); startSolving(fileURL, false); return true; } void Align::setExposure(double value) { exposureIN->setValue(value); } void Align::setBinningIndex(int binIndex) { syncSettings(); Options::setSolverBinningIndex(binIndex); // If sender is not our combo box, then we need to update the combobox itself if (dynamic_cast(sender()) != binningCombo) { binningCombo->blockSignals(true); binningCombo->setCurrentIndex(binIndex); binningCombo->blockSignals(false); } // Need to calculate FOV and args for APP if (Options::astrometryImageScaleUnits() == OpsAstrometry::SCALE_ARCSECPERPIX) { calculateFOV(); generateArgs(); } } void Align::setSolverArguments(const QString &value) { solverOptions->setText(value); } QString Align::solverArguments() { return solverOptions->text(); } void Align::setFOVTelescopeType(int index) { FOVScopeCombo->setCurrentIndex(index); } FOV *Align::getSolverFOV() { if (sOrientation == -1) return nullptr; else return solverFOV.get(); } void Align::addFilter(ISD::GDInterface *newFilter) { for (auto filter : Filters) { if (!strcmp(filter->getDeviceName(), newFilter->getDeviceName())) return; } FilterCaptureLabel->setEnabled(true); FilterDevicesCombo->setEnabled(true); FilterPosLabel->setEnabled(true); FilterPosCombo->setEnabled(true); FilterDevicesCombo->addItem(newFilter->getDeviceName()); Filters.append(static_cast(newFilter)); checkFilter(1); FilterDevicesCombo->setCurrentIndex(1); } bool Align::setFilterWheel(const QString &device) { bool deviceFound = false; for (int i = 1; i < FilterDevicesCombo->count(); i++) if (device == FilterDevicesCombo->itemText(i)) { checkFilter(i); deviceFound = true; break; } if (deviceFound == false) return false; return true; } QString Align::filterWheel() { if (FilterDevicesCombo->currentIndex() >= 1) return FilterDevicesCombo->currentText(); return QString(); } bool Align::setFilter(const QString &filter) { if (FilterDevicesCombo->currentIndex() >= 1) { FilterPosCombo->setCurrentText(filter); return true; } return false; } QString Align::filter() { return FilterPosCombo->currentText(); } void Align::checkFilter(int filterNum) { if (filterNum == -1) { filterNum = FilterDevicesCombo->currentIndex(); if (filterNum == -1) return; } // "--" is no filter if (filterNum == 0) { currentFilter = nullptr; currentFilterPosition = -1; FilterPosCombo->clear(); return; } if (filterNum <= Filters.count()) currentFilter = Filters.at(filterNum - 1); FilterPosCombo->clear(); FilterPosCombo->addItems(filterManager->getFilterLabels()); currentFilterPosition = filterManager->getFilterPosition(); FilterPosCombo->setCurrentIndex(Options::lockAlignFilterIndex()); syncSettings(); } void Align::setWCSEnabled(bool enable) { if (currentCCD == nullptr) return; ISwitchVectorProperty *wcsControl = currentCCD->getBaseDevice()->getSwitch("WCS_CONTROL"); ISwitch *wcs_enable = IUFindSwitch(wcsControl, "WCS_ENABLE"); ISwitch *wcs_disable = IUFindSwitch(wcsControl, "WCS_DISABLE"); if (!wcs_enable || !wcs_disable) return; if ((wcs_enable->s == ISS_ON && enable) || (wcs_disable->s == ISS_ON && !enable)) return; IUResetSwitch(wcsControl); if (enable) { appendLogText(i18n("World Coordinate System (WCS) is enabled. CCD rotation must be set either manually in the " "CCD driver or by solving an image before proceeding to capture any further images, " "otherwise the WCS information may be invalid.")); wcs_enable->s = ISS_ON; } else { wcs_disable->s = ISS_ON; m_wcsSynced = false; appendLogText(i18n("World Coordinate System (WCS) is disabled.")); } ClientManager *clientManager = currentCCD->getDriverInfo()->getClientManager(); clientManager->sendNewSwitch(wcsControl); } void Align::checkCCDExposureProgress(ISD::CCDChip *targetChip, double remaining, IPState state) { INDI_UNUSED(targetChip); INDI_UNUSED(remaining); if (state == IPS_ALERT) { if (++m_CaptureErrorCounter == 3 && pahStage != PAH_REFRESH) { appendLogText(i18n("Capture error. Aborting...")); abort(); return; } appendLogText(i18n("Restarting capture attempt #%1", m_CaptureErrorCounter)); int currentRow = solutionTable->rowCount() - 1; solutionTable->setCellWidget(currentRow, 3, new QWidget()); QTableWidgetItem *statusReport = new QTableWidgetItem(); statusReport->setIcon(QIcon(":/icons/AlignFailure.svg")); statusReport->setFlags(Qt::ItemIsSelectable); solutionTable->setItem(currentRow, 3, statusReport); captureAndSolve(); } } void Align::setFocusStatus(Ekos::FocusState state) { focusState = state; } QStringList Align::getSolverOptionsFromFITS(const QString &filename) { int status = 0, fits_ccd_width, fits_ccd_height, fits_binx = 1, fits_biny = 1; char comment[128], error_status[512]; fitsfile *fptr = nullptr; double ra = 0, dec = 0, fits_fov_x, fits_fov_y, fov_lower, fov_upper, fits_ccd_hor_pixel = -1, fits_ccd_ver_pixel = -1, fits_focal_length = -1; QString fov_low, fov_high; QStringList solver_args; QVariantMap optionsMap; if (Options::astrometryUseNoVerify()) optionsMap["noverify"] = true; if (Options::astrometryUseResort()) optionsMap["resort"] = true; if (Options::astrometryUseNoFITS2FITS()) optionsMap["nofits2fits"] = true; if (Options::astrometryUseDownsample()) optionsMap["downsample"] = Options::astrometryDownsample(); if (Options::astrometryCustomOptions().isEmpty() == false) optionsMap["custom"] = Options::astrometryCustomOptions(); solver_args = generateOptions(optionsMap); status = 0; #if 0 if (fits_open_image(&fptr, filename.toLatin1(), READONLY, &status)) { fits_report_error(stderr, status); fits_get_errstatus(status, error_status); qCritical(KSTARS_EKOS_ALIGN) << "Could not open file " << filename << " Error: " << QString::fromUtf8(error_status); return solver_args; } #endif // Use open diskfile as it does not use extended file names which has problems opening // files with [ ] or ( ) in their names. if (fits_open_diskfile(&fptr, filename.toLatin1(), READONLY, &status)) { fits_report_error(stderr, status); fits_get_errstatus(status, error_status); qCCritical(KSTARS_EKOS_ALIGN) << QString::fromUtf8(error_status); return solver_args; } status = 0; if (fits_movabs_hdu(fptr, 1, IMAGE_HDU, &status)) { fits_report_error(stderr, status); fits_get_errstatus(status, error_status); qCCritical(KSTARS_EKOS_ALIGN) << QString::fromUtf8(error_status); return solver_args; } status = 0; if (fits_read_key(fptr, TINT, "NAXIS1", &fits_ccd_width, comment, &status)) { fits_report_error(stderr, status); fits_get_errstatus(status, error_status); appendLogText(i18n("FITS header: cannot find NAXIS1.")); return solver_args; } status = 0; if (fits_read_key(fptr, TINT, "NAXIS2", &fits_ccd_height, comment, &status)) { fits_report_error(stderr, status); fits_get_errstatus(status, error_status); appendLogText(i18n("FITS header: cannot find NAXIS2.")); return solver_args; } // If we need to auto downsample, let us figure out the scale and regenerate options if (Options::astrometryAutoDownsample()) { optionsMap["downsample"] = getSolverDownsample(fits_ccd_width); solver_args = generateOptions(optionsMap); } bool coord_ok = true; status = 0; char objectra_str[32]; if (fits_read_key(fptr, TSTRING, "OBJCTRA", objectra_str, comment, &status)) { if (fits_read_key(fptr, TDOUBLE, "RA", &ra, comment, &status)) { fits_report_error(stderr, status); fits_get_errstatus(status, error_status); coord_ok = false; appendLogText(i18n("FITS header: cannot find OBJCTRA (%1).", QString(error_status))); } else // Degrees to hours ra /= 15; } else { dms raDMS = dms::fromString(objectra_str, false); ra = raDMS.Hours(); } status = 0; char objectde_str[32]; if (coord_ok && fits_read_key(fptr, TSTRING, "OBJCTDEC", objectde_str, comment, &status)) { if (fits_read_key(fptr, TDOUBLE, "DEC", &dec, comment, &status)) { fits_report_error(stderr, status); fits_get_errstatus(status, error_status); coord_ok = false; appendLogText(i18n("FITS header: cannot find OBJCTDEC (%1).", QString(error_status))); } } else { dms deDMS = dms::fromString(objectde_str, true); dec = deDMS.Degrees(); } /*if (coord_ok == false) { ra = telescopeCoord.ra0().Hours(); dec = telescopeCoord.dec0().Degrees(); }*/ if (coord_ok && Options::astrometryUsePosition()) solver_args << "-3" << QString::number(ra * 15.0) << "-4" << QString::number(dec) << "-5" << "15"; status = 0; double pixelScale = 0; // If we have pixel scale in arcsecs per pixel then lets use that directly // instead of calculating it from FOCAL length and other information if (fits_read_key(fptr, TDOUBLE, "SCALE", &pixelScale, comment, &status) == 0) { fov_low = QString::number(0.9 * pixelScale); fov_high = QString::number(1.1 * pixelScale); if (Options::astrometryUseImageScale()) solver_args << "-L" << fov_low << "-H" << fov_high << "-u" << "app"; return solver_args; } if (fits_read_key(fptr, TDOUBLE, "FOCALLEN", &fits_focal_length, comment, &status)) { int integer_focal_length = -1; if (fits_read_key(fptr, TINT, "FOCALLEN", &integer_focal_length, comment, &status)) { fits_report_error(stderr, status); fits_get_errstatus(status, error_status); appendLogText(i18n("FITS header: cannot find FOCALLEN (%1).", QString(error_status))); return solver_args; } else fits_focal_length = integer_focal_length; } status = 0; if (fits_read_key(fptr, TDOUBLE, "PIXSIZE1", &fits_ccd_hor_pixel, comment, &status)) { fits_report_error(stderr, status); fits_get_errstatus(status, error_status); appendLogText(i18n("FITS header: cannot find PIXSIZE1 (%1).", QString(error_status))); return solver_args; } status = 0; if (fits_read_key(fptr, TDOUBLE, "PIXSIZE2", &fits_ccd_ver_pixel, comment, &status)) { fits_report_error(stderr, status); fits_get_errstatus(status, error_status); appendLogText(i18n("FITS header: cannot find PIXSIZE2 (%1).", QString(error_status))); return solver_args; } status = 0; fits_read_key(fptr, TINT, "XBINNING", &fits_binx, comment, &status); status = 0; fits_read_key(fptr, TINT, "YBINNING", &fits_biny, comment, &status); // Calculate FOV fits_fov_x = 206264.8062470963552 * fits_ccd_width * fits_ccd_hor_pixel / 1000.0 / fits_focal_length * fits_binx; fits_fov_y = 206264.8062470963552 * fits_ccd_height * fits_ccd_ver_pixel / 1000.0 / fits_focal_length * fits_biny; fits_fov_x /= 60.0; fits_fov_y /= 60.0; // let's stretch the boundaries by 10% fov_lower = qMin(fits_fov_x, fits_fov_y); fov_upper = qMax(fits_fov_x, fits_fov_y); fov_lower *= 0.90; fov_upper *= 1.10; fov_low = QString::number(fov_lower); fov_high = QString::number(fov_upper); if (Options::astrometryUseImageScale()) solver_args << "-L" << fov_low << "-H" << fov_high << "-u" << "aw"; return solver_args; } uint8_t Align::getSolverDownsample(uint16_t binnedW) { uint8_t downsample = Options::astrometryDownsample(); if (!Options::astrometryAutoDownsample()) return downsample; while (downsample < 8) { if (binnedW / downsample <= 1024) break; downsample += 2; } return downsample; } void Align::saveSettleTime() { Options::setSettlingTime(delaySpin->value()); } void Align::setCaptureStatus(CaptureState newState) { switch (newState) { case CAPTURE_ALIGNING: if (currentTelescope && currentTelescope->hasAlignmentModel() && Options::resetMountModelAfterMeridian()) { mountModelReset = currentTelescope->clearAlignmentModel(); qCDebug(KSTARS_EKOS_ALIGN) << "Post meridian flip mount model reset" << (mountModelReset ? "successful." : "failed."); } QTimer::singleShot(Options::settlingTime(), this, &Ekos::Align::captureAndSolve); break; default: break; } } void Align::showFITSViewer() { FITSData *data = alignView->getImageData(); if (data) { QUrl url = QUrl::fromLocalFile(data->filename()); if (fv.isNull()) { if (Options::singleWindowCapturedFITS()) fv = KStars::Instance()->genericFITSViewer(); else { fv = new FITSViewer(Options::independentWindowFITS() ? nullptr : KStars::Instance()); KStars::Instance()->addFITSViewer(fv); } fv->addFITS(url); FITSView *currentView = fv->getCurrentView(); if (currentView) currentView->getImageData()->setAutoRemoveTemporaryFITS(false); } else fv->updateFITS(url, 0); fv->show(); } } void Align::toggleAlignWidgetFullScreen() { if (alignWidget->parent() == nullptr) { alignWidget->setParent(this); rightLayout->insertWidget(0, alignWidget); //rightLayout->setStretch(0, 2); // rightLayout->setStretch(1, 1); alignWidget->showNormal(); } else { alignWidget->setParent(nullptr); alignWidget->setWindowTitle(i18n("Align Frame")); alignWidget->setWindowFlags(Qt::Window | Qt::WindowTitleHint | Qt::CustomizeWindowHint); alignWidget->showMaximized(); alignWidget->show(); } } void Align::startPAHProcess() { qCInfo(KSTARS_EKOS_ALIGN) << "Starting Polar Alignment Assistant process..."; pahStage = PAH_FIRST_CAPTURE; emit newPAHStage(pahStage); nothingR->setChecked(true); currentGotoMode = GOTO_NOTHING; loadSlewB->setEnabled(false); rememberSolverWCS = Options::astrometrySolverWCS(); rememberAutoWCS = Options::autoWCS(); Options::setAutoWCS(false); Options::setAstrometrySolverWCS(true); if (Options::limitedResourcesMode()) appendLogText(i18n("Warning: Equatorial Grid Lines will not be drawn due to limited resources mode.")); if (currentTelescope->hasAlignmentModel()) { appendLogText(i18n("Clearing mount Alignment Model...")); mountModelReset = currentTelescope->clearAlignmentModel(); } // Unpark currentTelescope->UnPark(); // Set tracking ON if not already if (currentTelescope->canControlTrack() && currentTelescope->isTracking() == false) currentTelescope->setTrackEnabled(true); PAHStartB->setEnabled(false); PAHStopB->setEnabled(true); PAHWidgets->setCurrentWidget(PAHFirstCapturePage); emit newPAHMessage(firstCaptureText->text()); captureAndSolve(); } void Align::stopPAHProcess() { if (pahStage == PAH_IDLE) return; qCInfo(KSTARS_EKOS_ALIGN) << "Stopping Polar Alignment Assistant process..."; // Only display dialog if user explicitly restarts if ((static_cast(sender()) == PAHStopB) && KMessageBox::questionYesNo(KStars::Instance(), i18n("Are you sure you want to stop the polar alignment process?"), i18n("Polar Alignment Assistant"), KStandardGuiItem::yes(), KStandardGuiItem::no(), "restart_PAA_process_dialog") == KMessageBox::No) return; stopB->click(); if (currentTelescope && currentTelescope->isInMotion()) currentTelescope->Abort(); pahStage = PAH_IDLE; emit newPAHStage(pahStage); PAHStartB->setEnabled(true); PAHStopB->setEnabled(false); PAHRefreshB->setEnabled(true); PAHWidgets->setCurrentWidget(PAHIntroPage); emit newPAHMessage(introText->text()); qDeleteAll(pahImageInfos); pahImageInfos.clear(); correctionVector = QLineF(); correctionOffset = QPointF(); alignView->setCorrectionParams(correctionVector); alignView->setCorrectionOffset(correctionOffset); alignView->setRACircle(QVector3D()); alignView->setRefreshEnabled(false); emit newFrame(alignView); disconnect(alignView, &AlignView::trackingStarSelected, this, &Ekos::Align::setPAHCorrectionOffset); disconnect(alignView, &AlignView::newCorrectionVector, this, &Ekos::Align::newCorrectionVector); if (Options::pAHAutoPark()) { currentTelescope->Park(); appendLogText(i18n("Parking the mount...")); } state = ALIGN_IDLE; emit newStatus(state); } void Align::rotatePAH() { double raDiff = PAHRotationSpin->value(); bool westMeridian = PAHDirectionCombo->currentIndex() == 0; // West if (westMeridian) raDiff *= -1; // East else raDiff *= 1; // JM 2018-05-03: Hemispheres shouldn't affect rotation direction in RA #if 0 // North if (hemisphere == NORTH_HEMISPHERE) { // West if (westMeridian) raDiff *= -1; // East else raDiff *= 1; } // South else { // West if (westMeridian) raDiff *= 1; // East else raDiff *= -1; } #endif // if Manual slewing is selected, don't move the mount if (PAHManual->isChecked()) { appendLogText(i18n("Please rotate your mount about %1deg in RA", raDiff )); return; } // raDiff is in degrees dms newTelescopeRA = (telescopeCoord.ra() + dms(raDiff)).reduce(); targetPAH.setRA(newTelescopeRA); targetPAH.setDec(telescopeCoord.dec()); //currentTelescope->Slew(&targetPAH); // Set Selected Speed currentTelescope->setSlewRate(PAHSlewRateCombo->currentIndex()); // Go to direction currentTelescope->MoveWE(westMeridian ? ISD::Telescope::MOTION_WEST : ISD::Telescope::MOTION_EAST, ISD::Telescope::MOTION_START); appendLogText(i18n("Please wait until mount completes rotating to RA (%1) DE (%2)", targetPAH.ra().toHMSString(), targetPAH.dec().toDMSString())); } void Align::calculatePAHError() { QVector3D RACircle; bool rc = findRACircle(RACircle); if (rc == false) { appendLogText(i18n("Failed to find a solution. Try again.")); stopPAHProcess(); return; } if (alignView->isEQGridShown() == false) alignView->toggleEQGrid(); alignView->setRACircle(RACircle); FITSData *imageData = alignView->getImageData(); QPointF RACenterPoint(RACircle.x(), RACircle.y()); SkyPoint RACenter; rc = imageData->pixelToWCS(RACenterPoint, RACenter); if (rc == false) { appendLogText(i18n("Failed to find RA Axis center: %1.", imageData->getLastError())); return; } SkyPoint CP(0, (hemisphere == NORTH_HEMISPHERE) ? 90 : -90); RACenter.setRA(RACenter.ra0()); RACenter.setDec(RACenter.dec0()); double PA = 0; dms polarError = RACenter.angularDistanceTo(&CP, &PA); if (Options::alignmentLogging()) { qCDebug(KSTARS_EKOS_ALIGN) << "RA Axis Circle X: " << RACircle.x() << " Y: " << RACircle.y() << " Radius: " << RACircle.z(); qCDebug(KSTARS_EKOS_ALIGN) << "RA Axis Location RA: " << RACenter.ra0().toHMSString() << "DE: " << RACenter.dec0().toDMSString(); qCDebug(KSTARS_EKOS_ALIGN) << "RA Axis Offset: " << polarError.toDMSString() << "PA:" << PA; qCDebug(KSTARS_EKOS_ALIGN) << "CP Axis Location X:" << celestialPolePoint.x() << "Y:" << celestialPolePoint.y(); } RACenter.EquatorialToHorizontal(KStarsData::Instance()->lst(), KStarsData::Instance()->geo()->lat()); QString azDirection = RACenter.az().Degrees() < 30 ? "Right" : "Left"; QString atDirection = RACenter.alt().Degrees() < KStarsData::Instance()->geo()->lat()->Degrees() ? "Bottom" : "Top"; // FIXME should this be reversed for southern hemisphere? appendLogText(i18n("Mount axis is to the %1 %2 of the celestial pole", atDirection, azDirection)); PAHErrorLabel->setText(polarError.toDMSString()); // JM 2019-08-17: Flip for southern hemisphere. // Possible fix for: https://indilib.org/forum/ekos/5558-ekos-polar-alignment-vector-backwards.html correctionVector.setP1((hemisphere == NORTH_HEMISPHERE) ? celestialPolePoint : RACenterPoint); correctionVector.setP2((hemisphere == NORTH_HEMISPHERE) ? RACenterPoint : celestialPolePoint); /* bool RAAxisInside = imageData->contains(RACenterPoint); bool CPPointInside= imageData->contains(celestialPolePoint); if (RAAxisInside == false && CPPointInside == false) appendLogText(i18n("Warning: Mount axis and celestial pole are outside the field of view. Correction vector may be inaccurate.")); */ connect(alignView, &AlignView::trackingStarSelected, this, &Ekos::Align::setPAHCorrectionOffset); emit polarResultUpdated(correctionVector, polarError.toDMSString()); connect(alignView, &AlignView::newCorrectionVector, this, &Ekos::Align::newCorrectionVector, Qt::UniqueConnection); emit newCorrectionVector(correctionVector); alignView->setCorrectionParams(correctionVector); emit newFrame(alignView); } void Align::setPAHCorrectionOffsetPercentage(double dx, double dy) { double x = dx * alignView->zoomedWidth() * (alignView->getCurrentZoom() / 100); double y = dy * alignView->zoomedHeight() * (alignView->getCurrentZoom() / 100); setPAHCorrectionOffset(static_cast(round(x)), static_cast(round(y))); } void Align::setPAHCorrectionOffset(int x, int y) { correctionOffset.setX(x); correctionOffset.setY(y); alignView->setCorrectionOffset(correctionOffset); emit newFrame(alignView); } void Align::setPAHCorrectionSelectionComplete() { pahStage = PAH_PRE_REFRESH; emit newPAHStage(pahStage); // If user stops here, we restore the settings, if not we // disable again in the refresh process // and restore when refresh is complete Options::setAstrometrySolverWCS(rememberSolverWCS); Options::setAutoWCS(rememberAutoWCS); PAHWidgets->setCurrentWidget(PAHRefreshPage); emit newPAHMessage(refreshText->text()); } void Align::setPAHSlewDone() { emit newPAHMessage("Manual slew done."); switch(pahStage) { case PAH_FIRST_ROTATE : pahStage = PAH_SECOND_CAPTURE; emit newPAHStage(pahStage); PAHWidgets->setCurrentWidget(PAHSecondCapturePage); appendLogText(i18n("First manual rotation done.")); break; case PAH_SECOND_ROTATE : pahStage = PAH_THIRD_CAPTURE; emit newPAHStage(pahStage); PAHWidgets->setCurrentWidget(PAHThirdCapturePage); appendLogText(i18n("Second manual rotation done.")); break; default : return; // no other stage should be able to trigger this event } if (delaySpin->value() >= DELAY_THRESHOLD_NOTIFY) appendLogText(i18n("Settling...")); QTimer::singleShot(delaySpin->value(), this, &Ekos::Align::captureAndSolve); } void Align::startPAHRefreshProcess() { qCInfo(KSTARS_EKOS_ALIGN) << "Starting Polar Alignment Assistant refreshing..."; pahStage = PAH_REFRESH; emit newPAHStage(pahStage); PAHRefreshB->setEnabled(false); // Hide EQ Grids if shown if (alignView->isEQGridShown()) alignView->toggleEQGrid(); alignView->setRefreshEnabled(true); Options::setAstrometrySolverWCS(false); Options::setAutoWCS(false); // We for refresh, just capture really captureAndSolve(); } void Align::setPAHRefreshComplete() { abort(); Options::setAstrometrySolverWCS(rememberSolverWCS); Options::setAutoWCS(rememberAutoWCS); stopPAHProcess(); } void Align::processPAHStage(double orientation, double ra, double dec, double pixscale) { // Create temporary file to hold all WCS data // QTemporaryFile tmpFile(QDir::tempPath() + "/fitswcsXXXXXX"); // tmpFile.setAutoRemove(false); // tmpFile.open(); // QString newWCSFile = tmpFile.fileName(); // tmpFile.close(); QString newWCSFile = QDir::tempPath() + QString("/fitswcs%1").arg(QUuid::createUuid().toString().remove(QRegularExpression("[-{}]"))); //alignView->setLoadWCSEnabled(true); if (pahStage == PAH_FIND_CP) { setSolverAction(GOTO_NOTHING); appendLogText( i18n("Mount is synced to celestial pole. You can now continue Polar Alignment Assistant procedure.")); pahStage = PAH_FIRST_CAPTURE; emit newPAHStage(pahStage); return; } if (pahStage == PAH_FIRST_CAPTURE) { // Set First PAH Center PAHImageInfo *solution = new PAHImageInfo(); solution->skyCenter.setRA0(alignCoord.ra0()); solution->skyCenter.setDec0(alignCoord.dec0()); solution->orientation = orientation; solution->pixelScale = pixscale; pahImageInfos.append(solution); // Only invoke this if limited resource mode is false since we want to use CPU heavy WCS if (Options::limitedResourcesMode() == false) { appendLogText(i18n("Please wait while WCS data is processed...")); connect(alignView, &AlignView::wcsToggled, this, &Ekos::Align::setWCSToggled, Qt::UniqueConnection); alignView->createWCSFile(newWCSFile, orientation, ra, dec, pixscale); return; } pahStage = PAH_FIRST_ROTATE; emit newPAHStage(pahStage); PAHWidgets->setCurrentWidget(PAHFirstRotatePage); emit newPAHMessage(firstRotateText->text()); rotatePAH(); } else if (pahStage == PAH_SECOND_CAPTURE) { // Set 2nd PAH Center PAHImageInfo *solution = new PAHImageInfo(); solution->skyCenter.setRA0(alignCoord.ra0()); solution->skyCenter.setDec0(alignCoord.dec0()); solution->orientation = orientation; solution->pixelScale = pixscale; pahImageInfos.append(solution); // Only invoke this if limited resource mode is false since we want to use CPU heavy WCS if (Options::limitedResourcesMode() == false) { appendLogText(i18n("Please wait while WCS data is processed...")); connect(alignView, &AlignView::wcsToggled, this, &Ekos::Align::setWCSToggled, Qt::UniqueConnection); alignView->createWCSFile(newWCSFile, orientation, ra, dec, pixscale); return; } pahStage = PAH_SECOND_ROTATE; emit newPAHStage(pahStage); PAHWidgets->setCurrentWidget(PAHSecondRotatePage); emit newPAHMessage(secondRotateText->text()); rotatePAH(); } else if (pahStage == PAH_THIRD_CAPTURE) { // Set Third PAH Center PAHImageInfo *solution = new PAHImageInfo(); solution->skyCenter.setRA0(alignCoord.ra0()); solution->skyCenter.setDec0(alignCoord.dec0()); solution->orientation = orientation; solution->pixelScale = pixscale; pahImageInfos.append(solution); appendLogText(i18n("Please wait while WCS data is processed...")); connect(alignView, &AlignView::wcsToggled, this, &Ekos::Align::setWCSToggled, Qt::UniqueConnection); alignView->createWCSFile(newWCSFile, orientation, ra, dec, pixscale); return; } } void Align::setWCSToggled(bool result) { appendLogText(i18n("WCS data processing is complete.")); //alignView->disconnect(this); disconnect(alignView, &AlignView::wcsToggled, this, &Ekos::Align::setWCSToggled); if (pahStage == PAH_FIRST_CAPTURE) { // We need WCS to be synced first if (result == false && m_wcsSynced == true) { appendLogText(i18n("WCS info is now valid. Capturing next frame...")); pahImageInfos.clear(); captureAndSolve(); return; } // Not critical error /* if (result == false) { appendLogText( i18n("Warning: failed to load WCS data in file: %1", alignView->getImageData()->getLastError())); pahStage = PAH_FIRST_ROTATE; PAHWidgets->setCurrentWidget(PAHFirstRotatePage); return; }*/ // Find Celestial pole location SkyPoint CP(0, (hemisphere == NORTH_HEMISPHERE) ? 90 : -90); FITSData *imageData = alignView->getImageData(); QPointF pixelPoint, imagePoint; bool rc = imageData->wcsToPixel(CP, pixelPoint, imagePoint); pahImageInfos[0]->celestialPole = pixelPoint; // TODO check if pixelPoint is located TOO far from the current position as well // i.e. if X > Width * 2..etc if (rc == false) { appendLogText(i18n("Failed to process World Coordinate System: %1. Try again.", imageData->getLastError())); return; } // If celestial pole out of range, ask the user if they want to move to it if (pixelPoint.x() < (-1 * imageData->width()) || pixelPoint.x() > (imageData->width() * 2) || pixelPoint.y() < (-1 * imageData->height()) || pixelPoint.y() > (imageData->height() * 2)) { if (currentTelescope->canSync() && KMessageBox::questionYesNo( nullptr, i18n("Celestial pole is located outside of the field of view. Would you like to sync and slew " "the telescope to the celestial pole? WARNING: Slewing near poles may cause your mount to " "end up in unsafe position. Proceed with caution.")) == KMessageBox::Yes) { pahStage = PAH_FIND_CP; emit newPAHStage(pahStage); targetCoord.setRA(KStarsData::Instance()->lst()->Hours()); targetCoord.setDec(CP.dec().Degrees() > 0 ? 89.5 : -89.5); qDeleteAll(pahImageInfos); pahImageInfos.clear(); setSolverAction(GOTO_SLEW); Sync(); return; } else appendLogText(i18n("Warning: Celestial pole is located outside the field of view. Move the mount closer to the celestial pole.")); } pahStage = PAH_FIRST_ROTATE; emit newPAHStage(pahStage); PAHWidgets->setCurrentWidget(PAHFirstRotatePage); emit newPAHMessage(firstRotateText->text()); rotatePAH(); } else if (pahStage == PAH_SECOND_CAPTURE) { // Find Celestial pole location SkyPoint CP(0, (hemisphere == NORTH_HEMISPHERE) ? 90 : -90); FITSData *imageData = alignView->getImageData(); QPointF pixelPoint, imagePoint; imageData->wcsToPixel(CP, pixelPoint, imagePoint); pahImageInfos[1]->celestialPole = pixelPoint; pahStage = PAH_SECOND_ROTATE; emit newPAHStage(pahStage); PAHWidgets->setCurrentWidget(PAHSecondRotatePage); emit newPAHMessage(secondRotateText->text()); rotatePAH(); } else if (pahStage == PAH_THIRD_CAPTURE) { FITSData *imageData = alignView->getImageData(); // Critical error if (result == false) { appendLogText(i18n("Failed to process World Coordinate System: %1. Try again.", imageData->getLastError())); return; } // Find Celestial pole location SkyPoint CP(0, (hemisphere == NORTH_HEMISPHERE) ? 90 : -90); QPointF imagePoint; imageData->wcsToPixel(CP, celestialPolePoint, imagePoint); pahImageInfos[2]->celestialPole = celestialPolePoint; // Now find pixel locations for all recorded center coordinates in the 3rd frame reference imageData->wcsToPixel(pahImageInfos[0]->skyCenter, pahImageInfos[0]->pixelCenter, imagePoint); imageData->wcsToPixel(pahImageInfos[1]->skyCenter, pahImageInfos[1]->pixelCenter, imagePoint); imageData->wcsToPixel(pahImageInfos[2]->skyCenter, pahImageInfos[2]->pixelCenter, imagePoint); qCDebug(KSTARS_EKOS_ALIGN) << "P1 RA: " << pahImageInfos[0]->skyCenter.ra0().toHMSString() << "DE: " << pahImageInfos[0]->skyCenter.dec0().toDMSString(); qCDebug(KSTARS_EKOS_ALIGN) << "P2 RA: " << pahImageInfos[1]->skyCenter.ra0().toHMSString() << "DE: " << pahImageInfos[1]->skyCenter.dec0().toDMSString(); qCDebug(KSTARS_EKOS_ALIGN) << "P3 RA: " << pahImageInfos[2]->skyCenter.ra0().toHMSString() << "DE: " << pahImageInfos[2]->skyCenter.dec0().toDMSString(); qCDebug(KSTARS_EKOS_ALIGN) << "P1 X: " << pahImageInfos[0]->pixelCenter.x() << "Y: " << pahImageInfos[0]->pixelCenter.y(); qCDebug(KSTARS_EKOS_ALIGN) << "P2 X: " << pahImageInfos[1]->pixelCenter.x() << "Y: " << pahImageInfos[1]->pixelCenter.y(); qCDebug(KSTARS_EKOS_ALIGN) << "P3 X: " << pahImageInfos[2]->pixelCenter.x() << "Y: " << pahImageInfos[2]->pixelCenter.y(); qCDebug(KSTARS_EKOS_ALIGN) << "P1 CP X: " << pahImageInfos[0]->celestialPole.x() << "CP Y: " << pahImageInfos[0]->celestialPole.y(); qCDebug(KSTARS_EKOS_ALIGN) << "P2 CP X: " << pahImageInfos[1]->celestialPole.x() << "CP Y: " << pahImageInfos[1]->celestialPole.y(); qCDebug(KSTARS_EKOS_ALIGN) << "P3 CP X: " << pahImageInfos[2]->celestialPole.x() << "CP Y: " << pahImageInfos[2]->celestialPole.y(); // We have 3 points which uniquely defines a circle with its center representing the RA Axis // We have celestial pole location. So correction vector is just the vector between these two points calculatePAHError(); pahStage = PAH_STAR_SELECT; emit newPAHStage(pahStage); PAHWidgets->setCurrentWidget(PAHCorrectionPage); emit newPAHMessage(correctionText->text()); } } void Align::updateTelescopeType(int index) { if (currentCCD == nullptr) return; syncSettings(); /* bool rc = currentCCD->setTelescopeType(static_cast(index)); // If false, try to set it to existing known telescope if (rc == false) { focal_length = (index == ISD::CCD::TELESCOPE_PRIMARY) ? primaryFL : guideFL; aperture = (index == ISD::CCD::TELESCOPE_PRIMARY) ? primaryAperture : guideAperture; syncTelescopeInfo(); }*/ focal_length = (index == ISD::CCD::TELESCOPE_PRIMARY) ? primaryFL : guideFL; aperture = (index == ISD::CCD::TELESCOPE_PRIMARY) ? primaryAperture : guideAperture; Options::setSolverScopeType(index); syncTelescopeInfo(); } // Function adapted from https://rosettacode.org/wiki/Circles_of_given_radius_through_two_points Align::CircleSolution Align::findCircleSolutions(const QPointF &p1, const QPointF p2, double angle, QPair &circleSolutions) { QPointF solutionOne(1, 1), solutionTwo(1, 1); double radius = distance(p1, p2) / (dms::DegToRad * angle); if (p1 == p2) { if (angle == 0) { circleSolutions = qMakePair(p1, p2); appendLogText(i18n("Only one solution is found.")); return ONE_CIRCLE_SOLUTION; } else { circleSolutions = qMakePair(solutionOne, solutionTwo); appendLogText(i18n("Infinite number of solutions found.")); return INFINITE_CIRCLE_SOLUTION; } } QPointF center(p1.x() / 2 + p2.x() / 2, p1.y() / 2 + p2.y() / 2); double halfDistance = distance(center, p1); if (halfDistance > radius) { circleSolutions = qMakePair(solutionOne, solutionTwo); appendLogText(i18n("No solution is found. Points are too far away")); return NO_CIRCLE_SOLUTION; } if (halfDistance - radius == 0) { circleSolutions = qMakePair(center, solutionTwo); appendLogText(i18n("Only one solution is found.")); return ONE_CIRCLE_SOLUTION; } double root = std::hypotf(radius, halfDistance) / distance(p1, p2); solutionOne.setX(center.x() + root * (p1.y() - p2.y())); solutionOne.setY(center.y() + root * (p2.x() - p1.x())); solutionTwo.setX(center.x() - root * (p1.y() - p2.y())); solutionTwo.setY(center.y() - root * (p2.x() - p1.x())); circleSolutions = qMakePair(solutionOne, solutionTwo); return TWO_CIRCLE_SOLUTION; } double Align::distance(const QPointF &p1, const QPointF &p2) { return std::hypotf(p2.x() - p1.x(), p2.y() - p1.y()); } bool Align::findRACircle(QVector3D &RACircle) { bool rc = false; QPointF p1 = pahImageInfos[0]->pixelCenter; QPointF p2 = pahImageInfos[1]->pixelCenter; QPointF p3 = pahImageInfos[2]->pixelCenter; if (!isPerpendicular(p1, p2, p3)) rc = calcCircle(p1, p2, p3, RACircle); else if (!isPerpendicular(p1, p3, p2)) rc = calcCircle(p1, p3, p2, RACircle); else if (!isPerpendicular(p2, p1, p3)) rc = calcCircle(p2, p1, p3, RACircle); else if (!isPerpendicular(p2, p3, p1)) rc = calcCircle(p2, p3, p1, RACircle); else if (!isPerpendicular(p3, p2, p1)) rc = calcCircle(p3, p2, p1, RACircle); else if (!isPerpendicular(p3, p1, p2)) rc = calcCircle(p3, p1, p2, RACircle); else { //TRACE("\nThe three pts are perpendicular to axis\n"); return false; } return rc; } bool Align::isPerpendicular(const QPointF &p1, const QPointF &p2, const QPointF &p3) // Check the given point are perpendicular to x or y axis { double yDelta_a = p2.y() - p1.y(); double xDelta_a = p2.x() - p1.x(); double yDelta_b = p3.y() - p2.y(); double xDelta_b = p3.x() - p2.x(); // checking whether the line of the two pts are vertical if (fabs(xDelta_a) <= 0.000000001 && fabs(yDelta_b) <= 0.000000001) { //TRACE("The points are perpendicular and parallel to x-y axis\n"); return false; } if (fabs(yDelta_a) <= 0.0000001) { //TRACE(" A line of two point are perpendicular to x-axis 1\n"); return true; } else if (fabs(yDelta_b) <= 0.0000001) { //TRACE(" A line of two point are perpendicular to x-axis 2\n"); return true; } else if (fabs(xDelta_a) <= 0.000000001) { //TRACE(" A line of two point are perpendicular to y-axis 1\n"); return true; } else if (fabs(xDelta_b) <= 0.000000001) { //TRACE(" A line of two point are perpendicular to y-axis 2\n"); return true; } else return false; } bool Align::calcCircle(const QPointF &p1, const QPointF &p2, const QPointF &p3, QVector3D &RACircle) { double yDelta_a = p2.y() - p1.y(); double xDelta_a = p2.x() - p1.x(); double yDelta_b = p3.y() - p2.y(); double xDelta_b = p3.x() - p2.x(); if (fabs(xDelta_a) <= 0.000000001 && fabs(yDelta_b) <= 0.000000001) { RACircle.setX(0.5 * (p2.x() + p3.x())); RACircle.setY(0.5 * (p1.y() + p2.y())); QPointF center(RACircle.x(), RACircle.y()); RACircle.setZ(distance(center, p1)); return true; } // IsPerpendicular() assure that xDelta(s) are not zero double aSlope = yDelta_a / xDelta_a; // double bSlope = yDelta_b / xDelta_b; if (fabs(aSlope - bSlope) <= 0.000000001) { // checking whether the given points are colinear. //TRACE("The three ps are colinear\n"); return false; } // calc center RACircle.setX((aSlope * bSlope * (p1.y() - p3.y()) + bSlope * (p1.x() + p2.x()) - aSlope * (p2.x() + p3.x())) / (2 * (bSlope - aSlope))); RACircle.setY(-1 * (RACircle.x() - (p1.x() + p2.x()) / 2) / aSlope + (p1.y() + p2.y()) / 2); QPointF center(RACircle.x(), RACircle.y()); RACircle.setZ(distance(center, p1)); return true; } void Align::setMountStatus(ISD::Telescope::Status newState) { switch (newState) { case ISD::Telescope::MOUNT_PARKING: case ISD::Telescope::MOUNT_SLEWING: case ISD::Telescope::MOUNT_MOVING: solveB->setEnabled(false); loadSlewB->setEnabled(false); PAHStartB->setEnabled(false); break; default: if (state != ALIGN_PROGRESS) { solveB->setEnabled(true); if (pahStage == PAH_IDLE) { PAHStartB->setEnabled(true); loadSlewB->setEnabled(true); } } break; } } void Align::setAstrometryDevice(ISD::GDInterface *newAstrometry) { remoteParserDevice = newAstrometry; remoteSolverR->setEnabled(true); if (remoteParser.get() != nullptr) { remoteParser->setAstrometryDevice(remoteParserDevice); connect(remoteParser.get(), &AstrometryParser::solverFinished, this, &Ekos::Align::solverFinished, Qt::UniqueConnection); connect(remoteParser.get(), &AstrometryParser::solverFailed, this, &Ekos::Align::solverFailed, Qt::UniqueConnection); } } void Align::setRotator(ISD::GDInterface *newRotator) { currentRotator = newRotator; connect(currentRotator, &ISD::GDInterface::numberUpdated, this, &Ekos::Align::processNumber, Qt::UniqueConnection); } void Align::refreshAlignOptions() { if (getSolverFOV()) getSolverFOV()->setImageDisplay(Options::astrometrySolverWCS()); m_AlignTimer.setInterval(Options::astrometryTimeout() * 1000); } void Align::setFilterManager(const QSharedPointer &manager) { filterManager = manager; connect(filterManager.data(), &FilterManager::ready, [this]() { if (filterPositionPending) { focusState = FOCUS_IDLE; filterPositionPending = false; captureAndSolve(); } } ); connect(filterManager.data(), &FilterManager::failed, [this]() { appendLogText(i18n("Filter operation failed.")); abort(); } ); connect(filterManager.data(), &FilterManager::newStatus, [this](Ekos::FilterState filterState) { if (filterPositionPending) { 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...")); break; default: break; } } }); connect(filterManager.data(), &FilterManager::labelsChanged, this, [this]() { checkFilter(); }); connect(filterManager.data(), &FilterManager::positionChanged, this, [this]() { checkFilter(); }); } QVariantMap Align::getEffectiveFOV() { KStarsData::Instance()->userdb()->GetAllEffectiveFOVs(effectiveFOVs); fov_x = fov_y = 0; for (auto &map : effectiveFOVs) { if (map["Profile"].toString() == m_ActiveProfile->name) { if (map["Width"].toInt() == ccd_width && map["Height"].toInt() == ccd_height && map["PixelW"].toDouble() == ccd_hor_pixel && map["PixelH"].toDouble() == ccd_ver_pixel && map["FocalLength"].toDouble() == focal_length) { fov_x = map["FovW"].toDouble(); fov_y = map["FovH"].toDouble(); return map; } } } return QVariantMap(); } void Align::saveNewEffectiveFOV(double newFOVW, double newFOVH) { if (newFOVW < 0 || newFOVH < 0 || (newFOVW == fov_x && newFOVH == fov_y)) return; QVariantMap effectiveMap = getEffectiveFOV(); // If ID exists, delete it first. if (effectiveMap.isEmpty() == false) KStarsData::Instance()->userdb()->DeleteEffectiveFOV(effectiveMap["id"].toString()); // If FOV is 0x0, then we just remove existing effective FOV if (newFOVW == 0.0 && newFOVH == 0.0) { calculateFOV(); return; } effectiveMap["Profile"] = m_ActiveProfile->name; effectiveMap["Width"] = ccd_width; effectiveMap["Height"] = ccd_height; effectiveMap["PixelW"] = ccd_hor_pixel; effectiveMap["PixelH"] = ccd_ver_pixel; effectiveMap["FocalLength"] = focal_length; effectiveMap["FovW"] = newFOVW; effectiveMap["FovH"] = newFOVH; KStarsData::Instance()->userdb()->AddEffectiveFOV(effectiveMap); calculateFOV(); } QStringList Align::getActiveSolvers() const { QStringList solvers; solvers << "Online"; #ifndef Q_OS_WIN solvers << "Offline"; #endif if (remoteParserDevice != nullptr) solvers << "Remote"; return solvers; } int Align::getActiveSolverIndex() const { return solverTypeGroup->checkedId(); } QString Align::getPAHMessage() const { switch (pahStage) { case PAH_IDLE: case PAH_FIND_CP: default: return introText->text(); case PAH_FIRST_CAPTURE: return firstCaptureText->text(); case PAH_FIRST_ROTATE: return firstRotateText->text(); case PAH_SECOND_CAPTURE: return secondCaptureText->text(); case PAH_SECOND_ROTATE: return secondRotateText->text(); case PAH_THIRD_CAPTURE: return thirdCaptureText->text(); case PAH_STAR_SELECT: return correctionText->text(); case PAH_PRE_REFRESH: case PAH_REFRESH: return refreshText->text(); case PAH_ERROR: return PAHErrorDescriptionLabel->text(); } } void Align::zoomAlignView() { alignView->ZoomDefault(); emit newFrame(alignView); } QJsonObject Align::getSettings() const { QJsonObject settings; settings.insert("camera", CCDCaptureCombo->currentText()); settings.insert("fw", FilterDevicesCombo->currentText()); settings.insert("filter", FilterPosCombo->currentText()); settings.insert("exp", exposureIN->value()); settings.insert("bin", binningCombo->currentIndex() + 1); settings.insert("solverAction", gotoModeButtonGroup->checkedId()); settings.insert("solverType", solverTypeGroup->checkedId()); settings.insert("scopeType", FOVScopeCombo->currentIndex()); return settings; } void Align::setSettings(const QJsonObject &settings) { CCDCaptureCombo->setCurrentText(settings["camera"].toString()); FilterDevicesCombo->setCurrentText(settings["fw"].toString()); FilterPosCombo->setCurrentText(settings["filter"].toString()); Options::setLockAlignFilterIndex(FilterPosCombo->currentIndex()); exposureIN->setValue(settings["exp"].toDouble(1)); binningCombo->setCurrentIndex(settings["bin"].toInt() - 1); gotoModeButtonGroup->button(settings["solverAction"].toInt(1))->click(); solverTypeGroup->button(settings["solverType"].toInt(1))->click(); FOVScopeCombo->setCurrentIndex(settings["scopeType"].toInt(0)); } void Align::syncSettings() { emit settingsUpdated(getSettings()); } QJsonObject Align::getPAHSettings() const { QJsonObject settings = getSettings(); settings.insert("mountDirection", PAHDirectionCombo->currentIndex()); settings.insert("mountSpeed", PAHSlewRateCombo->currentIndex()); settings.insert("mountRotation", PAHRotationSpin->value()); settings.insert("refresh", PAHExposure->value()); settings.insert("manualslew", PAHManual->isChecked()); return settings; } void Align::setPAHSettings(const QJsonObject &settings) { setSettings(settings); PAHDirectionCombo->setCurrentIndex(settings["mountDirection"].toInt(0)); PAHRotationSpin->setValue(settings["mountRotation"].toInt(30)); PAHExposure->setValue(settings["refresh"].toDouble(1)); if (settings.contains("mountSpeed")) PAHSlewRateCombo->setCurrentIndex(settings["mountSpeed"].toInt(0)); PAHManual->setChecked(settings["manualslew"].toBool(false)); } void Align::syncFOV() { QString newFOV = FOVOut->text(); QRegularExpression re("(\\d+\\.*\\d*)\\D*x\\D*(\\d+\\.*\\d*)"); QRegularExpressionMatch match = re.match(newFOV); if (match.hasMatch()) { double newFOVW = match.captured(1).toDouble(); double newFOVH = match.captured(2).toDouble(); //if (newFOVW > 0 && newFOVH > 0) saveNewEffectiveFOV(newFOVW, newFOVH); FOVOut->setStyleSheet(QString()); } else { KSNotification::error(i18n("Invalid FOV.")); FOVOut->setStyleSheet("background-color:red"); } } } diff --git a/kstars/ekos/capture/capture.cpp b/kstars/ekos/capture/capture.cpp index 67594d9ca..2ea0b8530 100644 --- a/kstars/ekos/capture/capture.cpp +++ b/kstars/ekos/capture/capture.cpp @@ -1,6611 +1,6611 @@ /* 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::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(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. Refocus Every Check refocusEveryNCheck->setChecked(Options::enforceRefocusEveryN()); connect(refocusEveryNCheck, &QCheckBox::toggled, [ = ](bool checked) { Options::setEnforceRefocusEveryN(checked); }); // 6. Refocus Every Value refocusEveryN->setValue(Options::refocusEveryN()); connect(refocusEveryN, &QDoubleSpinBox::editingFinished, [ = ]() { Options::setRefocusEveryN(refocusEveryN->value()); }); // 7. File settings: filter name filterCheck->setChecked(Options::fileSettingsUseFilter()); connect(filterCheck, &QCheckBox::toggled, [ = ](bool checked) { Options::setFileSettingsUseFilter(checked); }); // 8. File settings: duration expDurationCheck->setChecked(Options::fileSettingsUseDuration()); connect(expDurationCheck, &QCheckBox::toggled, [ = ](bool checked) { Options::setFileSettingsUseDuration(checked); }); // 9. 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, 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(); }); 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); } Capture::~Capture() { qDeleteAll(jobs); } void Capture::setDefaultCCD(QString ccd) { Options::setDefaultCaptureCCD(ccd); } 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 (!strcmp(filter->getDeviceName(), newFilter->getDeviceName())) return; } FilterDevicesCombo->addItem(newFilter->getDeviceName()); Filters.append(static_cast(newFilter)); filterManagerB->setEnabled(true); checkFilter(1); FilterDevicesCombo->setCurrentIndex(1); 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.")); 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; } // Do not change any settings if we are capturing. if (targetChip && 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); GainSpin->setRange(min, max); GainSpin->setSingleStep(step); currentCCD->getGain(&value); targetCustomGain = getGain(); // Set the custom gain if we have one // otherwise just put the current ccd actual gain value if (targetCustomGain > 0) GainSpin->setValue(targetCustomGain); else GainSpin->setValue(value); GainSpin->setReadOnly(currentCCD->getGainPermission() == IP_RO); connect(GainSpin, &QDoubleSpinBox::editingFinished, [this]() { setGain(GainSpin->value()); }); gridLayout->addWidget(GainSpin, 4, 5, 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); // 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()); 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); } } } } 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 (strcmp(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; 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(); 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 && strcmp(activeFilter->text, currentFilter->getDeviceName())) { IUSaveText(activeFilter, currentFilter->getDeviceName()); currentCCD->getDriverInfo()->getClientManager()->sendNewText(activeDevices); } // Reset filter name in CCD driver else if (currentFilter == nullptr && strlen(activeFilter->text) > 0) { 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 || meridianFlipStage >= MF_ALIGNING) return; if (currentCCD->getUploadMode() != ISD::CCD::UPLOAD_LOCAL) { if (bp == nullptr) { appendLogText(i18n("Failed to save file to %1", activeJob->getSignature())); abort(); return; } if (!strcmp(bp->name, "CCD2")) tChip = currentCCD->getChip(ISD::CCDChip::GUIDE_CCD); else tChip = currentCCD->getChip(ISD::CCDChip::PRIMARY_CCD); if (tChip != targetChip) return; if (targetChip->getCaptureMode() == FITS_FOCUS || targetChip->getCaptureMode() == FITS_GUIDE) 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); // 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() || m_State == CAPTURE_IDLE || m_State == CAPTURE_ABORTED) return; // 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(); captureTimeoutCounter = 0; // 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); } 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--; } 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; // 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 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) emit checkFocus(0.1); else 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->getFrameType() != FRAME_LIGHT) return false; // if (autoFocusReady == false) // return 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; isRefocus = getRefocusEveryNTimerElapsedSec() >= refocusEveryN->value() * 60; } else isRefocus = false; if (isRefocus) { 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 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) emit checkFocus(0.1); else 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; } 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 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(); 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...")); 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.baseName()); 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", - QDateTime::currentDateTime().toString("yyyy-MM-ddThh:mm:ss"), text)); + 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()); } 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...")); //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()); 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(); 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", iso->text()); } else { iso->setText("--"); jsonJob.insert("ISO", "--"); } 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; if (m_ObserverName.isEmpty() == false) FITSHeader["FITS_OBSERVER"] = m_ObserverName; if (m_TargetName.isEmpty() == false) FITSHeader["FITS_OBJECT"] = m_TargetName; else if (activeJob->getRawPrefix().isEmpty() == false) { FITSHeader["FITS_OBJECT"] = activeJob->getRawPrefix(); } 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::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 = sqrt(delta_ra * delta_ra + delta_dec * 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); 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) { 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 status) { if (meridianFlipStage != status) { switch (status) { case MF_NONE: if (m_State == CAPTURE_PAUSED) // paused after meridian flip secondsLabel->setText(i18n("Paused...")); /* disabled since the focusing label will be overwritten else secondsLabel->setText(""); */ meridianFlipStage = status; 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 = status; 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 = status; break; case MF_COMPLETED: secondsLabel->setText(i18n("Flip complete.")); break; default: meridianFlipStage = status; break; } } } void Capture::meridianFlipStatusChanged(Mount::MeridianFlipStatus 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 = (guideState == GUIDE_GUIDING); 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) prefixIN->setText(target); }); 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()); 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), "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); 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); } 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(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.close(); 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); } 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) 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 imagePrefix += QString::number(exposureIN->value(), 'f', 3) + 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 = 0; if (job->getStatus() == SequenceJob::JOB_BUSY) remaining += (job->getExposure() + job->getDelay() / 1000) * (job->getCount() - job->getCompleted()) + job->getExposeLeft(); else remaining += (job->getExposure() + job->getDelay() / 1000) * (job->getCount() - job->getCompleted()); 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 (guideState >= GUIDE_GUIDING && deviationDetected) 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 (strcmp(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) { 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_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) { 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 (guideState == GUIDE_GUIDING && 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: 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: 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) { 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: // 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) { 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) { 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->getRawPrefix(); 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) { 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(); } } // FIXME Migrate to Filter Manager #if 0 void Capture::loadFilterOffsets() { // Get all OAL equipment filter list KStarsData::Instance()->userdb()->GetAllFilters(m_filterList); filterFocusOffsets.clear(); for (int i = 0; i < FilterPosCombo->count(); i++) { FocusOffset * oneOffset = new FocusOffset; oneOffset->filter = FilterPosCombo->itemText(i); oneOffset->offset = 0; // Find matching filter if any and loads its offset foreach (OAL::Filter * o, m_filterList) { if (o->vendor() == FilterCaptureCombo->currentText() && o->color() == oneOffset->filter) { oneOffset->offset = o->offset().toInt(); break; } } filterFocusOffsets.append(oneOffset); } } void Capture::showFilterOffsetDialog() { loadFilterOffsets(); QDialog filterOffsetDialog; filterOffsetDialog.setWindowTitle(i18n("Filter Focus Offsets")); QDialogButtonBox * buttonBox = new QDialogButtonBox(QDialogButtonBox::Ok | QDialogButtonBox::Cancel, Qt::Horizontal, &filterOffsetDialog); connect(buttonBox, SIGNAL(accepted()), &filterOffsetDialog, &Ekos::Capture::accept())); connect(buttonBox, SIGNAL(rejected()), &filterOffsetDialog, &Ekos::Capture::reject())); QVBoxLayout * mainLayout = new QVBoxLayout(&filterOffsetDialog); QGridLayout * grid = new QGridLayout(&filterOffsetDialog); QHBoxLayout * tipLayout = new QHBoxLayout(&filterOffsetDialog); QLabel * tipIcon = new QLabel(&filterOffsetDialog); QLabel * tipText = new QLabel(&filterOffsetDialog); tipIcon->setPixmap( QIcon::fromTheme("kstars_flag").pixmap(QSize(32, 32))); tipIcon->setFixedSize(32, 32); tipText->setText(i18n("Set relative filter focus offset in steps.")); tipLayout->addWidget(tipIcon); tipLayout->addWidget(tipText); mainLayout->addLayout(grid); mainLayout->addLayout(tipLayout); mainLayout->addWidget(buttonBox); //filterOffsetDialog.setLayout(mainLayout); for (int i = 0; i < filterFocusOffsets.count(); i++) { FocusOffset * oneOffset = filterFocusOffsets.at(i); QLabel * label = new QLabel(oneOffset->filter, &filterOffsetDialog); QSpinBox * spin = new QSpinBox(&filterOffsetDialog); spin->setMinimum(-10000); spin->setMaximum(10000); spin->setSingleStep(100); spin->setValue(oneOffset->offset); grid->addWidget(label, i, 0); grid->addWidget(spin, i, 1); } if (filterOffsetDialog.exec() == QDialog::Accepted) { for (int i = 0; i < filterFocusOffsets.count(); i++) { FocusOffset * oneOffset = filterFocusOffsets.at(i); oneOffset->offset = static_cast(grid->itemAtPosition(i, 1)->widget())->value(); // Find matching filter if any and save its offset OAL::Filter * matchedFilter = nullptr; foreach (OAL::Filter * o, m_filterList) { if (o->vendor() == FilterCaptureCombo->currentText() && o->color() == oneOffset->filter) { o->setOffset(QString::number(oneOffset->offset)); matchedFilter = o; break; } } #if 0 // If no filter exists, let's create one if (matchedFilter == nullptr) { KStarsData::Instance()->userdb()->AddFilter(FilterCaptureCombo->currentText(), "", "", QString::number(oneOffset->offset), oneOffset->filter, "1"); } // Or update Existing one else { KStarsData::Instance()->userdb()->AddFilter(FilterCaptureCombo->currentText(), "", "", QString::number(oneOffset->offset), oneOffset->filter, matchedFilter->exposure(), matchedFilter->id()); } #endif } } } #endif 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) { 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(); // 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() { captureTimeoutCounter++; if (captureTimeoutCounter >= 3) { captureTimeoutCounter = 0; appendLogText(i18n("Exposure timeout. Aborting...")); abort(); return; } appendLogText(i18n("Exposure timeout. Restarting exposure...")); ISD::CCDChip * targetChip = currentCCD->getChip(ISD::CCDChip::PRIMARY_CCD); targetChip->abortExposure(); targetChip->capture(exposureIN->value()); captureTimeout.start(exposureIN->value() * 1000 + CAPTURE_TIMEOUT_THRESHOLD); } 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 && !strcmp(currentTelescope->getDeviceName(), device->getDeviceName())) { currentTelescope = nullptr; } else if (currentDome && !strcmp(currentDome->getDeviceName(), device->getDeviceName())) { currentDome = nullptr; } else if (currentRotator && !strcmp(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; 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; 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() { 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; } } diff --git a/kstars/ekos/focus/focus.cpp b/kstars/ekos/focus/focus.cpp index f27951273..2ce16f87a 100644 --- a/kstars/ekos/focus/focus.cpp +++ b/kstars/ekos/focus/focus.cpp @@ -1,3396 +1,3397 @@ /* 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 "focus.h" #include "focusadaptor.h" #include "kstars.h" #include "kstarsdata.h" #include "Options.h" #include "auxiliary/kspaths.h" #include "auxiliary/ksmessagebox.h" #include "ekos/manager.h" #include "ekos/auxiliary/darklibrary.h" #include "fitsviewer/fitsdata.h" #include "fitsviewer/fitstab.h" #include "fitsviewer/fitsview.h" #include "indi/indifilter.h" #include "ksnotification.h" #include #include #include #include #include #define FOCUS_TIMEOUT_THRESHOLD 120000 #define MAXIMUM_ABS_ITERATIONS 30 #define MAXIMUM_RESET_ITERATIONS 2 #define AUTO_STAR_TIMEOUT 45000 #define MINIMUM_PULSE_TIMER 32 #define MAX_RECAPTURE_RETRIES 3 #define MINIMUM_POLY_SOLUTIONS 2 namespace Ekos { Focus::Focus() { // #1 Set the UI setupUi(this); // #2 Register DBus qRegisterMetaType("Ekos::FocusState"); qDBusRegisterMetaType(); new FocusAdaptor(this); QDBusConnection::sessionBus().registerObject("/KStars/Ekos/Focus", this); // #3 Init connections initConnections(); // #4 Init Plots initPlots(); // #5 Init View initView(); // #6 Reset all buttons to default states resetButtons(); // #7 Image Effects for (auto &filter : FITSViewer::filterTypes) filterCombo->addItem(filter); filterCombo->setCurrentIndex(Options::focusEffect()); defaultScale = static_cast(Options::focusEffect()); connect(filterCombo, static_cast(&QComboBox::activated), this, &Ekos::Focus::filterChangeWarning); // #8 Load All settings loadSettings(); // #9 Init Setting Connection now initSettingsConnections(); //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); appendLogText(i18n("Idle.")); } Focus::~Focus() { if (focusingWidget->parent() == nullptr) toggleFocusingWidgetFullScreen(); } void Focus::resetFrame() { if (currentCCD) { ISD::CCDChip *targetChip = currentCCD->getChip(ISD::CCDChip::PRIMARY_CCD); if (targetChip) { //fx=fy=fw=fh=0; targetChip->resetFrame(); int x, y, w, h; targetChip->getFrame(&x, &y, &w, &h); qCDebug(KSTARS_EKOS_FOCUS) << "Frame is reset. X:" << x << "Y:" << y << "W:" << w << "H:" << h << "binX:" << 1 << "binY:" << 1; QVariantMap settings; settings["x"] = x; settings["y"] = y; settings["w"] = w; settings["h"] = h; settings["binx"] = 1; settings["biny"] = 1; frameSettings[targetChip] = settings; starSelected = false; starCenter = QVector3D(); subFramed = false; focusView->setTrackingBox(QRect()); } } } bool Focus::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 Focus::camera() { if (currentCCD) return currentCCD->getDeviceName(); return QString(); } void Focus::checkCCD(int ccdNum) { if (ccdNum == -1) { ccdNum = CCDCaptureCombo->currentIndex(); if (ccdNum == -1) return; } if (ccdNum >= 0 && ccdNum <= CCDs.count()) { currentCCD = CCDs.at(ccdNum); ISD::CCDChip *targetChip = currentCCD->getChip(ISD::CCDChip::PRIMARY_CCD); if (targetChip && targetChip->isCapturing()) return; for (ISD::CCD *oneCCD : CCDs) { if (oneCCD == currentCCD) continue; if (captureInProgress == false) oneCCD->disconnect(this); } if (targetChip) { targetChip->setImageView(focusView, FITS_FOCUS); binningCombo->setEnabled(targetChip->canBin()); useSubFrame->setEnabled(targetChip->canSubframe()); if (targetChip->canBin()) { int subBinX = 1, subBinY = 1; binningCombo->clear(); targetChip->getMaxBin(&subBinX, &subBinY); for (int i = 1; i <= subBinX; i++) binningCombo->addItem(QString("%1x%2").arg(i).arg(i)); activeBin = Options::focusXBin(); binningCombo->setCurrentIndex(activeBin - 1); } else activeBin = 1; QStringList isoList = targetChip->getISOList(); ISOCombo->clear(); if (isoList.isEmpty()) { ISOCombo->setEnabled(false); ISOLabel->setEnabled(false); } else { ISOCombo->setEnabled(true); ISOLabel->setEnabled(true); ISOCombo->addItems(isoList); ISOCombo->setCurrentIndex(targetChip->getISOIndex()); } connect(currentCCD, &ISD::CCD::videoStreamToggled, this, &Ekos::Focus::setVideoStreamEnabled, Qt::UniqueConnection); liveVideoB->setEnabled(currentCCD->hasVideoStream()); if (currentCCD->hasVideoStream()) setVideoStreamEnabled(currentCCD->isStreamingEnabled()); else liveVideoB->setIcon(QIcon::fromTheme("camera-off")); bool hasGain = currentCCD->hasGain(); gainLabel->setEnabled(hasGain); gainIN->setEnabled(hasGain && currentCCD->getGainPermission() != IP_RO); if (hasGain) { double gain = 0, min = 0, max = 0, step = 1; currentCCD->getGainMinMaxStep(&min, &max, &step); if (currentCCD->getGain(&gain)) { gainIN->setMinimum(min); gainIN->setMaximum(max); if (step > 0) gainIN->setSingleStep(step); double defaultGain = Options::focusGain(); if (defaultGain > 0) gainIN->setValue(defaultGain); else gainIN->setValue(gain); } } else gainIN->clear(); } } syncCCDInfo(); } void Focus::syncCCDInfo() { if (currentCCD == nullptr) return; ISD::CCDChip *targetChip = currentCCD->getChip(ISD::CCDChip::PRIMARY_CCD); useSubFrame->setEnabled(targetChip->canSubframe()); if (frameSettings.contains(targetChip) == false) { int x, y, w, h; if (targetChip->getFrame(&x, &y, &w, &h)) { int binx = 1, biny = 1; targetChip->getBinning(&binx, &biny); if (w > 0 && h > 0) { int minX, maxX, minY, maxY, minW, maxW, minH, maxH; targetChip->getFrameMinMax(&minX, &maxX, &minY, &maxY, &minW, &maxW, &minH, &maxH); QVariantMap settings; settings["x"] = useSubFrame->isChecked() ? x : minX; settings["y"] = useSubFrame->isChecked() ? y : minY; settings["w"] = useSubFrame->isChecked() ? w : maxW; settings["h"] = useSubFrame->isChecked() ? h : maxH; settings["binx"] = binx; settings["biny"] = biny; frameSettings[targetChip] = settings; } } } } void Focus::addFilter(ISD::GDInterface *newFilter) { foreach (ISD::GDInterface *filter, Filters) { if (!strcmp(filter->getDeviceName(), newFilter->getDeviceName())) return; } FilterCaptureLabel->setEnabled(true); FilterDevicesCombo->setEnabled(true); FilterPosLabel->setEnabled(true); FilterPosCombo->setEnabled(true); filterManagerB->setEnabled(true); FilterDevicesCombo->addItem(newFilter->getDeviceName()); Filters.append(static_cast(newFilter)); checkFilter(1); FilterDevicesCombo->setCurrentIndex(1); if (Options::defaultFocusFilterWheel().isEmpty() == false) FilterDevicesCombo->setCurrentText(Options::defaultFocusFilterWheel()); } bool Focus::setFilterWheel(const QString &device) { bool deviceFound = false; for (int i = 1; i < FilterDevicesCombo->count(); i++) if (device == FilterDevicesCombo->itemText(i)) { checkFilter(i); deviceFound = true; break; } if (deviceFound == false) return false; return true; } QString Focus::filterWheel() { if (FilterDevicesCombo->currentIndex() >= 1) return FilterDevicesCombo->currentText(); return QString(); } bool Focus::setFilter(const QString &filter) { if (FilterDevicesCombo->currentIndex() >= 1) { FilterPosCombo->setCurrentText(filter); return true; } return false; } QString Focus::filter() { return FilterPosCombo->currentText(); } void Focus::checkFilter(int filterNum) { if (filterNum == -1) { filterNum = FilterDevicesCombo->currentIndex(); if (filterNum == -1) return; } // "--" is no filter if (filterNum == 0) { currentFilter = nullptr; currentFilterPosition = -1; FilterPosCombo->clear(); return; } if (filterNum <= Filters.count()) currentFilter = Filters.at(filterNum - 1); //Options::setDefaultFocusFilterWheel(currentFilter->getDeviceName()); filterManager->setCurrentFilterWheel(currentFilter); FilterPosCombo->clear(); FilterPosCombo->addItems(filterManager->getFilterLabels()); currentFilterPosition = filterManager->getFilterPosition(); FilterPosCombo->setCurrentIndex(currentFilterPosition - 1); //Options::setDefaultFocusFilterWheelFilter(FilterPosCombo->currentText()); exposureIN->setValue(filterManager->getFilterExposure()); } void Focus::addFocuser(ISD::GDInterface *newFocuser) { ISD::Focuser *oneFocuser = static_cast(newFocuser); if (Focusers.contains(oneFocuser)) return; focuserCombo->addItem(oneFocuser->getDeviceName()); Focusers.append(oneFocuser); currentFocuser = oneFocuser; checkFocuser(); } bool Focus::setFocuser(const QString &device) { for (int i = 0; i < focuserCombo->count(); i++) if (device == focuserCombo->itemText(i)) { focuserCombo->setCurrentIndex(i); checkFocuser(i); return true; } return false; } QString Focus::focuser() { if (currentFocuser) return currentFocuser->getDeviceName(); return QString(); } void Focus::checkFocuser(int FocuserNum) { if (FocuserNum == -1) FocuserNum = focuserCombo->currentIndex(); if (FocuserNum == -1) { currentFocuser = nullptr; return; } if (FocuserNum < Focusers.count()) currentFocuser = Focusers.at(FocuserNum); filterManager->setFocusReady(currentFocuser->isConnected()); // Disconnect all focusers for (auto &oneFocuser : Focusers) { disconnect(oneFocuser, &ISD::GDInterface::numberUpdated, this, &Ekos::Focus::processFocusNumber); } canAbsMove = currentFocuser->canAbsMove(); if (canAbsMove) { getAbsFocusPosition(); absTicksSpin->setEnabled(true); absTicksLabel->setEnabled(true); startGotoB->setEnabled(true); absTicksSpin->setValue(currentPosition); } else { absTicksSpin->setEnabled(false); absTicksLabel->setEnabled(false); startGotoB->setEnabled(false); } canRelMove = currentFocuser->canRelMove(); // In case we have a purely relative focuser, we pretend // it is an absolute focuser with initial point set at 50,000 // This is done we can use the same algorithm used for absolute focuser if (canAbsMove == false && canRelMove == true) { currentPosition = 50000; absMotionMax = 100000; absMotionMin = 0; } canTimerMove = currentFocuser->canTimerMove(); focusType = (canRelMove || canAbsMove || canTimerMove) ? FOCUS_AUTO : FOCUS_MANUAL; bool hasBacklash = currentFocuser->hasBacklash(); focusBacklashSpin->setEnabled(hasBacklash); focusBacklashSpin->disconnect(this); if (hasBacklash) { double min = 0, max = 0, step = 0; currentFocuser->getMinMaxStep("FOCUS_BACKLASH_STEPS", "FOCUS_BACKLASH_VALUE", &min, &max, &step); focusBacklashSpin->setMinimum(min); focusBacklashSpin->setMaximum(max); focusBacklashSpin->setSingleStep(step); focusBacklashSpin->setValue(currentFocuser->getBacklash()); connect(focusBacklashSpin, static_cast(&QSpinBox::valueChanged), this, [this](int value) { if (currentFocuser) currentFocuser->setBacklash(value); }); } else { focusBacklashSpin->setValue(0); } connect(currentFocuser, &ISD::GDInterface::numberUpdated, this, &Ekos::Focus::processFocusNumber, Qt::UniqueConnection); //connect(currentFocuser, SIGNAL(propertyDefined(INDI::Property*)), this, &Ekos::Focus::(registerFocusProperty(INDI::Property*)), Qt::UniqueConnection); resetButtons(); //if (!inAutoFocus && !inFocusLoop && !captureInProgress && !inSequenceFocus) // emit autoFocusFinished(true, -1); } void Focus::addCCD(ISD::GDInterface *newCCD) { if (CCDs.contains(static_cast(newCCD))) return; CCDs.append(static_cast(newCCD)); CCDCaptureCombo->addItem(newCCD->getDeviceName()); checkCCD(); } void Focus::getAbsFocusPosition() { if (!canAbsMove) return; INumberVectorProperty *absMove = currentFocuser->getBaseDevice()->getNumber("ABS_FOCUS_POSITION"); if (absMove) { currentPosition = absMove->np[0].value; absMotionMax = absMove->np[0].max; absMotionMin = absMove->np[0].min; absTicksSpin->setMinimum(absMove->np[0].min); absTicksSpin->setMaximum(absMove->np[0].max); absTicksSpin->setSingleStep(absMove->np[0].step); maxTravelIN->setMinimum(absMove->np[0].min); maxTravelIN->setMaximum(absMove->np[0].max); absTicksLabel->setText(QString::number(static_cast(currentPosition))); stepIN->setMaximum(absMove->np[0].max / 2); //absTicksSpin->setValue(currentPosition); } } void Focus::start() { if (currentCCD == nullptr) { appendLogText(i18n("No CCD connected.")); return; } lastFocusDirection = FOCUS_NONE; polySolutionFound = 0; waitStarSelectTimer.stop(); starsHFR.clear(); lastHFR = 0; if (canAbsMove) { absIterations = 0; getAbsFocusPosition(); pulseDuration = stepIN->value(); } else if (canRelMove) { //appendLogText(i18n("Setting dummy central position to 50000")); absIterations = 0; pulseDuration = stepIN->value(); //currentPosition = 50000; absMotionMax = 100000; absMotionMin = 0; } else { pulseDuration = stepIN->value(); if (pulseDuration <= MINIMUM_PULSE_TIMER) { appendLogText(i18n("Starting pulse step is too low. Increase the step size to %1 or higher...", MINIMUM_PULSE_TIMER * 5)); return; } } inAutoFocus = true; HFRFrames.clear(); resetButtons(); reverseDir = false; /*if (fw > 0 && fh > 0) starSelected= true; else starSelected= false;*/ clearDataPoints(); if (firstGaus) { profilePlot->removeGraph(firstGaus); firstGaus = nullptr; } // Options::setFocusTicks(stepIN->value()); // Options::setFocusTolerance(toleranceIN->value()); // //Options::setFocusExposure(exposureIN->value()); // Options::setFocusMaxTravel(maxTravelIN->value()); // Options::setFocusBoxSize(focusBoxSize->value()); // Options::setFocusSubFrame(useSubFrame->isChecked()); // Options::setFocusAutoStarEnabled(useAutoStar->isChecked()); // Options::setSuspendGuiding(suspendGuideCheck->isChecked()); // Options::setUseFocusDarkFrame(darkFrameCheck->isChecked()); // Options::setFocusFramesCount(focusFramesSpin->value()); // Options::setFocusUseFullField(useFullField->isChecked()); qCDebug(KSTARS_EKOS_FOCUS) << "Starting focus with box size: " << focusBoxSize->value() << " Subframe: " << ( useSubFrame->isChecked() ? "yes" : "no" ) << " Autostar: " << ( useAutoStar->isChecked() ? "yes" : "no" ) << " Full frame: " << ( useFullField->isChecked() ? "yes" : "no " ) << " [" << fullFieldInnerRing->value() << "%," << fullFieldOuterRing->value() << "%]" << " Step Size: " << stepIN->value() << " Threshold: " << thresholdSpin->value() << " Tolerance: " << toleranceIN->value() << " Frames: " << 1 /*focusFramesSpin->value()*/ << " Maximum Travel: " << maxTravelIN->value(); if (useAutoStar->isChecked()) appendLogText(i18n("Autofocus in progress...")); else appendLogText(i18n("Please wait until image capture is complete...")); if (suspendGuideCheck->isChecked()) { m_GuidingSuspended = true; emit suspendGuiding(); } //emit statusUpdated(true); state = Ekos::FOCUS_PROGRESS; qCDebug(KSTARS_EKOS_FOCUS) << "State:" << Ekos::getFocusStatusString(state); emit newStatus(state); // Denoise with median filter //defaultScale = FITS_MEDIAN; KSNotification::event(QLatin1String("FocusStarted"), i18n("Autofocus operation started")); capture(); } void Focus::checkStopFocus() { if (inSequenceFocus == true) { inSequenceFocus = false; setAutoFocusResult(false); } if (captureInProgress && inAutoFocus == false && inFocusLoop == false) { captureB->setEnabled(true); stopFocusB->setEnabled(false); appendLogText(i18n("Capture aborted.")); } abort(); } void Focus::abort() { stop(true); } void Focus::stop(bool aborted) { qCDebug(KSTARS_EKOS_FOCUS) << "Stopppig Focus"; captureTimeout.stop(); ISD::CCDChip *targetChip = currentCCD->getChip(ISD::CCDChip::PRIMARY_CCD); inAutoFocus = false; inFocusLoop = false; // Why starSelected is set to false below? We should retain star selection status under: // 1. Autostar is off, or // 2. Toggle subframe, or // 3. Reset frame // 4. Manual motion? //starSelected = false; polySolutionFound = 0; captureInProgress = false; minimumRequiredHFR = -1; noStarCount = 0; HFRFrames.clear(); //maxHFR=1; disconnect(currentCCD, &ISD::CCD::BLOBUpdated, this, &Ekos::Focus::newFITS); if (rememberUploadMode != currentCCD->getUploadMode()) currentCCD->setUploadMode(rememberUploadMode); if (rememberCCDExposureLooping) currentCCD->setExposureLoopingEnabled(true); targetChip->abortExposure(); resetButtons(); absIterations = 0; HFRInc = 0; reverseDir = false; //emit statusUpdated(false); if (aborted) { state = Ekos::FOCUS_ABORTED; qCDebug(KSTARS_EKOS_FOCUS) << "State:" << Ekos::getFocusStatusString(state); emit newStatus(state); } } void Focus::capture() { captureTimeout.stop(); if (captureInProgress) { qCWarning(KSTARS_EKOS_FOCUS) << "Capture called while already in progress. Capture is ignored."; return; } if (currentCCD == nullptr) { appendLogText(i18n("No CCD connected.")); return; } waitStarSelectTimer.stop(); ISD::CCDChip *targetChip = currentCCD->getChip(ISD::CCDChip::PRIMARY_CCD); double seqExpose = exposureIN->value(); if (currentCCD->isConnected() == false) { appendLogText(i18n("Error: Lost connection to CCD.")); return; } if (currentCCD->isBLOBEnabled() == false) { currentCCD->setBLOBEnabled(true); } if (currentFilter != nullptr && FilterPosCombo->currentIndex() != -1) { if (currentFilter->isConnected() == false) { appendLogText(i18n("Error: Lost connection to filter wheel.")); return; } int targetPosition = FilterPosCombo->currentIndex() + 1; QString lockedFilter = filterManager->getFilterLock(FilterPosCombo->currentText()); // We change filter if: // 1. Target position is not equal to current position. // 2. Locked filter of CURRENT filter is a different filter. if (lockedFilter != "--" && lockedFilter != FilterPosCombo->currentText()) { int lockedFilterIndex = FilterPosCombo->findText(lockedFilter); if (lockedFilterIndex >= 0) { // Go back to this filter one we are done fallbackFilterPending = true; fallbackFilterPosition = targetPosition; targetPosition = lockedFilterIndex + 1; } } filterPositionPending = (targetPosition != currentFilterPosition); // If either the target position is not equal to the current position, OR if (filterPositionPending) { // Apply all policies except autofocus since we are already in autofocus module doh. filterManager->setFilterPosition(targetPosition, static_cast(FilterManager::CHANGE_POLICY | FilterManager::OFFSET_POLICY)); return; } } if (currentCCD->getUploadMode() == ISD::CCD::UPLOAD_LOCAL) { rememberUploadMode = ISD::CCD::UPLOAD_LOCAL; currentCCD->setUploadMode(ISD::CCD::UPLOAD_CLIENT); } rememberCCDExposureLooping = currentCCD->isLooping(); if (rememberCCDExposureLooping) currentCCD->setExposureLoopingEnabled(false); currentCCD->setTransformFormat(ISD::CCD::FORMAT_FITS); targetChip->setBinning(activeBin, activeBin); targetChip->setCaptureMode(FITS_FOCUS); // Always disable filtering if using a dark frame and then re-apply after subtraction. TODO: Implement this in capture and guide and align if (darkFrameCheck->isChecked()) targetChip->setCaptureFilter(FITS_NONE); else targetChip->setCaptureFilter(defaultScale); if (ISOCombo->isEnabled() && ISOCombo->currentIndex() != -1 && targetChip->getISOIndex() != ISOCombo->currentIndex()) targetChip->setISOIndex(ISOCombo->currentIndex()); if (gainIN->isEnabled()) currentCCD->setGain(gainIN->value()); connect(currentCCD, &ISD::CCD::BLOBUpdated, this, &Ekos::Focus::newFITS); targetChip->setFrameType(FRAME_LIGHT); if (frameSettings.contains(targetChip)) { QVariantMap settings = frameSettings[targetChip]; targetChip->setFrame(settings["x"].toInt(), settings["y"].toInt(), settings["w"].toInt(), settings["h"].toInt()); settings["binx"] = activeBin; settings["biny"] = activeBin; frameSettings[targetChip] = settings; } captureInProgress = true; focusView->setBaseSize(focusingWidget->size()); // Timeout is exposure duration + timeout threshold in seconds captureTimeout.start(seqExpose * 1000 + FOCUS_TIMEOUT_THRESHOLD); targetChip->capture(seqExpose); if (inFocusLoop == false) { appendLogText(i18n("Capturing image...")); if (inAutoFocus == false) { captureB->setEnabled(false); stopFocusB->setEnabled(true); } } } bool Focus::focusIn(int ms) { if (currentFocuser == nullptr) return false; if (currentFocuser->isConnected() == false) { appendLogText(i18n("Error: Lost connection to Focuser.")); return false; } if (ms == -1) ms = stepIN->value(); qCDebug(KSTARS_EKOS_FOCUS) << "Focus in (" << ms << ")"; lastFocusDirection = FOCUS_IN; currentFocuser->focusIn(); if (canAbsMove) { currentFocuser->moveAbs(currentPosition - ms); appendLogText(i18n("Focusing inward by %1 steps...", ms)); } else if (canRelMove) { currentFocuser->moveRel(ms); appendLogText(i18n("Focusing inward by %1 steps...", ms)); } else { currentFocuser->moveByTimer(ms); appendLogText(i18n("Focusing inward by %1 ms...", ms)); } return true; } bool Focus::focusOut(int ms) { if (currentFocuser == nullptr) return false; if (currentFocuser->isConnected() == false) { appendLogText(i18n("Error: Lost connection to Focuser.")); return false; } lastFocusDirection = FOCUS_OUT; if (ms == -1) ms = stepIN->value(); qCDebug(KSTARS_EKOS_FOCUS) << "Focus out (" << ms << ")"; currentFocuser->focusOut(); if (canAbsMove) { currentFocuser->moveAbs(currentPosition + ms); appendLogText(i18n("Focusing outward by %1 steps...", ms)); } else if (canRelMove) { currentFocuser->moveRel(ms); appendLogText(i18n("Focusing outward by %1 steps...", ms)); } else { currentFocuser->moveByTimer(ms); appendLogText(i18n("Focusing outward by %1 ms...", ms)); } return true; } void Focus::newFITS(IBLOB *bp) { if (bp == nullptr) { capture(); return; } // Ignore guide head if there is any. if (!strcmp(bp->name, "CCD2")) return; captureTimeout.stop(); captureTimeoutCounter = 0; ISD::CCDChip *targetChip = currentCCD->getChip(ISD::CCDChip::PRIMARY_CCD); disconnect(currentCCD, &ISD::CCD::BLOBUpdated, this, &Ekos::Focus::newFITS); if (darkFrameCheck->isChecked()) { FITSData *darkData = DarkLibrary::Instance()->getDarkFrame(targetChip, exposureIN->value()); QVariantMap settings = frameSettings[targetChip]; uint16_t offsetX = settings["x"].toInt() / settings["binx"].toInt(); uint16_t offsetY = settings["y"].toInt() / settings["biny"].toInt(); connect(DarkLibrary::Instance(), &DarkLibrary::darkFrameCompleted, this, [&](bool completed) { DarkLibrary::Instance()->disconnect(this); darkFrameCheck->setChecked(completed); if (completed) setCaptureComplete(); else abort(); }); connect(DarkLibrary::Instance(), &DarkLibrary::newLog, this, &Ekos::Focus::appendLogText); targetChip->setCaptureFilter(defaultScale); if (darkData) DarkLibrary::Instance()->subtract(darkData, focusView, defaultScale, offsetX, offsetY); else { DarkLibrary::Instance()->captureAndSubtract(targetChip, focusView, exposureIN->value(), offsetX, offsetY); } return; } setCaptureComplete(); } void Focus::setCaptureComplete() { DarkLibrary::Instance()->disconnect(this); // Get Binning ISD::CCDChip *targetChip = currentCCD->getChip(ISD::CCDChip::PRIMARY_CCD); int subBinX = 1, subBinY = 1; targetChip->getBinning(&subBinX, &subBinY); // If we have a box, sync the bounding box to its position. syncTrackingBoxPosition(); // Notify user if we're not looping if (inFocusLoop == false) appendLogText(i18n("Image received.")); // If we're not looping and not in autofocus, enable user to capture again. if (captureInProgress && inFocusLoop == false && inAutoFocus == false) { captureB->setEnabled(true); stopFocusB->setEnabled(false); currentCCD->setUploadMode(rememberUploadMode); } if (rememberCCDExposureLooping) currentCCD->setExposureLoopingEnabled(true); captureInProgress = false; // Get handle to the image data FITSData *image_data = focusView->getImageData(); // Emit the tracking (bounding) box view emit newStarPixmap(focusView->getTrackingBoxPixmap(10)); // If we are not looping; OR // If we are looping but we already have tracking box enabled; OR // If we are asked to analyze _all_ the stars within the field // THEN let's find stars in the image and get current HFR if (inFocusLoop == false || (inFocusLoop && (focusView->isTrackingBoxEnabled() || Options::focusUseFullField()))) { // First check that we haven't already search for stars // Since star-searching algorithm are time-consuming, we should only search when necessary if (image_data->areStarsSearched() == false) { // Reset current HFR currentHFR = -1; // When we're using FULL field view, we always use either CENTROID algorithm which is the default // standard algorithm in KStars, or SEP. The other algorithms are too inefficient to run on full frames and require // a bounding box for them to be effective in near real-time application. if (Options::focusUseFullField()) { if (focusDetection != ALGORITHM_CENTROID && focusDetection != ALGORITHM_SEP) focusView->findStars(ALGORITHM_CENTROID); else focusView->findStars(focusDetection); focusView->setStarFilterRange(static_cast (fullFieldInnerRing->value() / 100.0), static_cast (fullFieldOuterRing->value() / 100.0)); focusView->filterStars(); focusView->updateFrame(); // Get the average HFR of the whole frame currentHFR = image_data->getHFR(HFR_AVERAGE); } else { // If star is already selected then use whatever algorithm currently selected. if (starSelected) { focusView->findStars(focusDetection); focusView->updateFrame(); currentHFR = image_data->getHFR(HFR_MAX); } else { // Disable tracking box focusView->setTrackingBoxEnabled(false); // If algorithm is set something other than Centeroid or SEP, then force Centroid // Since it is the most reliable detector when nothing was selected before. if (focusDetection != ALGORITHM_CENTROID && focusDetection != ALGORITHM_SEP) focusView->findStars(ALGORITHM_CENTROID); else // Otherwise, continue to find use using the selected algorithm focusView->findStars(focusDetection); // Reenable tracking box focusView->setTrackingBoxEnabled(true); focusView->updateFrame(); // Get maximum HFR in the frame currentHFR = image_data->getHFR(HFR_MAX); } } } // Let's now report the current HFR qCDebug(KSTARS_EKOS_FOCUS) << "Focus newFITS #" << HFRFrames.count() + 1 << ": Current HFR " << currentHFR << " Num stars " << (starSelected ? 1 : image_data->getDetectedStars()); // Add it to existing frames in case we need to take an average HFRFrames.append(currentHFR); // Check if we need to average more than a single frame if (HFRFrames.count() >= focusFramesSpin->value()) { currentHFR = 0; // Remove all -1 QMutableVectorIterator i(HFRFrames); while (i.hasNext()) { if (i.next() == -1) i.remove(); } if (HFRFrames.isEmpty()) currentHFR = -1; else { // Perform simple sigma clipping if frames count > 3 if (HFRFrames.count() > 3) { // Sort all HFRs std::sort(HFRFrames.begin(), HFRFrames.end()); const auto median = ((HFRFrames.size() % 2) ? HFRFrames[HFRFrames.size() / 2] : (static_cast(HFRFrames[HFRFrames.size() / 2 - 1]) + HFRFrames[HFRFrames.size() / 2]) * .5); const auto mean = std::accumulate(HFRFrames.begin(), HFRFrames.end(), .0) / HFRFrames.size(); double variance = 0; foreach (auto val, HFRFrames) variance += (val - mean) * (val - mean); const double stddev = sqrt(variance / HFRFrames.size()); // Reject those 2 sigma away from median const double sigmaHigh = median + stddev * 2; const double sigmaLow = median - stddev * 2; QMutableVectorIterator i(HFRFrames); while (i.hasNext()) { auto val = i.next(); if (val > sigmaHigh || val < sigmaLow) i.remove(); } } // Find average HFR currentHFR = std::accumulate(HFRFrames.begin(), HFRFrames.end(), .0) / HFRFrames.size(); HFRFrames.clear(); } } else { // If we need to capture more frames to average the HFR, let's do that now. capture(); return; } // Let signal the current HFR now depending on whether the focuser is absolute or relative if (canAbsMove) emit newHFR(currentHFR, static_cast(currentPosition)); else emit newHFR(currentHFR, -1); // Format the HFR value into a string QString HFRText = QString("%1").arg(currentHFR, 0, 'f', 2); HFROut->setText(HFRText); starsOut->setText(QString("%1").arg(image_data->getDetectedStars())); // Display message in case _last_ HFR was negative if (lastHFR == -1) appendLogText(i18n("FITS received. No stars detected.")); // If we have a valid HFR value if (currentHFR > 0) { // Check if we're done from polynomial fitting algorithm if (focusAlgorithm == FOCUS_POLYNOMIAL && polySolutionFound == MINIMUM_POLY_SOLUTIONS) { polySolutionFound = 0; appendLogText(i18n("Autofocus complete after %1 iterations.", hfr_position.count())); stop(); setAutoFocusResult(true); return; } Edge *maxStarHFR = nullptr; // Center tracking box around selected star (if it valid) either in: // 1. Autofocus // 2. CheckFocus (minimumHFRCheck) // The starCenter _must_ already be defined, otherwise, we proceed until // the latter half of the function searches for a star and define it. if (starCenter.isNull() == false && (inAutoFocus || minimumRequiredHFR >= 0) && (maxStarHFR = image_data->getMaxHFRStar()) != nullptr) { // Now we have star selected in the frame starSelected = true; starCenter.setX(qMax(0, static_cast(maxStarHFR->x))); starCenter.setY(qMax(0, static_cast(maxStarHFR->y))); syncTrackingBoxPosition(); // Record the star information (X, Y, currentHFR) QVector3D oneStar = starCenter; oneStar.setZ(currentHFR); starsHFR.append(oneStar); } else { // Record the star information (X, Y, currentHFR) QVector3D oneStar(starCenter.x(), starCenter.y(), currentHFR); starsHFR.append(oneStar); } if (currentHFR > maxHFR) maxHFR = currentHFR; // Append point to the #Iterations vs #HFR chart in case of looping or in case in autofocus with a focus // that does not support position feedback. if (inFocusLoop || (inAutoFocus && canAbsMove == false && canRelMove == false)) { if (hfr_position.empty()) hfr_position.append(1); else hfr_position.append(hfr_position.last() + 1); hfr_value.append(currentHFR); drawHFRPlot(); } } else { // Let's record an invalid star result QVector3D oneStar(starCenter.x(), starCenter.y(), -1); starsHFR.append(oneStar); } // Try to average values and find if we have bogus results if (inAutoFocus && starsHFR.count() > 3) { float mean = 0, sum = 0, stddev = 0, noHFR = 0; for (int i = 0; i < starsHFR.count(); i++) { sum += starsHFR[i].x(); if (starsHFR[i].z() == -1) noHFR++; } mean = sum / starsHFR.count(); // Calculate standard deviation for (int i = 0; i < starsHFR.count(); i++) stddev += pow(starsHFR[i].x() - mean, 2); stddev = sqrt(stddev / starsHFR.count()); if (currentHFR == -1 && (stddev > focusBoxSize->value() / 10.0 || noHFR / starsHFR.count() > 0.75)) { appendLogText(i18n("No reliable star is detected. Aborting...")); abort(); setAutoFocusResult(false); return; } } } // If we are just framing, let's capture again if (inFocusLoop) { capture(); return; } // If star is NOT yet selected in a non-full-frame situation // then let's now try to find the star. This step is skipped for full frames // since there isn't a single star to select as we are only interested in the overall average HFR. // We need to check if we can find the star right away, or if we need to _subframe_ around the // selected star. if (Options::focusUseFullField() == false && starCenter.isNull()) { int x = 0, y = 0, w = 0, h = 0; // Let's get the stored frame settings for this particular chip if (frameSettings.contains(targetChip)) { QVariantMap settings = frameSettings[targetChip]; x = settings["x"].toInt(); y = settings["y"].toInt(); w = settings["w"].toInt(); h = settings["h"].toInt(); } else // Otherwise let's get the target chip frame coordinates. targetChip->getFrame(&x, &y, &w, &h); // In case auto star is selected. if (useAutoStar->isChecked()) { // Do we have a valid star detected? Edge *maxStar = image_data->getMaxHFRStar(); if (maxStar == nullptr) { appendLogText(i18n("Failed to automatically select a star. Please select a star manually.")); // Center the tracking box in the frame and display it focusView->setTrackingBox(QRect(w - focusBoxSize->value() / (subBinX * 2), h - focusBoxSize->value() / (subBinY * 2), focusBoxSize->value() / subBinX, focusBoxSize->value() / subBinY)); focusView->setTrackingBoxEnabled(true); // Use can now move it to select the desired star state = Ekos::FOCUS_WAITING; qCDebug(KSTARS_EKOS_FOCUS) << "State:" << Ekos::getFocusStatusString(state); emit newStatus(state); // Start the wait timer so we abort after a timeout if the user does not make a choice waitStarSelectTimer.start(); return; } // set the tracking box on maxStar starCenter.setX(maxStar->x); starCenter.setY(maxStar->y); starCenter.setZ(subBinX); syncTrackingBoxPosition(); // Do we need to subframe? if (subFramed == false && useSubFrame->isEnabled() && useSubFrame->isChecked()) { int offset = (static_cast(focusBoxSize->value()) / subBinX) * 1.5; int subX = (maxStar->x - offset) * subBinX; int subY = (maxStar->y - offset) * subBinY; int subW = offset * 2 * subBinX; int subH = offset * 2 * subBinY; int minX, maxX, minY, maxY, minW, maxW, minH, maxH; targetChip->getFrameMinMax(&minX, &maxX, &minY, &maxY, &minW, &maxW, &minH, &maxH); // Try to limit the subframed selection if (subX < minX) subX = minX; if (subY < minY) subY = minY; if ((subW + subX) > maxW) subW = maxW - subX; if ((subH + subY) > maxH) subH = maxH - subY; // Now we store the subframe coordinates in the target chip frame settings so we // reuse it later when we capture again. QVariantMap settings = frameSettings[targetChip]; settings["x"] = subX; settings["y"] = subY; settings["w"] = subW; settings["h"] = subH; settings["binx"] = subBinX; settings["biny"] = subBinY; qCDebug(KSTARS_EKOS_FOCUS) << "Frame is subframed. X:" << subX << "Y:" << subY << "W:" << subW << "H:" << subH << "binX:" << subBinX << "binY:" << subBinY; starsHFR.clear(); frameSettings[targetChip] = settings; // Set the star center in the center of the subframed coordinates starCenter.setX(subW / (2 * subBinX)); starCenter.setY(subH / (2 * subBinY)); starCenter.setZ(subBinX); subFramed = true; focusView->setFirstLoad(true); // Now let's capture again for the actual requested subframed image. capture(); } // If we're subframed or don't need subframe, let's record the max star coordinates else { starCenter.setX(maxStar->x); starCenter.setY(maxStar->y); starCenter.setZ(subBinX); // Let's now capture again if we're autofocusing if (inAutoFocus) capture(); } defaultScale = static_cast(filterCombo->currentIndex()); return; } // If manual selection is enabled then let's ask the user to select the focus star else { appendLogText(i18n("Capture complete. Select a star to focus.")); starSelected = false; // Let's now display and set the tracking box in the center of the frame // so that the user moves it around to select the desired star. int subBinX = 1, subBinY = 1; targetChip->getBinning(&subBinX, &subBinY); focusView->setTrackingBox(QRect((w - focusBoxSize->value()) / (subBinX * 2), (h - focusBoxSize->value()) / (2 * subBinY), focusBoxSize->value() / subBinX, focusBoxSize->value() / subBinY)); focusView->setTrackingBoxEnabled(true); // Now we wait state = Ekos::FOCUS_WAITING; qCDebug(KSTARS_EKOS_FOCUS) << "State:" << Ekos::getFocusStatusString(state); emit newStatus(state); // If the user does not select for a timeout period, we abort. waitStarSelectTimer.start(); return; } } // Check if the focus module is requested to verify if the minimum HFR value is met. if (minimumRequiredHFR >= 0) { // In case we failed to detected, we capture again. if (currentHFR == -1) { if (noStarCount++ < MAX_RECAPTURE_RETRIES) { appendLogText(i18n("No stars detected, capturing again...")); // On Last Attempt reset focus frame to capture full frame and recapture star if possible if (noStarCount == MAX_RECAPTURE_RETRIES) resetFrame(); capture(); return; } // If we exceeded maximum tries we abort else { noStarCount = 0; setAutoFocusResult(false); } } // If the detect current HFR is more than the minimum required HFR // then we should start the autofocus process now to bring it down. else if (currentHFR > minimumRequiredHFR) { qCDebug(KSTARS_EKOS_FOCUS) << "Current HFR:" << currentHFR << "is above required minimum HFR:" << minimumRequiredHFR << ". Starting AutoFocus..."; inSequenceFocus = true; start(); } // Otherwise, the current HFR is fine and lower than the required minimum HFR so we announce success. else { qCDebug(KSTARS_EKOS_FOCUS) << "Current HFR:" << currentHFR << "is below required minimum HFR:" << minimumRequiredHFR << ". Autofocus successful."; setAutoFocusResult(true); drawProfilePlot(); } // We reset minimum required HFR and call it a day. minimumRequiredHFR = -1; return; } // Let's draw the HFR Plot drawProfilePlot(); // If focus logging is enabled, let's save the frame. if (Options::focusLogging() && Options::saveFocusImages()) { QDir dir; + QDateTime now = KStarsData::Instance()->lt(); QString path = KSPaths::writableLocation(QStandardPaths::GenericDataLocation) + "autofocus/" + - QDateTime::currentDateTime().toString("yyyy-MM-dd"); + now.toString("yyyy-MM-dd"); dir.mkpath(path); // IS8601 contains colons but they are illegal under Windows OS, so replacing them with '-' // The timestamp is no longer ISO8601 but it should solve interoperality issues between different OS hosts - QString name = "autofocus_frame_" + QDateTime::currentDateTime().toString("HH-mm-ss") + ".fits"; + QString name = "autofocus_frame_" + now.toString("HH-mm-ss") + ".fits"; QString filename = path + QStringLiteral("/") + name; focusView->getImageData()->saveFITS(filename); } // If we are not in autofocus process, we're done. if (inAutoFocus == false) return; // Set state to progress if (state != Ekos::FOCUS_PROGRESS) { state = Ekos::FOCUS_PROGRESS; qCDebug(KSTARS_EKOS_FOCUS) << "State:" << Ekos::getFocusStatusString(state); emit newStatus(state); } // Now let's kick in the algorithms // Position-based algorithms if (canAbsMove || canRelMove) autoFocusAbs(); else // Time open-looped algorithms autoFocusRel(); } void Focus::clearDataPoints() { maxHFR = 1; hfr_position.clear(); hfr_value.clear(); drawHFRPlot(); } void Focus::drawHFRPlot() { v_graph->setData(hfr_position, hfr_value); if (inFocusLoop == false && (canAbsMove || canRelMove)) { //HFRPlot->xAxis->setLabel(i18n("Position")); HFRPlot->xAxis->setRange(minPos - pulseDuration, maxPos + pulseDuration); HFRPlot->yAxis->setRange(currentHFR / 1.5, maxHFR); } else { //HFRPlot->xAxis->setLabel(i18n("Iteration")); HFRPlot->xAxis->setRange(1, hfr_value.count() + 1); HFRPlot->yAxis->setRange(currentHFR / 1.5, maxHFR * 1.25); } HFRPlot->replot(); } void Focus::drawProfilePlot() { QVector currentIndexes; QVector currentFrequencies; // HFR = 50% * 1.36 = 68% aka one standard deviation double stdDev = currentHFR * 1.36; float start = -stdDev * 4; float end = stdDev * 4; float step = stdDev * 4 / 20.0; for (double x = start; x < end; x += step) { currentIndexes.append(x); currentFrequencies.append((1 / (stdDev * sqrt(2 * M_PI))) * exp(-1 * (x * x) / (2 * (stdDev * stdDev)))); } currentGaus->setData(currentIndexes, currentFrequencies); if (lastGausIndexes.count() > 0) lastGaus->setData(lastGausIndexes, lastGausFrequencies); if (focusType == FOCUS_AUTO && firstGaus == nullptr) { firstGaus = profilePlot->addGraph(); QPen pen; pen.setStyle(Qt::DashDotLine); pen.setWidth(2); pen.setColor(Qt::darkMagenta); firstGaus->setPen(pen); firstGaus->setData(currentIndexes, currentFrequencies); } else if (firstGaus) { profilePlot->removeGraph(firstGaus); firstGaus = nullptr; } profilePlot->rescaleAxes(); profilePlot->replot(); lastGausIndexes = currentIndexes; lastGausFrequencies = currentFrequencies; profilePixmap = profilePlot->grab(); //.scaled(200, 200, Qt::KeepAspectRatio, Qt::SmoothTransformation); emit newProfilePixmap(profilePixmap); } void Focus::autoFocusAbs() { static int minHFRPos = 0, focusOutLimit = 0, focusInLimit = 0; static double minHFR = 0; double targetPosition = 0, delta = 0; QString deltaTxt = QString("%1").arg(fabs(currentHFR - minHFR) * 100.0, 0, 'g', 3); QString HFRText = QString("%1").arg(currentHFR, 0, 'g', 3); qCDebug(KSTARS_EKOS_FOCUS) << "========================================"; qCDebug(KSTARS_EKOS_FOCUS) << "Current HFR: " << currentHFR << " Current Position: " << currentPosition; qCDebug(KSTARS_EKOS_FOCUS) << "Last minHFR: " << minHFR << " Last MinHFR Pos: " << minHFRPos; qCDebug(KSTARS_EKOS_FOCUS) << "Delta: " << deltaTxt << "%"; qCDebug(KSTARS_EKOS_FOCUS) << "========================================"; if (minHFR) appendLogText(i18n("FITS received. HFR %1 @ %2. Delta (%3%)", HFRText, currentPosition, deltaTxt)); else appendLogText(i18n("FITS received. HFR %1 @ %2.", HFRText, currentPosition)); if (++absIterations > MAXIMUM_ABS_ITERATIONS) { appendLogText(i18n("Autofocus failed to reach proper focus. Try increasing tolerance value.")); abort(); setAutoFocusResult(false); return; } // No stars detected, try to capture again if (currentHFR == -1) { if (noStarCount < MAX_RECAPTURE_RETRIES) { appendLogText(i18n("No stars detected, capturing again...")); capture(); noStarCount++; return; } else if (noStarCount == MAX_RECAPTURE_RETRIES) { currentHFR = 20; noStarCount++; } else { appendLogText(i18n("Failed to detect any stars. Reset frame and try again.")); abort(); setAutoFocusResult(false); return; } } else noStarCount = 0; if (hfr_position.empty()) { maxPos = 1; minPos = 1e6; } if (currentPosition > maxPos) maxPos = currentPosition; if (currentPosition < minPos) minPos = currentPosition; hfr_position.append(currentPosition); hfr_value.append(currentHFR); drawHFRPlot(); switch (lastFocusDirection) { case FOCUS_NONE: lastHFR = currentHFR; initialFocuserAbsPosition = currentPosition; minHFR = currentHFR; minHFRPos = currentPosition; HFRDec = 0; HFRInc = 0; focusOutLimit = 0; focusInLimit = 0; if (focusOut(pulseDuration) == false) { abort(); setAutoFocusResult(false); } break; case FOCUS_IN: case FOCUS_OUT: static int lastHFRPos = 0, initSlopePos = 0; static double initSlopeHFR = 0; if (reverseDir && focusInLimit && focusOutLimit && fabs(currentHFR - minHFR) < (toleranceIN->value() / 100.0) && HFRInc == 0) { if (absIterations <= 2) { appendLogText( i18n("Change in HFR is too small. Try increasing the step size or decreasing the tolerance.")); abort(); setAutoFocusResult(false); } else if (noStarCount > 0) { appendLogText(i18n("Failed to detect focus star in frame. Capture and select a focus star.")); abort(); setAutoFocusResult(false); } else { appendLogText(i18n("Autofocus complete after %1 iterations.", hfr_position.count())); stop(); setAutoFocusResult(true); } break; } else if (currentHFR < lastHFR) { double slope = 0; // Let's try to calculate slope of the V curve. if (initSlopeHFR == 0 && HFRInc == 0 && HFRDec >= 1) { initSlopeHFR = lastHFR; initSlopePos = lastHFRPos; qCDebug(KSTARS_EKOS_FOCUS) << "Setting initial slop to " << initSlopePos << " @ HFR " << initSlopeHFR; } // Let's now limit the travel distance of the focuser if (lastFocusDirection == FOCUS_OUT && lastHFRPos < focusInLimit && fabs(currentHFR - lastHFR) > 0.1) { focusInLimit = lastHFRPos; qCDebug(KSTARS_EKOS_FOCUS) << "New FocusInLimit " << focusInLimit; } else if (lastFocusDirection == FOCUS_IN && lastHFRPos > focusOutLimit && fabs(currentHFR - lastHFR) > 0.1) { focusOutLimit = lastHFRPos; qCDebug(KSTARS_EKOS_FOCUS) << "New FocusOutLimit " << focusOutLimit; } // If we have slope, get next target position if (initSlopeHFR && absMotionMax > 50) { double factor = 0.5; slope = (currentHFR - initSlopeHFR) / (currentPosition - initSlopePos); if (fabs(currentHFR - minHFR) * 100.0 < 0.5) factor = 1 - fabs(currentHFR - minHFR) * 10; targetPosition = currentPosition + (currentHFR * factor - currentHFR) / slope; if (targetPosition < 0) { factor = 1; while (targetPosition < 0 && factor > 0) { factor -= 0.005; targetPosition = currentPosition + (currentHFR * factor - currentHFR) / slope; } } qCDebug(KSTARS_EKOS_FOCUS) << "Using slope to calculate target pulse..."; } // Otherwise proceed iteratively else { if (lastFocusDirection == FOCUS_IN) targetPosition = currentPosition - pulseDuration; else targetPosition = currentPosition + pulseDuration; qCDebug(KSTARS_EKOS_FOCUS) << "Proceeding iteratively to next target pulse ..."; } qCDebug(KSTARS_EKOS_FOCUS) << "V-Curve Slope " << slope << " current Position " << currentPosition << " targetPosition " << targetPosition; lastHFR = currentHFR; // Let's keep track of the minimum HFR if (lastHFR < minHFR) { minHFR = lastHFR; minHFRPos = currentPosition; qCDebug(KSTARS_EKOS_FOCUS) << "new minHFR " << minHFR << " @ position " << minHFRPos; } lastHFRPos = currentPosition; // HFR is decreasing, we are on the right direction HFRDec++; HFRInc = 0; } else { // HFR increased, let's deal with it. HFRInc++; HFRDec = 0; // Reality Check: If it's first time, let's capture again and see if it changes. /*if (HFRInc <= 1 && reverseDir == false) { capture(); return; } // Looks like we're going away from optimal HFR else {*/ reverseDir = true; lastHFR = currentHFR; lastHFRPos = currentPosition; initSlopeHFR = 0; HFRInc = 0; qCDebug(KSTARS_EKOS_FOCUS) << "Focus is moving away from optimal HFR."; // Let's set new limits if (lastFocusDirection == FOCUS_IN) { focusInLimit = currentPosition; qCDebug(KSTARS_EKOS_FOCUS) << "Setting focus IN limit to " << focusInLimit; if (hfr_position.count() > 3) { focusOutLimit = hfr_position[hfr_position.count() - 3]; qCDebug(KSTARS_EKOS_FOCUS) << "Setting focus OUT limit to " << focusOutLimit; } } else { focusOutLimit = currentPosition; qCDebug(KSTARS_EKOS_FOCUS) << "Setting focus OUT limit to " << focusOutLimit; if (hfr_position.count() > 3) { focusInLimit = hfr_position[hfr_position.count() - 3]; qCDebug(KSTARS_EKOS_FOCUS) << "Setting focus IN limit to " << focusInLimit; } } bool polyMinimumFound = false; if (focusAlgorithm == FOCUS_POLYNOMIAL && hfr_position.count() > 5) { double chisq = 0, min_position = 0, min_hfr = 0; coeff = gsl_polynomial_fit(hfr_position.data(), hfr_value.data(), hfr_position.count(), 3, chisq); polyMinimumFound = findMinimum(minHFRPos, &min_position, &min_hfr); qCDebug(KSTARS_EKOS_FOCUS) << "Polynomial Coefficients c0:" << coeff[0] << "c1:" << coeff[1] << "c2:" << coeff[2] << "c3:" << coeff[3]; qCDebug(KSTARS_EKOS_FOCUS) << "Found Minimum?" << (polyMinimumFound ? "Yes" : "No"); if (polyMinimumFound) { qCDebug(KSTARS_EKOS_FOCUS) << "Minimum Solution:" << min_hfr << "@" << min_position; polySolutionFound++; targetPosition = floor(min_position); appendLogText(i18n("Found polynomial solution @ %1", QString::number(min_position, 'f', 0))); } } if (polyMinimumFound == false) { // Decrease pulse pulseDuration = pulseDuration * 0.75; // Let's get close to the minimum HFR position so far detected if (lastFocusDirection == FOCUS_OUT) targetPosition = minHFRPos - pulseDuration / 2; else targetPosition = minHFRPos + pulseDuration / 2; } qCDebug(KSTARS_EKOS_FOCUS) << "new targetPosition " << targetPosition; } // Limit target Pulse to algorithm limits if (focusInLimit != 0 && lastFocusDirection == FOCUS_IN && targetPosition < focusInLimit) { targetPosition = focusInLimit; qCDebug(KSTARS_EKOS_FOCUS) << "Limiting target pulse to focus in limit " << targetPosition; } else if (focusOutLimit != 0 && lastFocusDirection == FOCUS_OUT && targetPosition > focusOutLimit) { targetPosition = focusOutLimit; qCDebug(KSTARS_EKOS_FOCUS) << "Limiting target pulse to focus out limit " << targetPosition; } // Limit target pulse to focuser limits if (targetPosition < absMotionMin) targetPosition = absMotionMin; else if (targetPosition > absMotionMax) targetPosition = absMotionMax; // Ops, we can't go any further, we're done. if (targetPosition == currentPosition) { appendLogText(i18n("Autofocus complete after %1 iterations.", hfr_position.count())); stop(); setAutoFocusResult(true); return; } // Ops, deadlock if (focusOutLimit && focusOutLimit == focusInLimit) { appendLogText(i18n("Deadlock reached. Please try again with different settings.")); abort(); setAutoFocusResult(false); return; } if (fabs(targetPosition - initialFocuserAbsPosition) > maxTravelIN->value()) { int minTravelLimit = qMax(0.0, initialFocuserAbsPosition - maxTravelIN->value()); int maxTravelLimit = qMin(absMotionMax, initialFocuserAbsPosition + maxTravelIN->value()); // In case we are asked to go below travel limit, but we are not there yet // let us go there and see the result before aborting if (fabs(currentPosition - minTravelLimit) > 10 && targetPosition < minTravelLimit) { targetPosition = minTravelLimit; } // Same for max travel else if (fabs(currentPosition - maxTravelLimit) > 10 && targetPosition > maxTravelLimit) { targetPosition = maxTravelLimit; } else { qCDebug(KSTARS_EKOS_FOCUS) << "targetPosition (" << targetPosition << ") - initHFRAbsPos (" << initialFocuserAbsPosition << ") exceeds maxTravel distance of " << maxTravelIN->value(); appendLogText("Maximum travel limit reached. Autofocus aborted."); abort(); setAutoFocusResult(false); break; } } // Get delta for next move delta = (targetPosition - currentPosition); qCDebug(KSTARS_EKOS_FOCUS) << "delta (targetPosition - currentPosition) " << delta; qCDebug(KSTARS_EKOS_FOCUS) << "Focusing " << ((delta < 0) ? "IN" : "OUT"); // Now cross your fingers and wait bool rc = false; if (delta > 0) rc = focusOut(delta); else rc = focusIn(fabs(delta)); if (rc == false) { abort(); setAutoFocusResult(false); } break; } } void Focus::autoFocusRel() { static int noStarCount = 0; static double minHFR = 1e6; QString deltaTxt = QString("%1").arg(fabs(currentHFR - minHFR) * 100.0, 0, 'g', 2); QString minHFRText = QString("%1").arg(minHFR, 0, 'g', 3); QString HFRText = QString("%1").arg(currentHFR, 0, 'g', 3); appendLogText(i18n("FITS received. HFR %1. Delta (%2%) Min HFR (%3)", HFRText, deltaTxt, minHFRText)); if (pulseDuration <= MINIMUM_PULSE_TIMER) { appendLogText(i18n("Autofocus failed to reach proper focus. Try adjusting the tolerance value.")); abort(); setAutoFocusResult(false); return; } // No stars detected, try to capture again if (currentHFR == -1) { if (noStarCount++ < MAX_RECAPTURE_RETRIES) { appendLogText(i18n("No stars detected, capturing again...")); capture(); return; } else currentHFR = 20; } else noStarCount = 0; switch (lastFocusDirection) { case FOCUS_NONE: lastHFR = currentHFR; minHFR = 1e6; focusIn(pulseDuration); break; case FOCUS_IN: case FOCUS_OUT: if (fabs(currentHFR - minHFR) < (toleranceIN->value() / 100.0) && HFRInc == 0) { appendLogText(i18n("Autofocus complete after %1 iterations.", hfr_position.count())); stop(); setAutoFocusResult(true); break; } else if (currentHFR < lastHFR) { if (currentHFR < minHFR) minHFR = currentHFR; lastHFR = currentHFR; if (lastFocusDirection == FOCUS_IN) focusIn(pulseDuration); else focusOut(pulseDuration); HFRInc = 0; } else { HFRInc++; lastHFR = currentHFR; HFRInc = 0; pulseDuration *= 0.75; bool rc = false; if (lastFocusDirection == FOCUS_IN) rc = focusOut(pulseDuration); else rc = focusIn(pulseDuration); if (rc == false) { abort(); setAutoFocusResult(false); } } break; } } /*void Focus::registerFocusProperty(INDI::Property *prop) { // Return if it is not our current focuser if (strcmp(prop->getDeviceName(), currentFocuser->getDeviceName())) return; // Do not make unnecessary function call // Check if current focuser supports absolute mode if (canAbsMove == false && currentFocuser->canAbsMove()) { canAbsMove = true; getAbsFocusPosition(); absTicksSpin->setEnabled(true); absTicksLabel->setEnabled(true); startGotoB->setEnabled(true); } // Do not make unnecessary function call // Check if current focuser supports relative mode if (canRelMove == false && currentFocuser->canRelMove()) canRelMove = true; if (canTimerMove == false && currentFocuser->canTimerMove()) { canTimerMove = true; resetButtons(); } }*/ void Focus::processFocusNumber(INumberVectorProperty *nvp) { // Return if it is not our current focuser if (strcmp(nvp->device, currentFocuser->getDeviceName())) return; if (!strcmp(nvp->name, "FOCUS_BACKLASH_STEPS")) { focusBacklashSpin->setValue(nvp->np[0].value); return; } if (!strcmp(nvp->name, "ABS_FOCUS_POSITION")) { INumber *pos = IUFindNumber(nvp, "FOCUS_ABSOLUTE_POSITION"); if (pos) { currentPosition = pos->value; absTicksLabel->setText(QString::number(static_cast(currentPosition))); emit absolutePositionChanged(currentPosition); } if (adjustFocus && nvp->s == IPS_OK) { adjustFocus = false; lastFocusDirection = FOCUS_NONE; emit focusPositionAdjusted(); return; } if (resetFocus && nvp->s == IPS_OK) { resetFocus = false; appendLogText(i18n("Restarting autofocus process...")); start(); } if (canAbsMove && inAutoFocus) { if (nvp->s == IPS_OK && captureInProgress == false) QTimer::singleShot(FocusSettleTime->value() * 1000, this, &Ekos::Focus::capture); //capture(); else if (nvp->s == IPS_ALERT) { appendLogText(i18n("Focuser error, check INDI panel.")); abort(); setAutoFocusResult(false); } } else if (nvp->s == IPS_ALERT) appendLogText(i18n("Focuser error, check INDI panel.")); return; } if (canAbsMove) return; if (!strcmp(nvp->name, "REL_FOCUS_POSITION")) { INumber *pos = IUFindNumber(nvp, "FOCUS_RELATIVE_POSITION"); if (pos && nvp->s == IPS_OK) { currentPosition += pos->value * (lastFocusDirection == FOCUS_IN ? -1 : 1); absTicksLabel->setText(QString::number(static_cast(currentPosition))); emit absolutePositionChanged(currentPosition); } if (adjustFocus && nvp->s == IPS_OK) { adjustFocus = false; lastFocusDirection = FOCUS_NONE; emit focusPositionAdjusted(); return; } if (resetFocus && nvp->s == IPS_OK) { resetFocus = false; appendLogText(i18n("Restarting autofocus process...")); start(); } if (canRelMove && inAutoFocus) { if (nvp->s == IPS_OK && captureInProgress == false) QTimer::singleShot(FocusSettleTime->value() * 1000, this, &Ekos::Focus::capture); else if (nvp->s == IPS_ALERT) { appendLogText(i18n("Focuser error, check INDI panel.")); abort(); setAutoFocusResult(false); } } else if (nvp->s == IPS_ALERT) appendLogText(i18n("Focuser error, check INDI panel.")); return; } if (canRelMove) return; if (!strcmp(nvp->name, "FOCUS_TIMER")) { if (resetFocus && nvp->s == IPS_OK) { resetFocus = false; appendLogText(i18n("Restarting autofocus process...")); start(); } if (canAbsMove == false && canRelMove == false && inAutoFocus) { if (nvp->s == IPS_OK && captureInProgress == false) QTimer::singleShot(FocusSettleTime->value() * 1000, this, &Ekos::Focus::capture); else if (nvp->s == IPS_ALERT) { appendLogText(i18n("Focuser error, check INDI panel.")); abort(); setAutoFocusResult(false); } } else if (nvp->s == IPS_ALERT) appendLogText(i18n("Focuser error, check INDI panel.")); return; } } void Focus::appendLogText(const QString &text) { m_LogText.insert(0, i18nc("log entry; %1 is the date, %2 is the text", "%1 %2", - QDateTime::currentDateTime().toString("yyyy-MM-ddThh:mm:ss"), text)); + KStarsData::Instance()->lt().toString("yyyy-MM-ddThh:mm:ss"), text)); qCInfo(KSTARS_EKOS_FOCUS) << text; emit newLog(text); } void Focus::clearLog() { m_LogText.clear(); emit newLog(QString()); } void Focus::startFraming() { if (currentCCD == nullptr) { appendLogText(i18n("No CCD connected.")); return; } waitStarSelectTimer.stop(); inFocusLoop = true; HFRFrames.clear(); clearDataPoints(); //emit statusUpdated(true); state = Ekos::FOCUS_FRAMING; qCDebug(KSTARS_EKOS_FOCUS) << "State:" << Ekos::getFocusStatusString(state); emit newStatus(state); resetButtons(); appendLogText(i18n("Starting continuous exposure...")); capture(); } void Focus::resetButtons() { if (inFocusLoop) { startFocusB->setEnabled(false); startLoopB->setEnabled(false); stopFocusB->setEnabled(true); captureB->setEnabled(false); return; } if (inAutoFocus) { stopFocusB->setEnabled(true); startFocusB->setEnabled(false); startLoopB->setEnabled(false); captureB->setEnabled(false); focusOutB->setEnabled(false); focusInB->setEnabled(false); startGotoB->setEnabled(false); stopGotoB->setEnabled(false); resetFrameB->setEnabled(false); return; } if (currentFocuser) { focusOutB->setEnabled(true); focusInB->setEnabled(true); startFocusB->setEnabled(focusType == FOCUS_AUTO); startGotoB->setEnabled(canAbsMove); stopGotoB->setEnabled(true); } else { focusOutB->setEnabled(false); focusInB->setEnabled(false); startFocusB->setEnabled(false); startGotoB->setEnabled(false); stopGotoB->setEnabled(false); } stopFocusB->setEnabled(false); startLoopB->setEnabled(true); if (captureInProgress == false) { captureB->setEnabled(true); resetFrameB->setEnabled(true); } } void Focus::updateBoxSize(int value) { if (currentCCD == nullptr) return; ISD::CCDChip *targetChip = currentCCD->getChip(ISD::CCDChip::PRIMARY_CCD); if (targetChip == nullptr) return; int subBinX, subBinY; targetChip->getBinning(&subBinX, &subBinY); QRect trackBox = focusView->getTrackingBox(); QPoint center(trackBox.x() + (trackBox.width() / 2), trackBox.y() + (trackBox.height() / 2)); trackBox = QRect(center.x() - value / (2 * subBinX), center.y() - value / (2 * subBinY), value / subBinX, value / subBinY); focusView->setTrackingBox(trackBox); } void Focus::focusStarSelected(int x, int y) { if (state == Ekos::FOCUS_PROGRESS) return; if (subFramed == false) { rememberStarCenter.setX(x); rememberStarCenter.setY(y); } ISD::CCDChip *targetChip = currentCCD->getChip(ISD::CCDChip::PRIMARY_CCD); int subBinX, subBinY; targetChip->getBinning(&subBinX, &subBinY); // If binning was changed outside of the focus module, recapture if (subBinX != activeBin) { capture(); return; } int offset = (static_cast(focusBoxSize->value()) / subBinX) * 1.5; QRect starRect; bool squareMovedOutside = false; if (subFramed == false && useSubFrame->isChecked() && targetChip->canSubframe()) { int minX, maxX, minY, maxY, minW, maxW, minH, maxH; //, fx,fy,fw,fh; targetChip->getFrameMinMax(&minX, &maxX, &minY, &maxY, &minW, &maxW, &minH, &maxH); //targetChip->getFrame(&fx, &fy, &fw, &fy); x = (x - offset) * subBinX; y = (y - offset) * subBinY; int w = offset * 2 * subBinX; int h = offset * 2 * subBinY; if (x < minX) x = minX; if (y < minY) y = minY; if ((x + w) > maxW) w = maxW - x; if ((y + h) > maxH) h = maxH - y; //fx += x; //fy += y; //fw = w; //fh = h; //targetChip->setFocusFrame(fx, fy, fw, fh); //frameModified=true; QVariantMap settings = frameSettings[targetChip]; settings["x"] = x; settings["y"] = y; settings["w"] = w; settings["h"] = h; settings["binx"] = subBinX; settings["biny"] = subBinY; frameSettings[targetChip] = settings; subFramed = true; qCDebug(KSTARS_EKOS_FOCUS) << "Frame is subframed. X:" << x << "Y:" << y << "W:" << w << "H:" << h << "binX:" << subBinX << "binY:" << subBinY; focusView->setFirstLoad(true); capture(); //starRect = QRect((w-focusBoxSize->value())/(subBinX*2), (h-focusBoxSize->value())/(subBinY*2), focusBoxSize->value()/subBinX, focusBoxSize->value()/subBinY); starCenter.setX(w / (2 * subBinX)); starCenter.setY(h / (2 * subBinY)); } else { //starRect = QRect(x-focusBoxSize->value()/(subBinX*2), y-focusBoxSize->value()/(subBinY*2), focusBoxSize->value()/subBinX, focusBoxSize->value()/subBinY); double dist = sqrt((starCenter.x() - x) * (starCenter.x() - x) + (starCenter.y() - y) * (starCenter.y() - y)); squareMovedOutside = (dist > (static_cast(focusBoxSize->value()) / subBinX)); starCenter.setX(x); starCenter.setY(y); //starRect = QRect( starCenter.x()-focusBoxSize->value()/(2*subBinX), starCenter.y()-focusBoxSize->value()/(2*subBinY), focusBoxSize->value()/subBinX, focusBoxSize->value()/subBinY); starRect = QRect(starCenter.x() - focusBoxSize->value() / (2 * subBinX), starCenter.y() - focusBoxSize->value() / (2 * subBinY), focusBoxSize->value() / subBinX, focusBoxSize->value() / subBinY); focusView->setTrackingBox(starRect); } starsHFR.clear(); starCenter.setZ(subBinX); //starSelected=true; defaultScale = static_cast(filterCombo->currentIndex()); if (squareMovedOutside && inAutoFocus == false && useAutoStar->isChecked()) { useAutoStar->blockSignals(true); useAutoStar->setChecked(false); useAutoStar->blockSignals(false); appendLogText(i18n("Disabling Auto Star Selection as star selection box was moved manually.")); starSelected = false; } else if (starSelected == false) { appendLogText(i18n("Focus star is selected.")); starSelected = true; capture(); } waitStarSelectTimer.stop(); state = inAutoFocus ? FOCUS_PROGRESS : FOCUS_IDLE; qCDebug(KSTARS_EKOS_FOCUS) << "State:" << Ekos::getFocusStatusString(state); emit newStatus(state); } void Focus::checkFocus(double requiredHFR) { qCDebug(KSTARS_EKOS_FOCUS) << "Check Focus requested with minimum required HFR" << requiredHFR; minimumRequiredHFR = requiredHFR; capture(); } void Focus::toggleSubframe(bool enable) { if (enable == false) resetFrame(); starSelected = false; starCenter = QVector3D(); if (useFullField->isChecked()) useFullField->setChecked(!enable); } void Focus::filterChangeWarning(int index) { // index = 4 is MEDIAN filter which helps reduce noise if (index != 0 && index != FITS_MEDIAN) appendLogText(i18n("Warning: Only use filters for preview as they may interface with autofocus operation.")); Options::setFocusEffect(index); defaultScale = static_cast(index); } void Focus::setExposure(double value) { exposureIN->setValue(value); } void Focus::setBinning(int subBinX, int subBinY) { INDI_UNUSED(subBinY); binningCombo->setCurrentIndex(subBinX - 1); } void Focus::setImageFilter(const QString &value) { for (int i = 0; i < filterCombo->count(); i++) if (filterCombo->itemText(i) == value) { filterCombo->setCurrentIndex(i); break; } } void Focus::setAutoStarEnabled(bool enable) { useAutoStar->setChecked(enable); Options::setFocusAutoStarEnabled(enable); } void Focus::setAutoSubFrameEnabled(bool enable) { useSubFrame->setChecked(enable); Options::setFocusSubFrame(enable); } void Focus::setAutoFocusParameters(int boxSize, int stepSize, int maxTravel, double tolerance) { focusBoxSize->setValue(boxSize); stepIN->setValue(stepSize); maxTravelIN->setValue(maxTravel); toleranceIN->setValue(tolerance); } void Focus::setAutoFocusResult(bool status) { qCDebug(KSTARS_EKOS_FOCUS) << "AutoFocus result:" << status; // In case of failure, go back to last position if the focuser is absolute if (status == false && canAbsMove && currentFocuser && currentFocuser->isConnected() && initialFocuserAbsPosition >= 0) { currentFocuser->moveAbs(initialFocuserAbsPosition); appendLogText(i18n("Autofocus failed, moving back to initial focus position %1.", initialFocuserAbsPosition)); // If we're doing in sequence focusing using an absolute focuser, let's retry focusing starting from last known good position before we give up if (inSequenceFocus && resetFocusIteration++ < MAXIMUM_RESET_ITERATIONS && resetFocus == false) { resetFocus = true; // Reset focus frame in case the star in subframe was lost resetFrame(); return; } } int settleTime = m_GuidingSuspended ? GuideSettleTime->value() : 0; // Always resume guiding if we suspended it before if (m_GuidingSuspended) { emit resumeGuiding(); m_GuidingSuspended = false; } resetFocusIteration = 0; if (settleTime > 0) appendLogText(i18n("Settling...")); QTimer::singleShot(settleTime * 1000, this, [ &, status, settleTime]() { if (settleTime > 0) appendLogText(i18n("Settling complete.")); if (status) { KSNotification::event(QLatin1String("FocusSuccessful"), i18n("Autofocus operation completed successfully")); state = Ekos::FOCUS_COMPLETE; } else { KSNotification::event(QLatin1String("FocusFailed"), i18n("Autofocus operation failed with errors"), KSNotification::EVENT_ALERT); state = Ekos::FOCUS_FAILED; } qCDebug(KSTARS_EKOS_FOCUS) << "State:" << Ekos::getFocusStatusString(state); // Do not emit result back yet if we have a locked filter pending return to original filter if (fallbackFilterPending) { filterManager->setFilterPosition(fallbackFilterPosition, static_cast(FilterManager::CHANGE_POLICY | FilterManager::OFFSET_POLICY)); return; } emit newStatus(state); }); } void Focus::checkAutoStarTimeout() { //if (starSelected == false && inAutoFocus) if (starCenter.isNull() && (inAutoFocus || minimumRequiredHFR > 0)) { if (inAutoFocus) { if (rememberStarCenter.isNull() == false) { focusStarSelected(rememberStarCenter.x(), rememberStarCenter.y()); appendLogText(i18n("No star was selected. Using last known position...")); return; } } appendLogText(i18n("No star was selected. Aborting...")); initialFocuserAbsPosition = -1; abort(); setAutoFocusResult(false); } else if (state == FOCUS_WAITING) { state = FOCUS_IDLE; qCDebug(KSTARS_EKOS_FOCUS) << "State:" << Ekos::getFocusStatusString(state); emit newStatus(state); } } void Focus::setAbsoluteFocusTicks() { if (currentFocuser == nullptr) return; if (currentFocuser->isConnected() == false) { appendLogText(i18n("Error: Lost connection to Focuser.")); return; } qCDebug(KSTARS_EKOS_FOCUS) << "Setting focus ticks to " << absTicksSpin->value(); currentFocuser->moveAbs(absTicksSpin->value()); } //void Focus::setActiveBinning(int bin) //{ // activeBin = bin + 1; // Options::setFocusXBin(activeBin); //} // TODO remove from kstars.kcfg /*void Focus::setFrames(int value) { Options::setFocusFrames(value); }*/ void Focus::syncTrackingBoxPosition() { ISD::CCDChip *targetChip = currentCCD->getChip(ISD::CCDChip::PRIMARY_CCD); Q_ASSERT(targetChip); int subBinX = 1, subBinY = 1; targetChip->getBinning(&subBinX, &subBinY); if (starCenter.isNull() == false) { double boxSize = focusBoxSize->value(); int x, y, w, h; targetChip->getFrame(&x, &y, &w, &h); // If box size is larger than image size, set it to lower index if (boxSize / subBinX >= w || boxSize / subBinY >= h) { focusBoxSize->setValue((boxSize / subBinX >= w) ? w : h); return; } // If binning changed, update coords accordingly if (subBinX != starCenter.z()) { if (starCenter.z() > 0) { starCenter.setX(starCenter.x() * (starCenter.z() / subBinX)); starCenter.setY(starCenter.y() * (starCenter.z() / subBinY)); } starCenter.setZ(subBinX); } QRect starRect = QRect(starCenter.x() - boxSize / (2 * subBinX), starCenter.y() - boxSize / (2 * subBinY), boxSize / subBinX, boxSize / subBinY); focusView->setTrackingBoxEnabled(true); focusView->setTrackingBox(starRect); } } void Focus::showFITSViewer() { FITSData *data = focusView->getImageData(); if (data) { QUrl url = QUrl::fromLocalFile(data->filename()); if (fv.isNull()) { if (Options::singleWindowCapturedFITS()) fv = KStars::Instance()->genericFITSViewer(); else { fv = new FITSViewer(Options::independentWindowFITS() ? nullptr : KStars::Instance()); KStars::Instance()->addFITSViewer(fv); } fv->addFITS(url); FITSView *currentView = fv->getCurrentView(); if (currentView) currentView->getImageData()->setAutoRemoveTemporaryFITS(false); } else fv->updateFITS(url, 0); fv->show(); } } void Focus::adjustFocusOffset(int value, bool useAbsoluteOffset) { adjustFocus = true; int relativeOffset = 0; if (useAbsoluteOffset == false) relativeOffset = value; else relativeOffset = value - currentPosition; if (relativeOffset > 0) focusOut(relativeOffset); else focusIn(abs(relativeOffset)); } void Focus::toggleFocusingWidgetFullScreen() { if (focusingWidget->parent() == nullptr) { focusingWidget->setParent(this); rightLayout->insertWidget(0, focusingWidget); focusingWidget->showNormal(); } else { focusingWidget->setParent(nullptr); focusingWidget->setWindowTitle(i18n("Focus Frame")); focusingWidget->setWindowFlags(Qt::Window | Qt::WindowTitleHint | Qt::CustomizeWindowHint); focusingWidget->showMaximized(); focusingWidget->show(); } } void Focus::setMountStatus(ISD::Telescope::Status newState) { switch (newState) { case ISD::Telescope::MOUNT_PARKING: case ISD::Telescope::MOUNT_SLEWING: case ISD::Telescope::MOUNT_MOVING: captureB->setEnabled(false); startFocusB->setEnabled(false); startLoopB->setEnabled(false); // If mount is moved while we have a star selected and subframed // let us reset the frame. if (subFramed) resetFrame(); break; default: resetButtons(); break; } } double Focus::fn1(double x, void *params) { Focus *module = static_cast(params); return (module->coeff[0] + module->coeff[1] * x + module->coeff[2] * pow(x, 2) + module->coeff[3] * pow(x, 3)); } bool Focus::findMinimum(double expected, double *position, double *hfr) { int status; int iter = 0, max_iter = 100; const gsl_min_fminimizer_type *T; gsl_min_fminimizer *s; double m = expected; double a = *std::min_element(hfr_position.constBegin(), hfr_position.constEnd()); double b = *std::max_element(hfr_position.constBegin(), hfr_position.constEnd()); ; gsl_function F; F.function = &Focus::fn1; F.params = this; // Must turn off error handler or it aborts on error gsl_set_error_handler_off(); T = gsl_min_fminimizer_brent; s = gsl_min_fminimizer_alloc(T); status = gsl_min_fminimizer_set(s, &F, m, a, b); if (status != GSL_SUCCESS) { qCWarning(KSTARS_EKOS_FOCUS) << "Focus GSL error:" << gsl_strerror(status); return false; } do { iter++; status = gsl_min_fminimizer_iterate(s); m = gsl_min_fminimizer_x_minimum(s); a = gsl_min_fminimizer_x_lower(s); b = gsl_min_fminimizer_x_upper(s); status = gsl_min_test_interval(a, b, 0.01, 0.0); if (status == GSL_SUCCESS) { *position = m; *hfr = fn1(m, this); } } while (status == GSL_CONTINUE && iter < max_iter); gsl_min_fminimizer_free(s); return (status == GSL_SUCCESS); } void Focus::removeDevice(ISD::GDInterface *deviceRemoved) { // Check in Focusers for (ISD::GDInterface *focuser : Focusers) { if (!strcmp(focuser->getDeviceName(), deviceRemoved->getDeviceName())) { Focusers.removeAll(dynamic_cast(focuser)); focuserCombo->removeItem(focuserCombo->findText(focuser->getDeviceName())); checkFocuser(); resetButtons(); } } // Check in CCDs for (ISD::GDInterface *ccd : CCDs) { if (!strcmp(ccd->getDeviceName(), deviceRemoved->getDeviceName())) { CCDs.removeAll(dynamic_cast(ccd)); CCDCaptureCombo->removeItem(CCDCaptureCombo->findText(ccd->getDeviceName())); CCDCaptureCombo->removeItem(CCDCaptureCombo->findText(ccd->getDeviceName() + QString(" Guider"))); checkCCD(); resetButtons(); } } // Check in Filters for (ISD::GDInterface *filter : Filters) { if (!strcmp(filter->getDeviceName(), deviceRemoved->getDeviceName())) { Filters.removeAll(filter); FilterDevicesCombo->removeItem(FilterDevicesCombo->findText(filter->getDeviceName())); if (Filters.empty()) currentFilter = nullptr; checkFilter(); resetButtons(); } } } void Focus::setFilterManager(const QSharedPointer &manager) { filterManager = manager; connect(filterManagerB, &QPushButton::clicked, [this]() { filterManager->show(); filterManager->raise(); }); connect(filterManager.data(), &FilterManager::ready, [this]() { if (filterPositionPending) { filterPositionPending = false; capture(); } else if (fallbackFilterPending) { fallbackFilterPending = false; emit newStatus(state); } } ); connect(filterManager.data(), &FilterManager::failed, [this]() { appendLogText(i18n("Filter operation failed.")); abort(); } ); connect(this, &Focus::newStatus, [this](Ekos::FocusState state) { if (FilterPosCombo->currentIndex() != -1 && canAbsMove && state == Ekos::FOCUS_COMPLETE) { filterManager->setFilterAbsoluteFocusPosition(FilterPosCombo->currentIndex(), currentPosition); } }); connect(exposureIN, &QDoubleSpinBox::editingFinished, [this]() { if (currentFilter) filterManager->setFilterExposure(FilterPosCombo->currentIndex(), exposureIN->value()); }); connect(filterManager.data(), &FilterManager::labelsChanged, this, [this]() { FilterPosCombo->clear(); FilterPosCombo->addItems(filterManager->getFilterLabels()); currentFilterPosition = filterManager->getFilterPosition(); FilterPosCombo->setCurrentIndex(currentFilterPosition - 1); //Options::setDefaultFocusFilterWheelFilter(FilterPosCombo->currentText()); }); connect(filterManager.data(), &FilterManager::positionChanged, this, [this]() { currentFilterPosition = filterManager->getFilterPosition(); FilterPosCombo->setCurrentIndex(currentFilterPosition - 1); //Options::setDefaultFocusFilterWheelFilter(FilterPosCombo->currentText()); }); connect(filterManager.data(), &FilterManager::exposureChanged, this, [this]() { exposureIN->setValue(filterManager->getFilterExposure()); ; }); connect(FilterPosCombo, static_cast(&QComboBox::currentIndexChanged), [ = ](const QString & text) { exposureIN->setValue(filterManager->getFilterExposure(text)); //Options::setDefaultFocusFilterWheelFilter(text); }); } void Focus::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->setVideoStreamEnabled(enabled); }); KSMessageBox::Instance()->questionYesNo(i18n("Image transfer is disabled for this camera. Would you like to enable it?")); } } else currentCCD->setVideoStreamEnabled(enabled); } void Focus::setVideoStreamEnabled(bool enabled) { if (enabled) { liveVideoB->setChecked(true); liveVideoB->setIcon(QIcon::fromTheme("camera-on")); } else { liveVideoB->setChecked(false); liveVideoB->setIcon(QIcon::fromTheme("camera-ready")); } } void Focus::processCaptureTimeout() { captureTimeoutCounter++; if (captureTimeoutCounter >= 3) { captureTimeoutCounter = 0; appendLogText(i18n("Exposure timeout. Aborting...")); abort(); if (inAutoFocus) setAutoFocusResult(false); else if (m_GuidingSuspended) { emit resumeGuiding(); m_GuidingSuspended = false; } return; } appendLogText(i18n("Exposure timeout. Restarting exposure...")); ISD::CCDChip *targetChip = currentCCD->getChip(ISD::CCDChip::PRIMARY_CCD); targetChip->abortExposure(); targetChip->capture(exposureIN->value()); captureTimeout.start(exposureIN->value() * 1000 + FOCUS_TIMEOUT_THRESHOLD); } void Focus::syncSettings() { QDoubleSpinBox *dsb = nullptr; QSpinBox *sb = nullptr; QCheckBox *cb = nullptr; QComboBox *cbox = nullptr; if ( (dsb = qobject_cast(sender()))) { /////////////////////////////////////////////////////////////////////////// /// Focuser Group /////////////////////////////////////////////////////////////////////////// if (dsb == FocusSettleTime) Options::setFocusSettleTime(dsb->value()); /////////////////////////////////////////////////////////////////////////// /// CCD & Filter Wheel Group /////////////////////////////////////////////////////////////////////////// else if (dsb == gainIN) Options::setFocusGain(dsb->value()); /////////////////////////////////////////////////////////////////////////// /// Settings Group /////////////////////////////////////////////////////////////////////////// else if (dsb == fullFieldInnerRing) Options::setFocusFullFieldInnerRadius(dsb->value()); else if (dsb == fullFieldOuterRing) Options::setFocusFullFieldOuterRadius(dsb->value()); else if (dsb == GuideSettleTime) Options::setGuideSettleTime(dsb->value()); else if (dsb == maxTravelIN) Options::setFocusMaxTravel(dsb->value()); else if (dsb == toleranceIN) Options::setFocusTolerance(dsb->value()); else if (dsb == thresholdSpin) Options::setFocusThreshold(dsb->value()); } else if ( (sb = qobject_cast(sender()))) { /////////////////////////////////////////////////////////////////////////// /// Settings Group /////////////////////////////////////////////////////////////////////////// if (sb == focusBoxSize) Options::setFocusBoxSize(sb->value()); else if (sb == stepIN) Options::setFocusTicks(sb->value()); else if (sb == focusFramesSpin) Options::setFocusFramesCount(sb->value()); } else if ( (cb = qobject_cast(sender()))) { /////////////////////////////////////////////////////////////////////////// /// Settings Group /////////////////////////////////////////////////////////////////////////// if (cb == useAutoStar) Options::setFocusAutoStarEnabled(cb->isChecked()); else if (cb == useSubFrame) Options::setFocusSubFrame(cb->isChecked()); else if (cb == darkFrameCheck) Options::setUseFocusDarkFrame(cb->isChecked()); else if (cb == useFullField) Options::setFocusUseFullField(cb->isChecked()); else if (cb == suspendGuideCheck) Options::setSuspendGuiding(cb->isChecked()); } else if ( (cbox = qobject_cast(sender()))) { /////////////////////////////////////////////////////////////////////////// /// CCD & Filter Wheel Group /////////////////////////////////////////////////////////////////////////// if (cbox == focuserCombo) Options::setDefaultFocusFocuser(cbox->currentText()); else if (cbox == CCDCaptureCombo) Options::setDefaultFocusCCD(cbox->currentText()); else if (cbox == binningCombo) { activeBin = cbox->currentIndex() + 1; Options::setFocusXBin(activeBin); } else if (cbox == FilterDevicesCombo) Options::setDefaultFocusFilterWheel(cbox->currentText()); // Filter Effects already taken care of in filterChangeWarning /////////////////////////////////////////////////////////////////////////// /// Settings Group /////////////////////////////////////////////////////////////////////////// else if (cbox == focusAlgorithmCombo) Options::setFocusAlgorithm(cbox->currentIndex()); else if (cbox == focusDetectionCombo) Options::setFocusDetection(cbox->currentIndex()); } } void Focus::loadSettings() { /////////////////////////////////////////////////////////////////////////// /// Focuser Group /////////////////////////////////////////////////////////////////////////// // Focus settle time FocusSettleTime->setValue(Options::focusSettleTime()); /////////////////////////////////////////////////////////////////////////// /// CCD & Filter Wheel Group /////////////////////////////////////////////////////////////////////////// // Binning activeBin = Options::focusXBin(); binningCombo->setCurrentIndex(activeBin - 1); // Gain gainIN->setValue(Options::focusGain()); /////////////////////////////////////////////////////////////////////////// /// Settings Group /////////////////////////////////////////////////////////////////////////// // Auto Star? useAutoStar->setChecked(Options::focusAutoStarEnabled()); // Subframe? useSubFrame->setChecked(Options::focusSubFrame()); // Dark frame? darkFrameCheck->setChecked(Options::useFocusDarkFrame()); // Use full field? useFullField->setChecked(Options::focusUseFullField()); // full field inner ring fullFieldInnerRing->setValue(Options::focusFullFieldInnerRadius()); // full field outer ring fullFieldOuterRing->setValue(Options::focusFullFieldOuterRadius()); // Suspend guiding? suspendGuideCheck->setChecked(Options::suspendGuiding()); // Guide Setting time GuideSettleTime->setValue(Options::guideSettleTime()); // Box Size focusBoxSize->setValue(Options::focusBoxSize()); // Max Travel if (Options::focusMaxTravel() > maxTravelIN->maximum()) maxTravelIN->setMaximum(Options::focusMaxTravel()); maxTravelIN->setValue(Options::focusMaxTravel()); // Step stepIN->setValue(Options::focusTicks()); // Tolerance toleranceIN->setValue(Options::focusTolerance()); // Threshold spin thresholdSpin->setValue(Options::focusThreshold()); // Focus Algorithm focusAlgorithm = static_cast(Options::focusAlgorithm()); focusAlgorithmCombo->setCurrentIndex(focusAlgorithm); // Frames Count focusFramesSpin->setValue(Options::focusFramesCount()); // Focus Detection focusDetection = static_cast(Options::focusDetection()); thresholdSpin->setEnabled(focusDetection == ALGORITHM_THRESHOLD); focusDetectionCombo->setCurrentIndex(focusDetection); } void Focus::initSettingsConnections() { /////////////////////////////////////////////////////////////////////////// /// Focuser Group /////////////////////////////////////////////////////////////////////////// connect(focuserCombo, static_cast(&QComboBox::activated), this, &Ekos::Focus::syncSettings); connect(FocusSettleTime, &QDoubleSpinBox::editingFinished, this, &Focus::syncSettings); /////////////////////////////////////////////////////////////////////////// /// CCD & Filter Wheel Group /////////////////////////////////////////////////////////////////////////// connect(CCDCaptureCombo, static_cast(&QComboBox::activated), this, &Ekos::Focus::syncSettings); connect(binningCombo, static_cast(&QComboBox::activated), this, &Ekos::Focus::syncSettings); connect(gainIN, &QDoubleSpinBox::editingFinished, this, &Focus::syncSettings); connect(FilterDevicesCombo, static_cast(&QComboBox::activated), this, &Ekos::Focus::syncSettings); connect(FilterPosCombo, static_cast(&QComboBox::activated), this, &Ekos::Focus::syncSettings); /////////////////////////////////////////////////////////////////////////// /// Settings Group /////////////////////////////////////////////////////////////////////////// connect(useAutoStar, &QCheckBox::toggled, this, &Ekos::Focus::syncSettings); connect(useSubFrame, &QCheckBox::toggled, this, &Ekos::Focus::syncSettings); connect(darkFrameCheck, &QCheckBox::toggled, this, &Ekos::Focus::syncSettings); connect(useFullField, &QCheckBox::toggled, this, &Ekos::Focus::syncSettings); connect(fullFieldInnerRing, &QDoubleSpinBox::editingFinished, this, &Focus::syncSettings); connect(fullFieldOuterRing, &QDoubleSpinBox::editingFinished, this, &Focus::syncSettings); connect(suspendGuideCheck, &QCheckBox::toggled, this, &Ekos::Focus::syncSettings); connect(GuideSettleTime, &QDoubleSpinBox::editingFinished, this, &Focus::syncSettings); connect(focusBoxSize, static_cast(&QSpinBox::valueChanged), this, &Focus::syncSettings); connect(maxTravelIN, &QDoubleSpinBox::editingFinished, this, &Focus::syncSettings); connect(stepIN, &QDoubleSpinBox::editingFinished, this, &Focus::syncSettings); connect(toleranceIN, &QDoubleSpinBox::editingFinished, this, &Focus::syncSettings); connect(thresholdSpin, &QDoubleSpinBox::editingFinished, this, &Focus::syncSettings); connect(focusAlgorithmCombo, static_cast(&QComboBox::activated), this, &Ekos::Focus::syncSettings); connect(focusFramesSpin, static_cast(&QSpinBox::valueChanged), this, &Focus::syncSettings); connect(focusDetectionCombo, static_cast(&QComboBox::activated), this, &Ekos::Focus::syncSettings); } void Focus::initPlots() { connect(clearDataB, &QPushButton::clicked, this, &Ekos::Focus::clearDataPoints); profileDialog = new QDialog(this); profileDialog->setWindowFlags(Qt::Tool | Qt::WindowStaysOnTopHint); QVBoxLayout *profileLayout = new QVBoxLayout(profileDialog); profileDialog->setWindowTitle(i18n("Relative Profile")); profilePlot = new QCustomPlot(profileDialog); profilePlot->setBackground(QBrush(Qt::black)); profilePlot->xAxis->setBasePen(QPen(Qt::white, 1)); profilePlot->yAxis->setBasePen(QPen(Qt::white, 1)); profilePlot->xAxis->grid()->setPen(QPen(QColor(140, 140, 140), 1, Qt::DotLine)); profilePlot->yAxis->grid()->setPen(QPen(QColor(140, 140, 140), 1, Qt::DotLine)); profilePlot->xAxis->grid()->setSubGridPen(QPen(QColor(80, 80, 80), 1, Qt::DotLine)); profilePlot->yAxis->grid()->setSubGridPen(QPen(QColor(80, 80, 80), 1, Qt::DotLine)); profilePlot->xAxis->grid()->setZeroLinePen(Qt::NoPen); profilePlot->yAxis->grid()->setZeroLinePen(Qt::NoPen); profilePlot->xAxis->setBasePen(QPen(Qt::white, 1)); profilePlot->yAxis->setBasePen(QPen(Qt::white, 1)); profilePlot->xAxis->setTickPen(QPen(Qt::white, 1)); profilePlot->yAxis->setTickPen(QPen(Qt::white, 1)); profilePlot->xAxis->setSubTickPen(QPen(Qt::white, 1)); profilePlot->yAxis->setSubTickPen(QPen(Qt::white, 1)); profilePlot->xAxis->setTickLabelColor(Qt::white); profilePlot->yAxis->setTickLabelColor(Qt::white); profilePlot->xAxis->setLabelColor(Qt::white); profilePlot->yAxis->setLabelColor(Qt::white); profileLayout->addWidget(profilePlot); profileDialog->setLayout(profileLayout); profileDialog->resize(400, 300); connect(relativeProfileB, &QPushButton::clicked, profileDialog, &QDialog::show); currentGaus = profilePlot->addGraph(); currentGaus->setLineStyle(QCPGraph::lsLine); currentGaus->setPen(QPen(Qt::red, 2)); lastGaus = profilePlot->addGraph(); lastGaus->setLineStyle(QCPGraph::lsLine); QPen pen(Qt::darkGreen); pen.setStyle(Qt::DashLine); pen.setWidth(2); lastGaus->setPen(pen); HFRPlot->setBackground(QBrush(Qt::black)); HFRPlot->xAxis->setBasePen(QPen(Qt::white, 1)); HFRPlot->yAxis->setBasePen(QPen(Qt::white, 1)); HFRPlot->xAxis->setTickPen(QPen(Qt::white, 1)); HFRPlot->yAxis->setTickPen(QPen(Qt::white, 1)); HFRPlot->xAxis->setSubTickPen(QPen(Qt::white, 1)); HFRPlot->yAxis->setSubTickPen(QPen(Qt::white, 1)); HFRPlot->xAxis->setTickLabelColor(Qt::white); HFRPlot->yAxis->setTickLabelColor(Qt::white); HFRPlot->xAxis->setLabelColor(Qt::white); HFRPlot->yAxis->setLabelColor(Qt::white); HFRPlot->xAxis->grid()->setPen(QPen(QColor(140, 140, 140), 1, Qt::DotLine)); HFRPlot->yAxis->grid()->setPen(QPen(QColor(140, 140, 140), 1, Qt::DotLine)); HFRPlot->xAxis->grid()->setSubGridPen(QPen(QColor(80, 80, 80), 1, Qt::DotLine)); HFRPlot->yAxis->grid()->setSubGridPen(QPen(QColor(80, 80, 80), 1, Qt::DotLine)); HFRPlot->xAxis->grid()->setZeroLinePen(Qt::NoPen); HFRPlot->yAxis->grid()->setZeroLinePen(Qt::NoPen); HFRPlot->yAxis->setLabel(i18n("HFR")); HFRPlot->setInteractions(QCP::iRangeZoom); HFRPlot->setInteraction(QCP::iRangeDrag, true); v_graph = HFRPlot->addGraph(); v_graph->setLineStyle(QCPGraph::lsNone); v_graph->setScatterStyle(QCPScatterStyle(QCPScatterStyle::ssCircle, Qt::white, Qt::red, 3)); } void Focus::initConnections() { // How long do we wait until the user select a star? waitStarSelectTimer.setInterval(AUTO_STAR_TIMEOUT); connect(&waitStarSelectTimer, &QTimer::timeout, this, &Ekos::Focus::checkAutoStarTimeout); connect(liveVideoB, &QPushButton::clicked, this, &Ekos::Focus::toggleVideo); // Show FITS Image in a new window showFITSViewerB->setIcon(QIcon::fromTheme("kstars_fitsviewer")); showFITSViewerB->setAttribute(Qt::WA_LayoutUsesWidgetRect); connect(showFITSViewerB, &QPushButton::clicked, this, &Ekos::Focus::showFITSViewer); // Toggle FITS View to full screen toggleFullScreenB->setIcon(QIcon::fromTheme("view-fullscreen")); toggleFullScreenB->setShortcut(Qt::Key_F4); toggleFullScreenB->setAttribute(Qt::WA_LayoutUsesWidgetRect); connect(toggleFullScreenB, &QPushButton::clicked, this, &Ekos::Focus::toggleFocusingWidgetFullScreen); // How long do we wait until an exposure times out and needs a retry? captureTimeout.setSingleShot(true); connect(&captureTimeout, &QTimer::timeout, this, &Ekos::Focus::processCaptureTimeout); // Start/Stop focus connect(startFocusB, &QPushButton::clicked, this, &Ekos::Focus::start); connect(stopFocusB, &QPushButton::clicked, this, &Ekos::Focus::checkStopFocus); // Focus IN/OUT connect(focusOutB, &QPushButton::clicked, [&]() { focusOut(); }); connect(focusInB, &QPushButton::clicked, [&]() { focusIn(); }); // Capture a single frame connect(captureB, &QPushButton::clicked, this, &Ekos::Focus::capture); // Start continuous capture connect(startLoopB, &QPushButton::clicked, this, &Ekos::Focus::startFraming); // Use a subframe when capturing connect(useSubFrame, &QCheckBox::toggled, this, &Ekos::Focus::toggleSubframe); // Reset frame dimensions to default connect(resetFrameB, &QPushButton::clicked, this, &Ekos::Focus::resetFrame); // Sync setting if full field setting is toggled. connect(useFullField, &QCheckBox::toggled, [&](bool toggled) { fullFieldInnerRing->setEnabled(toggled); fullFieldOuterRing->setEnabled(toggled); if (toggled) { useSubFrame->setChecked(false); useAutoStar->setChecked(false); } else { // Disable the overlay focusView->setStarFilterRange(0, 1); } }); // Sync settings if the CCD selection is updated. connect(CCDCaptureCombo, static_cast(&QComboBox::activated), this, &Ekos::Focus::checkCCD); // Sync settings if the Focuser selection is updated. connect(focuserCombo, static_cast(&QComboBox::activated), this, &Ekos::Focus::checkFocuser); // Sync settings if the filter selection is updated. connect(FilterDevicesCombo, static_cast(&QComboBox::activated), this, &Ekos::Focus::checkFilter); // Set focuser absolute position connect(startGotoB, &QPushButton::clicked, this, &Ekos::Focus::setAbsoluteFocusTicks); connect(stopGotoB, &QPushButton::clicked, [this]() { if (currentFocuser) currentFocuser->stop(); }); // Update the focuser box size used to enclose a star connect(focusBoxSize, static_cast(&QSpinBox::valueChanged), this, &Ekos::Focus::updateBoxSize); // Update the focuser star detection if the detection algorithm selection changes. connect(focusDetectionCombo, static_cast(&QComboBox::activated), this, [&](int index) { focusDetection = static_cast(index); thresholdSpin->setEnabled(focusDetection == ALGORITHM_THRESHOLD); }); // Update the focuser solution algorithm if the selection changes. connect(focusAlgorithmCombo, static_cast(&QComboBox::activated), this, [&](int index) { focusAlgorithm = static_cast(index); }); // Reset star center on auto star check toggle connect(useAutoStar, &QCheckBox::toggled, this, [&](bool enabled) { if (enabled) { starCenter = QVector3D(); starSelected = false; focusView->setTrackingBox(QRect()); } }); } void Focus::initView() { focusView = new FITSView(focusingWidget, FITS_FOCUS); focusView->setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Expanding); focusView->setBaseSize(focusingWidget->size()); focusView->createFloatingToolBar(); QVBoxLayout *vlayout = new QVBoxLayout(); vlayout->addWidget(focusView); focusingWidget->setLayout(vlayout); connect(focusView, &FITSView::trackingStarSelected, this, &Ekos::Focus::focusStarSelected, Qt::UniqueConnection); focusView->setStarsEnabled(true); focusView->setStarsHFREnabled(true); } } diff --git a/kstars/ekos/guide/guide.cpp b/kstars/ekos/guide/guide.cpp index aadf2e6a4..b1d0f8047 100644 --- a/kstars/ekos/guide/guide.cpp +++ b/kstars/ekos/guide/guide.cpp @@ -1,3529 +1,3529 @@ /* 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 "guide.h" #include "guideadaptor.h" #include "kstars.h" #include "ksmessagebox.h" #include "ksnotification.h" #include "kstarsdata.h" #include "opscalibration.h" #include "opsguide.h" #include "Options.h" #include "auxiliary/QProgressIndicator.h" #include "ekos/auxiliary/darklibrary.h" #include "externalguide/linguider.h" #include "externalguide/phd2.h" #include "fitsviewer/fitsdata.h" #include "fitsviewer/fitsview.h" #include "fitsviewer/fitsviewer.h" #include "internalguide/internalguider.h" #include #include #include #include "ui_manualdither.h" #define CAPTURE_TIMEOUT_THRESHOLD 30000 namespace Ekos { Guide::Guide() : QWidget() { // #1 Setup UI setupUi(this); // #2 Register DBus qRegisterMetaType("Ekos::GuideState"); qDBusRegisterMetaType(); new GuideAdaptor(this); QDBusConnection::sessionBus().registerObject("/KStars/Ekos/Guide", this); // #3 Init Plots initPlots(); // #4 Init View initView(); // #5 Load all settings loadSettings(); // #6 Init Connections initConnections(); // Image Filters for (auto &filter : FITSViewer::filterTypes) filterCombo->addItem(filter); // Progress Indicator pi = new QProgressIndicator(this); controlLayout->addWidget(pi, 1, 2, 1, 1); showFITSViewerB->setIcon( QIcon::fromTheme("kstars_fitsviewer")); connect(showFITSViewerB, &QPushButton::clicked, this, &Ekos::Guide::showFITSViewer); showFITSViewerB->setAttribute(Qt::WA_LayoutUsesWidgetRect); guideAutoScaleGraphB->setIcon( QIcon::fromTheme("zoom-fit-best")); connect(guideAutoScaleGraphB, &QPushButton::clicked, this, &Ekos::Guide::slotAutoScaleGraphs); guideAutoScaleGraphB->setAttribute(Qt::WA_LayoutUsesWidgetRect); guideSaveDataB->setIcon( QIcon::fromTheme("document-save")); connect(guideSaveDataB, &QPushButton::clicked, this, &Ekos::Guide::exportGuideData); guideSaveDataB->setAttribute(Qt::WA_LayoutUsesWidgetRect); guideDataClearB->setIcon( QIcon::fromTheme("application-exit")); connect(guideDataClearB, &QPushButton::clicked, this, &Ekos::Guide::clearGuideGraphs); guideDataClearB->setAttribute(Qt::WA_LayoutUsesWidgetRect); // Exposure //Should we set the range for the spin box here? QList exposureValues; exposureValues << 0.02 << 0.05 << 0.1 << 0.2 << 0.5 << 1 << 1.5 << 2 << 2.5 << 3 << 3.5 << 4 << 4.5 << 5 << 6 << 7 << 8 << 9 << 10 << 15 << 30; exposureIN->setRecommendedValues(exposureValues); connect(exposureIN, &NonLinearDoubleSpinBox::editingFinished, this, &Ekos::Guide::saveDefaultGuideExposure); // Init Internal Guider always internalGuider = new InternalGuider(); KConfigDialog *dialog = new KConfigDialog(this, "guidesettings", Options::self()); opsCalibration = new OpsCalibration(internalGuider); KPageWidgetItem *page = dialog->addPage(opsCalibration, i18n("Calibration")); page->setIcon(QIcon::fromTheme("tool-measure")); opsGuide = new OpsGuide(); connect(opsGuide, &OpsGuide::settingsUpdated, [this]() { onThresholdChanged(Options::guideAlgorithm()); }); page = dialog->addPage(opsGuide, i18n("Guide")); page->setIcon(QIcon::fromTheme("kstars_guides")); internalGuider->setGuideView(guideView); // Set current guide type setGuiderType(-1); //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); } Guide::~Guide() { delete guider; } void Guide::handleHorizontalPlotSizeChange() { driftPlot->xAxis->setScaleRatio(driftPlot->yAxis, 1.0); driftPlot->replot(); } void Guide::handleVerticalPlotSizeChange() { driftPlot->yAxis->setScaleRatio(driftPlot->xAxis, 1.0); driftPlot->replot(); } void Guide::resizeEvent(QResizeEvent *event) { if (event->oldSize().width() != -1) { if (event->oldSize().width() != size().width()) handleHorizontalPlotSizeChange(); else if (event->oldSize().height() != size().height()) handleVerticalPlotSizeChange(); } else { QTimer::singleShot(10, this, &Ekos::Guide::handleHorizontalPlotSizeChange); } } void Guide::buildTarget() { double accuracyRadius = accuracyRadiusSpin->value(); Options::setGuiderAccuracyThreshold(accuracyRadius); if (centralTarget) { concentricRings->data()->clear(); redTarget->data()->clear(); yellowTarget->data()->clear(); centralTarget->data()->clear(); } else { concentricRings = new QCPCurve(driftPlot->xAxis, driftPlot->yAxis); redTarget = new QCPCurve(driftPlot->xAxis, driftPlot->yAxis); yellowTarget = new QCPCurve(driftPlot->xAxis, driftPlot->yAxis); centralTarget = new QCPCurve(driftPlot->xAxis, driftPlot->yAxis); } const int pointCount = 200; QVector circleRings( pointCount * (5)); //Have to multiply by the number of rings, Rings at : 25%, 50%, 75%, 125%, 175% QVector circleCentral(pointCount); QVector circleYellow(pointCount); QVector circleRed(pointCount); int circleRingPt = 0; for (int i = 0; i < pointCount; i++) { double theta = i / static_cast(pointCount) * 2 * M_PI; for (double ring = 1; ring < 8; ring++) { if (ring != 4 && ring != 6) { if (i % (9 - static_cast(ring)) == 0) //This causes fewer points to draw on the inner circles. { circleRings[circleRingPt] = QCPCurveData(circleRingPt, accuracyRadius * ring * 0.25 * qCos(theta), accuracyRadius * ring * 0.25 * qSin(theta)); circleRingPt++; } } } circleCentral[i] = QCPCurveData(i, accuracyRadius * qCos(theta), accuracyRadius * qSin(theta)); circleYellow[i] = QCPCurveData(i, accuracyRadius * 1.5 * qCos(theta), accuracyRadius * 1.5 * qSin(theta)); circleRed[i] = QCPCurveData(i, accuracyRadius * 2 * qCos(theta), accuracyRadius * 2 * qSin(theta)); } concentricRings->setLineStyle(QCPCurve::lsNone); concentricRings->setScatterSkip(0); concentricRings->setScatterStyle(QCPScatterStyle(QCPScatterStyle::ssDisc, QColor(255, 255, 255, 150), 1)); concentricRings->data()->set(circleRings, true); redTarget->data()->set(circleRed, true); yellowTarget->data()->set(circleYellow, true); centralTarget->data()->set(circleCentral, true); concentricRings->setPen(QPen(Qt::white)); redTarget->setPen(QPen(Qt::red)); yellowTarget->setPen(QPen(Qt::yellow)); centralTarget->setPen(QPen(Qt::green)); concentricRings->setBrush(Qt::NoBrush); redTarget->setBrush(QBrush(QColor(255, 0, 0, 50))); yellowTarget->setBrush( QBrush(QColor(0, 255, 0, 50))); //Note this is actually yellow. It is green on top of red with equal opacity. centralTarget->setBrush(QBrush(QColor(0, 255, 0, 50))); if (driftPlot->size().width() > 0) driftPlot->replot(); } void Guide::clearGuideGraphs() { driftGraph->graph(0)->data()->clear(); //RA data driftGraph->graph(1)->data()->clear(); //DEC data driftGraph->graph(2)->data()->clear(); //RA highlighted point driftGraph->graph(3)->data()->clear(); //DEC highlighted point driftGraph->graph(4)->data()->clear(); //RA Pulses driftGraph->graph(5)->data()->clear(); //DEC Pulses driftPlot->graph(0)->data()->clear(); //Guide data driftPlot->graph(1)->data()->clear(); //Guide highlighted point driftGraph->clearItems(); //Clears dither text items from the graph driftGraph->replot(); driftPlot->replot(); //Since the labels got cleared with clearItems above. setupNSEWLabels(); } void Guide::setupNSEWLabels() { //Labels for N/S/E/W QColor raLabelColor(KStarsData::Instance()->colorScheme()->colorNamed("RAGuideError")); QColor deLabelColor(KStarsData::Instance()->colorScheme()->colorNamed("DEGuideError")); //DriftGraph { QCPItemText *northLabel = new QCPItemText(driftGraph); northLabel->setColor(deLabelColor); northLabel->setText(i18nc("North","N")); northLabel->position->setType(QCPItemPosition::ptViewportRatio); northLabel->position->setCoords(0.6,0.1); northLabel->setVisible(true); QCPItemText *southLabel = new QCPItemText(driftGraph); southLabel->setColor(deLabelColor); southLabel->setText(i18nc("South","S")); southLabel->position->setType(QCPItemPosition::ptViewportRatio); southLabel->position->setCoords(0.6,0.8); southLabel->setVisible(true); QCPItemText *westLabel = new QCPItemText(driftGraph); westLabel->setColor(raLabelColor); westLabel->setText(i18nc("West","W")); westLabel->position->setType(QCPItemPosition::ptViewportRatio); westLabel->position->setCoords(0.8,0.1); westLabel->setVisible(true); QCPItemText *eastLabel = new QCPItemText(driftGraph); eastLabel->setColor(raLabelColor); eastLabel->setText(i18nc("East","E")); eastLabel->position->setType(QCPItemPosition::ptViewportRatio); eastLabel->position->setCoords(0.8,0.8); eastLabel->setVisible(true); } //DriftPlot { QCPItemText *northLabel = new QCPItemText(driftPlot); northLabel->setColor(deLabelColor); northLabel->setText(i18nc("North","N")); northLabel->position->setType(QCPItemPosition::ptViewportRatio); northLabel->position->setCoords(0.25,0.2); northLabel->setVisible(true); QCPItemText *southLabel = new QCPItemText(driftPlot); southLabel->setColor(deLabelColor); southLabel->setText(i18nc("South","S")); southLabel->position->setType(QCPItemPosition::ptViewportRatio); southLabel->position->setCoords(0.25,0.7); southLabel->setVisible(true); QCPItemText *westLabel = new QCPItemText(driftPlot); westLabel->setColor(raLabelColor); westLabel->setText(i18nc("West","W")); westLabel->position->setType(QCPItemPosition::ptViewportRatio); westLabel->position->setCoords(0.8,0.75); westLabel->setVisible(true); QCPItemText *eastLabel = new QCPItemText(driftPlot); eastLabel->setColor(raLabelColor); eastLabel->setText(i18nc("East","E")); eastLabel->position->setType(QCPItemPosition::ptViewportRatio); eastLabel->position->setCoords(0.3,0.75); eastLabel->setVisible(true); } } void Guide::slotAutoScaleGraphs() { double accuracyRadius = accuracyRadiusSpin->value(); double key = guideTimer.elapsed() / 1000.0; driftGraph->xAxis->setRange(key - 60, key); driftGraph->yAxis->setRange(-3, 3); driftGraph->graph(0)->rescaleValueAxis(true); driftGraph->replot(); driftPlot->xAxis->setRange(-accuracyRadius * 3, accuracyRadius * 3); driftPlot->yAxis->setRange(-accuracyRadius * 3, accuracyRadius * 3); driftPlot->graph(0)->rescaleAxes(true); driftPlot->yAxis->setScaleRatio(driftPlot->xAxis, 1.0); driftPlot->xAxis->setScaleRatio(driftPlot->yAxis, 1.0); driftPlot->replot(); } void Guide::guideHistory() { int sliderValue = guideSlider->value(); latestCheck->setChecked(sliderValue == guideSlider->maximum() - 1 || sliderValue == guideSlider->maximum()); driftGraph->graph(2)->data()->clear(); //Clear RA highlighted point driftGraph->graph(3)->data()->clear(); //Clear DEC highlighted point driftPlot->graph(1)->data()->clear(); //Clear Guide highlighted point double t = driftGraph->graph(0)->dataMainKey(sliderValue); //Get time from RA data double ra = driftGraph->graph(0)->dataMainValue(sliderValue); //Get RA from RA data double de = driftGraph->graph(1)->dataMainValue(sliderValue); //Get DEC from DEC data double raPulse = driftGraph->graph(4)->dataMainValue(sliderValue); //Get RA Pulse from RA pulse data double dePulse = driftGraph->graph(5)->dataMainValue(sliderValue); //Get DEC Pulse from DEC pulse data driftGraph->graph(2)->addData(t, ra); //Set RA highlighted point driftGraph->graph(3)->addData(t, de); //Set DEC highlighted point //This will allow the graph to scroll left and right along with the guide slider if (driftGraph->xAxis->range().contains(t) == false) { if(t < driftGraph->xAxis->range().lower) { driftGraph->xAxis->setRange(t, t + driftGraph->xAxis->range().size()); } if(t > driftGraph->xAxis->range().upper) { driftGraph->xAxis->setRange(t - driftGraph->xAxis->range().size(), t); } } driftGraph->replot(); driftPlot->graph(1)->addData(ra, de); //Set guide highlighted point driftPlot->replot(); if(!graphOnLatestPt) { QTime localTime = guideTimer; localTime = localTime.addSecs(t); QPoint localTooltipCoordinates = driftGraph->graph(0)->dataPixelPosition(sliderValue).toPoint(); QPoint globalTooltipCoordinates = driftGraph->mapToGlobal(localTooltipCoordinates); if(raPulse == 0 && dePulse == 0) { QToolTip::showText( globalTooltipCoordinates, i18nc("Drift graphics tooltip; %1 is local time; %2 is RA deviation; %3 is DE deviation in arcseconds", "" "" "" "" "
LT: %1
RA: %2 \"
DE: %3 \"
", localTime.toString("hh:mm:ss AP"), QString::number(ra, 'f', 2), QString::number(de, 'f', 2))); } else { QToolTip::showText( globalTooltipCoordinates, i18nc("Drift graphics tooltip; %1 is local time; %2 is RA deviation; %3 is DE deviation in arcseconds; %4 is RA Pulse in ms; %5 is DE Pulse in ms", "" "" "" "" "" "" "
LT: %1
RA: %2 \"
DE: %3 \"
RA Pulse: %4 ms
DE Pulse: %5 ms
", localTime.toString("hh:mm:ss AP"), QString::number(ra, 'f', 2), QString::number(de, 'f', 2), QString::number(raPulse, 'f', 2), QString::number(dePulse, 'f', 2))); //The pulses were divided by 100 before they were put on the graph. } } } void Guide::setLatestGuidePoint(bool isChecked) { graphOnLatestPt = isChecked; if(isChecked) guideSlider->setValue(guideSlider->maximum()); } void Guide::toggleShowRAPlot(bool isChecked) { Options::setRADisplayedOnGuideGraph(isChecked); driftGraph->graph(0)->setVisible(isChecked); driftGraph->graph(2)->setVisible(isChecked); driftGraph->replot(); } void Guide::toggleShowDEPlot(bool isChecked) { Options::setDEDisplayedOnGuideGraph(isChecked); driftGraph->graph(1)->setVisible(isChecked); driftGraph->graph(3)->setVisible(isChecked); driftGraph->replot(); } void Guide::toggleRACorrectionsPlot(bool isChecked) { Options::setRACorrDisplayedOnGuideGraph(isChecked); driftGraph->graph(4)->setVisible(isChecked); updateCorrectionsScaleVisibility(); } void Guide::toggleDECorrectionsPlot(bool isChecked) { Options::setDECorrDisplayedOnGuideGraph(isChecked); driftGraph->graph(5)->setVisible(isChecked); updateCorrectionsScaleVisibility(); } void Guide::updateCorrectionsScaleVisibility() { bool isVisible = (Options::rACorrDisplayedOnGuideGraph() || Options::dECorrDisplayedOnGuideGraph()); driftGraph->yAxis2->setVisible(isVisible); correctionSlider->setVisible(isVisible); driftGraph->replot(); } void Guide::setCorrectionGraphScale() { driftGraph->yAxis2->setRange(driftGraph->yAxis->range().lower * correctionSlider->value(), driftGraph->yAxis->range().upper * correctionSlider->value()); driftGraph->replot(); } void Guide::exportGuideData() { int numPoints = driftGraph->graph(0)->dataCount(); if (numPoints == 0) return; QUrl exportFile = QFileDialog::getSaveFileUrl(KStars::Instance(), i18n("Export Guide Data"), guideURLPath, "CSV File (*.csv)"); if (exportFile.isEmpty()) // if user presses cancel return; if (exportFile.toLocalFile().endsWith(QLatin1String(".csv")) == false) exportFile.setPath(exportFile.toLocalFile() + ".csv"); QString path = exportFile.toLocalFile(); if (QFile::exists(path)) { int r = KMessageBox::warningContinueCancel(nullptr, i18n("A file named \"%1\" already exists. " "Overwrite it?", exportFile.fileName()), i18n("Overwrite File?"), KStandardGuiItem::overwrite()); if (r == KMessageBox::Cancel) return; } if (!exportFile.isValid()) { QString message = i18n("Invalid URL: %1", exportFile.url()); KSNotification::sorry(message, i18n("Invalid URL")); return; } QFile file; 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; } QTextStream outstream(&file); outstream << "Frame #, Time Elapsed (sec), Local Time (HMS), RA Error (arcsec), DE Error (arcsec), RA Pulse (ms), DE Pulse (ms)" << endl; for (int i = 0; i < numPoints; i++) { double t = driftGraph->graph(0)->dataMainKey(i); double ra = driftGraph->graph(0)->dataMainValue(i); double de = driftGraph->graph(1)->dataMainValue(i); double raPulse = driftGraph->graph(4)->dataMainValue(i); double dePulse = driftGraph->graph(5)->dataMainValue(i); QTime localTime = guideTimer; localTime = localTime.addSecs(t); outstream << i << ',' << t << ',' << localTime.toString("hh:mm:ss AP") << ',' << ra << ',' << de << ',' << raPulse << ',' << dePulse << ',' << endl; } appendLogText(i18n("Guide Data Saved as: %1", path)); file.close(); } QString Guide::setRecommendedExposureValues(QList values) { exposureIN->setRecommendedValues(values); return exposureIN->getRecommendedValuesString(); } void Guide::addCamera(ISD::GDInterface *newCCD) { ISD::CCD *ccd = static_cast(newCCD); if (CCDs.contains(ccd)) return; if (guiderType != GUIDE_INTERNAL) { connect(ccd, &ISD::CCD::newBLOBManager, [ccd](INDI::Property * prop) { if (!strcmp(prop->getName(), "CCD1") || !strcmp(prop->getName(), "CCD2")) ccd->setBLOBEnabled(Options::guideRemoteImagesEnabled(), prop->getName()); }); guiderCombo->clear(); guiderCombo->setEnabled(false); if (guiderType == GUIDE_PHD2) guiderCombo->addItem("PHD2"); else guiderCombo->addItem("LinGuider"); return; } else guiderCombo->setEnabled(true); CCDs.append(ccd); guiderCombo->addItem(ccd->getDeviceName()); checkCCD(); } void Guide::addGuideHead(ISD::GDInterface *newCCD) { if (guiderType != GUIDE_INTERNAL) return; ISD::CCD *ccd = static_cast(newCCD); CCDs.append(ccd); QString guiderName = ccd->getDeviceName() + QString(" Guider"); if (guiderCombo->findText(guiderName) == -1) { guiderCombo->addItem(guiderName); //CCDs.append(static_cast (newCCD)); } //checkCCD(CCDs.count()-1); //guiderCombo->setCurrentIndex(CCDs.count()-1); //setGuiderProcess(Options::useEkosGuider() ? GUIDE_INTERNAL : GUIDE_PHD2); } void Guide::setTelescope(ISD::GDInterface *newTelescope) { currentTelescope = dynamic_cast(newTelescope); syncTelescopeInfo(); } bool Guide::setCamera(const QString &device) { if (guiderType != GUIDE_INTERNAL) return true; for (int i = 0; i < guiderCombo->count(); i++) if (device == guiderCombo->itemText(i)) { guiderCombo->setCurrentIndex(i); checkCCD(i); return true; } return false; } QString Guide::camera() { if (currentCCD) return currentCCD->getDeviceName(); return QString(); } void Guide::checkCCD(int ccdNum) { if (guiderType != GUIDE_INTERNAL) return; if (ccdNum == -1) { ccdNum = guiderCombo->currentIndex(); if (ccdNum == -1) return; } if (ccdNum <= CCDs.count()) { currentCCD = CCDs.at(ccdNum); if (currentCCD->hasGuideHead() && guiderCombo->currentText().contains("Guider")) useGuideHead = true; else useGuideHead = false; ISD::CCDChip *targetChip = currentCCD->getChip(useGuideHead ? ISD::CCDChip::GUIDE_CCD : ISD::CCDChip::PRIMARY_CCD); if (targetChip && targetChip->isCapturing()) return; if (guiderType != GUIDE_INTERNAL) { syncCCDInfo(); return; } //connect(currentCCD, SIGNAL(FITSViewerClosed()), this, &Ekos::Guide::viewerClosed()), Qt::UniqueConnection); connect(currentCCD, &ISD::CCD::numberUpdated, this, &Ekos::Guide::processCCDNumber, Qt::UniqueConnection); connect(currentCCD, &ISD::CCD::newExposureValue, this, &Ekos::Guide::checkExposureValue, Qt::UniqueConnection); // If guider is external and already connected and remote images option was disabled AND it was already // disabled, then let's go ahead and disable it. #if 0 if (guiderType != GUIDE_INTERNAL && Options::guideRemoteImagesEnabled() == false && guider->isConnected()) { for (int i = 0; i < CCDs.count(); i++) { ISD::CCD * oneCCD = CCDs[i]; if (i == ccdNum && oneCCD->getDriverInfo()->getClientManager()->getBLOBMode(oneCCD->getDeviceName(), "CCD1") != B_NEVER) { appendLogText(i18n("Disabling remote image reception from %1", oneCCD->getDeviceName())); oneCCD->getDriverInfo()->getClientManager()->setBLOBMode(B_NEVER, oneCCD->getDeviceName(), "CCD1"); oneCCD->getDriverInfo()->getClientManager()->setBLOBMode(B_NEVER, oneCCD->getDeviceName(), "CCD2"); } // If it was already disabled, enable it back else if (i != ccdNum && oneCCD->getDriverInfo()->getClientManager()->getBLOBMode(oneCCD->getDeviceName(), "CCD1") == B_NEVER) { appendLogText(i18n("Enabling remote image reception from %1", oneCCD->getDeviceName())); oneCCD->getDriverInfo()->getClientManager()->setBLOBMode(B_ALSO, oneCCD->getDeviceName(), "CCD1"); oneCCD->getDriverInfo()->getClientManager()->setBLOBMode(B_ALSO, oneCCD->getDeviceName(), "CCD2"); } } } #endif targetChip->setImageView(guideView, FITS_GUIDE); syncCCDInfo(); } } void Guide::syncCCDInfo() { INumberVectorProperty *nvp = nullptr; if (currentCCD == nullptr) return; if (useGuideHead) nvp = currentCCD->getBaseDevice()->getNumber("GUIDER_INFO"); else nvp = currentCCD->getBaseDevice()->getNumber("CCD_INFO"); if (nvp) { INumber *np = IUFindNumber(nvp, "CCD_PIXEL_SIZE_X"); if (np) ccdPixelSizeX = np->value; np = IUFindNumber(nvp, "CCD_PIXEL_SIZE_Y"); if (np) ccdPixelSizeY = np->value; np = IUFindNumber(nvp, "CCD_PIXEL_SIZE_Y"); if (np) ccdPixelSizeY = np->value; } updateGuideParams(); } void Guide::setTelescopeInfo(double primaryFocalLength, double primaryAperture, double guideFocalLength, double guideAperture) { if (primaryFocalLength > 0) focal_length = primaryFocalLength; if (primaryAperture > 0) aperture = primaryAperture; // If we have guide scope info, always prefer that over primary if (guideFocalLength > 0) focal_length = guideFocalLength; if (guideAperture > 0) aperture = guideAperture; updateGuideParams(); } void Guide::syncTelescopeInfo() { if (currentTelescope == nullptr || currentTelescope->isConnected() == false) return; INumberVectorProperty *nvp = currentTelescope->getBaseDevice()->getNumber("TELESCOPE_INFO"); if (nvp) { INumber *np = IUFindNumber(nvp, "TELESCOPE_APERTURE"); if (np && np->value > 0) primaryAperture = np->value; np = IUFindNumber(nvp, "GUIDER_APERTURE"); if (np && np->value > 0) guideAperture = np->value; aperture = primaryAperture; //if (currentCCD && currentCCD->getTelescopeType() == ISD::CCD::TELESCOPE_GUIDE) if (FOVScopeCombo->currentIndex() == ISD::CCD::TELESCOPE_GUIDE) aperture = guideAperture; np = IUFindNumber(nvp, "TELESCOPE_FOCAL_LENGTH"); if (np && np->value > 0) primaryFL = np->value; np = IUFindNumber(nvp, "GUIDER_FOCAL_LENGTH"); if (np && np->value > 0) guideFL = np->value; focal_length = primaryFL; //if (currentCCD && currentCCD->getTelescopeType() == ISD::CCD::TELESCOPE_GUIDE) if (FOVScopeCombo->currentIndex() == ISD::CCD::TELESCOPE_GUIDE) focal_length = guideFL; } updateGuideParams(); } void Guide::updateGuideParams() { if (currentCCD == nullptr) return; if (currentCCD->hasGuideHead() == false) useGuideHead = false; ISD::CCDChip *targetChip = currentCCD->getChip(useGuideHead ? ISD::CCDChip::GUIDE_CCD : ISD::CCDChip::PRIMARY_CCD); if (targetChip == nullptr) { appendLogText(i18n("Connection to the guide CCD is lost.")); return; } if (targetChip->getFrameType() != FRAME_LIGHT) return; binningCombo->setEnabled(targetChip->canBin()); int subBinX = 1, subBinY = 1; if (targetChip->canBin()) { int maxBinX, maxBinY; targetChip->getBinning(&subBinX, &subBinY); targetChip->getMaxBin(&maxBinX, &maxBinY); binningCombo->blockSignals(true); binningCombo->clear(); for (int i = 1; i <= maxBinX; i++) binningCombo->addItem(QString("%1x%2").arg(i).arg(i)); binningCombo->setCurrentIndex(subBinX - 1); binningCombo->blockSignals(false); } if (frameSettings.contains(targetChip) == false) { int x, y, w, h; if (targetChip->getFrame(&x, &y, &w, &h)) { if (w > 0 && h > 0) { int minX, maxX, minY, maxY, minW, maxW, minH, maxH; targetChip->getFrameMinMax(&minX, &maxX, &minY, &maxY, &minW, &maxW, &minH, &maxH); QVariantMap settings; settings["x"] = Options::guideSubframeEnabled() ? x : minX; settings["y"] = Options::guideSubframeEnabled() ? y : minY; settings["w"] = Options::guideSubframeEnabled() ? w : maxW; settings["h"] = Options::guideSubframeEnabled() ? h : maxH; settings["binx"] = subBinX; settings["biny"] = subBinY; frameSettings[targetChip] = settings; } } } if (ccdPixelSizeX != -1 && ccdPixelSizeY != -1 && aperture != -1 && focal_length != -1) { FOVScopeCombo->setItemData( ISD::CCD::TELESCOPE_PRIMARY, i18nc("F-Number, Focal Length, Aperture", "F%1 Focal Length: %2 mm Aperture: %3 mm2", QString::number(primaryFL / primaryAperture, 'f', 1), QString::number(primaryFL, 'f', 2), QString::number(primaryAperture, 'f', 2)), Qt::ToolTipRole); FOVScopeCombo->setItemData( ISD::CCD::TELESCOPE_GUIDE, i18nc("F-Number, Focal Length, Aperture", "F%1 Focal Length: %2 mm Aperture: %3 mm2", QString::number(guideFL / guideAperture, 'f', 1), QString::number(guideFL, 'f', 2), QString::number(guideAperture, 'f', 2)), Qt::ToolTipRole); guider->setGuiderParams(ccdPixelSizeX, ccdPixelSizeY, aperture, focal_length); emit guideChipUpdated(targetChip); int x, y, w, h; if (targetChip->getFrame(&x, &y, &w, &h)) { guider->setFrameParams(x, y, w, h, subBinX, subBinY); } l_Focal->setText(QString::number(focal_length, 'f', 1)); l_Aperture->setText(QString::number(aperture, 'f', 1)); if (aperture == 0) { l_FbyD->setText("0"); // Pixel scale in arcsec/pixel pixScaleX = 0; pixScaleY = 0; } else { l_FbyD->setText(QString::number(focal_length / aperture, 'f', 1)); // Pixel scale in arcsec/pixel pixScaleX = 206264.8062470963552 * ccdPixelSizeX / 1000.0 / focal_length; pixScaleY = 206264.8062470963552 * ccdPixelSizeY / 1000.0 / focal_length; } // FOV in arcmin double fov_w = (w * pixScaleX) / 60.0; double fov_h = (h * pixScaleY) / 60.0; l_FOV->setText(QString("%1x%2").arg(QString::number(fov_w, 'f', 1), QString::number(fov_h, 'f', 1))); } } void Guide::addST4(ISD::ST4 *newST4) { if (guiderType != GUIDE_INTERNAL) return; foreach (ISD::ST4 *guidePort, ST4List) { if (!strcmp(guidePort->getDeviceName(), newST4->getDeviceName())) return; } ST4List.append(newST4); ST4Combo->addItem(newST4->getDeviceName()); setST4(0); } bool Guide::setST4(const QString &device) { if (guiderType != GUIDE_INTERNAL) return true; for (int i = 0; i < ST4List.count(); i++) if (ST4List.at(i)->getDeviceName() == device) { ST4Combo->setCurrentIndex(i); setST4(i); return true; } return false; } QString Guide::st4() { if (guiderType != GUIDE_INTERNAL || ST4Combo->currentIndex() == -1) return QString(); return ST4Combo->currentText(); } void Guide::setST4(int index) { if (ST4List.empty() || index >= ST4List.count() || guiderType != GUIDE_INTERNAL) return; ST4Driver = ST4List.at(index); GuideDriver = ST4Driver; } void Guide::setAO(ISD::ST4 *newAO) { AODriver = newAO; //guider->setAO(true); } bool Guide::capture() { buildOperationStack(GUIDE_CAPTURE); return executeOperationStack(); } bool Guide::captureOneFrame() { captureTimeout.stop(); if (currentCCD == nullptr) return false; if (currentCCD->isConnected() == false) { appendLogText(i18n("Error: lost connection to CCD.")); return false; } // If CCD Telescope Type does not match desired scope type, change it if (currentCCD->getTelescopeType() != FOVScopeCombo->currentIndex()) currentCCD->setTelescopeType(static_cast(FOVScopeCombo->currentIndex())); double seqExpose = exposureIN->value(); ISD::CCDChip *targetChip = currentCCD->getChip(useGuideHead ? ISD::CCDChip::GUIDE_CCD : ISD::CCDChip::PRIMARY_CCD); targetChip->setCaptureMode(FITS_GUIDE); targetChip->setFrameType(FRAME_LIGHT); if (darkFrameCheck->isChecked()) targetChip->setCaptureFilter(FITS_NONE); else targetChip->setCaptureFilter(static_cast(filterCombo->currentIndex())); guideView->setBaseSize(guideWidget->size()); setBusy(true); if (frameSettings.contains(targetChip)) { QVariantMap settings = frameSettings[targetChip]; targetChip->setFrame(settings["x"].toInt(), settings["y"].toInt(), settings["w"].toInt(), settings["h"].toInt()); } #if 0 switch (state) { case GUIDE_GUIDING: if (Options::rapidGuideEnabled() == false) connect(currentCCD, SIGNAL(BLOBUpdated(IBLOB*)), this, &Ekos::Guide::newFITS(IBLOB *)), Qt::UniqueConnection); targetChip->capture(seqExpose); return true; break; default: break; } #endif currentCCD->setTransformFormat(ISD::CCD::FORMAT_FITS); connect(currentCCD, &ISD::CCD::BLOBUpdated, this, &Ekos::Guide::newFITS, Qt::UniqueConnection); qCDebug(KSTARS_EKOS_GUIDE) << "Capturing frame..."; double finalExposure = seqExpose; // Increase exposure for calibration frame if we need auto-select a star // To increase chances we detect one. if (operationStack.contains(GUIDE_STAR_SELECT) && Options::guideAutoStarEnabled()) finalExposure *= 3; // Timeout is exposure duration + timeout threshold in seconds captureTimeout.start(finalExposure * 1000 + CAPTURE_TIMEOUT_THRESHOLD); targetChip->capture(finalExposure); return true; } bool Guide::abort() { if (currentCCD && guiderType == GUIDE_INTERNAL) { captureTimeout.stop(); pulseTimer.stop(); ISD::CCDChip *targetChip = currentCCD->getChip(useGuideHead ? ISD::CCDChip::GUIDE_CCD : ISD::CCDChip::PRIMARY_CCD); if (targetChip->isCapturing()) targetChip->abortExposure(); } manualDitherB->setEnabled(false); setBusy(false); switch (state) { case GUIDE_IDLE: case GUIDE_CONNECTED: setBLOBEnabled(false); break; case GUIDE_DISCONNECTED: setBLOBEnabled(true); break; case GUIDE_CALIBRATING: case GUIDE_DITHERING: case GUIDE_STAR_SELECT: case GUIDE_CAPTURE: case GUIDE_GUIDING: case GUIDE_LOOPING: guider->abort(); break; default: break; } return true; } void Guide::setBusy(bool enable) { if (enable && pi->isAnimated()) return; else if (enable == false && pi->isAnimated() == false) return; if (enable) { clearCalibrationB->setEnabled(false); guideB->setEnabled(false); captureB->setEnabled(false); loopB->setEnabled(false); darkFrameCheck->setEnabled(false); subFrameCheck->setEnabled(false); autoStarCheck->setEnabled(false); stopB->setEnabled(true); pi->startAnimation(); //disconnect(guideView, SIGNAL(trackingStarSelected(int,int)), this, &Ekos::Guide::setTrackingStar(int,int))); } else { if (guiderType == GUIDE_INTERNAL) { captureB->setEnabled(true); loopB->setEnabled(true); darkFrameCheck->setEnabled(true); subFrameCheck->setEnabled(true); autoStarCheck->setEnabled(true); } if (calibrationComplete) clearCalibrationB->setEnabled(true); guideB->setEnabled(true); stopB->setEnabled(false); pi->stopAnimation(); connect(guideView, &FITSView::trackingStarSelected, this, &Ekos::Guide::setTrackingStar, Qt::UniqueConnection); } } void Guide::processCaptureTimeout() { captureTimeoutCounter++; if (captureTimeoutCounter >= 3) { captureTimeoutCounter = 0; if (state == GUIDE_GUIDING) appendLogText(i18n("Exposure timeout. Aborting Autoguide.")); else if (state == GUIDE_DITHERING) appendLogText(i18n("Exposure timeout. Aborting Dithering.")); else if (state == GUIDE_CALIBRATING) appendLogText(i18n("Exposure timeout. Aborting Calibration.")); abort(); return; } 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); } void Guide::newFITS(IBLOB *bp) { INDI_UNUSED(bp); captureTimeout.stop(); captureTimeoutCounter = 0; disconnect(currentCCD, &ISD::CCD::BLOBUpdated, this, &Ekos::Guide::newFITS); qCDebug(KSTARS_EKOS_GUIDE) << "Received guide frame."; ISD::CCDChip *targetChip = currentCCD->getChip(useGuideHead ? ISD::CCDChip::GUIDE_CCD : ISD::CCDChip::PRIMARY_CCD); int subBinX = 1, subBinY = 1; targetChip->getBinning(&subBinX, &subBinY); if (starCenter.x() == 0 && starCenter.y() == 0) { int x = 0, y = 0, w = 0, h = 0; if (frameSettings.contains(targetChip)) { QVariantMap settings = frameSettings[targetChip]; x = settings["x"].toInt(); y = settings["y"].toInt(); w = settings["w"].toInt(); h = settings["h"].toInt(); } else targetChip->getFrame(&x, &y, &w, &h); starCenter.setX(w / (2 * subBinX)); starCenter.setY(h / (2 * subBinY)); starCenter.setZ(subBinX); } syncTrackingBoxPosition(); setCaptureComplete(); } void Guide::setCaptureComplete() { DarkLibrary::Instance()->disconnect(this); if (operationStack.isEmpty() == false) { executeOperationStack(); return; } switch (state) { case GUIDE_IDLE: case GUIDE_ABORTED: case GUIDE_CONNECTED: case GUIDE_DISCONNECTED: case GUIDE_CALIBRATION_SUCESS: case GUIDE_CALIBRATION_ERROR: case GUIDE_DITHERING_ERROR: setBusy(false); break; case GUIDE_CAPTURE: state = GUIDE_IDLE; emit newStatus(state); setBusy(false); break; case GUIDE_LOOPING: capture(); break; case GUIDE_CALIBRATING: guider->calibrate(); break; case GUIDE_GUIDING: guider->guide(); break; case GUIDE_DITHERING: guider->dither(Options::ditherPixels()); break; // Feature only of internal guider case GUIDE_MANUAL_DITHERING: dynamic_cast(guider)->processManualDithering(); break; case GUIDE_REACQUIRE: guider->reacquire(); break; case GUIDE_DITHERING_SETTLE: if (Options::ditherNoGuiding()) return; capture(); break; default: break; } emit newStarPixmap(guideView->getTrackingBoxPixmap(10)); } void Guide::appendLogText(const QString &text) { m_LogText.insert(0, i18nc("log entry; %1 is the date, %2 is the text", "%1 %2", - QDateTime::currentDateTime().toString("yyyy-MM-ddThh:mm:ss"), text)); + KStarsData::Instance()->lt().toString("yyyy-MM-ddThh:mm:ss"), text)); qCInfo(KSTARS_EKOS_GUIDE) << text; emit newLog(text); } void Guide::clearLog() { m_LogText.clear(); emit newLog(QString()); } void Guide::setDECSwap(bool enable) { if (ST4Driver == nullptr || guider == nullptr) return; if (guiderType == GUIDE_INTERNAL) { dynamic_cast(guider)->setDECSwap(enable); ST4Driver->setDECSwap(enable); } } bool Guide::sendPulse(GuideDirection ra_dir, int ra_msecs, GuideDirection dec_dir, int dec_msecs) { if (GuideDriver == nullptr || (ra_dir == NO_DIR && dec_dir == NO_DIR)) return false; if (state == GUIDE_CALIBRATING) pulseTimer.start((ra_msecs > dec_msecs ? ra_msecs : dec_msecs) + 100); return GuideDriver->doPulse(ra_dir, ra_msecs, dec_dir, dec_msecs); } bool Guide::sendPulse(GuideDirection dir, int msecs) { if (GuideDriver == nullptr || dir == NO_DIR) return false; if (state == GUIDE_CALIBRATING) pulseTimer.start(msecs + 100); return GuideDriver->doPulse(dir, msecs); } QStringList Guide::getST4Devices() { QStringList devices; foreach (ISD::ST4 *driver, ST4List) devices << driver->getDeviceName(); return devices; } #if 0 void Guide::processRapidStarData(ISD::CCDChip * targetChip, double dx, double dy, double fit) { // Check if guide star is lost if (dx == -1 && dy == -1 && fit == -1) { KSNotification::error(i18n("Lost track of the guide star. Rapid guide aborted.")); guider->abort(); return; } FITSView * targetImage = targetChip->getImage(FITS_GUIDE); if (targetImage == nullptr) { pmath->setImageView(nullptr); guider->setImageView(nullptr); calibration->setImageView(nullptr); } if (rapidGuideReticleSet == false) { // Let's set reticle parameter on first capture to those of the star, then we check if there // is any set double x, y, angle; pmath->getReticleParameters(&x, &y, &angle); pmath->setReticleParameters(dx, dy, angle); rapidGuideReticleSet = true; } pmath->setRapidStarData(dx, dy); if (guider->isDithering()) { pmath->performProcessing(); if (guider->dither() == false) { appendLogText(i18n("Dithering failed. Autoguiding aborted.")); emit newStatus(GUIDE_DITHERING_ERROR); guider->abort(); //emit ditherFailed(); } } else { guider->guide(); capture(); } } void Guide::startRapidGuide() { ISD::CCDChip * targetChip = currentCCD->getChip(useGuideHead ? ISD::CCDChip::GUIDE_CCD : ISD::CCDChip::PRIMARY_CCD); if (currentCCD->setRapidGuide(targetChip, true) == false) { appendLogText(i18n("The CCD does not support Rapid Guiding. Aborting...")); guider->abort(); return; } rapidGuideReticleSet = false; pmath->setRapidGuide(true); currentCCD->configureRapidGuide(targetChip, true); connect(currentCCD, SIGNAL(newGuideStarData(ISD::CCDChip*, double, double, double)), this, &Ekos::Guide::processRapidStarData(ISD::CCDChip *, double, double, double))); } void Guide::stopRapidGuide() { ISD::CCDChip * targetChip = currentCCD->getChip(useGuideHead ? ISD::CCDChip::GUIDE_CCD : ISD::CCDChip::PRIMARY_CCD); pmath->setRapidGuide(false); rapidGuideReticleSet = false; currentCCD->disconnect(SIGNAL(newGuideStarData(ISD::CCDChip*, double, double, double))); currentCCD->configureRapidGuide(targetChip, false, false, false); currentCCD->setRapidGuide(targetChip, false); } #endif bool Guide::calibrate() { // Set status to idle and let the operations change it as they get executed state = GUIDE_IDLE; emit newStatus(state); if (guiderType == GUIDE_INTERNAL) { ISD::CCDChip *targetChip = currentCCD->getChip(useGuideHead ? ISD::CCDChip::GUIDE_CCD : ISD::CCDChip::PRIMARY_CCD); if (frameSettings.contains(targetChip)) { targetChip->resetFrame(); int x, y, w, h; targetChip->getFrame(&x, &y, &w, &h); QVariantMap settings = frameSettings[targetChip]; settings["x"] = x; settings["y"] = y; settings["w"] = w; settings["h"] = h; frameSettings[targetChip] = settings; subFramed = false; } } saveSettings(); buildOperationStack(GUIDE_CALIBRATING); executeOperationStack(); qCDebug(KSTARS_EKOS_GUIDE) << "Starting calibration using CCD:" << currentCCD->getDeviceName() << "via" << ST4Combo->currentText(); return true; } bool Guide::guide() { auto executeGuide = [this]() { if(guiderType != GUIDE_PHD2) { if (calibrationComplete == false) { calibrate(); return; } } saveSettings(); guider->guide(); }; if (Options::defaultCaptureCCD() == guiderCombo->currentText()) { connect(KSMessageBox::Instance(), &KSMessageBox::accepted, this, [this, executeGuide]() { //QObject::disconnect(KSMessageBox::Instance(), &KSMessageBox::accepted, this, nullptr); KSMessageBox::Instance()->disconnect(this); executeGuide(); }); KSMessageBox::Instance()->questionYesNo(i18n("The guide camera is identical to the primary imaging camera. Are you sure you want to continue?")); return false; } executeGuide(); return true; } bool Guide::dither() { if (Options::ditherNoGuiding() && state == GUIDE_IDLE) { ditherDirectly(); return true; } if (state == GUIDE_DITHERING || state == GUIDE_DITHERING_SETTLE) return true; //This adds a dither text item to the graph where dithering occurred. double time = guideTimer.elapsed() / 1000.0; QCPItemText *ditherLabel = new QCPItemText(driftGraph); ditherLabel->setPositionAlignment(Qt::AlignVCenter | Qt::AlignLeft); ditherLabel->position->setType(QCPItemPosition::ptPlotCoords); ditherLabel->position->setCoords(time, 1.5); ditherLabel->setColor(Qt::white); ditherLabel->setBrush(Qt::NoBrush); ditherLabel->setPen(Qt::NoPen); ditherLabel->setText("Dither"); ditherLabel->setFont(QFont(font().family(), 10)); if (guiderType == GUIDE_INTERNAL) { if (state != GUIDE_GUIDING) capture(); setStatus(GUIDE_DITHERING); return true; } else return guider->dither(Options::ditherPixels()); } bool Guide::suspend() { if (state == GUIDE_SUSPENDED) return true; else if (state >= GUIDE_CAPTURE) return guider->suspend(); else return false; } bool Guide::resume() { if (state == GUIDE_GUIDING) return true; else if (state == GUIDE_SUSPENDED) return guider->resume(); else return false; } void Guide::setCaptureStatus(CaptureState newState) { switch (newState) { case CAPTURE_DITHERING: dither(); break; default: break; } } void Guide::setPierSide(ISD::Telescope::PierSide newSide) { Q_UNUSED(newSide); // If pier side changes in internal guider // and calibration was already done // then let's swap if (guiderType == GUIDE_INTERNAL && state != GUIDE_GUIDING && state != GUIDE_CALIBRATING && calibrationComplete) { clearCalibration(); appendLogText(i18n("Pier side change detected. Clearing calibration.")); } } void Guide::setMountStatus(ISD::Telescope::Status newState) { // If we're guiding, and the mount either slews or parks, then we abort. if ((state == GUIDE_GUIDING || state == GUIDE_DITHERING) && (newState == ISD::Telescope::MOUNT_PARKING || newState == ISD::Telescope::MOUNT_SLEWING)) { if (newState == ISD::Telescope::MOUNT_PARKING) appendLogText(i18n("Mount is parking. Aborting guide...")); else appendLogText(i18n("Mount is slewing. Aborting guide...")); abort(); } if (guiderType != GUIDE_INTERNAL) return; switch (newState) { case ISD::Telescope::MOUNT_SLEWING: case ISD::Telescope::MOUNT_PARKING: case ISD::Telescope::MOUNT_MOVING: captureB->setEnabled(false); loopB->setEnabled(false); clearCalibrationB->setEnabled(false); break; default: if (pi->isAnimated() == false) { captureB->setEnabled(true); loopB->setEnabled(true); clearCalibrationB->setEnabled(true); } } } void Guide::setExposure(double value) { exposureIN->setValue(value); } void Guide::setImageFilter(const QString &value) { for (int i = 0; i < filterCombo->count(); i++) if (filterCombo->itemText(i) == value) { filterCombo->setCurrentIndex(i); break; } } void Guide::setCalibrationTwoAxis(bool enable) { Options::setTwoAxisEnabled(enable); } void Guide::setCalibrationAutoStar(bool enable) { autoStarCheck->setChecked(enable); } void Guide::setCalibrationAutoSquareSize(bool enable) { Options::setGuideAutoSquareSizeEnabled(enable); } void Guide::setCalibrationPulseDuration(int pulseDuration) { Options::setCalibrationPulseDuration(pulseDuration); } void Guide::setGuideBoxSizeIndex(int index) { Options::setGuideSquareSizeIndex(index); } void Guide::setGuideAlgorithmIndex(int index) { Options::setGuideAlgorithm(index); } void Guide::setSubFrameEnabled(bool enable) { Options::setGuideSubframeEnabled(enable); if (subFrameCheck->isChecked() != enable) subFrameCheck->setChecked(enable); } #if 0 void Guide::setGuideRapidEnabled(bool enable) { //guider->setGuideOptions(guider->getAlgorithm(), guider->useSubFrame() , enable); } #endif void Guide::setDitherSettings(bool enable, double value) { Options::setDitherEnabled(enable); Options::setDitherPixels(value); } #if 0 void Guide::startAutoCalibrateGuide() { // A must for auto stuff Options::setGuideAutoStarEnabled(true); if (Options::resetGuideCalibration()) clearCalibration(); guide(); #if 0 if (guiderType == GUIDE_INTERNAL) { calibrationComplete = false; autoCalibrateGuide = true; calibrate(); } else { calibrationComplete = true; autoCalibrateGuide = true; guide(); } #endif } #endif void Guide::clearCalibration() { calibrationComplete = false; guider->clearCalibration(); appendLogText(i18n("Calibration is cleared.")); } void Guide::setStatus(Ekos::GuideState newState) { if (newState == state) { // pass through the aborted state if (newState == GUIDE_ABORTED) emit newStatus(state); return; } GuideState previousState = state; state = newState; emit newStatus(state); switch (state) { case GUIDE_CONNECTED: appendLogText(i18n("External guider connected.")); externalConnectB->setEnabled(false); externalDisconnectB->setEnabled(true); captureB->setEnabled(false); loopB->setEnabled(false); clearCalibrationB->setEnabled(true); guideB->setEnabled(true); setBLOBEnabled(false); break; case GUIDE_DISCONNECTED: appendLogText(i18n("External guider disconnected.")); setBusy(false); //This needs to come before caputureB since it will set it to enabled again. externalConnectB->setEnabled(true); externalDisconnectB->setEnabled(false); clearCalibrationB->setEnabled(false); guideB->setEnabled(false); captureB->setEnabled(false); loopB->setEnabled(false); setBLOBEnabled(true); #ifdef Q_OS_OSX repaint(); //This is a band-aid for a bug in QT 5.10.0 #endif break; case GUIDE_CALIBRATION_SUCESS: appendLogText(i18n("Calibration completed.")); calibrationComplete = true; /*if (autoCalibrateGuide) { autoCalibrateGuide = false; guide(); } else setBusy(false);*/ if(guiderType != GUIDE_PHD2) //PHD2 will take care of this. If this command is executed for PHD2, it might start guiding when it is first connected, if the calibration was completed already. guide(); break; case GUIDE_IDLE: case GUIDE_CALIBRATION_ERROR: setBusy(false); manualDitherB->setEnabled(false); break; case GUIDE_CALIBRATING: appendLogText(i18n("Calibration started.")); setBusy(true); break; case GUIDE_GUIDING: if (previousState == GUIDE_SUSPENDED || previousState == GUIDE_DITHERING_SUCCESS) appendLogText(i18n("Guiding resumed.")); else { appendLogText(i18n("Autoguiding started.")); setBusy(true); clearGuideGraphs(); guideTimer = QTime::currentTime(); refreshColorScheme(); } manualDitherB->setEnabled(true); break; case GUIDE_ABORTED: appendLogText(i18n("Autoguiding aborted.")); setBusy(false); break; case GUIDE_SUSPENDED: appendLogText(i18n("Guiding suspended.")); break; case GUIDE_REACQUIRE: capture(); break; case GUIDE_MANUAL_DITHERING: appendLogText(i18n("Manual dithering in progress.")); break; case GUIDE_DITHERING: appendLogText(i18n("Dithering in progress.")); break; case GUIDE_DITHERING_SETTLE: if (Options::ditherSettle() > 0) appendLogText(i18np("Post-dither settling for %1 second...", "Post-dither settling for %1 seconds...", Options::ditherSettle())); capture(); break; case GUIDE_DITHERING_ERROR: appendLogText(i18n("Dithering failed.")); // LinGuider guide continue after dithering failure if (guiderType != GUIDE_LINGUIDER) { //state = GUIDE_IDLE; state = GUIDE_ABORTED; setBusy(false); } break; case GUIDE_DITHERING_SUCCESS: appendLogText(i18n("Dithering completed successfully.")); // Go back to guiding state immediately if using regular guider if (Options::ditherNoGuiding() == false) { setStatus(GUIDE_GUIDING); // Only capture again if we are using internal guider if (guiderType == GUIDE_INTERNAL) capture(); } break; default: break; } } void Guide::updateCCDBin(int index) { if (currentCCD == nullptr || guiderType != GUIDE_INTERNAL) return; ISD::CCDChip *targetChip = currentCCD->getChip(useGuideHead ? ISD::CCDChip::GUIDE_CCD : ISD::CCDChip::PRIMARY_CCD); targetChip->setBinning(index + 1, index + 1); QVariantMap settings = frameSettings[targetChip]; settings["binx"] = index + 1; settings["biny"] = index + 1; frameSettings[targetChip] = settings; guider->setFrameParams(settings["x"].toInt(), settings["y"].toInt(), settings["w"].toInt(), settings["h"].toInt(), settings["binx"].toInt(), settings["biny"].toInt()); } void Guide::processCCDNumber(INumberVectorProperty *nvp) { if (currentCCD == nullptr || strcmp(nvp->device, currentCCD->getDeviceName()) || guiderType != GUIDE_INTERNAL) return; if ((!strcmp(nvp->name, "CCD_BINNING") && useGuideHead == false) || (!strcmp(nvp->name, "GUIDER_BINNING") && useGuideHead)) { binningCombo->disconnect(); binningCombo->setCurrentIndex(nvp->np[0].value - 1); connect(binningCombo, static_cast(&QComboBox::activated), this, &Ekos::Guide::updateCCDBin); } } void Guide::checkExposureValue(ISD::CCDChip *targetChip, double exposure, IPState expState) { if (guiderType != GUIDE_INTERNAL) return; INDI_UNUSED(exposure); if (expState == IPS_ALERT && ((state == GUIDE_GUIDING) || (state == GUIDE_DITHERING) || (state == GUIDE_CALIBRATING))) { appendLogText(i18n("Exposure failed. Restarting exposure...")); currentCCD->setTransformFormat(ISD::CCD::FORMAT_FITS); targetChip->capture(exposureIN->value()); } } void Guide::setDarkFrameEnabled(bool enable) { Options::setGuideDarkFrameEnabled(enable); if (darkFrameCheck->isChecked() != enable) darkFrameCheck->setChecked(enable); } void Guide::saveDefaultGuideExposure() { Options::setGuideExposure(exposureIN->value()); if(guiderType == GUIDE_PHD2) phd2Guider->requestSetExposureTime(exposureIN->value() * 1000); } void Guide::setStarPosition(const QVector3D &newCenter, bool updateNow) { starCenter.setX(newCenter.x()); starCenter.setY(newCenter.y()); if (newCenter.z() > 0) starCenter.setZ(newCenter.z()); if (updateNow) syncTrackingBoxPosition(); } void Guide::syncTrackingBoxPosition() { ISD::CCDChip *targetChip = currentCCD->getChip(useGuideHead ? ISD::CCDChip::GUIDE_CCD : ISD::CCDChip::PRIMARY_CCD); Q_ASSERT(targetChip); int subBinX = 1, subBinY = 1; targetChip->getBinning(&subBinX, &subBinY); if (starCenter.isNull() == false) { double boxSize = boxSizeCombo->currentText().toInt(); int x, y, w, h; targetChip->getFrame(&x, &y, &w, &h); // If box size is larger than image size, set it to lower index if (boxSize / subBinX >= w || boxSize / subBinY >= h) { int newIndex = boxSizeCombo->currentIndex() - 1; if (newIndex >= 0) boxSizeCombo->setCurrentIndex(newIndex); return; } // If binning changed, update coords accordingly if (subBinX != starCenter.z()) { if (starCenter.z() > 0) { starCenter.setX(starCenter.x() * (starCenter.z() / subBinX)); starCenter.setY(starCenter.y() * (starCenter.z() / subBinY)); } starCenter.setZ(subBinX); } QRect starRect = QRect(starCenter.x() - boxSize / (2 * subBinX), starCenter.y() - boxSize / (2 * subBinY), boxSize / subBinX, boxSize / subBinY); guideView->setTrackingBoxEnabled(true); guideView->setTrackingBox(starRect); } } bool Guide::setGuiderType(int type) { // Use default guider option if (type == -1) type = Options::guiderType(); else if (type == guiderType) return true; if (state == GUIDE_CALIBRATING || state == GUIDE_GUIDING || state == GUIDE_DITHERING) { appendLogText(i18n("Cannot change guider type while active.")); return false; } if (guider != nullptr) { // Disconnect from host if (guider->isConnected()) guider->Disconnect(); // Disconnect signals guider->disconnect(); } guiderType = static_cast(type); switch (type) { case GUIDE_INTERNAL: { connect(internalGuider, SIGNAL(newPulse(GuideDirection, int)), this, SLOT(sendPulse(GuideDirection, int))); connect(internalGuider, SIGNAL(newPulse(GuideDirection, int, GuideDirection, int)), this, SLOT(sendPulse(GuideDirection, int, GuideDirection, int))); connect(internalGuider, SIGNAL(DESwapChanged(bool)), swapCheck, SLOT(setChecked(bool))); connect(internalGuider, SIGNAL(newStarPixmap(QPixmap &)), this, SIGNAL(newStarPixmap(QPixmap &))); guider = internalGuider; internalGuider->setSquareAlgorithm(opsGuide->kcfg_GuideAlgorithm->currentIndex()); internalGuider->setRegionAxis(opsGuide->kcfg_GuideRegionAxis->currentText().toInt()); clearCalibrationB->setEnabled(true); guideB->setEnabled(true); captureB->setEnabled(true); loopB->setEnabled(true); darkFrameCheck->setEnabled(true); subFrameCheck->setEnabled(true); autoStarCheck->setEnabled(true); guiderCombo->setEnabled(true); ST4Combo->setEnabled(true); exposureIN->setEnabled(true); binningCombo->setEnabled(true); boxSizeCombo->setEnabled(true); filterCombo->setEnabled(true); externalConnectB->setEnabled(false); externalDisconnectB->setEnabled(false); controlGroup->setEnabled(true); infoGroup->setEnabled(true); label_6->setEnabled(true); FOVScopeCombo->setEnabled(true); l_3->setEnabled(true); spinBox_GuideRate->setEnabled(true); l_RecommendedGain->setEnabled(true); l_5->setEnabled(true); l_6->setEnabled(true); l_7->setEnabled(true); l_8->setEnabled(true); l_Aperture->setEnabled(true); l_FOV->setEnabled(true); l_FbyD->setEnabled(true); l_Focal->setEnabled(true); driftGraphicsGroup->setEnabled(true); guiderCombo->setToolTip(i18n("Select guide camera.")); updateGuideParams(); } break; case GUIDE_PHD2: if (phd2Guider.isNull()) phd2Guider = new PHD2(); guider = phd2Guider; phd2Guider->setGuideView(guideView); connect(phd2Guider, SIGNAL(newStarPixmap(QPixmap &)), this, SIGNAL(newStarPixmap(QPixmap &))); clearCalibrationB->setEnabled(true); captureB->setEnabled(false); loopB->setEnabled(false); darkFrameCheck->setEnabled(false); subFrameCheck->setEnabled(false); autoStarCheck->setEnabled(false); guideB->setEnabled(false); //This will be enabled later when equipment connects (or not) externalConnectB->setEnabled(false); checkBox_DirRA->setEnabled(false); eastControlCheck->setEnabled(false); westControlCheck->setEnabled(false); swapCheck->setEnabled(false); controlGroup->setEnabled(false); infoGroup->setEnabled(true); label_6->setEnabled(false); FOVScopeCombo->setEnabled(false); l_3->setEnabled(false); spinBox_GuideRate->setEnabled(false); l_RecommendedGain->setEnabled(false); l_5->setEnabled(false); l_6->setEnabled(false); l_7->setEnabled(false); l_8->setEnabled(false); l_Aperture->setEnabled(false); l_FOV->setEnabled(false); l_FbyD->setEnabled(false); l_Focal->setEnabled(false); driftGraphicsGroup->setEnabled(true); ST4Combo->setEnabled(false); exposureIN->setEnabled(true); binningCombo->setEnabled(false); boxSizeCombo->setEnabled(false); filterCombo->setEnabled(false); if (Options::guideRemoteImagesEnabled() == false) { //guiderCombo->setCurrentIndex(-1); guiderCombo->setToolTip(i18n("Select a camera to disable remote streaming.")); } else guiderCombo->setEnabled(false); if (Options::resetGuideCalibration()) appendLogText(i18n("Warning: Reset Guiding Calibration is enabled. It is recommended to turn this option off for PHD2.")); updateGuideParams(); break; case GUIDE_LINGUIDER: if (linGuider.isNull()) linGuider = new LinGuider(); guider = linGuider; clearCalibrationB->setEnabled(true); captureB->setEnabled(false); loopB->setEnabled(false); darkFrameCheck->setEnabled(false); subFrameCheck->setEnabled(false); autoStarCheck->setEnabled(false); guideB->setEnabled(true); externalConnectB->setEnabled(true); controlGroup->setEnabled(false); infoGroup->setEnabled(false); driftGraphicsGroup->setEnabled(false); ST4Combo->setEnabled(false); exposureIN->setEnabled(false); binningCombo->setEnabled(false); boxSizeCombo->setEnabled(false); filterCombo->setEnabled(false); if (Options::guideRemoteImagesEnabled() == false) { guiderCombo->setCurrentIndex(-1); guiderCombo->setToolTip(i18n("Select a camera to disable remote streaming.")); } else guiderCombo->setEnabled(false); updateGuideParams(); break; } if (guider != nullptr) { connect(guider, &Ekos::GuideInterface::frameCaptureRequested, this, &Ekos::Guide::capture); connect(guider, &Ekos::GuideInterface::newLog, this, &Ekos::Guide::appendLogText); connect(guider, &Ekos::GuideInterface::newStatus, this, &Ekos::Guide::setStatus); connect(guider, &Ekos::GuideInterface::newStarPosition, this, &Ekos::Guide::setStarPosition); connect(guider, &Ekos::GuideInterface::newAxisDelta, this, &Ekos::Guide::setAxisDelta); connect(guider, &Ekos::GuideInterface::newAxisPulse, this, &Ekos::Guide::setAxisPulse); connect(guider, &Ekos::GuideInterface::newAxisSigma, this, &Ekos::Guide::setAxisSigma); } externalConnectB->setEnabled(false); externalDisconnectB->setEnabled(false); if (guider != nullptr && guiderType != GUIDE_INTERNAL) { externalConnectB->setEnabled(!guider->isConnected()); externalDisconnectB->setEnabled(guider->isConnected()); } if (guider != nullptr) guider->Connect(); return true; } void Guide::updateTrackingBoxSize(int currentIndex) { if (currentIndex >= 0) { Options::setGuideSquareSizeIndex(currentIndex); if (guiderType == GUIDE_INTERNAL) dynamic_cast(guider)->setGuideBoxSize(boxSizeCombo->currentText().toInt()); syncTrackingBoxPosition(); } } /* void Guide::onXscaleChanged( int i ) { int rx, ry; driftGraphics->getVisibleRanges( &rx, &ry ); driftGraphics->setVisibleRanges( i*driftGraphics->getGridN(), ry ); driftGraphics->update(); } void Guide::onYscaleChanged( int i ) { int rx, ry; driftGraphics->getVisibleRanges( &rx, &ry ); driftGraphics->setVisibleRanges( rx, i*driftGraphics->getGridN() ); driftGraphics->update(); } */ void Guide::onThresholdChanged(int index) { switch (guiderType) { case GUIDE_INTERNAL: dynamic_cast(guider)->setSquareAlgorithm(index); break; default: break; } } void Guide::onInfoRateChanged(double val) { Options::setGuidingRate(val); double gain = 0; if (val > 0.01) gain = 1000.0 / (val * 15.0); l_RecommendedGain->setText(i18n("P: %1", QString().setNum(gain, 'f', 2))); } void Guide::onEnableDirRA(bool enable) { Options::setRAGuideEnabled(enable); } void Guide::onEnableDirDEC(bool enable) { Options::setDECGuideEnabled(enable); updatePHD2Directions(); } void Guide::syncSettings() { QSpinBox *pSB = nullptr; QDoubleSpinBox *pDSB = nullptr; QCheckBox *pCB = nullptr; QObject *obj = sender(); if ((pSB = qobject_cast(obj))) { if (pSB == spinBox_MaxPulseRA) Options::setRAMaximumPulse(pSB->value()); else if (pSB == spinBox_MaxPulseDEC) Options::setDECMaximumPulse(pSB->value()); else if (pSB == spinBox_MinPulseRA) Options::setRAMinimumPulse(pSB->value()); else if (pSB == spinBox_MinPulseDEC) Options::setDECMinimumPulse(pSB->value()); } else if ((pDSB = qobject_cast(obj))) { if (pDSB == spinBox_PropGainRA) Options::setRAProportionalGain(pDSB->value()); else if (pDSB == spinBox_PropGainDEC) Options::setDECProportionalGain(pDSB->value()); else if (pDSB == spinBox_IntGainRA) Options::setRAIntegralGain(pDSB->value()); else if (pDSB == spinBox_IntGainDEC) Options::setDECIntegralGain(pDSB->value()); else if (pDSB == spinBox_DerGainRA) Options::setRADerivativeGain(pDSB->value()); else if (pDSB == spinBox_DerGainDEC) Options::setDECDerivativeGain(pDSB->value()); } else if ((pCB = qobject_cast(obj))) { if (pCB == autoStarCheck) Options::setGuideAutoStarEnabled(pCB->isChecked()); } } void Guide::onControlDirectionChanged(bool enable) { QObject *obj = sender(); if (northControlCheck == dynamic_cast(obj)) { Options::setNorthDECGuideEnabled(enable); updatePHD2Directions(); } else if (southControlCheck == dynamic_cast(obj)) { Options::setSouthDECGuideEnabled(enable); updatePHD2Directions(); } else if (westControlCheck == dynamic_cast(obj)) { Options::setWestRAGuideEnabled(enable); } else if (eastControlCheck == dynamic_cast(obj)) { Options::setEastRAGuideEnabled(enable); } } void Guide::updatePHD2Directions() { if(guiderType == GUIDE_PHD2) phd2Guider -> requestSetDEGuideMode(checkBox_DirDEC->isChecked(), northControlCheck->isChecked(), southControlCheck->isChecked()); } void Guide::updateDirectionsFromPHD2(QString mode) { //disable connections disconnect(checkBox_DirDEC, &QCheckBox::toggled, this, &Ekos::Guide::onEnableDirDEC); disconnect(northControlCheck, &QCheckBox::toggled, this, &Ekos::Guide::onControlDirectionChanged); disconnect(southControlCheck, &QCheckBox::toggled, this, &Ekos::Guide::onControlDirectionChanged); if(mode == "Auto") { checkBox_DirDEC->setChecked(true); northControlCheck->setChecked(true); southControlCheck->setChecked(true); } else if(mode == "North") { checkBox_DirDEC->setChecked(true); northControlCheck->setChecked(true); southControlCheck->setChecked(false); } else if(mode == "South") { checkBox_DirDEC->setChecked(true); northControlCheck->setChecked(false); southControlCheck->setChecked(true); } else //Off { checkBox_DirDEC->setChecked(false); northControlCheck->setChecked(true); southControlCheck->setChecked(true); } //Re-enable connections connect(checkBox_DirDEC, &QCheckBox::toggled, this, &Ekos::Guide::onEnableDirDEC); connect(northControlCheck, &QCheckBox::toggled, this, &Ekos::Guide::onControlDirectionChanged); connect(southControlCheck, &QCheckBox::toggled, this, &Ekos::Guide::onControlDirectionChanged); } #if 0 void Guide::onRapidGuideChanged(bool enable) { if (m_isStarted) { guideModule->appendLogText(i18n("You must stop auto guiding before changing this setting.")); return; } m_useRapidGuide = enable; if (m_useRapidGuide) { guideModule->appendLogText(i18n("Rapid Guiding is enabled. Guide star will be determined automatically by the CCD driver. No frames are sent to Ekos unless explicitly enabled by the user in the CCD driver settings.")); } else guideModule->appendLogText(i18n("Rapid Guiding is disabled.")); } #endif void Guide::loadSettings() { // Exposure exposureIN->setValue(Options::guideExposure()); // Box Size boxSizeCombo->setCurrentIndex(Options::guideSquareSizeIndex()); // Dark frame? darkFrameCheck->setChecked(Options::guideDarkFrameEnabled()); // Subframed? subFrameCheck->setChecked(Options::guideSubframeEnabled()); // Guiding Rate spinBox_GuideRate->setValue(Options::guidingRate()); // RA/DEC enabled? checkBox_DirRA->setChecked(Options::rAGuideEnabled()); checkBox_DirDEC->setChecked(Options::dECGuideEnabled()); // N/S enabled? northControlCheck->setChecked(Options::northDECGuideEnabled()); southControlCheck->setChecked(Options::southDECGuideEnabled()); // W/E enabled? westControlCheck->setChecked(Options::westRAGuideEnabled()); eastControlCheck->setChecked(Options::eastRAGuideEnabled()); // PID Control - Proportional Gain spinBox_PropGainRA->setValue(Options::rAProportionalGain()); spinBox_PropGainDEC->setValue(Options::dECProportionalGain()); // PID Control - Integral Gain spinBox_IntGainRA->setValue(Options::rAIntegralGain()); spinBox_IntGainDEC->setValue(Options::dECIntegralGain()); // PID Control - Derivative Gain spinBox_DerGainRA->setValue(Options::rADerivativeGain()); spinBox_DerGainDEC->setValue(Options::dECDerivativeGain()); // Max Pulse Duration (ms) spinBox_MaxPulseRA->setValue(Options::rAMaximumPulse()); spinBox_MaxPulseDEC->setValue(Options::dECMaximumPulse()); // Min Pulse Duration (ms) spinBox_MinPulseRA->setValue(Options::rAMinimumPulse()); spinBox_MinPulseDEC->setValue(Options::dECMinimumPulse()); // Autostar autoStarCheck->setChecked(Options::guideAutoStarEnabled()); } void Guide::saveSettings() { // Exposure Options::setGuideExposure(exposureIN->value()); // Box Size Options::setGuideSquareSizeIndex(boxSizeCombo->currentIndex()); // Dark frame? Options::setGuideDarkFrameEnabled(darkFrameCheck->isChecked()); // Subframed? Options::setGuideSubframeEnabled(subFrameCheck->isChecked()); // Guiding Rate? Options::setGuidingRate(spinBox_GuideRate->value()); // RA/DEC enabled? Options::setRAGuideEnabled(checkBox_DirRA->isChecked()); Options::setDECGuideEnabled(checkBox_DirDEC->isChecked()); // N/S enabled? Options::setNorthDECGuideEnabled(northControlCheck->isChecked()); Options::setSouthDECGuideEnabled(southControlCheck->isChecked()); // W/E enabled? Options::setWestRAGuideEnabled(westControlCheck->isChecked()); Options::setEastRAGuideEnabled(eastControlCheck->isChecked()); // PID Control - Proportional Gain Options::setRAProportionalGain(spinBox_PropGainRA->value()); Options::setDECProportionalGain(spinBox_PropGainDEC->value()); // PID Control - Integral Gain Options::setRAIntegralGain(spinBox_IntGainRA->value()); Options::setDECIntegralGain(spinBox_IntGainDEC->value()); // PID Control - Derivative Gain Options::setRADerivativeGain(spinBox_DerGainRA->value()); Options::setDECDerivativeGain(spinBox_DerGainDEC->value()); // Max Pulse Duration (ms) Options::setRAMaximumPulse(spinBox_MaxPulseRA->value()); Options::setDECMaximumPulse(spinBox_MaxPulseDEC->value()); // Min Pulse Duration (ms) Options::setRAMinimumPulse(spinBox_MinPulseRA->value()); Options::setDECMinimumPulse(spinBox_MinPulseDEC->value()); } void Guide::setTrackingStar(int x, int y) { QVector3D newStarPosition(x, y, -1); setStarPosition(newStarPosition, true); /*if (state == GUIDE_STAR_SELECT) { guider->setStarPosition(newStarPosition); guider->calibrate(); }*/ if (operationStack.isEmpty() == false) executeOperationStack(); } void Guide::setAxisDelta(double ra, double de) { // Time since timer started. double key = guideTimer.elapsed() / 1000.0; ra = -ra; //The ra is backwards in sign from how it should be displayed on the graph. driftGraph->graph(0)->addData(key, ra); driftGraph->graph(1)->addData(key, de); int currentNumPoints = driftGraph->graph(0)->dataCount(); guideSlider->setMaximum(currentNumPoints); if(graphOnLatestPt) guideSlider->setValue(currentNumPoints); // Expand range if it doesn't fit already if (driftGraph->yAxis->range().contains(ra) == false) driftGraph->yAxis->setRange(-1.25 * ra, 1.25 * ra); if (driftGraph->yAxis->range().contains(de) == false) driftGraph->yAxis->setRange(-1.25 * de, 1.25 * de); // Show last 120 seconds //driftGraph->xAxis->setRange(key, 120, Qt::AlignRight); if(graphOnLatestPt) { driftGraph->xAxis->setRange(key, driftGraph->xAxis->range().size(), Qt::AlignRight); driftGraph->graph(2)->data()->clear(); //Clear highlighted RA point driftGraph->graph(3)->data()->clear(); //Clear highlighted DEC point driftGraph->graph(2)->addData(key, ra); //Set highlighted RA point to latest point driftGraph->graph(3)->addData(key, de); //Set highlighted DEC point to latest point } driftGraph->replot(); //Add to Drift Plot driftPlot->graph(0)->addData(ra, de); if(graphOnLatestPt) { driftPlot->graph(1)->data()->clear(); //Clear highlighted point driftPlot->graph(1)->addData(ra, de); //Set highlighted point to latest point } if (driftPlot->xAxis->range().contains(ra) == false || driftPlot->yAxis->range().contains(de) == false) { driftPlot->setBackground(QBrush(Qt::gray)); QTimer::singleShot(300, this, [ = ]() { driftPlot->setBackground(QBrush(Qt::black)); driftPlot->replot(); }); } driftPlot->replot(); l_DeltaRA->setText(QString::number(ra, 'f', 2)); l_DeltaDEC->setText(QString::number(de, 'f', 2)); emit newAxisDelta(ra, de); profilePixmap = driftGraph->grab(); emit newProfilePixmap(profilePixmap); } void Guide::setAxisSigma(double ra, double de) { l_ErrRA->setText(QString::number(ra, 'f', 2)); l_ErrDEC->setText(QString::number(de, 'f', 2)); l_TotalRMS->setText(QString::number(sqrt(ra * ra + de * de), 'f', 2)); emit newAxisSigma(ra, de); } QList Guide::axisDelta() { QList delta; delta << l_DeltaRA->text().toDouble() << l_DeltaDEC->text().toDouble(); return delta; } QList Guide::axisSigma() { QList sigma; sigma << l_ErrRA->text().toDouble() << l_ErrDEC->text().toDouble(); return sigma; } void Guide::setAxisPulse(double ra, double de) { l_PulseRA->setText(QString::number(static_cast(ra))); l_PulseDEC->setText(QString::number(static_cast(de))); double key = guideTimer.elapsed() / 1000.0; driftGraph->graph(4)->addData(key, ra); driftGraph->graph(5)->addData(key, de); } void Guide::refreshColorScheme() { // Drift color legend if (driftGraph) { if (driftGraph->graph(0) && driftGraph->graph(1) && driftGraph->graph(2) && driftGraph->graph(3) && driftGraph->graph(4) && driftGraph->graph(5)) { driftGraph->graph(0)->setPen(QPen(KStarsData::Instance()->colorScheme()->colorNamed("RAGuideError"))); driftGraph->graph(1)->setPen(QPen(KStarsData::Instance()->colorScheme()->colorNamed("DEGuideError"))); driftGraph->graph(2)->setPen(QPen(KStarsData::Instance()->colorScheme()->colorNamed("RAGuideError"))); driftGraph->graph(2)->setScatterStyle(QCPScatterStyle(QCPScatterStyle::ssPlusCircle, QPen(KStarsData::Instance()->colorScheme()->colorNamed("RAGuideError"), 2), QBrush(), 10)); driftGraph->graph(3)->setPen(QPen(KStarsData::Instance()->colorScheme()->colorNamed("DEGuideError"))); driftGraph->graph(3)->setScatterStyle(QCPScatterStyle(QCPScatterStyle::ssPlusCircle, QPen(KStarsData::Instance()->colorScheme()->colorNamed("DEGuideError"), 2), QBrush(), 10)); QColor raPulseColor(KStarsData::Instance()->colorScheme()->colorNamed("RAGuideError")); raPulseColor.setAlpha(75); driftGraph->graph(4)->setPen(QPen(raPulseColor)); driftGraph->graph(4)->setBrush(QBrush(raPulseColor, Qt::Dense4Pattern)); QColor dePulseColor(KStarsData::Instance()->colorScheme()->colorNamed("DEGuideError")); dePulseColor.setAlpha(75); driftGraph->graph(5)->setPen(QPen(dePulseColor)); driftGraph->graph(5)->setBrush(QBrush(dePulseColor, Qt::Dense4Pattern)); } } } void Guide::driftMouseClicked(QMouseEvent *event) { if (event->buttons() & Qt::RightButton) { driftGraph->yAxis->setRange(-3, 3); } } void Guide::driftMouseOverLine(QMouseEvent *event) { double key = driftGraph->xAxis->pixelToCoord(event->localPos().x()); if (driftGraph->xAxis->range().contains(key)) { QCPGraph *graph = qobject_cast(driftGraph->plottableAt(event->pos(), false)); if (graph) { int raIndex = driftGraph->graph(0)->findBegin(key); int deIndex = driftGraph->graph(1)->findBegin(key); double raDelta = driftGraph->graph(0)->dataMainValue(raIndex); double deDelta = driftGraph->graph(1)->dataMainValue(deIndex); double raPulse = driftGraph->graph(4)->dataMainValue(raIndex); //Get RA Pulse from RA pulse data double dePulse = driftGraph->graph(5)->dataMainValue(deIndex); //Get DEC Pulse from DEC pulse data // Compute time value: QTime localTime = guideTimer; localTime = localTime.addSecs(key); QToolTip::hideText(); if(raPulse == 0 && dePulse == 0) { QToolTip::showText( event->globalPos(), i18nc("Drift graphics tooltip; %1 is local time; %2 is RA deviation; %3 is DE deviation in arcseconds;", "" "" "" "" "
LT: %1
RA: %2 \"
DE: %3 \"
", localTime.toString("hh:mm:ss AP"), QString::number(raDelta, 'f', 2), QString::number(deDelta, 'f', 2))); } else { QToolTip::showText( event->globalPos(), i18nc("Drift graphics tooltip; %1 is local time; %2 is RA deviation; %3 is DE deviation in arcseconds; %4 is RA Pulse in ms; %5 is DE Pulse in ms", "" "" "" "" "" "" "
LT: %1
RA: %2 \"
DE: %3 \"
RA Pulse: %4 ms
DE Pulse: %5 ms
", localTime.toString("hh:mm:ss AP"), QString::number(raDelta, 'f', 2), QString::number(deDelta, 'f', 2), QString::number(raPulse, 'f', 2), QString::number(dePulse, 'f', 2))); //The pulses were divided by 100 before they were put on the graph. } } else QToolTip::hideText(); driftGraph->replot(); } } void Guide::buildOperationStack(GuideState operation) { operationStack.clear(); switch (operation) { case GUIDE_CAPTURE: if (Options::guideDarkFrameEnabled()) operationStack.push(GUIDE_DARK); operationStack.push(GUIDE_CAPTURE); operationStack.push(GUIDE_SUBFRAME); break; case GUIDE_CALIBRATING: operationStack.push(GUIDE_CALIBRATING); if (guiderType == GUIDE_INTERNAL) { if (Options::guideDarkFrameEnabled()) operationStack.push(GUIDE_DARK); // Auto Star Selected Path if (Options::guideAutoStarEnabled()) { // If subframe is enabled and we need to auto select a star, then we need to make the final capture // of the subframed image. This is only done if we aren't already subframed. if (subFramed == false && Options::guideSubframeEnabled()) operationStack.push(GUIDE_CAPTURE); // Do not subframe and auto-select star on Image Guiding mode if (Options::imageGuidingEnabled() == false) { operationStack.push(GUIDE_SUBFRAME); operationStack.push(GUIDE_STAR_SELECT); } operationStack.push(GUIDE_CAPTURE); // If we are being ask to go full frame, let's do that first if (subFramed == true && Options::guideSubframeEnabled() == false) operationStack.push(GUIDE_SUBFRAME); } // Manual Star Selection Path else { // In Image Guiding, we never need to subframe if (Options::imageGuidingEnabled() == false) { // Final capture before we start calibrating if (subFramed == false && Options::guideSubframeEnabled()) operationStack.push(GUIDE_CAPTURE); // Subframe if required operationStack.push(GUIDE_SUBFRAME); } // First capture an image operationStack.push(GUIDE_CAPTURE); } } break; default: break; } } bool Guide::executeOperationStack() { if (operationStack.isEmpty()) return false; GuideState nextOperation = operationStack.pop(); bool actionRequired = false; switch (nextOperation) { case GUIDE_SUBFRAME: actionRequired = executeOneOperation(nextOperation); break; case GUIDE_DARK: actionRequired = executeOneOperation(nextOperation); break; case GUIDE_CAPTURE: actionRequired = captureOneFrame(); break; case GUIDE_STAR_SELECT: actionRequired = executeOneOperation(nextOperation); break; case GUIDE_CALIBRATING: if (guiderType == GUIDE_INTERNAL) { guider->setStarPosition(starCenter); dynamic_cast(guider)->setImageGuideEnabled(Options::imageGuidingEnabled()); // No need to calibrate if (Options::imageGuidingEnabled()) { setStatus(GUIDE_CALIBRATION_SUCESS); break; } // Tracking must be engaged if (currentTelescope && currentTelescope->canControlTrack() && currentTelescope->isTracking() == false) currentTelescope->setTrackEnabled(true); } if (guider->calibrate()) { if (guiderType == GUIDE_INTERNAL) disconnect(guideView, SIGNAL(trackingStarSelected(int, int)), this, SLOT(setTrackingStar(int, int))); setBusy(true); } else { emit newStatus(GUIDE_CALIBRATION_ERROR); state = GUIDE_IDLE; appendLogText(i18n("Calibration failed to start.")); setBusy(false); } break; default: break; } // If an additional action is required, return return and continue later if (actionRequired) return true; // Otherwise, continue processing the stack else return executeOperationStack(); } bool Guide::executeOneOperation(GuideState operation) { bool actionRequired = false; ISD::CCDChip *targetChip = currentCCD->getChip(useGuideHead ? ISD::CCDChip::GUIDE_CCD : ISD::CCDChip::PRIMARY_CCD); int subBinX, subBinY; targetChip->getBinning(&subBinX, &subBinY); switch (operation) { case GUIDE_SUBFRAME: { // Check if we need and can subframe if (subFramed == false && Options::guideSubframeEnabled() == true && targetChip->canSubframe()) { int minX, maxX, minY, maxY, minW, maxW, minH, maxH; targetChip->getFrameMinMax(&minX, &maxX, &minY, &maxY, &minW, &maxW, &minH, &maxH); int offset = boxSizeCombo->currentText().toInt() / subBinX; int x = starCenter.x(); int y = starCenter.y(); x = (x - offset * 2) * subBinX; y = (y - offset * 2) * subBinY; int w = offset * 4 * subBinX; int h = offset * 4 * subBinY; if (x < minX) x = minX; if (y < minY) y = minY; if ((x + w) > maxW) w = maxW - x; if ((y + h) > maxH) h = maxH - y; targetChip->setFrame(x, y, w, h); subFramed = true; QVariantMap settings = frameSettings[targetChip]; settings["x"] = x; settings["y"] = y; settings["w"] = w; settings["h"] = h; settings["binx"] = subBinX; settings["biny"] = subBinY; frameSettings[targetChip] = settings; starCenter.setX(w / (2 * subBinX)); starCenter.setY(h / (2 * subBinX)); } // Otherwise check if we are already subframed // and we need to go back to full frame // or if we need to go back to full frame since we need // to reaquire a star else if (subFramed && (Options::guideSubframeEnabled() == false || state == GUIDE_REACQUIRE)) { targetChip->resetFrame(); int x, y, w, h; targetChip->getFrame(&x, &y, &w, &h); QVariantMap settings; settings["x"] = x; settings["y"] = y; settings["w"] = w; settings["h"] = h; settings["binx"] = 1; settings["biny"] = 1; frameSettings[targetChip] = settings; subFramed = false; starCenter.setX(w / (2 * subBinX)); starCenter.setY(h / (2 * subBinX)); //starCenter.setX(0); //starCenter.setY(0); } } break; case GUIDE_DARK: { // Do we need to take a dark frame? if (Options::guideDarkFrameEnabled()) { QVariantMap settings = frameSettings[targetChip]; uint16_t offsetX = settings["x"].toInt() / settings["binx"].toInt(); uint16_t offsetY = settings["y"].toInt() / settings["biny"].toInt(); FITSData *darkData = DarkLibrary::Instance()->getDarkFrame(targetChip, exposureIN->value()); connect(DarkLibrary::Instance(), &DarkLibrary::darkFrameCompleted, this, [&](bool completed) { DarkLibrary::Instance()->disconnect(this); if (completed != darkFrameCheck->isChecked()) setDarkFrameEnabled(completed); if (completed) setCaptureComplete(); else abort(); }); connect(DarkLibrary::Instance(), &DarkLibrary::newLog, this, &Ekos::Guide::appendLogText); actionRequired = true; targetChip->setCaptureFilter(static_cast(filterCombo->currentIndex())); if (darkData) DarkLibrary::Instance()->subtract(darkData, guideView, targetChip->getCaptureFilter(), offsetX, offsetY); else { DarkLibrary::Instance()->captureAndSubtract(targetChip, guideView, exposureIN->value(), offsetX, offsetY); } } } break; case GUIDE_STAR_SELECT: { state = GUIDE_STAR_SELECT; emit newStatus(state); if (Options::guideAutoStarEnabled()) { bool autoStarCaptured = internalGuider->selectAutoStar(); if (autoStarCaptured) { appendLogText(i18n("Auto star selected.")); } else { appendLogText(i18n("Failed to select an auto star.")); actionRequired = true; state = GUIDE_CALIBRATION_ERROR; emit newStatus(state); setBusy(false); } } else { appendLogText(i18n("Select a guide star to calibrate.")); actionRequired = true; } } break; default: break; } return actionRequired; } void Guide::processGuideOptions() { if (Options::guiderType() != guiderType) { guiderType = static_cast(Options::guiderType()); setGuiderType(Options::guiderType()); } } void Guide::showFITSViewer() { FITSData *data = guideView->getImageData(); if (data) { QUrl url = QUrl::fromLocalFile(data->filename()); if (fv.isNull()) { if (Options::singleWindowCapturedFITS()) fv = KStars::Instance()->genericFITSViewer(); else { fv = new FITSViewer(Options::independentWindowFITS() ? nullptr : KStars::Instance()); KStars::Instance()->addFITSViewer(fv); } fv->addFITS(url); FITSView *currentView = fv->getCurrentView(); if (currentView) currentView->getImageData()->setAutoRemoveTemporaryFITS(false); } else fv->updateFITS(url, 0); fv->show(); } } void Guide::setBLOBEnabled(bool enable, const QString &ccd) { // Nothing to do if guider is international or remote images are enabled if (guiderType == GUIDE_INTERNAL || Options::guideRemoteImagesEnabled()) return; // If guider is external and remote images option is disabled AND BLOB is enabled, then we disabled it foreach(ISD::CCD *oneCCD, CCDs) { // If it's not the desired CCD, continue. if (ccd.isEmpty() == false && QString(oneCCD->getDeviceName()) != ccd) continue; if (enable == false && oneCCD->isBLOBEnabled()) { appendLogText(i18n("Disabling remote image reception from %1", oneCCD->getDeviceName())); oneCCD->setBLOBEnabled(enable); } // Re-enable BLOB reception if it was disabled before when using external guiders else if (enable && oneCCD->isBLOBEnabled() == false) { appendLogText(i18n("Enabling remote image reception from %1", oneCCD->getDeviceName())); oneCCD->setBLOBEnabled(enable); } } } void Guide::ditherDirectly() { double ditherPulse = Options::ditherNoGuidingPulse(); // Randomize pulse length. It is equal to 50% of pulse length + random value up to 50% // e.g. if ditherPulse is 500ms then final pulse is = 250 + rand(0 to 250) int ra_msec = static_cast((static_cast(rand()) / RAND_MAX) * ditherPulse / 2.0 + ditherPulse / 2.0); int ra_polarity = (rand() % 2 == 0) ? 1 : -1; int de_msec = static_cast((static_cast(rand()) / RAND_MAX) * ditherPulse / 2.0 + ditherPulse / 2.0); int de_polarity = (rand() % 2 == 0) ? 1 : -1; qCInfo(KSTARS_EKOS_GUIDE) << "Starting non-guiding dither..."; qCDebug(KSTARS_EKOS_GUIDE) << "dither ra_msec:" << ra_msec << "ra_polarity:" << ra_polarity << "de_msec:" << de_msec << "de_polarity:" << de_polarity; bool rc = sendPulse(ra_polarity > 0 ? RA_INC_DIR : RA_DEC_DIR, ra_msec, de_polarity > 0 ? DEC_INC_DIR : DEC_DEC_DIR, de_msec); if (rc) { qCInfo(KSTARS_EKOS_GUIDE) << "Non-guiding dither successful."; QTimer::singleShot( (ra_msec > de_msec ? ra_msec : de_msec) + Options::ditherSettle() * 1000 + 100, [this]() { emit newStatus(GUIDE_DITHERING_SUCCESS); state = GUIDE_IDLE; }); } else { qCWarning(KSTARS_EKOS_GUIDE) << "Non-guiding dither failed."; emit newStatus(GUIDE_DITHERING_ERROR); state = GUIDE_IDLE; } } void Guide::updateTelescopeType(int index) { if (currentCCD == nullptr) return; focal_length = (index == ISD::CCD::TELESCOPE_PRIMARY) ? primaryFL : guideFL; aperture = (index == ISD::CCD::TELESCOPE_PRIMARY) ? primaryAperture : guideAperture; Options::setGuideScopeType(index); syncTelescopeInfo(); } void Guide::setDefaultST4(const QString &driver) { Options::setDefaultST4Driver(driver); } void Guide::setDefaultCCD(const QString &ccd) { if (guiderType == GUIDE_INTERNAL) Options::setDefaultGuideCCD(ccd); else if (ccd.isEmpty() == false) { QString ccdName = ccd; ccdName = ccdName.remove(" Guider"); setBLOBEnabled(Options::guideRemoteImagesEnabled(), ccdName); } } void Guide::handleManualDither() { ISD::CCDChip *targetChip = currentCCD->getChip(useGuideHead ? ISD::CCDChip::GUIDE_CCD : ISD::CCDChip::PRIMARY_CCD); if (targetChip == nullptr) return; Ui::ManualDither ditherDialog; QDialog container(this); ditherDialog.setupUi(&container); if (guiderType != GUIDE_INTERNAL) { ditherDialog.coordinatesR->setEnabled(false); ditherDialog.x->setEnabled(false); ditherDialog.y->setEnabled(false); } int minX, maxX, minY, maxY, minW, maxW, minH, maxH; targetChip->getFrameMinMax(&minX, &maxX, &minY, &maxY, &minW, &maxW, &minH, &maxH); ditherDialog.x->setMinimum(minX); ditherDialog.x->setMaximum(maxX); ditherDialog.y->setMinimum(minY); ditherDialog.y->setMaximum(maxY); ditherDialog.x->setValue(starCenter.x()); ditherDialog.y->setValue(starCenter.y()); if (container.exec() == QDialog::Accepted) { if (ditherDialog.magnitudeR->isChecked()) guider->dither(ditherDialog.magnitude->value()); else { dynamic_cast(guider)->ditherXY(ditherDialog.x->value(), ditherDialog.y->value()); } } } bool Guide::connectGuider() { return guider->Connect(); } bool Guide::disconnectGuider() { return guider->Disconnect(); } void Guide::initPlots() { // Drift Graph Color Settings driftGraph->setBackground(QBrush(Qt::black)); driftGraph->xAxis->setBasePen(QPen(Qt::white, 1)); driftGraph->yAxis->setBasePen(QPen(Qt::white, 1)); driftGraph->xAxis->grid()->setPen(QPen(QColor(140, 140, 140), 1, Qt::DotLine)); driftGraph->yAxis->grid()->setPen(QPen(QColor(140, 140, 140), 1, Qt::DotLine)); driftGraph->xAxis->grid()->setSubGridPen(QPen(QColor(80, 80, 80), 1, Qt::DotLine)); driftGraph->yAxis->grid()->setSubGridPen(QPen(QColor(80, 80, 80), 1, Qt::DotLine)); driftGraph->xAxis->grid()->setZeroLinePen(Qt::NoPen); driftGraph->yAxis->grid()->setZeroLinePen(QPen(Qt::white, 1)); driftGraph->xAxis->setBasePen(QPen(Qt::white, 1)); driftGraph->yAxis->setBasePen(QPen(Qt::white, 1)); driftGraph->yAxis2->setBasePen(QPen(Qt::white, 1)); driftGraph->xAxis->setTickPen(QPen(Qt::white, 1)); driftGraph->yAxis->setTickPen(QPen(Qt::white, 1)); driftGraph->yAxis2->setTickPen(QPen(Qt::white, 1)); driftGraph->xAxis->setSubTickPen(QPen(Qt::white, 1)); driftGraph->yAxis->setSubTickPen(QPen(Qt::white, 1)); driftGraph->yAxis2->setSubTickPen(QPen(Qt::white, 1)); driftGraph->xAxis->setTickLabelColor(Qt::white); driftGraph->yAxis->setTickLabelColor(Qt::white); driftGraph->yAxis2->setTickLabelColor(Qt::white); driftGraph->xAxis->setLabelColor(Qt::white); driftGraph->yAxis->setLabelColor(Qt::white); driftGraph->yAxis2->setLabelColor(Qt::white); //Horizontal Axis Time Ticker Settings QSharedPointer timeTicker(new QCPAxisTickerTime); timeTicker->setTimeFormat("%m:%s"); driftGraph->xAxis->setTicker(timeTicker); //Vertical Axis Labels Settings driftGraph->yAxis2->setVisible(true); driftGraph->yAxis2->setTickLabels(true); driftGraph->yAxis->setLabelFont(QFont(font().family(), 10)); driftGraph->yAxis2->setLabelFont(QFont(font().family(), 10)); driftGraph->yAxis->setTickLabelFont(QFont(font().family(), 9)); driftGraph->yAxis2->setTickLabelFont(QFont(font().family(), 9)); driftGraph->yAxis->setLabelPadding(1); driftGraph->yAxis2->setLabelPadding(1); driftGraph->yAxis->setLabel(i18n("drift (arcsec)")); driftGraph->yAxis2->setLabel(i18n("pulse (ms)")); setupNSEWLabels(); //Sets the default ranges driftGraph->xAxis->setRange(0, 60, Qt::AlignRight); driftGraph->yAxis->setRange(-3, 3); int scale = 50; //This is a scaling value between the left and the right axes of the driftGraph, it could be stored in kstars kcfg correctionSlider->setValue(scale); driftGraph->yAxis2->setRange(-3 * scale, 3 * scale); //This sets up the legend driftGraph->legend->setVisible(true); driftGraph->legend->setFont(QFont("Helvetica", 9)); driftGraph->legend->setTextColor(Qt::white); driftGraph->legend->setBrush(QBrush(Qt::black)); driftGraph->legend->setFillOrder(QCPLegend::foColumnsFirst); driftGraph->axisRect()->insetLayout()->setInsetAlignment(0, Qt::AlignLeft | Qt::AlignBottom); // RA Curve driftGraph->addGraph(driftGraph->xAxis, driftGraph->yAxis); driftGraph->graph(0)->setPen(QPen(KStarsData::Instance()->colorScheme()->colorNamed("RAGuideError"))); driftGraph->graph(0)->setName("RA"); driftGraph->graph(0)->setLineStyle(QCPGraph::lsStepLeft); // DE Curve driftGraph->addGraph(driftGraph->xAxis, driftGraph->yAxis); driftGraph->graph(1)->setPen(QPen(KStarsData::Instance()->colorScheme()->colorNamed("DEGuideError"))); driftGraph->graph(1)->setName("DE"); driftGraph->graph(1)->setLineStyle(QCPGraph::lsStepLeft); // RA highlighted Point driftGraph->addGraph(driftGraph->xAxis, driftGraph->yAxis); driftGraph->graph(2)->setLineStyle(QCPGraph::lsNone); driftGraph->graph(2)->setPen(QPen(KStarsData::Instance()->colorScheme()->colorNamed("RAGuideError"))); driftGraph->graph(2)->setScatterStyle(QCPScatterStyle(QCPScatterStyle::ssPlusCircle, QPen(KStarsData::Instance()->colorScheme()->colorNamed("RAGuideError"), 2), QBrush(), 10)); // DE highlighted Point driftGraph->addGraph(driftGraph->xAxis, driftGraph->yAxis); driftGraph->graph(3)->setLineStyle(QCPGraph::lsNone); driftGraph->graph(3)->setPen(QPen(KStarsData::Instance()->colorScheme()->colorNamed("DEGuideError"))); driftGraph->graph(3)->setScatterStyle(QCPScatterStyle(QCPScatterStyle::ssPlusCircle, QPen(KStarsData::Instance()->colorScheme()->colorNamed("DEGuideError"), 2), QBrush(), 10)); // RA Pulse driftGraph->addGraph(driftGraph->xAxis, driftGraph->yAxis2); QColor raPulseColor(KStarsData::Instance()->colorScheme()->colorNamed("RAGuideError")); raPulseColor.setAlpha(75); driftGraph->graph(4)->setPen(QPen(raPulseColor)); driftGraph->graph(4)->setBrush(QBrush(raPulseColor, Qt::Dense4Pattern)); driftGraph->graph(4)->setName("RA Pulse"); driftGraph->graph(4)->setLineStyle(QCPGraph::lsStepLeft); // DEC Pulse driftGraph->addGraph(driftGraph->xAxis, driftGraph->yAxis2); QColor dePulseColor(KStarsData::Instance()->colorScheme()->colorNamed("DEGuideError")); dePulseColor.setAlpha(75); driftGraph->graph(5)->setPen(QPen(dePulseColor)); driftGraph->graph(5)->setBrush(QBrush(dePulseColor, Qt::Dense4Pattern)); driftGraph->graph(5)->setName("DEC Pulse"); driftGraph->graph(5)->setLineStyle(QCPGraph::lsStepLeft); //This will prevent the highlighted points and Pulses from showing up in the legend. driftGraph->legend->removeItem(5); driftGraph->legend->removeItem(4); driftGraph->legend->removeItem(3); driftGraph->legend->removeItem(2); //Dragging and zooming settings // make bottom axis transfer its range to the top axis if the graph gets zoomed: connect(driftGraph->xAxis, static_cast(&QCPAxis::rangeChanged), driftGraph->xAxis2, static_cast(&QCPAxis::setRange)); // update the second vertical axis properly if the graph gets zoomed. connect(driftGraph->yAxis, static_cast(&QCPAxis::rangeChanged), this, &Ekos::Guide::setCorrectionGraphScale); driftGraph->setInteractions(QCP::iRangeZoom); driftGraph->setInteraction(QCP::iRangeDrag, true); connect(driftGraph, &QCustomPlot::mouseMove, this, &Ekos::Guide::driftMouseOverLine); connect(driftGraph, &QCustomPlot::mousePress, this, &Ekos::Guide::driftMouseClicked); //drift plot double accuracyRadius = 2; driftPlot->setBackground(QBrush(Qt::black)); driftPlot->setSelectionTolerance(10); driftPlot->xAxis->setBasePen(QPen(Qt::white, 1)); driftPlot->yAxis->setBasePen(QPen(Qt::white, 1)); driftPlot->xAxis->setTickPen(QPen(Qt::white, 1)); driftPlot->yAxis->setTickPen(QPen(Qt::white, 1)); driftPlot->xAxis->setSubTickPen(QPen(Qt::white, 1)); driftPlot->yAxis->setSubTickPen(QPen(Qt::white, 1)); driftPlot->xAxis->setTickLabelColor(Qt::white); driftPlot->yAxis->setTickLabelColor(Qt::white); driftPlot->xAxis->setLabelColor(Qt::white); driftPlot->yAxis->setLabelColor(Qt::white); driftPlot->xAxis->setLabelFont(QFont(font().family(), 10)); driftPlot->yAxis->setLabelFont(QFont(font().family(), 10)); driftPlot->xAxis->setTickLabelFont(QFont(font().family(), 9)); driftPlot->yAxis->setTickLabelFont(QFont(font().family(), 9)); driftPlot->xAxis->setLabelPadding(2); driftPlot->yAxis->setLabelPadding(2); driftPlot->xAxis->grid()->setPen(QPen(QColor(140, 140, 140), 1, Qt::DotLine)); driftPlot->yAxis->grid()->setPen(QPen(QColor(140, 140, 140), 1, Qt::DotLine)); driftPlot->xAxis->grid()->setSubGridPen(QPen(QColor(80, 80, 80), 1, Qt::DotLine)); driftPlot->yAxis->grid()->setSubGridPen(QPen(QColor(80, 80, 80), 1, Qt::DotLine)); driftPlot->xAxis->grid()->setZeroLinePen(QPen(Qt::gray)); driftPlot->yAxis->grid()->setZeroLinePen(QPen(Qt::gray)); driftPlot->xAxis->setLabel(i18n("dRA (arcsec)")); driftPlot->yAxis->setLabel(i18n("dDE (arcsec)")); driftPlot->xAxis->setRange(-accuracyRadius * 3, accuracyRadius * 3); driftPlot->yAxis->setRange(-accuracyRadius * 3, accuracyRadius * 3); driftPlot->setInteractions(QCP::iRangeZoom); driftPlot->setInteraction(QCP::iRangeDrag, true); driftPlot->addGraph(); driftPlot->graph(0)->setLineStyle(QCPGraph::lsNone); driftPlot->graph(0)->setScatterStyle(QCPScatterStyle(QCPScatterStyle::ssStar, Qt::gray, 5)); driftPlot->addGraph(); driftPlot->graph(1)->setLineStyle(QCPGraph::lsNone); driftPlot->graph(1)->setScatterStyle(QCPScatterStyle(QCPScatterStyle::ssPlusCircle, QPen(Qt::yellow, 2), QBrush(), 10)); connect(rightLayout, &QSplitter::splitterMoved, this, &Ekos::Guide::handleVerticalPlotSizeChange); connect(driftSplitter, &QSplitter::splitterMoved, this, &Ekos::Guide::handleHorizontalPlotSizeChange); //This sets the values of all the Graph Options that are stored. accuracyRadiusSpin->setValue(Options::guiderAccuracyThreshold()); showRAPlotCheck->setChecked(Options::rADisplayedOnGuideGraph()); showDECPlotCheck->setChecked(Options::dEDisplayedOnGuideGraph()); showRACorrectionsCheck->setChecked(Options::rACorrDisplayedOnGuideGraph()); showDECorrectionsCheck->setChecked(Options::dECorrDisplayedOnGuideGraph()); //This sets the visibility of graph components to the stored values. driftGraph->graph(0)->setVisible(Options::rADisplayedOnGuideGraph()); //RA data driftGraph->graph(1)->setVisible(Options::dEDisplayedOnGuideGraph()); //DEC data driftGraph->graph(2)->setVisible(Options::rADisplayedOnGuideGraph()); //RA highlighted point driftGraph->graph(3)->setVisible(Options::dEDisplayedOnGuideGraph()); //DEC highlighted point driftGraph->graph(4)->setVisible(Options::rACorrDisplayedOnGuideGraph()); //RA Pulses driftGraph->graph(5)->setVisible(Options::dECorrDisplayedOnGuideGraph()); //DEC Pulses updateCorrectionsScaleVisibility(); driftPlot->resize(190, 190); driftPlot->replot(); buildTarget(); } void Guide::initView() { guideView = new FITSView(guideWidget, FITS_GUIDE); guideView->setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Expanding); guideView->setBaseSize(guideWidget->size()); guideView->createFloatingToolBar(); QVBoxLayout *vlayout = new QVBoxLayout(); vlayout->addWidget(guideView); guideWidget->setLayout(vlayout); connect(guideView, &FITSView::trackingStarSelected, this, &Ekos::Guide::setTrackingStar); } void Guide::initConnections() { // Exposure Timeout captureTimeout.setSingleShot(true); connect(&captureTimeout, &QTimer::timeout, this, &Ekos::Guide::processCaptureTimeout); // Guiding Box Size connect(boxSizeCombo, static_cast(&QComboBox::currentIndexChanged), this, &Ekos::Guide::updateTrackingBoxSize); // Guider CCD Selection connect(guiderCombo, static_cast(&QComboBox::activated), this, &Ekos::Guide::setDefaultCCD); connect(guiderCombo, static_cast(&QComboBox::activated), this, [&](int index) { if (guiderType == GUIDE_INTERNAL) { starCenter = QVector3D(); checkCCD(index); } else if (index >= 0) { // Disable or enable selected CCD based on options QString ccdName = guiderCombo->currentText().remove(" Guider"); setBLOBEnabled(Options::guideRemoteImagesEnabled(), ccdName); checkCCD(index); } } ); FOVScopeCombo->setCurrentIndex(Options::guideScopeType()); connect(FOVScopeCombo, static_cast(&QComboBox::currentIndexChanged), this, &Ekos::Guide::updateTelescopeType); // Dark Frame Check connect(darkFrameCheck, &QCheckBox::toggled, this, &Ekos::Guide::setDarkFrameEnabled); // Subframe check connect(subFrameCheck, &QCheckBox::toggled, this, &Ekos::Guide::setSubFrameEnabled); // ST4 Selection connect(ST4Combo, static_cast(&QComboBox::activated), [&](const QString & text) { setDefaultST4(text); setST4(text); }); // Binning Combo Selection connect(binningCombo, static_cast(&QComboBox::currentIndexChanged), this, &Ekos::Guide::updateCCDBin); // RA/DEC Enable directions connect(checkBox_DirRA, &QCheckBox::toggled, this, &Ekos::Guide::onEnableDirRA); connect(checkBox_DirDEC, &QCheckBox::toggled, this, &Ekos::Guide::onEnableDirDEC); // N/W and W/E direction enable connect(northControlCheck, &QCheckBox::toggled, this, &Ekos::Guide::onControlDirectionChanged); connect(southControlCheck, &QCheckBox::toggled, this, &Ekos::Guide::onControlDirectionChanged); connect(westControlCheck, &QCheckBox::toggled, this, &Ekos::Guide::onControlDirectionChanged); connect(eastControlCheck, &QCheckBox::toggled, this, &Ekos::Guide::onControlDirectionChanged); // Auto star check connect(autoStarCheck, &QCheckBox::toggled, this, &Ekos::Guide::syncSettings); // Declination Swap connect(swapCheck, &QCheckBox::toggled, this, &Ekos::Guide::setDECSwap); // PID Control - Proportional Gain connect(spinBox_PropGainRA, &QSpinBox::editingFinished, this, &Ekos::Guide::syncSettings); connect(spinBox_PropGainDEC, &QSpinBox::editingFinished, this, &Ekos::Guide::syncSettings); // PID Control - Integral Gain connect(spinBox_IntGainRA, &QSpinBox::editingFinished, this, &Ekos::Guide::syncSettings); connect(spinBox_IntGainDEC, &QSpinBox::editingFinished, this, &Ekos::Guide::syncSettings); // PID Control - Derivative Gain connect(spinBox_DerGainRA, &QSpinBox::editingFinished, this, &Ekos::Guide::syncSettings); connect(spinBox_DerGainDEC, &QSpinBox::editingFinished, this, &Ekos::Guide::syncSettings); // Max Pulse Duration (ms) connect(spinBox_MaxPulseRA, &QSpinBox::editingFinished, this, &Ekos::Guide::syncSettings); connect(spinBox_MaxPulseDEC, &QSpinBox::editingFinished, this, &Ekos::Guide::syncSettings); // Min Pulse Duration (ms) connect(spinBox_MinPulseRA, &QSpinBox::editingFinished, this, &Ekos::Guide::syncSettings); connect(spinBox_MinPulseDEC, &QSpinBox::editingFinished, this, &Ekos::Guide::syncSettings); // Capture connect(captureB, &QPushButton::clicked, this, [this]() { state = GUIDE_CAPTURE; emit newStatus(state); capture(); }); connect(loopB, &QPushButton::clicked, this, [this]() { state = GUIDE_LOOPING; emit newStatus(state); capture(); }); // Stop connect(stopB, &QPushButton::clicked, this, &Ekos::Guide::abort); // Clear Calibrate //connect(calibrateB, &QPushButton::clicked, this, &Ekos::Guide::calibrate())); connect(clearCalibrationB, &QPushButton::clicked, this, &Ekos::Guide::clearCalibration); // Guide connect(guideB, &QPushButton::clicked, this, &Ekos::Guide::guide); // Connect External Guide connect(externalConnectB, &QPushButton::clicked, this, [&]() { setBLOBEnabled(false); guider->Connect(); }); connect(externalDisconnectB, &QPushButton::clicked, this, [&]() { setBLOBEnabled(true); guider->Disconnect(); }); // Pulse Timer pulseTimer.setSingleShot(true); connect(&pulseTimer, &QTimer::timeout, this, &Ekos::Guide::capture); //This connects all the buttons and slider below the guide plots. connect(accuracyRadiusSpin, static_cast(&QDoubleSpinBox::valueChanged), this, &Ekos::Guide::buildTarget); connect(guideSlider, &QSlider::sliderMoved, this, &Ekos::Guide::guideHistory); connect(latestCheck, &QCheckBox::toggled, this, &Ekos::Guide::setLatestGuidePoint); connect(showRAPlotCheck, &QCheckBox::toggled, this, &Ekos::Guide::toggleShowRAPlot); connect(showDECPlotCheck, &QCheckBox::toggled, this, &Ekos::Guide::toggleShowDEPlot); connect(showRACorrectionsCheck, &QCheckBox::toggled, this, &Ekos::Guide::toggleRACorrectionsPlot); connect(showDECorrectionsCheck, &QCheckBox::toggled, this, &Ekos::Guide::toggleDECorrectionsPlot); connect(correctionSlider, &QSlider::sliderMoved, this, &Ekos::Guide::setCorrectionGraphScale); connect(showGuideRateToolTipB, &QPushButton::clicked, [this]() { QToolTip::showText(showGuideRateToolTipB->mapToGlobal(QPoint(10, 10)), showGuideRateToolTipB->toolTip(), showGuideRateToolTipB); }); connect(manualDitherB, &QPushButton::clicked, this, &Guide::handleManualDither); // Guiding Rate - Advisory only onInfoRateChanged(spinBox_GuideRate->value()); connect(spinBox_GuideRate, static_cast(&QDoubleSpinBox::valueChanged), this, &Ekos::Guide::onInfoRateChanged); } void Guide::removeDevice(ISD::GDInterface *device) { device->disconnect(this); if (currentTelescope && !strcmp(currentTelescope->getDeviceName(), device->getDeviceName())) { currentTelescope = nullptr; } else if (CCDs.contains(static_cast(device))) { CCDs.removeAll(static_cast(device)); guiderCombo->removeItem(guiderCombo->findText(device->getDeviceName())); guiderCombo->removeItem(guiderCombo->findText(device->getDeviceName() + QString(" Guider"))); if (CCDs.empty()) currentCCD = nullptr; checkCCD(); } auto st4 = std::find_if(ST4List.begin(), ST4List.end(), [device](ISD::ST4 * st) { return !strcmp(st->getDeviceName(), device->getDeviceName()); }); if (st4 != ST4List.end()) { ST4List.removeOne(*st4); if (AODriver && !strcmp(device->getDeviceName(), AODriver->getDeviceName())) AODriver = nullptr; ST4Combo->removeItem(ST4Combo->findText(device->getDeviceName())); if (ST4List.empty()) { ST4Driver = GuideDriver = nullptr; } else { setST4(ST4Combo->currentText()); } } } } diff --git a/kstars/ekos/manager.cpp b/kstars/ekos/manager.cpp index 4509c4a96..e5bb92962 100644 --- a/kstars/ekos/manager.cpp +++ b/kstars/ekos/manager.cpp @@ -1,3304 +1,3304 @@ /* 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 "manager.h" #include "ekosadaptor.h" #include "kstars.h" #include "kstarsdata.h" #include "opsekos.h" #include "Options.h" #include "profileeditor.h" #include "profilewizard.h" #include "skymap.h" #include "auxiliary/darklibrary.h" #include "auxiliary/QProgressIndicator.h" #include "auxiliary/ksmessagebox.h" #include "capture/sequencejob.h" #include "fitsviewer/fitstab.h" #include "fitsviewer/fitsview.h" #include "fitsviewer/fitsdata.h" #include "indi/clientmanager.h" #include "indi/driverinfo.h" #include "indi/drivermanager.h" #include "indi/guimanager.h" #include "indi/indielement.h" #include "indi/indilistener.h" #include "indi/indiproperty.h" #include "indi/indiwebmanager.h" #include "ekoslive/ekosliveclient.h" #include "ekoslive/message.h" #include "ekoslive/media.h" #include #include #include #include #include #include #include #define MAX_REMOTE_INDI_TIMEOUT 15000 #define MAX_LOCAL_INDI_TIMEOUT 5000 namespace Ekos { Manager::Manager(QWidget * parent) : QDialog(parent) { #ifdef Q_OS_OSX if (Options::independentWindowEkos()) setWindowFlags(Qt::Window); else { setWindowFlags(Qt::Window | Qt::WindowStaysOnTopHint); connect(QApplication::instance(), SIGNAL(applicationStateChanged(Qt::ApplicationState)), this, SLOT(changeAlwaysOnTop(Qt::ApplicationState))); } #else // if (Options::independentWindowEkos()) // setWindowFlags(Qt::Window | Qt::WindowStaysOnTopHint); #endif setupUi(this); qRegisterMetaType("Ekos::CommunicationStatus"); qDBusRegisterMetaType(); new EkosAdaptor(this); QDBusConnection::sessionBus().registerObject("/KStars/Ekos", this); setWindowIcon(QIcon::fromTheme("kstars_ekos")); profileModel.reset(new QStandardItemModel(0, 4)); profileModel->setHorizontalHeaderLabels(QStringList() << "id" << "name" << "host" << "port"); captureProgress->setValue(0); sequenceProgress->setValue(0); sequenceProgress->setDecimals(0); sequenceProgress->setFormat("%v"); imageProgress->setValue(0); imageProgress->setDecimals(1); imageProgress->setFormat("%v"); imageProgress->setBarStyle(QRoundProgressBar::StyleLine); countdownTimer.setInterval(1000); connect(&countdownTimer, &QTimer::timeout, this, &Ekos::Manager::updateCaptureCountDown); toolsWidget->setIconSize(QSize(48, 48)); connect(toolsWidget, &QTabWidget::currentChanged, this, &Ekos::Manager::processTabChange, Qt::UniqueConnection); // Enable scheduler Tab toolsWidget->setTabEnabled(1, false); // Start/Stop INDI Server connect(processINDIB, &QPushButton::clicked, this, &Ekos::Manager::processINDI); processINDIB->setIcon(QIcon::fromTheme("media-playback-start")); processINDIB->setToolTip(i18n("Start")); // Connect/Disconnect INDI devices connect(connectB, &QPushButton::clicked, this, &Ekos::Manager::connectDevices); connect(disconnectB, &QPushButton::clicked, this, &Ekos::Manager::disconnectDevices); ekosLiveB->setAttribute(Qt::WA_LayoutUsesWidgetRect); ekosLiveClient.reset(new EkosLive::Client(this)); connect(ekosLiveClient.get(), &EkosLive::Client::connected, [this]() { emit ekosLiveStatusChanged(true); }); connect(ekosLiveClient.get(), &EkosLive::Client::disconnected, [this]() { emit ekosLiveStatusChanged(false); }); // INDI Control Panel //connect(controlPanelB, &QPushButton::clicked, GUIManager::Instance(), SLOT(show())); connect(ekosLiveB, &QPushButton::clicked, [&]() { ekosLiveClient.get()->show(); ekosLiveClient.get()->raise(); }); connect(this, &Manager::ekosStatusChanged, ekosLiveClient.get()->message(), &EkosLive::Message::setEkosStatingStatus); connect(ekosLiveClient.get()->message(), &EkosLive::Message::connected, [&]() { ekosLiveB->setIcon(QIcon(":/icons/cloud-online.svg")); }); connect(ekosLiveClient.get()->message(), &EkosLive::Message::disconnected, [&]() { ekosLiveB->setIcon(QIcon::fromTheme("folder-cloud")); }); connect(ekosLiveClient.get()->media(), &EkosLive::Media::newBoundingRect, ekosLiveClient.get()->message(), &EkosLive::Message::setBoundingRect); connect(ekosLiveClient.get()->message(), &EkosLive::Message::resetPolarView, ekosLiveClient.get()->media(), &EkosLive::Media::resetPolarView); connect(ekosLiveClient.get()->message(), &EkosLive::Message::previewJPEGGenerated, ekosLiveClient.get()->media(), &EkosLive::Media::sendPreviewJPEG); connect(KSMessageBox::Instance(), &KSMessageBox::newMessage, ekosLiveClient.get()->message(), &EkosLive::Message::sendDialog); // Serial Port Assistant connect(serialPortAssistantB, &QPushButton::clicked, [&]() { serialPortAssistant->show(); serialPortAssistant->raise(); }); connect(this, &Ekos::Manager::ekosStatusChanged, [&](Ekos::CommunicationStatus status) { indiControlPanelB->setEnabled(status == Ekos::Success); }); connect(indiControlPanelB, &QPushButton::clicked, [&]() { KStars::Instance()->actionCollection()->action("show_control_panel")->trigger(); }); connect(optionsB, &QPushButton::clicked, [&]() { KStars::Instance()->actionCollection()->action("configure")->trigger(); }); // Save as above, but it appears in all modules connect(ekosOptionsB, &QPushButton::clicked, this, &Ekos::Manager::showEkosOptions); // Clear Ekos Log connect(clearB, &QPushButton::clicked, this, &Ekos::Manager::clearLog); // Logs KConfigDialog * dialog = new KConfigDialog(this, "logssettings", Options::self()); opsLogs = new Ekos::OpsLogs(); KPageWidgetItem * page = dialog->addPage(opsLogs, i18n("Logging")); page->setIcon(QIcon::fromTheme("configure")); connect(logsB, &QPushButton::clicked, dialog, &KConfigDialog::show); connect(dialog->button(QDialogButtonBox::Apply), &QPushButton::clicked, this, &Ekos::Manager::updateDebugInterfaces); connect(dialog->button(QDialogButtonBox::Ok), &QPushButton::clicked, this, &Ekos::Manager::updateDebugInterfaces); // Summary // previewPixmap = new QPixmap(QPixmap(":/images/noimage.png")); // Profiles connect(addProfileB, &QPushButton::clicked, this, &Ekos::Manager::addProfile); connect(editProfileB, &QPushButton::clicked, this, &Ekos::Manager::editProfile); connect(deleteProfileB, &QPushButton::clicked, this, &Ekos::Manager::deleteProfile); connect(profileCombo, static_cast(&QComboBox::currentTextChanged), [ = ](const QString & text) { Options::setProfile(text); if (text == "Simulators") { editProfileB->setEnabled(false); deleteProfileB->setEnabled(false); } else { editProfileB->setEnabled(true); deleteProfileB->setEnabled(true); } }); // Ekos Wizard connect(wizardProfileB, &QPushButton::clicked, this, &Ekos::Manager::wizardProfile); addProfileB->setAttribute(Qt::WA_LayoutUsesWidgetRect); editProfileB->setAttribute(Qt::WA_LayoutUsesWidgetRect); deleteProfileB->setAttribute(Qt::WA_LayoutUsesWidgetRect); // Set Profile icons addProfileB->setIcon(QIcon::fromTheme("list-add")); addProfileB->setAttribute(Qt::WA_LayoutUsesWidgetRect); editProfileB->setIcon(QIcon::fromTheme("document-edit")); editProfileB->setAttribute(Qt::WA_LayoutUsesWidgetRect); deleteProfileB->setIcon(QIcon::fromTheme("list-remove")); deleteProfileB->setAttribute(Qt::WA_LayoutUsesWidgetRect); wizardProfileB->setIcon(QIcon::fromTheme("tools-wizard")); wizardProfileB->setAttribute(Qt::WA_LayoutUsesWidgetRect); customDriversB->setIcon(QIcon::fromTheme("roll")); customDriversB->setAttribute(Qt::WA_LayoutUsesWidgetRect); connect(customDriversB, &QPushButton::clicked, DriverManager::Instance(), &DriverManager::showCustomDrivers); // Load all drivers loadDrivers(); // Load add driver profiles loadProfiles(); // INDI Control Panel and Ekos Options optionsB->setIcon(QIcon::fromTheme("configure", QIcon(":/icons/ekos_setup.png"))); optionsB->setAttribute(Qt::WA_LayoutUsesWidgetRect); // Setup Tab toolsWidget->tabBar()->setTabIcon(0, QIcon(":/icons/ekos_setup.png")); toolsWidget->tabBar()->setTabToolTip(0, i18n("Setup")); // Initialize Ekos Scheduler Module schedulerProcess.reset(new Ekos::Scheduler()); toolsWidget->addTab(schedulerProcess.get(), QIcon(":/icons/ekos_scheduler.png"), ""); toolsWidget->tabBar()->setTabToolTip(1, i18n("Scheduler")); connect(schedulerProcess.get(), &Scheduler::newLog, this, &Ekos::Manager::updateLog); //connect(schedulerProcess.get(), SIGNAL(newTarget(QString)), mountTarget, SLOT(setText(QString))); connect(schedulerProcess.get(), &Ekos::Scheduler::newTarget, [&](const QString & target) { mountTarget->setText(target); ekosLiveClient.get()->message()->updateMountStatus(QJsonObject({{"target", target}})); }); // Temporary fix. Not sure how to resize Ekos Dialog to fit contents of the various tabs in the QScrollArea which are added // dynamically. I used setMinimumSize() but it doesn't appear to make any difference. // Also set Layout policy to SetMinAndMaxSize as well. Any idea how to fix this? // FIXME //resize(1000,750); summaryPreview.reset(new FITSView(previewWidget, FITS_NORMAL)); previewWidget->setContentsMargins(0, 0, 0, 0); summaryPreview->setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Expanding); summaryPreview->setBaseSize(previewWidget->size()); summaryPreview->createFloatingToolBar(); summaryPreview->setCursorMode(FITSView::dragCursor); QVBoxLayout * vlayout = new QVBoxLayout(); vlayout->addWidget(summaryPreview.get()); previewWidget->setLayout(vlayout); // JM 2019-01-19: Why cloud images depend on summary preview? // connect(summaryPreview.get(), &FITSView::loaded, [&]() // { // // UUID binds the cloud & preview frames by a common key // QString uuid = QUuid::createUuid().toString(); // uuid = uuid.remove(QRegularExpression("[-{}]")); // //ekosLiveClient.get()->media()->sendPreviewImage(summaryPreview.get(), uuid); // ekosLiveClient.get()->cloud()->sendPreviewImage(summaryPreview.get(), uuid); // }); if (Options::ekosLeftIcons()) { toolsWidget->setTabPosition(QTabWidget::West); QTransform trans; trans.rotate(90); QIcon icon = toolsWidget->tabIcon(0); QPixmap pix = icon.pixmap(QSize(48, 48)); icon = QIcon(pix.transformed(trans)); toolsWidget->setTabIcon(0, icon); icon = toolsWidget->tabIcon(1); pix = icon.pixmap(QSize(48, 48)); icon = QIcon(pix.transformed(trans)); toolsWidget->setTabIcon(1, icon); } //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); resize(Options::ekosWindowWidth(), Options::ekosWindowHeight()); } void Manager::changeAlwaysOnTop(Qt::ApplicationState state) { if (isVisible()) { if (state == Qt::ApplicationActive) setWindowFlags(Qt::Window | Qt::WindowStaysOnTopHint); else setWindowFlags(windowFlags() & ~Qt::WindowStaysOnTopHint); show(); } } Manager::~Manager() { toolsWidget->disconnect(this); //delete previewPixmap; } void Manager::closeEvent(QCloseEvent * event) { // QAction * a = KStars::Instance()->actionCollection()->action("show_ekos"); // a->setChecked(false); // 2019-02-14 JM: Close event, for some reason, make all the children disappear // when the widget is shown again. Applying a workaround here event->ignore(); hide(); } void Manager::hideEvent(QHideEvent * /*event*/) { Options::setEkosWindowWidth(width()); Options::setEkosWindowHeight(height()); QAction * a = KStars::Instance()->actionCollection()->action("show_ekos"); a->setChecked(false); } void Manager::showEvent(QShowEvent * /*event*/) { QAction * a = KStars::Instance()->actionCollection()->action("show_ekos"); a->setChecked(true); // Just show the profile wizard ONCE per session if (profileWizardLaunched == false && profiles.count() == 1) { profileWizardLaunched = true; wizardProfile(); } } void Manager::resizeEvent(QResizeEvent *) { //previewImage->setPixmap(previewPixmap->scaled(previewImage->width(), previewImage->height(), Qt::KeepAspectRatio, Qt::SmoothTransformation)); if (focusStarPixmap.get() != nullptr) focusStarImage->setPixmap(focusStarPixmap->scaled(focusStarImage->width(), focusStarImage->height(), Qt::KeepAspectRatio, Qt::SmoothTransformation)); //if (focusProfilePixmap) //focusProfileImage->setPixmap(focusProfilePixmap->scaled(focusProfileImage->width(), focusProfileImage->height(), Qt::KeepAspectRatio, Qt::SmoothTransformation)); if (guideStarPixmap.get() != nullptr) guideStarImage->setPixmap(guideStarPixmap->scaled(guideStarImage->width(), guideStarImage->height(), Qt::KeepAspectRatio, Qt::SmoothTransformation)); //if (guideProfilePixmap) //guideProfileImage->setPixmap(guideProfilePixmap->scaled(guideProfileImage->width(), guideProfileImage->height(), Qt::KeepAspectRatio, Qt::SmoothTransformation)); } void Manager::loadProfiles() { profiles.clear(); KStarsData::Instance()->userdb()->GetAllProfiles(profiles); profileModel->clear(); for (auto &pi : profiles) { QList info; info << new QStandardItem(pi->id) << new QStandardItem(pi->name) << new QStandardItem(pi->host) << new QStandardItem(pi->port); profileModel->appendRow(info); } profileModel->sort(0); profileCombo->blockSignals(true); profileCombo->setModel(profileModel.get()); profileCombo->setModelColumn(1); profileCombo->blockSignals(false); // Load last used profile from options int index = profileCombo->findText(Options::profile()); // If not found, set it to first item if (index == -1) index = 0; profileCombo->setCurrentIndex(index); } void Manager::loadDrivers() { foreach (DriverInfo * dv, DriverManager::Instance()->getDrivers()) { if (dv->getDriverSource() != HOST_SOURCE) driversList[dv->getLabel()] = dv; } } void Manager::reset() { qCDebug(KSTARS_EKOS) << "Resetting Ekos Manager..."; // Filter Manager filterManager.reset(new Ekos::FilterManager()); nDevices = 0; useGuideHead = false; useST4 = false; removeTabs(); genericDevices.clear(); managedDevices.clear(); captureProcess.reset(); focusProcess.reset(); guideProcess.reset(); domeProcess.reset(); alignProcess.reset(); mountProcess.reset(); weatherProcess.reset(); observatoryProcess.reset(); dustCapProcess.reset(); DarkLibrary::Instance()->reset(); Ekos::CommunicationStatus previousStatus = m_ekosStatus; m_ekosStatus = Ekos::Idle; if (previousStatus != m_ekosStatus) emit ekosStatusChanged(m_ekosStatus); previousStatus = m_indiStatus; m_indiStatus = Ekos::Idle; if (previousStatus != m_indiStatus) emit indiStatusChanged(m_indiStatus); connectB->setEnabled(false); disconnectB->setEnabled(false); //controlPanelB->setEnabled(false); processINDIB->setEnabled(true); mountGroup->setEnabled(false); focusGroup->setEnabled(false); captureGroup->setEnabled(false); guideGroup->setEnabled(false); sequenceLabel->setText(i18n("Sequence")); sequenceProgress->setValue(0); captureProgress->setValue(0); overallRemainingTime->setText("--:--:--"); sequenceRemainingTime->setText("--:--:--"); imageRemainingTime->setText("--:--:--"); mountStatus->setText(i18n("Idle")); mountStatus->setStyleSheet(QString()); captureStatus->setText(i18n("Idle")); focusStatus->setText(i18n("Idle")); guideStatus->setText(i18n("Idle")); if (capturePI) capturePI->stopAnimation(); if (mountPI) mountPI->stopAnimation(); if (focusPI) focusPI->stopAnimation(); if (guidePI) guidePI->stopAnimation(); m_isStarted = false; //processINDIB->setText(i18n("Start INDI")); processINDIB->setIcon(QIcon::fromTheme("media-playback-start")); processINDIB->setToolTip(i18n("Start")); } void Manager::processINDI() { if (m_isStarted == false) start(); else stop(); } bool Manager::stop() { cleanDevices(); serialPortAssistant.reset(); serialPortAssistantB->setEnabled(false); profileGroup->setEnabled(true); setWindowTitle(i18n("Ekos")); return true; } bool Manager::start() { // Don't start if it is already started before if (m_ekosStatus == Ekos::Pending || m_ekosStatus == Ekos::Success) { qCDebug(KSTARS_EKOS) << "Ekos Manager start called but current Ekos Status is" << m_ekosStatus << "Ignoring request."; return true; } if (m_LocalMode) qDeleteAll(managedDrivers); managedDrivers.clear(); // If clock was paused, unpaused it and sync time if (KStarsData::Instance()->clock()->isActive() == false) { KStarsData::Instance()->changeDateTime(KStarsDateTime::currentDateTimeUtc()); KStarsData::Instance()->clock()->start(); } reset(); // connect(KSMessageBox::Instance(), &KSMessageBox::accepted, this, [this]() // { // QObject::disconnect(KSMessageBox::Instance(), &KSMessageBox::accepted, this, nullptr); // qDebug() << "Dialog was accepted!"; // }); // KSMessageBox::Instance()->questionYesNo("Do you want to proceed?", "Confirm", 21); currentProfile = getCurrentProfile(); m_LocalMode = currentProfile->isLocal(); // Load profile location if one exists updateProfileLocation(currentProfile); bool haveCCD = false, haveGuider = false; if (currentProfile->guidertype == Ekos::Guide::GUIDE_PHD2) { Options::setPHD2Host(currentProfile->guiderhost); Options::setPHD2Port(currentProfile->guiderport); } else if (currentProfile->guidertype == Ekos::Guide::GUIDE_LINGUIDER) { Options::setLinGuiderHost(currentProfile->guiderhost); Options::setLinGuiderPort(currentProfile->guiderport); } if (m_LocalMode) { DriverInfo * drv = driversList.value(currentProfile->mount()); if (drv != nullptr) managedDrivers.append(drv->clone()); drv = driversList.value(currentProfile->ccd()); if (drv != nullptr) { managedDrivers.append(drv->clone()); haveCCD = true; } Options::setGuiderType(currentProfile->guidertype); drv = driversList.value(currentProfile->guider()); if (drv != nullptr) { haveGuider = true; // If the guider and ccd are the same driver, we have two cases: // #1 Drivers that only support ONE device per driver (such as sbig) // #2 Drivers that supports multiples devices per driver (such as sx) // For #1, we modify guider_di to make a unique label for the other device with postfix "Guide" // For #2, we set guider_di to nullptr and we prompt the user to select which device is primary ccd and which is guider // since this is the only way to find out in real time. if (haveCCD && currentProfile->guider() == currentProfile->ccd()) { if (drv->getAuxInfo().value("mdpd", false).toBool() == true) { drv = nullptr; } else { drv->setUniqueLabel(drv->getLabel() + " Guide"); } } if (drv) managedDrivers.append(drv->clone()); } drv = driversList.value(currentProfile->ao()); if (drv != nullptr) managedDrivers.append(drv->clone()); drv = driversList.value(currentProfile->filter()); if (drv != nullptr) managedDrivers.append(drv->clone()); drv = driversList.value(currentProfile->focuser()); if (drv != nullptr) managedDrivers.append(drv->clone()); drv = driversList.value(currentProfile->dome()); if (drv != nullptr) managedDrivers.append(drv->clone()); drv = driversList.value(currentProfile->weather()); if (drv != nullptr) managedDrivers.append(drv->clone()); drv = driversList.value(currentProfile->aux1()); if (drv != nullptr) managedDrivers.append(drv->clone()); drv = driversList.value(currentProfile->aux2()); if (drv != nullptr) managedDrivers.append(drv->clone()); drv = driversList.value(currentProfile->aux3()); if (drv != nullptr) managedDrivers.append(drv->clone()); drv = driversList.value(currentProfile->aux4()); if (drv != nullptr) managedDrivers.append(drv->clone()); // Add remote drivers if we have any if (currentProfile->remotedrivers.isEmpty() == false && currentProfile->remotedrivers.contains("@")) { for (auto remoteDriver : currentProfile->remotedrivers.split(",")) { QString name, label, host("localhost"), port("7624"); QStringList properties = remoteDriver.split(QRegExp("[@:]")); if (properties.length() > 1) { name = properties[0]; host = properties[1]; if (properties.length() > 2) port = properties[2]; } DriverInfo * dv = new DriverInfo(name); dv->setRemoteHost(host); dv->setRemotePort(port); label = name; // Remove extra quotes label.remove("\""); dv->setLabel(label); dv->setUniqueLabel(label); managedDrivers.append(dv); } } if (haveCCD == false && haveGuider == false) { KSNotification::error(i18n("Ekos requires at least one CCD or Guider to operate.")); managedDrivers.clear(); return false; } nDevices = managedDrivers.count(); } else { DriverInfo * remote_indi = new DriverInfo(QString("Ekos Remote Host")); remote_indi->setHostParameters(currentProfile->host, QString::number(currentProfile->port)); remote_indi->setDriverSource(GENERATED_SOURCE); managedDrivers.append(remote_indi); haveCCD = currentProfile->drivers.contains("CCD"); haveGuider = currentProfile->drivers.contains("Guider"); Options::setGuiderType(currentProfile->guidertype); if (haveCCD == false && haveGuider == false) { KSNotification::error(i18n("Ekos requires at least one CCD or Guider to operate.")); delete (remote_indi); nDevices = 0; return false; } nDevices = currentProfile->drivers.count(); } connect(INDIListener::Instance(), &INDIListener::newDevice, this, &Ekos::Manager::processNewDevice); connect(INDIListener::Instance(), &INDIListener::newTelescope, this, &Ekos::Manager::setTelescope); connect(INDIListener::Instance(), &INDIListener::newCCD, this, &Ekos::Manager::setCCD); connect(INDIListener::Instance(), &INDIListener::newFilter, this, &Ekos::Manager::setFilter); connect(INDIListener::Instance(), &INDIListener::newFocuser, this, &Ekos::Manager::setFocuser); connect(INDIListener::Instance(), &INDIListener::newDome, this, &Ekos::Manager::setDome); connect(INDIListener::Instance(), &INDIListener::newWeather, this, &Ekos::Manager::setWeather); connect(INDIListener::Instance(), &INDIListener::newDustCap, this, &Ekos::Manager::setDustCap); connect(INDIListener::Instance(), &INDIListener::newLightBox, this, &Ekos::Manager::setLightBox); connect(INDIListener::Instance(), &INDIListener::newST4, this, &Ekos::Manager::setST4); connect(INDIListener::Instance(), &INDIListener::deviceRemoved, this, &Ekos::Manager::removeDevice, Qt::DirectConnection); #ifdef Q_OS_OSX if (m_LocalMode || currentProfile->host == "localhost") { if (isRunning("PTPCamera")) { if (KMessageBox::Yes == (KMessageBox::questionYesNo(nullptr, i18n("Ekos detected that PTP Camera is running and may prevent a Canon or Nikon camera from connecting to Ekos. Do you want to quit PTP Camera now?"), i18n("PTP Camera"), KStandardGuiItem::yes(), KStandardGuiItem::no(), "ekos_shutdown_PTPCamera"))) { //TODO is there a better way to do this. QProcess p; p.start("killall PTPCamera"); p.waitForFinished(); } } } #endif if (m_LocalMode) { if (isRunning("indiserver")) { if (KMessageBox::Yes == (KMessageBox::questionYesNo(nullptr, i18n("Ekos detected an instance of INDI server running. Do you wish to " "shut down the existing instance before starting a new one?"), i18n("INDI Server"), KStandardGuiItem::yes(), KStandardGuiItem::no(), "ekos_shutdown_existing_indiserver"))) { DriverManager::Instance()->stopAllDevices(); //TODO is there a better way to do this. QProcess p; p.start("pkill indiserver"); p.waitForFinished(); } } appendLogText(i18n("Starting INDI services...")); if (DriverManager::Instance()->startDevices(managedDrivers) == false) { INDIListener::Instance()->disconnect(this); qDeleteAll(managedDrivers); managedDrivers.clear(); m_ekosStatus = Ekos::Error; emit ekosStatusChanged(m_ekosStatus); return false; } connect(DriverManager::Instance(), SIGNAL(serverTerminated(QString, QString)), this, SLOT(processServerTermination(QString, QString))); m_ekosStatus = Ekos::Pending; emit ekosStatusChanged(m_ekosStatus); if (currentProfile->autoConnect) appendLogText(i18n("INDI services started on port %1.", managedDrivers.first()->getPort())); else appendLogText( i18n("INDI services started on port %1. Please connect devices.", managedDrivers.first()->getPort())); QTimer::singleShot(MAX_LOCAL_INDI_TIMEOUT, this, &Ekos::Manager::checkINDITimeout); } else { // If we need to use INDI Web Manager if (currentProfile->INDIWebManagerPort > 0) { appendLogText(i18n("Establishing communication with remote INDI Web Manager...")); m_RemoteManagerStart = false; if (INDI::WebManager::isOnline(currentProfile)) { INDI::WebManager::syncCustomDrivers(currentProfile); currentProfile->isStellarMate = INDI::WebManager::isStellarMate(currentProfile); if (INDI::WebManager::areDriversRunning(currentProfile) == false) { INDI::WebManager::stopProfile(currentProfile); if (INDI::WebManager::startProfile(currentProfile) == false) { appendLogText(i18n("Failed to start profile on remote INDI Web Manager.")); return false; } appendLogText(i18n("Starting profile on remote INDI Web Manager...")); m_RemoteManagerStart = true; } } else appendLogText(i18n("Warning: INDI Web Manager is not online.")); } appendLogText( i18n("Connecting to remote INDI server at %1 on port %2 ...", currentProfile->host, currentProfile->port)); qApp->processEvents(); QApplication::setOverrideCursor(Qt::WaitCursor); if (DriverManager::Instance()->connectRemoteHost(managedDrivers.first()) == false) { appendLogText(i18n("Failed to connect to remote INDI server.")); INDIListener::Instance()->disconnect(this); qDeleteAll(managedDrivers); managedDrivers.clear(); m_ekosStatus = Ekos::Error; emit ekosStatusChanged(m_ekosStatus); QApplication::restoreOverrideCursor(); return false; } connect(DriverManager::Instance(), SIGNAL(serverTerminated(QString, QString)), this, SLOT(processServerTermination(QString, QString))); QApplication::restoreOverrideCursor(); m_ekosStatus = Ekos::Pending; emit ekosStatusChanged(m_ekosStatus); appendLogText( i18n("INDI services started. Connection to remote INDI server is successful. Waiting for devices...")); QTimer::singleShot(MAX_REMOTE_INDI_TIMEOUT, this, &Ekos::Manager::checkINDITimeout); } connectB->setEnabled(false); disconnectB->setEnabled(false); //controlPanelB->setEnabled(false); profileGroup->setEnabled(false); m_isStarted = true; //processINDIB->setText(i18n("Stop INDI")); processINDIB->setIcon(QIcon::fromTheme("media-playback-stop")); processINDIB->setToolTip(i18n("Stop")); setWindowTitle(i18n("Ekos - %1 Profile", currentProfile->name)); return true; } void Manager::checkINDITimeout() { // Don't check anything unless we're still pending if (m_ekosStatus != Ekos::Pending) return; if (nDevices <= 0) { m_ekosStatus = Ekos::Success; emit ekosStatusChanged(m_ekosStatus); return; } if (m_LocalMode) { QStringList remainingDevices; foreach (DriverInfo * drv, managedDrivers) { if (drv->getDevices().count() == 0) remainingDevices << QString("+ %1").arg( drv->getUniqueLabel().isEmpty() == false ? drv->getUniqueLabel() : drv->getName()); } if (remainingDevices.count() == 1) { appendLogText(i18n("Unable to establish:\n%1\nPlease ensure the device is connected and powered on.", remainingDevices.at(0))); KNotification::beep(i18n("Ekos startup error")); } else { appendLogText(i18n("Unable to establish the following devices:\n%1\nPlease ensure each device is connected " "and powered on.", remainingDevices.join("\n"))); KNotification::beep(i18n("Ekos startup error")); } } else { QStringList remainingDevices; for (auto &driver : currentProfile->drivers.values()) { bool driverFound = false; for (auto &device : genericDevices) { if (device->getBaseDevice()->getDriverName() == driver) { driverFound = true; break; } } if (driverFound == false) remainingDevices << QString("+ %1").arg(driver); } if (remainingDevices.count() == 1) { appendLogText(i18n("Unable to establish remote device:\n%1\nPlease ensure remote device name corresponds " "to actual device name.", remainingDevices.at(0))); KNotification::beep(i18n("Ekos startup error")); } else { appendLogText(i18n("Unable to establish remote devices:\n%1\nPlease ensure remote device name corresponds " "to actual device name.", remainingDevices.join("\n"))); KNotification::beep(i18n("Ekos startup error")); } } m_ekosStatus = Ekos::Error; } void Manager::connectDevices() { // Check if already connected int nConnected = 0; Ekos::CommunicationStatus previousStatus = m_indiStatus; for (auto &device : genericDevices) { if (device->isConnected()) nConnected++; } if (genericDevices.count() == nConnected) { m_indiStatus = Ekos::Success; emit indiStatusChanged(m_indiStatus); return; } m_indiStatus = Ekos::Pending; if (previousStatus != m_indiStatus) emit indiStatusChanged(m_indiStatus); for (auto &device : genericDevices) { qCDebug(KSTARS_EKOS) << "Connecting " << device->getDeviceName(); device->Connect(); } connectB->setEnabled(false); disconnectB->setEnabled(true); appendLogText(i18n("Connecting INDI devices...")); } void Manager::disconnectDevices() { for (auto &device : genericDevices) { qCDebug(KSTARS_EKOS) << "Disconnecting " << device->getDeviceName(); device->Disconnect(); } appendLogText(i18n("Disconnecting INDI devices...")); } void Manager::processServerTermination(const QString &host, const QString &port) { if ((m_LocalMode && managedDrivers.first()->getPort() == port) || (currentProfile->host == host && currentProfile->port == port.toInt())) { cleanDevices(false); } } void Manager::cleanDevices(bool stopDrivers) { if (m_ekosStatus == Ekos::Idle) return; INDIListener::Instance()->disconnect(this); DriverManager::Instance()->disconnect(this); if (managedDrivers.isEmpty() == false) { if (m_LocalMode) { if (stopDrivers) DriverManager::Instance()->stopDevices(managedDrivers); } else { if (stopDrivers) DriverManager::Instance()->disconnectRemoteHost(managedDrivers.first()); if (m_RemoteManagerStart && currentProfile->INDIWebManagerPort != -1) { INDI::WebManager::stopProfile(currentProfile); m_RemoteManagerStart = false; } } } reset(); profileGroup->setEnabled(true); appendLogText(i18n("INDI services stopped.")); } void Manager::processNewDevice(ISD::GDInterface * devInterface) { qCInfo(KSTARS_EKOS) << "Ekos received a new device: " << devInterface->getDeviceName(); Ekos::CommunicationStatus previousStatus = m_indiStatus; for(auto &device : genericDevices) { if (!strcmp(device->getDeviceName(), devInterface->getDeviceName())) { qCWarning(KSTARS_EKOS) << "Found duplicate device, ignoring..."; return; } } // Always reset INDI Connection status if we receive a new device m_indiStatus = Ekos::Idle; if (previousStatus != m_indiStatus) emit indiStatusChanged(m_indiStatus); genericDevices.append(devInterface); nDevices--; connect(devInterface, &ISD::GDInterface::Connected, this, &Ekos::Manager::deviceConnected); connect(devInterface, &ISD::GDInterface::Disconnected, this, &Ekos::Manager::deviceDisconnected); connect(devInterface, &ISD::GDInterface::propertyDefined, this, &Ekos::Manager::processNewProperty); connect(devInterface, &ISD::GDInterface::interfaceDefined, this, &Ekos::Manager::syncActiveDevices); if (currentProfile->isStellarMate) { connect(devInterface, &ISD::GDInterface::systemPortDetected, [this, devInterface]() { if (!serialPortAssistant) { serialPortAssistant.reset(new SerialPortAssistant(currentProfile, this)); serialPortAssistantB->setEnabled(true); } uint32_t driverInterface = devInterface->getDriverInterface(); // Ignore CCD interface if (driverInterface & INDI::BaseDevice::CCD_INTERFACE) return; if (driverInterface & INDI::BaseDevice::TELESCOPE_INTERFACE || driverInterface & INDI::BaseDevice::FOCUSER_INTERFACE || driverInterface & INDI::BaseDevice::FILTER_INTERFACE || driverInterface & INDI::BaseDevice::AUX_INTERFACE || driverInterface & INDI::BaseDevice::GPS_INTERFACE) serialPortAssistant->addDevice(devInterface); if (Options::autoLoadSerialAssistant()) serialPortAssistant->show(); }); } if (nDevices <= 0) { m_ekosStatus = Ekos::Success; emit ekosStatusChanged(m_ekosStatus); connectB->setEnabled(true); disconnectB->setEnabled(false); //controlPanelB->setEnabled(true); if (m_LocalMode == false && nDevices == 0) { if (currentProfile->autoConnect) appendLogText(i18n("Remote devices established.")); else appendLogText(i18n("Remote devices established. Please connect devices.")); } } } void Manager::deviceConnected() { connectB->setEnabled(false); disconnectB->setEnabled(true); processINDIB->setEnabled(false); Ekos::CommunicationStatus previousStatus = m_indiStatus; if (Options::verboseLogging()) { ISD::GDInterface * device = qobject_cast(sender()); qCInfo(KSTARS_EKOS) << device->getDeviceName() << "Version:" << device->getDriverVersion() << "Interface:" << device->getDriverInterface() << "is connected."; } int nConnectedDevices = 0; foreach (ISD::GDInterface * device, genericDevices) { if (device->isConnected()) nConnectedDevices++; } qCDebug(KSTARS_EKOS) << nConnectedDevices << " devices connected out of " << genericDevices.count(); //if (nConnectedDevices >= pi->drivers.count()) if (nConnectedDevices >= genericDevices.count()) { m_indiStatus = Ekos::Success; qCInfo(KSTARS_EKOS) << "All INDI devices are now connected."; } else m_indiStatus = Ekos::Pending; if (previousStatus != m_indiStatus) emit indiStatusChanged(m_indiStatus); ISD::GDInterface * dev = static_cast(sender()); if (dev->getBaseDevice()->getDriverInterface() & INDI::BaseDevice::TELESCOPE_INTERFACE) { if (mountProcess.get() != nullptr) { mountProcess->setEnabled(true); if (alignProcess.get() != nullptr) alignProcess->setEnabled(true); } } else if (dev->getBaseDevice()->getDriverInterface() & INDI::BaseDevice::CCD_INTERFACE) { if (captureProcess.get() != nullptr) captureProcess->setEnabled(true); if (focusProcess.get() != nullptr) focusProcess->setEnabled(true); if (alignProcess.get() != nullptr) { if (mountProcess.get() && mountProcess->isEnabled()) alignProcess->setEnabled(true); else alignProcess->setEnabled(false); } if (guideProcess.get() != nullptr) guideProcess->setEnabled(true); } else if (dev->getBaseDevice()->getDriverInterface() & INDI::BaseDevice::FOCUSER_INTERFACE) { if (focusProcess.get() != nullptr) focusProcess->setEnabled(true); } if (Options::neverLoadConfig()) return; INDIConfig tConfig = Options::loadConfigOnConnection() ? LOAD_LAST_CONFIG : LOAD_DEFAULT_CONFIG; foreach (ISD::GDInterface * device, genericDevices) { if (device == dev) { connect(dev, &ISD::GDInterface::switchUpdated, this, &Ekos::Manager::watchDebugProperty); ISwitchVectorProperty * configProp = device->getBaseDevice()->getSwitch("CONFIG_PROCESS"); if (configProp && configProp->s == IPS_IDLE) device->setConfig(tConfig); break; } } } void Manager::deviceDisconnected() { ISD::GDInterface * dev = static_cast(sender()); Ekos::CommunicationStatus previousStatus = m_indiStatus; if (dev != nullptr) { if (dev->getState("CONNECTION") == IPS_ALERT) m_indiStatus = Ekos::Error; else if (dev->getState("CONNECTION") == IPS_BUSY) m_indiStatus = Ekos::Pending; else m_indiStatus = Ekos::Idle; if (Options::verboseLogging()) qCDebug(KSTARS_EKOS) << dev->getDeviceName() << " is disconnected."; appendLogText(i18n("%1 is disconnected.", dev->getDeviceName())); } else m_indiStatus = Ekos::Idle; if (previousStatus != m_indiStatus) emit indiStatusChanged(m_indiStatus); connectB->setEnabled(true); disconnectB->setEnabled(false); processINDIB->setEnabled(true); if (dev != nullptr && dev->getBaseDevice() && (dev->getBaseDevice()->getDriverInterface() & INDI::BaseDevice::TELESCOPE_INTERFACE)) { if (mountProcess.get() != nullptr) mountProcess->setEnabled(false); } // Do not disable modules on device connection loss, let them handle it /* else if (dev->getBaseDevice()->getDriverInterface() & INDI::BaseDevice::CCD_INTERFACE) { if (captureProcess.get() != nullptr) captureProcess->setEnabled(false); if (focusProcess.get() != nullptr) focusProcess->setEnabled(false); if (alignProcess.get() != nullptr) alignProcess->setEnabled(false); if (guideProcess.get() != nullptr) guideProcess->setEnabled(false); } else if (dev->getBaseDevice()->getDriverInterface() & INDI::BaseDevice::FOCUSER_INTERFACE) { if (focusProcess.get() != nullptr) focusProcess->setEnabled(false); }*/ } void Manager::setTelescope(ISD::GDInterface * scopeDevice) { //mount = scopeDevice; managedDevices[KSTARS_TELESCOPE] = scopeDevice; appendLogText(i18n("%1 is online.", scopeDevice->getDeviceName())); connect(scopeDevice, SIGNAL(numberUpdated(INumberVectorProperty *)), this, SLOT(processNewNumber(INumberVectorProperty *)), Qt::UniqueConnection); initMount(); mountProcess->setTelescope(scopeDevice); double primaryScopeFL = 0, primaryScopeAperture = 0, guideScopeFL = 0, guideScopeAperture = 0; getCurrentProfileTelescopeInfo(primaryScopeFL, primaryScopeAperture, guideScopeFL, guideScopeAperture); // Save telescope info in mount driver mountProcess->setTelescopeInfo(QList() << primaryScopeFL << primaryScopeAperture << guideScopeFL << guideScopeAperture); if (guideProcess.get() != nullptr) { guideProcess->setTelescope(scopeDevice); guideProcess->setTelescopeInfo(primaryScopeFL, primaryScopeAperture, guideScopeFL, guideScopeAperture); } if (alignProcess.get() != nullptr) { alignProcess->setTelescope(scopeDevice); alignProcess->setTelescopeInfo(primaryScopeFL, primaryScopeAperture, guideScopeFL, guideScopeAperture); } // if (domeProcess.get() != nullptr) // domeProcess->setTelescope(scopeDevice); ekosLiveClient->message()->sendMounts(); ekosLiveClient->message()->sendScopes(); } void Manager::setCCD(ISD::GDInterface * ccdDevice) { // No duplicates for (auto oneCCD : findDevices(KSTARS_CCD)) if (oneCCD == ccdDevice) return; managedDevices.insertMulti(KSTARS_CCD, ccdDevice); initCapture(); captureProcess->setEnabled(true); captureProcess->addCCD(ccdDevice); QString primaryCCD, guiderCCD; // Only look for primary & guider CCDs if we can tell a difference between them // otherwise rely on saved options if (currentProfile->ccd() != currentProfile->guider()) { foreach (ISD::GDInterface * device, findDevices(KSTARS_CCD)) { if (QString(device->getDeviceName()).startsWith(currentProfile->ccd(), Qt::CaseInsensitive)) primaryCCD = QString(device->getDeviceName()); else if (QString(device->getDeviceName()).startsWith(currentProfile->guider(), Qt::CaseInsensitive)) guiderCCD = QString(device->getDeviceName()); } } bool rc = false; if (Options::defaultCaptureCCD().isEmpty() == false) rc = captureProcess->setCamera(Options::defaultCaptureCCD()); if (rc == false && primaryCCD.isEmpty() == false) captureProcess->setCamera(primaryCCD); initFocus(); focusProcess->addCCD(ccdDevice); rc = false; if (Options::defaultFocusCCD().isEmpty() == false) rc = focusProcess->setCamera(Options::defaultFocusCCD()); if (rc == false && primaryCCD.isEmpty() == false) focusProcess->setCamera(primaryCCD); initAlign(); alignProcess->addCCD(ccdDevice); rc = false; if (Options::defaultAlignCCD().isEmpty() == false) rc = alignProcess->setCamera(Options::defaultAlignCCD()); if (rc == false && primaryCCD.isEmpty() == false) alignProcess->setCamera(primaryCCD); initGuide(); guideProcess->addCamera(ccdDevice); rc = false; if (Options::defaultGuideCCD().isEmpty() == false) rc = guideProcess->setCamera(Options::defaultGuideCCD()); if (rc == false && guiderCCD.isEmpty() == false) guideProcess->setCamera(guiderCCD); appendLogText(i18n("%1 is online.", ccdDevice->getDeviceName())); connect(ccdDevice, SIGNAL(numberUpdated(INumberVectorProperty *)), this, SLOT(processNewNumber(INumberVectorProperty *)), Qt::UniqueConnection); if (managedDevices.contains(KSTARS_TELESCOPE)) { alignProcess->setTelescope(managedDevices[KSTARS_TELESCOPE]); captureProcess->setTelescope(managedDevices[KSTARS_TELESCOPE]); guideProcess->setTelescope(managedDevices[KSTARS_TELESCOPE]); } } void Manager::setFilter(ISD::GDInterface * filterDevice) { // No duplicates if (findDevices(KSTARS_FILTER).contains(filterDevice) == false) managedDevices.insertMulti(KSTARS_FILTER, filterDevice); appendLogText(i18n("%1 filter is online.", filterDevice->getDeviceName())); initCapture(); connect(filterDevice, SIGNAL(numberUpdated(INumberVectorProperty *)), this, SLOT(processNewNumber(INumberVectorProperty *)), Qt::UniqueConnection); connect(filterDevice, SIGNAL(textUpdated(ITextVectorProperty *)), this, SLOT(processNewText(ITextVectorProperty *)), Qt::UniqueConnection); captureProcess->addFilter(filterDevice); initFocus(); focusProcess->addFilter(filterDevice); initAlign(); alignProcess->addFilter(filterDevice); if (Options::defaultAlignFW().isEmpty() == false) alignProcess->setFilterWheel(Options::defaultAlignFW()); } void Manager::setFocuser(ISD::GDInterface * focuserDevice) { // No duplicates if (findDevices(KSTARS_FOCUSER).contains(focuserDevice) == false) managedDevices.insertMulti(KSTARS_FOCUSER, focuserDevice); initCapture(); initFocus(); focusProcess->addFocuser(focuserDevice); if (Options::defaultFocusFocuser().isEmpty() == false) focusProcess->setFocuser(Options::defaultFocusFocuser()); appendLogText(i18n("%1 focuser is online.", focuserDevice->getDeviceName())); } void Manager::setDome(ISD::GDInterface * domeDevice) { managedDevices[KSTARS_DOME] = domeDevice; initDome(); domeProcess->setDome(domeDevice); if (captureProcess.get() != nullptr) captureProcess->setDome(domeDevice); if (alignProcess.get() != nullptr) alignProcess->setDome(domeDevice); // if (managedDevices.contains(KSTARS_TELESCOPE)) // domeProcess->setTelescope(managedDevices[KSTARS_TELESCOPE]); appendLogText(i18n("%1 is online.", domeDevice->getDeviceName())); } void Manager::setWeather(ISD::GDInterface * weatherDevice) { managedDevices[KSTARS_WEATHER] = weatherDevice; initWeather(); weatherProcess->setWeather(weatherDevice); appendLogText(i18n("%1 is online.", weatherDevice->getDeviceName())); } void Manager::setDustCap(ISD::GDInterface * dustCapDevice) { // No duplicates if (findDevices(KSTARS_AUXILIARY).contains(dustCapDevice) == false) managedDevices.insertMulti(KSTARS_AUXILIARY, dustCapDevice); initDustCap(); dustCapProcess->setDustCap(dustCapDevice); appendLogText(i18n("%1 is online.", dustCapDevice->getDeviceName())); if (captureProcess.get() != nullptr) captureProcess->setDustCap(dustCapDevice); DarkLibrary::Instance()->setRemoteCap(dustCapDevice); } void Manager::setLightBox(ISD::GDInterface * lightBoxDevice) { // No duplicates if (findDevices(KSTARS_AUXILIARY).contains(lightBoxDevice) == false) managedDevices.insertMulti(KSTARS_AUXILIARY, lightBoxDevice); if (captureProcess.get() != nullptr) captureProcess->setLightBox(lightBoxDevice); } void Manager::removeDevice(ISD::GDInterface * devInterface) { // switch (devInterface->getType()) // { // case KSTARS_CCD: // removeTabs(); // break; // default: // break; // } if (alignProcess) alignProcess->removeDevice(devInterface); if (captureProcess) captureProcess->removeDevice(devInterface); if (focusProcess) focusProcess->removeDevice(devInterface); if (mountProcess) mountProcess->removeDevice(devInterface); if (guideProcess) guideProcess->removeDevice(devInterface); if (domeProcess) domeProcess->removeDevice(devInterface); if (weatherProcess) weatherProcess->removeDevice(devInterface); if (dustCapProcess) { dustCapProcess->removeDevice(devInterface); DarkLibrary::Instance()->removeDevice(devInterface); } appendLogText(i18n("%1 is offline.", devInterface->getDeviceName())); // #1 Remove from Generic Devices // Generic devices are ALL the devices we receive from INDI server // Whether Ekos cares about them (i.e. selected equipment) or extra devices we // do not care about for (auto &device : genericDevices) { if (!strcmp(device->getDeviceName(), devInterface->getDeviceName())) { genericDevices.removeOne(device); } } // #2 Remove from Ekos Managed Device // Managed devices are devices selected by the user in the device profile for (auto &device : managedDevices.values()) { if (!strcmp(device->getDeviceName(), devInterface->getDeviceName())) { managedDevices.remove(managedDevices.key(device)); if (managedDevices.count() == 0) cleanDevices(); //break; } } if (managedDevices.isEmpty()) removeTabs(); } void Manager::processNewText(ITextVectorProperty * tvp) { if (!strcmp(tvp->name, "FILTER_NAME")) { ekosLiveClient.get()->message()->sendFilterWheels(); } } void Manager::processNewNumber(INumberVectorProperty * nvp) { if (!strcmp(nvp->name, "TELESCOPE_INFO") && managedDevices.contains(KSTARS_TELESCOPE)) { if (guideProcess.get() != nullptr) { guideProcess->setTelescope(managedDevices[KSTARS_TELESCOPE]); //guideProcess->syncTelescopeInfo(); } if (alignProcess.get() != nullptr) { alignProcess->setTelescope(managedDevices[KSTARS_TELESCOPE]); //alignProcess->syncTelescopeInfo(); } if (mountProcess.get() != nullptr) { mountProcess->setTelescope(managedDevices[KSTARS_TELESCOPE]); //mountProcess->syncTelescopeInfo(); } return; } if (!strcmp(nvp->name, "CCD_INFO") || !strcmp(nvp->name, "GUIDER_INFO") || !strcmp(nvp->name, "CCD_FRAME") || !strcmp(nvp->name, "GUIDER_FRAME")) { if (focusProcess.get() != nullptr) focusProcess->syncCCDInfo(); if (guideProcess.get() != nullptr) guideProcess->syncCCDInfo(); if (alignProcess.get() != nullptr) alignProcess->syncCCDInfo(); return; } /* if (!strcmp(nvp->name, "FILTER_SLOT")) { if (captureProcess.get() != nullptr) captureProcess->checkFilter(); if (focusProcess.get() != nullptr) focusProcess->checkFilter(); if (alignProcess.get() != nullptr) alignProcess->checkFilter(); } */ } void Manager::processNewProperty(INDI::Property * prop) { ISD::GenericDevice * deviceInterface = qobject_cast(sender()); if (!strcmp(prop->getName(), "CONNECTION") && currentProfile->autoConnect) { // Check if we need to do any mappings const QString port = m_ProfileMapping.value(QString(deviceInterface->getDeviceName())).toString(); // If we don't have port mapping, then we connect immediately. if (port.isEmpty()) deviceInterface->Connect(); return; } if (!strcmp(prop->getName(), "DEVICE_PORT")) { // Check if we need to do any mappings const QString port = m_ProfileMapping.value(QString(deviceInterface->getDeviceName())).toString(); if (!port.isEmpty()) { ITextVectorProperty *tvp = prop->getText(); IUSaveText(&(tvp->tp[0]), port.toLatin1().data()); deviceInterface->getDriverInfo()->getClientManager()->sendNewText(tvp); // Now connect if we need to. if (currentProfile->autoConnect) deviceInterface->Connect(); return; } } // Check if we need to turn on DEBUG for logging purposes if (!strcmp(prop->getName(), "DEBUG")) { uint16_t interface = deviceInterface->getBaseDevice()->getDriverInterface(); if ( opsLogs->getINDIDebugInterface() & interface ) { // Check if we need to enable debug logging for the INDI drivers. ISwitchVectorProperty * debugSP = prop->getSwitch(); debugSP->sp[0].s = ISS_ON; debugSP->sp[1].s = ISS_OFF; deviceInterface->getDriverInfo()->getClientManager()->sendNewSwitch(debugSP); } } // Handle debug levels for logging purposes if (!strcmp(prop->getName(), "DEBUG_LEVEL")) { uint16_t interface = deviceInterface->getBaseDevice()->getDriverInterface(); // Check if the logging option for the specific device class is on and if the device interface matches it. if ( opsLogs->getINDIDebugInterface() & interface ) { // Turn on everything ISwitchVectorProperty * debugLevel = prop->getSwitch(); for (int i = 0; i < debugLevel->nsp; i++) debugLevel->sp[i].s = ISS_ON; deviceInterface->getDriverInfo()->getClientManager()->sendNewSwitch(debugLevel); } } if (!strcmp(prop->getName(), "TELESCOPE_INFO") || !strcmp(prop->getName(), "TELESCOPE_SLEW_RATE") || !strcmp(prop->getName(), "TELESCOPE_PARK")) { ekosLiveClient.get()->message()->sendMounts(); ekosLiveClient.get()->message()->sendScopes(); } if (!strcmp(prop->getName(), "CCD_INFO") || !strcmp(prop->getName(), "CCD_TEMPERATURE") || !strcmp(prop->getName(), "CCD_ISO") || !strcmp(prop->getName(), "CCD_GAIN") || !strcmp(prop->getName(), "CCD_CONTROLS")) { ekosLiveClient.get()->message()->sendCameras(); ekosLiveClient.get()->media()->registerCameras(); } if (!strcmp(prop->getName(), "ABS_DOME_POSITION") || !strcmp(prop->getName(), "DOME_ABORT_MOTION") || !strcmp(prop->getName(), "DOME_PARK")) { ekosLiveClient.get()->message()->sendDomes(); } if (!strcmp(prop->getName(), "CAP_PARK") || !strcmp(prop->getName(), "FLAT_LIGHT_CONTROL")) { ekosLiveClient.get()->message()->sendCaps(); } if (!strcmp(prop->getName(), "FILTER_NAME")) ekosLiveClient.get()->message()->sendFilterWheels(); if (!strcmp(prop->getName(), "FILTER_NAME")) filterManager.data()->initFilterProperties(); if (!strcmp(prop->getName(), "CONFIRM_FILTER_SET")) filterManager.data()->initFilterProperties(); if (!strcmp(prop->getName(), "CCD_INFO") || !strcmp(prop->getName(), "GUIDER_INFO")) { if (focusProcess.get() != nullptr) focusProcess->syncCCDInfo(); if (guideProcess.get() != nullptr) guideProcess->syncCCDInfo(); if (alignProcess.get() != nullptr) alignProcess->syncCCDInfo(); return; } if (!strcmp(prop->getName(), "TELESCOPE_INFO") && managedDevices.contains(KSTARS_TELESCOPE)) { if (guideProcess.get() != nullptr) { guideProcess->setTelescope(managedDevices[KSTARS_TELESCOPE]); //guideProcess->syncTelescopeInfo(); } if (alignProcess.get() != nullptr) { alignProcess->setTelescope(managedDevices[KSTARS_TELESCOPE]); //alignProcess->syncTelescopeInfo(); } if (mountProcess.get() != nullptr) { mountProcess->setTelescope(managedDevices[KSTARS_TELESCOPE]); //mountProcess->syncTelescopeInfo(); } return; } if (!strcmp(prop->getName(), "GUIDER_EXPOSURE")) { foreach (ISD::GDInterface * device, findDevices(KSTARS_CCD)) { if (!strcmp(device->getDeviceName(), prop->getDeviceName())) { initCapture(); initGuide(); useGuideHead = true; captureProcess->addGuideHead(device); guideProcess->addGuideHead(device); bool rc = false; if (Options::defaultGuideCCD().isEmpty() == false) rc = guideProcess->setCamera(Options::defaultGuideCCD()); if (rc == false) guideProcess->setCamera(QString(device->getDeviceName()) + QString(" Guider")); return; } } return; } if (!strcmp(prop->getName(), "CCD_FRAME_TYPE")) { if (captureProcess.get() != nullptr) { foreach (ISD::GDInterface * device, findDevices(KSTARS_CCD)) { if (!strcmp(device->getDeviceName(), prop->getDeviceName())) { captureProcess->syncFrameType(device); return; } } } return; } if (!strcmp(prop->getName(), "CCD_ISO")) { if (captureProcess.get() != nullptr) captureProcess->checkCCD(); return; } if (!strcmp(prop->getName(), "TELESCOPE_PARK") && managedDevices.contains(KSTARS_TELESCOPE)) { if (captureProcess.get() != nullptr) captureProcess->setTelescope(managedDevices[KSTARS_TELESCOPE]); if (mountProcess.get() != nullptr) mountProcess->setTelescope(managedDevices[KSTARS_TELESCOPE]); return; } /* if (!strcmp(prop->getName(), "FILTER_NAME")) { if (captureProcess.get() != nullptr) captureProcess->checkFilter(); if (focusProcess.get() != nullptr) focusProcess->checkFilter(); if (alignProcess.get() != nullptr) alignProcess->checkFilter(); return; } */ if (!strcmp(prop->getName(), "ASTROMETRY_SOLVER")) { foreach (ISD::GDInterface * device, genericDevices) { if (!strcmp(device->getDeviceName(), prop->getDeviceName())) { initAlign(); alignProcess->setAstrometryDevice(device); break; } } } if (!strcmp(prop->getName(), "ABS_ROTATOR_ANGLE")) { managedDevices[KSTARS_ROTATOR] = deviceInterface; if (captureProcess.get() != nullptr) captureProcess->setRotator(deviceInterface); if (alignProcess.get() != nullptr) alignProcess->setRotator(deviceInterface); } if (!strcmp(prop->getName(), "GPS_REFRESH")) { managedDevices.insertMulti(KSTARS_AUXILIARY, deviceInterface); if (mountProcess.get() != nullptr) mountProcess->setGPS(deviceInterface); } if (focusProcess.get() != nullptr && strstr(prop->getName(), "FOCUS_")) { focusProcess->checkFocuser(); } } QList Manager::getAllDevices() { QList deviceList; QMapIterator i(managedDevices); while (i.hasNext()) { i.next(); deviceList.append(i.value()); } return deviceList; } QList Manager::findDevices(DeviceFamily type) { QList deviceList; QMapIterator i(managedDevices); while (i.hasNext()) { i.next(); if (i.key() == type) deviceList.append(i.value()); } return deviceList; } void Manager::processTabChange() { QWidget * currentWidget = toolsWidget->currentWidget(); //if (focusProcess.get() != nullptr && currentWidget != focusProcess) //focusProcess->resetFrame(); if (alignProcess.get() && alignProcess.get() == currentWidget) { if (alignProcess->isEnabled() == false && captureProcess->isEnabled()) { if (managedDevices[KSTARS_CCD]->isConnected() && managedDevices.contains(KSTARS_TELESCOPE)) { if (alignProcess->isParserOK()) alignProcess->setEnabled(true); //#ifdef Q_OS_WIN else { // If current setting is remote astrometry and profile doesn't contain // remote astrometry, then we switch to online solver. Otherwise, the whole align // module remains disabled and the user cannot change re-enable it since he cannot select online solver ProfileInfo * pi = getCurrentProfile(); if (Options::solverType() == Ekos::Align::SOLVER_REMOTE && pi->aux1() != "Astrometry" && pi->aux2() != "Astrometry" && pi->aux3() != "Astrometry" && pi->aux4() != "Astrometry") { Options::setSolverType(Ekos::Align::SOLVER_ONLINE); alignModule()->setSolverType(Ekos::Align::SOLVER_ONLINE); alignProcess->setEnabled(true); } } //#endif } } alignProcess->checkCCD(); } else if (captureProcess.get() != nullptr && currentWidget == captureProcess.get()) { captureProcess->checkCCD(); } else if (focusProcess.get() != nullptr && currentWidget == focusProcess.get()) { focusProcess->checkCCD(); } else if (guideProcess.get() != nullptr && currentWidget == guideProcess.get()) { guideProcess->checkCCD(); } updateLog(); } void Manager::updateLog() { //if (enableLoggingCheck->isChecked() == false) //return; QWidget * currentWidget = toolsWidget->currentWidget(); if (currentWidget == setupTab) ekosLogOut->setPlainText(m_LogText.join("\n")); else if (currentWidget == alignProcess.get()) ekosLogOut->setPlainText(alignProcess->getLogText()); else if (currentWidget == captureProcess.get()) ekosLogOut->setPlainText(captureProcess->getLogText()); else if (currentWidget == focusProcess.get()) ekosLogOut->setPlainText(focusProcess->getLogText()); else if (currentWidget == guideProcess.get()) ekosLogOut->setPlainText(guideProcess->getLogText()); else if (currentWidget == mountProcess.get()) ekosLogOut->setPlainText(mountProcess->getLogText()); else if (currentWidget == schedulerProcess.get()) ekosLogOut->setPlainText(schedulerProcess->getLogText()); else if (currentWidget == observatoryProcess.get()) ekosLogOut->setPlainText(observatoryProcess->getLogText()); #ifdef Q_OS_OSX repaint(); //This is a band-aid for a bug in QT 5.10.0 #endif } void Manager::appendLogText(const QString &text) { m_LogText.insert(0, i18nc("log entry; %1 is the date, %2 is the text", "%1 %2", - QDateTime::currentDateTime().toString("yyyy-MM-ddThh:mm:ss"), text)); + KStarsData::Instance()->lt().toString("yyyy-MM-ddThh:mm:ss"), text)); qCInfo(KSTARS_EKOS) << text; emit newLog(text); updateLog(); } void Manager::clearLog() { QWidget * currentWidget = toolsWidget->currentWidget(); if (currentWidget == setupTab) { m_LogText.clear(); updateLog(); } else if (currentWidget == alignProcess.get()) alignProcess->clearLog(); else if (currentWidget == captureProcess.get()) captureProcess->clearLog(); else if (currentWidget == focusProcess.get()) focusProcess->clearLog(); else if (currentWidget == guideProcess.get()) guideProcess->clearLog(); else if (currentWidget == mountProcess.get()) mountProcess->clearLog(); else if (currentWidget == schedulerProcess.get()) schedulerProcess->clearLog(); else if (currentWidget == observatoryProcess.get()) observatoryProcess->clearLog(); } void Manager::initCapture() { if (captureProcess.get() != nullptr) return; captureProcess.reset(new Ekos::Capture()); captureProcess->setEnabled(false); int index = toolsWidget->addTab(captureProcess.get(), QIcon(":/icons/ekos_ccd.png"), ""); toolsWidget->tabBar()->setTabToolTip(index, i18nc("Charge-Coupled Device", "CCD")); if (Options::ekosLeftIcons()) { QTransform trans; trans.rotate(90); QIcon icon = toolsWidget->tabIcon(index); QPixmap pix = icon.pixmap(QSize(48, 48)); icon = QIcon(pix.transformed(trans)); toolsWidget->setTabIcon(index, icon); } connect(captureProcess.get(), &Ekos::Capture::newLog, this, &Ekos::Manager::updateLog); connect(captureProcess.get(), &Ekos::Capture::newStatus, this, &Ekos::Manager::updateCaptureStatus); connect(captureProcess.get(), &Ekos::Capture::newImage, this, &Ekos::Manager::updateCaptureProgress); connect(captureProcess.get(), &Ekos::Capture::newSequenceImage, [&](const QString & filename, const QString & previewFITS) { if (Options::useSummaryPreview() && QFile::exists(filename)) { if (Options::autoImageToFITS()) { if (previewFITS.isEmpty() == false) summaryPreview->loadFITS(previewFITS); } else summaryPreview->loadFITS(filename); } }); connect(captureProcess.get(), &Ekos::Capture::newExposureProgress, this, &Ekos::Manager::updateExposureProgress); captureGroup->setEnabled(true); sequenceProgress->setEnabled(true); captureProgress->setEnabled(true); imageProgress->setEnabled(true); captureProcess->setFilterManager(filterManager); if (!capturePI) { capturePI = new QProgressIndicator(captureProcess.get()); captureStatusLayout->insertWidget(0, capturePI); } foreach (ISD::GDInterface * device, findDevices(KSTARS_AUXILIARY)) { if (device->getBaseDevice()->getDriverInterface() & INDI::BaseDevice::DUSTCAP_INTERFACE) captureProcess->setDustCap(device); if (device->getBaseDevice()->getDriverInterface() & INDI::BaseDevice::LIGHTBOX_INTERFACE) captureProcess->setLightBox(device); } if (managedDevices.contains(KSTARS_DOME)) { captureProcess->setDome(managedDevices[KSTARS_DOME]); } if (managedDevices.contains(KSTARS_ROTATOR)) { captureProcess->setRotator(managedDevices[KSTARS_ROTATOR]); } connectModules(); emit newModule("Capture"); } void Manager::initAlign() { if (alignProcess.get() != nullptr) return; alignProcess.reset(new Ekos::Align(currentProfile)); double primaryScopeFL = 0, primaryScopeAperture = 0, guideScopeFL = 0, guideScopeAperture = 0; getCurrentProfileTelescopeInfo(primaryScopeFL, primaryScopeAperture, guideScopeFL, guideScopeAperture); alignProcess->setTelescopeInfo(primaryScopeFL, primaryScopeAperture, guideScopeFL, guideScopeAperture); alignProcess->setEnabled(false); int index = toolsWidget->addTab(alignProcess.get(), QIcon(":/icons/ekos_align.png"), ""); toolsWidget->tabBar()->setTabToolTip(index, i18n("Align")); connect(alignProcess.get(), &Ekos::Align::newLog, this, &Ekos::Manager::updateLog); if (Options::ekosLeftIcons()) { QTransform trans; trans.rotate(90); QIcon icon = toolsWidget->tabIcon(index); QPixmap pix = icon.pixmap(QSize(48, 48)); icon = QIcon(pix.transformed(trans)); toolsWidget->setTabIcon(index, icon); } alignProcess->setFilterManager(filterManager); if (managedDevices.contains(KSTARS_DOME)) { alignProcess->setDome(managedDevices[KSTARS_DOME]); } if (managedDevices.contains(KSTARS_ROTATOR)) { alignProcess->setRotator(managedDevices[KSTARS_ROTATOR]); } connectModules(); emit newModule("Align"); } void Manager::initFocus() { if (focusProcess.get() != nullptr) return; focusProcess.reset(new Ekos::Focus()); int index = toolsWidget->addTab(focusProcess.get(), QIcon(":/icons/ekos_focus.png"), ""); toolsWidget->tabBar()->setTabToolTip(index, i18n("Focus")); // Focus <---> Manager connections connect(focusProcess.get(), &Ekos::Focus::newLog, this, &Ekos::Manager::updateLog); connect(focusProcess.get(), &Ekos::Focus::newStatus, this, &Ekos::Manager::setFocusStatus); connect(focusProcess.get(), &Ekos::Focus::newStarPixmap, this, &Ekos::Manager::updateFocusStarPixmap); connect(focusProcess.get(), &Ekos::Focus::newProfilePixmap, this, &Ekos::Manager::updateFocusProfilePixmap); connect(focusProcess.get(), &Ekos::Focus::newHFR, this, &Ekos::Manager::updateCurrentHFR); // Focus <---> Filter Manager connections focusProcess->setFilterManager(filterManager); connect(filterManager.data(), &Ekos::FilterManager::checkFocus, focusProcess.get(), &Ekos::Focus::checkFocus, Qt::UniqueConnection); connect(focusProcess.get(), &Ekos::Focus::newStatus, filterManager.data(), &Ekos::FilterManager::setFocusStatus, Qt::UniqueConnection); connect(filterManager.data(), &Ekos::FilterManager::newFocusOffset, focusProcess.get(), &Ekos::Focus::adjustFocusOffset, Qt::UniqueConnection); connect(focusProcess.get(), &Ekos::Focus::focusPositionAdjusted, filterManager.data(), &Ekos::FilterManager::setFocusOffsetComplete, Qt::UniqueConnection); connect(focusProcess.get(), &Ekos::Focus::absolutePositionChanged, filterManager.data(), &Ekos::FilterManager::setFocusAbsolutePosition, Qt::UniqueConnection); if (Options::ekosLeftIcons()) { QTransform trans; trans.rotate(90); QIcon icon = toolsWidget->tabIcon(index); QPixmap pix = icon.pixmap(QSize(48, 48)); icon = QIcon(pix.transformed(trans)); toolsWidget->setTabIcon(index, icon); } focusGroup->setEnabled(true); if (!focusPI) { focusPI = new QProgressIndicator(focusProcess.get()); focusStatusLayout->insertWidget(0, focusPI); } connectModules(); emit newModule("Focus"); } void Manager::updateCurrentHFR(double newHFR, int position) { currentHFR->setText(QString("%1").arg(newHFR, 0, 'f', 2) + " px"); QJsonObject cStatus = { {"hfr", newHFR}, {"pos", position} }; ekosLiveClient.get()->message()->updateFocusStatus(cStatus); } void Manager::updateSigmas(double ra, double de) { errRA->setText(QString::number(ra, 'f', 2) + "\""); errDEC->setText(QString::number(de, 'f', 2) + "\""); QJsonObject cStatus = { {"rarms", ra}, {"derms", de} }; ekosLiveClient.get()->message()->updateGuideStatus(cStatus); } void Manager::initMount() { if (mountProcess.get() != nullptr) return; mountProcess.reset(new Ekos::Mount()); int index = toolsWidget->addTab(mountProcess.get(), QIcon(":/icons/ekos_mount.png"), ""); toolsWidget->tabBar()->setTabToolTip(index, i18n("Mount")); connect(mountProcess.get(), &Ekos::Mount::newLog, this, &Ekos::Manager::updateLog); connect(mountProcess.get(), &Ekos::Mount::newCoords, this, &Ekos::Manager::updateMountCoords); connect(mountProcess.get(), &Ekos::Mount::newStatus, this, &Ekos::Manager::updateMountStatus); connect(mountProcess.get(), &Ekos::Mount::newTarget, [&](const QString & target) { mountTarget->setText(target); ekosLiveClient.get()->message()->updateMountStatus(QJsonObject({{"target", target}})); }); connect(mountProcess.get(), &Ekos::Mount::slewRateChanged, [&](int slewRate) { QJsonObject status = { { "slewRate", slewRate} }; ekosLiveClient.get()->message()->updateMountStatus(status); } ); foreach (ISD::GDInterface * device, findDevices(KSTARS_AUXILIARY)) { if (device->getBaseDevice()->getDriverInterface() & INDI::BaseDevice::GPS_INTERFACE) mountProcess->setGPS(device); } if (Options::ekosLeftIcons()) { QTransform trans; trans.rotate(90); QIcon icon = toolsWidget->tabIcon(index); QPixmap pix = icon.pixmap(QSize(48, 48)); icon = QIcon(pix.transformed(trans)); toolsWidget->setTabIcon(index, icon); } if (!mountPI) { mountPI = new QProgressIndicator(mountProcess.get()); mountStatusLayout->insertWidget(0, mountPI); } mountGroup->setEnabled(true); connectModules(); emit newModule("Mount"); } void Manager::initGuide() { if (guideProcess.get() == nullptr) { guideProcess.reset(new Ekos::Guide()); double primaryScopeFL = 0, primaryScopeAperture = 0, guideScopeFL = 0, guideScopeAperture = 0; getCurrentProfileTelescopeInfo(primaryScopeFL, primaryScopeAperture, guideScopeFL, guideScopeAperture); // Save telescope info in mount driver guideProcess->setTelescopeInfo(primaryScopeFL, primaryScopeAperture, guideScopeFL, guideScopeAperture); } //if ( (haveGuider || ccdCount > 1 || useGuideHead) && useST4 && toolsWidget->indexOf(guideProcess) == -1) if ((findDevices(KSTARS_CCD).isEmpty() == false || useGuideHead) && useST4 && toolsWidget->indexOf(guideProcess.get()) == -1) { //if (mount && mount->isConnected()) if (managedDevices.contains(KSTARS_TELESCOPE) && managedDevices[KSTARS_TELESCOPE]->isConnected()) guideProcess->setTelescope(managedDevices[KSTARS_TELESCOPE]); int index = toolsWidget->addTab(guideProcess.get(), QIcon(":/icons/ekos_guide.png"), ""); toolsWidget->tabBar()->setTabToolTip(index, i18n("Guide")); connect(guideProcess.get(), &Ekos::Guide::newLog, this, &Ekos::Manager::updateLog); guideGroup->setEnabled(true); if (!guidePI) { guidePI = new QProgressIndicator(guideProcess.get()); guideStatusLayout->insertWidget(0, guidePI); } connect(guideProcess.get(), &Ekos::Guide::newStatus, this, &Ekos::Manager::updateGuideStatus); connect(guideProcess.get(), &Ekos::Guide::newStarPixmap, this, &Ekos::Manager::updateGuideStarPixmap); connect(guideProcess.get(), &Ekos::Guide::newProfilePixmap, this, &Ekos::Manager::updateGuideProfilePixmap); connect(guideProcess.get(), &Ekos::Guide::newAxisSigma, this, &Ekos::Manager::updateSigmas); if (Options::ekosLeftIcons()) { QTransform trans; trans.rotate(90); QIcon icon = toolsWidget->tabIcon(index); QPixmap pix = icon.pixmap(QSize(48, 48)); icon = QIcon(pix.transformed(trans)); toolsWidget->setTabIcon(index, icon); } } connectModules(); emit newModule("Guide"); } void Manager::initDome() { if (domeProcess.get() != nullptr) return; domeProcess.reset(new Ekos::Dome()); connect(domeProcess.get(), &Ekos::Dome::newStatus, [&](ISD::Dome::Status newStatus) { QJsonObject status = { { "status", ISD::Dome::getStatusString(newStatus)} }; ekosLiveClient.get()->message()->updateDomeStatus(status); }); connect(domeProcess.get(), &Ekos::Dome::azimuthPositionChanged, [&](double pos) { QJsonObject status = { { "az", pos} }; ekosLiveClient.get()->message()->updateDomeStatus(status); }); initObservatory(nullptr, domeProcess.get()); emit newModule("Dome"); ekosLiveClient->message()->sendDomes(); } void Manager::initWeather() { if (weatherProcess.get() != nullptr) return; weatherProcess.reset(new Ekos::Weather()); initObservatory(weatherProcess.get(), nullptr); emit newModule("Weather"); } void Manager::initObservatory(Weather *weather, Dome *dome) { if (observatoryProcess.get() == nullptr) { // Initialize the Observatory Module observatoryProcess.reset(new Ekos::Observatory()); int index = toolsWidget->addTab(observatoryProcess.get(), QIcon(":/icons/ekos_observatory.png"), ""); toolsWidget->tabBar()->setTabToolTip(index, i18n("Observatory")); connect(observatoryProcess.get(), &Ekos::Observatory::newLog, this, &Ekos::Manager::updateLog); } Observatory *obs = observatoryProcess.get(); if (weather != nullptr) obs->getWeatherModel()->initModel(weather); if (dome != nullptr) obs->getDomeModel()->initModel(dome); emit newModule("Observatory"); } void Manager::initDustCap() { if (dustCapProcess.get() != nullptr) return; dustCapProcess.reset(new Ekos::DustCap()); connect(dustCapProcess.get(), &Ekos::DustCap::newStatus, [&](ISD::DustCap::Status newStatus) { QJsonObject status = { { "status", ISD::DustCap::getStatusString(newStatus)} }; ekosLiveClient.get()->message()->updateCapStatus(status); }); connect(dustCapProcess.get(), &Ekos::DustCap::lightToggled, [&](bool enabled) { QJsonObject status = { { "lightS", enabled} }; ekosLiveClient.get()->message()->updateCapStatus(status); }); connect(dustCapProcess.get(), &Ekos::DustCap::lightIntensityChanged, [&](uint16_t value) { QJsonObject status = { { "lightB", value} }; ekosLiveClient.get()->message()->updateCapStatus(status); }); emit newModule("DustCap"); ekosLiveClient->message()->sendCaps(); } void Manager::setST4(ISD::ST4 * st4Driver) { appendLogText(i18n("Guider port from %1 is ready.", st4Driver->getDeviceName())); useST4 = true; initGuide(); guideProcess->addST4(st4Driver); if (Options::defaultST4Driver().isEmpty() == false) guideProcess->setST4(Options::defaultST4Driver()); } void Manager::removeTabs() { disconnect(toolsWidget, &QTabWidget::currentChanged, this, &Ekos::Manager::processTabChange); for (int i = 2; i < toolsWidget->count(); i++) toolsWidget->removeTab(i); alignProcess.reset(); captureProcess.reset(); focusProcess.reset(); guideProcess.reset(); mountProcess.reset(); domeProcess.reset(); weatherProcess.reset(); observatoryProcess.reset(); dustCapProcess.reset(); managedDevices.clear(); connect(toolsWidget, &QTabWidget::currentChanged, this, &Ekos::Manager::processTabChange, Qt::UniqueConnection); } bool Manager::isRunning(const QString &process) { QProcess ps; #ifdef Q_OS_OSX ps.start("pgrep", QStringList() << process); ps.waitForFinished(); QString output = ps.readAllStandardOutput(); return output.length() > 0; #else ps.start("ps", QStringList() << "-o" << "comm" << "--no-headers" << "-C" << process); ps.waitForFinished(); QString output = ps.readAllStandardOutput(); return output.contains(process); #endif } void Manager::addObjectToScheduler(SkyObject * object) { if (schedulerProcess.get() != nullptr) schedulerProcess->addObject(object); } QString Manager::getCurrentJobName() { return schedulerProcess->getCurrentJobName(); } bool Manager::setProfile(const QString &profileName) { int index = profileCombo->findText(profileName); if (index < 0) return false; profileCombo->setCurrentIndex(index); return true; } void Manager::editNamedProfile(const QJsonObject &profileInfo) { ProfileEditor editor(this); setProfile(profileInfo["name"].toString()); currentProfile = getCurrentProfile(); editor.setPi(currentProfile); editor.setSettings(profileInfo); editor.saveProfile(); } void Manager::addNamedProfile(const QJsonObject &profileInfo) { ProfileEditor editor(this); editor.setSettings(profileInfo); editor.saveProfile(); profiles.clear(); loadProfiles(); profileCombo->setCurrentIndex(profileCombo->count() - 1); currentProfile = getCurrentProfile(); } void Manager::deleteNamedProfile(const QString &name) { currentProfile = getCurrentProfile(); for (auto &pi : profiles) { // Do not delete an actively running profile // Do not delete simulator profile if (pi->name == "Simulators" || pi->name != name || (pi.get() == currentProfile && ekosStatus() != Idle)) continue; KStarsData::Instance()->userdb()->DeleteProfile(pi.get()); profiles.clear(); loadProfiles(); currentProfile = getCurrentProfile(); return; } } QJsonObject Manager::getNamedProfile(const QString &name) { QJsonObject profileInfo; // Get current profile for (auto &pi : profiles) { if (name == pi->name) return pi->toJson(); } return QJsonObject(); } QStringList Manager::getProfiles() { QStringList profiles; for (int i = 0; i < profileCombo->count(); i++) profiles << profileCombo->itemText(i); return profiles; } void Manager::addProfile() { ProfileEditor editor(this); if (editor.exec() == QDialog::Accepted) { profiles.clear(); loadProfiles(); profileCombo->setCurrentIndex(profileCombo->count() - 1); } currentProfile = getCurrentProfile(); } void Manager::editProfile() { ProfileEditor editor(this); currentProfile = getCurrentProfile(); editor.setPi(currentProfile); if (editor.exec() == QDialog::Accepted) { int currentIndex = profileCombo->currentIndex(); profiles.clear(); loadProfiles(); profileCombo->setCurrentIndex(currentIndex); } currentProfile = getCurrentProfile(); } void Manager::deleteProfile() { currentProfile = getCurrentProfile(); if (currentProfile->name == "Simulators") return; auto executeDeleteProfile = [&]() { KStarsData::Instance()->userdb()->DeleteProfile(currentProfile); profiles.clear(); loadProfiles(); currentProfile = getCurrentProfile(); }; connect(KSMessageBox::Instance(), &KSMessageBox::accepted, this, [this, executeDeleteProfile]() { //QObject::disconnect(KSMessageBox::Instance(), &KSMessageBox::accepted, this, nullptr); KSMessageBox::Instance()->disconnect(this); executeDeleteProfile(); }); KSMessageBox::Instance()->questionYesNo(i18n("Are you sure you want to delete the profile?"), i18n("Confirm Delete")); } void Manager::wizardProfile() { ProfileWizard wz; if (wz.exec() != QDialog::Accepted) return; ProfileEditor editor(this); editor.setProfileName(wz.profileName); editor.setAuxDrivers(wz.selectedAuxDrivers()); if (wz.useInternalServer == false) editor.setHostPort(wz.host, wz.port); editor.setWebManager(wz.useWebManager); editor.setGuiderType(wz.selectedExternalGuider()); // Disable connection options editor.setConnectionOptionsEnabled(false); if (editor.exec() == QDialog::Accepted) { profiles.clear(); loadProfiles(); profileCombo->setCurrentIndex(profileCombo->count() - 1); } currentProfile = getCurrentProfile(); } ProfileInfo * Manager::getCurrentProfile() { ProfileInfo * currProfile = nullptr; // Get current profile for (auto &pi : profiles) { if (profileCombo->currentText() == pi->name) { currProfile = pi.get(); break; } } return currProfile; } void Manager::updateProfileLocation(ProfileInfo * pi) { if (pi->city.isEmpty() == false) { bool cityFound = KStars::Instance()->setGeoLocation(pi->city, pi->province, pi->country); if (cityFound) appendLogText(i18n("Site location updated to %1.", KStarsData::Instance()->geo()->fullName())); else appendLogText(i18n("Failed to update site location to %1. City not found.", KStarsData::Instance()->geo()->fullName())); } } void Manager::updateMountStatus(ISD::Telescope::Status status) { static ISD::Telescope::Status lastStatus = ISD::Telescope::MOUNT_IDLE; if (status == lastStatus) return; lastStatus = status; mountStatus->setText(dynamic_cast(managedDevices[KSTARS_TELESCOPE])->getStatusString(status)); mountStatus->setStyleSheet(QString()); switch (status) { case ISD::Telescope::MOUNT_PARKING: case ISD::Telescope::MOUNT_SLEWING: case ISD::Telescope::MOUNT_MOVING: mountPI->setColor(QColor(KStarsData::Instance()->colorScheme()->colorNamed("TargetColor"))); if (mountPI->isAnimated() == false) mountPI->startAnimation(); break; case ISD::Telescope::MOUNT_TRACKING: mountPI->setColor(Qt::darkGreen); if (mountPI->isAnimated() == false) mountPI->startAnimation(); break; case ISD::Telescope::MOUNT_PARKED: mountStatus->setStyleSheet("font-weight:bold;background-color:red;border:2px solid black;"); if (mountPI->isAnimated()) mountPI->stopAnimation(); break; default: if (mountPI->isAnimated()) mountPI->stopAnimation(); } QJsonObject cStatus = { {"status", mountStatus->text()} }; ekosLiveClient.get()->message()->updateMountStatus(cStatus); } void Manager::updateMountCoords(const QString &ra, const QString &dec, const QString &az, const QString &alt) { raOUT->setText(ra); decOUT->setText(dec); azOUT->setText(az); altOUT->setText(alt); QJsonObject cStatus = { {"ra", dms::fromString(ra, false).Degrees()}, {"de", dms::fromString(dec, true).Degrees()}, {"az", dms::fromString(az, true).Degrees()}, {"at", dms::fromString(alt, true).Degrees()}, }; ekosLiveClient.get()->message()->updateMountStatus(cStatus); } void Manager::updateCaptureStatus(Ekos::CaptureState status) { captureStatus->setText(Ekos::getCaptureStatusString(status)); captureProgress->setValue(captureProcess->getProgressPercentage()); overallCountDown.setHMS(0, 0, 0); overallCountDown = overallCountDown.addSecs(captureProcess->getOverallRemainingTime()); sequenceCountDown.setHMS(0, 0, 0); sequenceCountDown = sequenceCountDown.addSecs(captureProcess->getActiveJobRemainingTime()); if (status != Ekos::CAPTURE_ABORTED && status != Ekos::CAPTURE_COMPLETE && status != Ekos::CAPTURE_IDLE) { if (status == Ekos::CAPTURE_CAPTURING) capturePI->setColor(Qt::darkGreen); else capturePI->setColor(QColor(KStarsData::Instance()->colorScheme()->colorNamed("TargetColor"))); if (capturePI->isAnimated() == false) { capturePI->startAnimation(); countdownTimer.start(); } } else { if (capturePI->isAnimated()) { capturePI->stopAnimation(); countdownTimer.stop(); if (focusStatus->text() == "Complete") { if (focusPI->isAnimated()) focusPI->stopAnimation(); } imageProgress->setValue(0); sequenceLabel->setText(i18n("Sequence")); imageRemainingTime->setText("--:--:--"); overallRemainingTime->setText("--:--:--"); sequenceRemainingTime->setText("--:--:--"); } } QJsonObject cStatus = { {"status", captureStatus->text()}, {"seqt", sequenceRemainingTime->text()}, {"ovt", overallRemainingTime->text()} }; ekosLiveClient.get()->message()->updateCaptureStatus(cStatus); } void Manager::updateCaptureProgress(Ekos::SequenceJob * job) { // Image is set to nullptr only on initial capture start up int completed = job->getCompleted(); // if (job->getUploadMode() == ISD::CCD::UPLOAD_LOCAL) // completed = job->getCompleted() + 1; // else // completed = job->isPreview() ? job->getCompleted() : job->getCompleted() + 1; if (job->isPreview() == false) { sequenceLabel->setText(QString("Job # %1/%2 %3 (%4/%5)") .arg(captureProcess->getActiveJobID() + 1) .arg(captureProcess->getJobCount()) .arg(job->getFullPrefix()) .arg(completed) .arg(job->getCount())); } else sequenceLabel->setText(i18n("Preview")); sequenceProgress->setRange(0, job->getCount()); sequenceProgress->setValue(completed); QJsonObject status = { {"seqv", completed}, {"seqr", job->getCount()}, {"seql", sequenceLabel->text()} }; ekosLiveClient.get()->message()->updateCaptureStatus(status); if (job->getStatus() == SequenceJob::JOB_BUSY) { QString uuid = QUuid::createUuid().toString(); uuid = uuid.remove(QRegularExpression("[-{}]")); // FITSView *image = job->getActiveChip()->getImageView(FITS_NORMAL); // ekosLiveClient.get()->media()->sendPreviewImage(image, uuid); // ekosLiveClient.get()->cloud()->sendPreviewImage(image, uuid); QString filename = job->property("filename").toString(); ekosLiveClient.get()->media()->sendPreviewImage(filename, uuid); if (job->isPreview() == false) ekosLiveClient.get()->cloud()->sendPreviewImage(filename, uuid); } } void Manager::updateExposureProgress(Ekos::SequenceJob * job) { imageCountDown.setHMS(0, 0, 0); imageCountDown = imageCountDown.addSecs(job->getExposeLeft()); if (imageCountDown.hour() == 23) imageCountDown.setHMS(0, 0, 0); imageProgress->setRange(0, job->getExposure()); imageProgress->setValue(job->getExposeLeft()); imageRemainingTime->setText(imageCountDown.toString("hh:mm:ss")); QJsonObject status { {"expv", job->getExposeLeft()}, {"expr", job->getExposure()} }; ekosLiveClient.get()->message()->updateCaptureStatus(status); } void Manager::updateCaptureCountDown() { overallCountDown = overallCountDown.addSecs(-1); if (overallCountDown.hour() == 23) overallCountDown.setHMS(0, 0, 0); sequenceCountDown = sequenceCountDown.addSecs(-1); if (sequenceCountDown.hour() == 23) sequenceCountDown.setHMS(0, 0, 0); overallRemainingTime->setText(overallCountDown.toString("hh:mm:ss")); sequenceRemainingTime->setText(sequenceCountDown.toString("hh:mm:ss")); QJsonObject status = { {"seqt", sequenceRemainingTime->text()}, {"ovt", overallRemainingTime->text()} }; ekosLiveClient.get()->message()->updateCaptureStatus(status); } void Manager::updateFocusStarPixmap(QPixmap &starPixmap) { if (starPixmap.isNull()) return; focusStarPixmap.reset(new QPixmap(starPixmap)); focusStarImage->setPixmap(focusStarPixmap->scaled(focusStarImage->width(), focusStarImage->height(), Qt::KeepAspectRatio, Qt::SmoothTransformation)); } void Manager::updateFocusProfilePixmap(QPixmap &profilePixmap) { if (profilePixmap.isNull()) return; focusProfileImage->setPixmap(profilePixmap); } void Manager::setFocusStatus(Ekos::FocusState status) { focusStatus->setText(Ekos::getFocusStatusString(status)); if (status >= Ekos::FOCUS_PROGRESS) { focusPI->setColor(QColor(KStarsData::Instance()->colorScheme()->colorNamed("TargetColor"))); if (focusPI->isAnimated() == false) focusPI->startAnimation(); } else if (status == Ekos::FOCUS_COMPLETE && Options::enforceAutofocus() && captureProcess->getActiveJobID() != -1) { focusPI->setColor(Qt::darkGreen); if (focusPI->isAnimated() == false) focusPI->startAnimation(); } else { if (focusPI->isAnimated()) focusPI->stopAnimation(); } QJsonObject cStatus = { {"status", focusStatus->text()} }; ekosLiveClient.get()->message()->updateFocusStatus(cStatus); } void Manager::updateGuideStatus(Ekos::GuideState status) { guideStatus->setText(Ekos::getGuideStatusString(status)); switch (status) { case Ekos::GUIDE_IDLE: case Ekos::GUIDE_CALIBRATION_ERROR: case Ekos::GUIDE_ABORTED: case Ekos::GUIDE_SUSPENDED: case Ekos::GUIDE_DITHERING_ERROR: case Ekos::GUIDE_CALIBRATION_SUCESS: if (guidePI->isAnimated()) guidePI->stopAnimation(); break; case Ekos::GUIDE_CALIBRATING: guidePI->setColor(QColor(KStarsData::Instance()->colorScheme()->colorNamed("TargetColor"))); if (guidePI->isAnimated() == false) guidePI->startAnimation(); break; case Ekos::GUIDE_GUIDING: guidePI->setColor(Qt::darkGreen); if (guidePI->isAnimated() == false) guidePI->startAnimation(); break; case Ekos::GUIDE_DITHERING: guidePI->setColor(QColor(KStarsData::Instance()->colorScheme()->colorNamed("TargetColor"))); if (guidePI->isAnimated() == false) guidePI->startAnimation(); break; case Ekos::GUIDE_DITHERING_SUCCESS: guidePI->setColor(Qt::darkGreen); if (guidePI->isAnimated() == false) guidePI->startAnimation(); break; default: if (guidePI->isAnimated()) guidePI->stopAnimation(); break; } QJsonObject cStatus = { {"status", guideStatus->text()} }; ekosLiveClient.get()->message()->updateGuideStatus(cStatus); } void Manager::updateGuideStarPixmap(QPixmap &starPix) { if (starPix.isNull()) return; guideStarPixmap.reset(new QPixmap(starPix)); guideStarImage->setPixmap(guideStarPixmap->scaled(guideStarImage->width(), guideStarImage->height(), Qt::KeepAspectRatio, Qt::SmoothTransformation)); } void Manager::updateGuideProfilePixmap(QPixmap &profilePix) { if (profilePix.isNull()) return; guideProfileImage->setPixmap(profilePix); } void Manager::setTarget(SkyObject * o) { mountTarget->setText(o->name()); ekosLiveClient.get()->message()->updateMountStatus(QJsonObject({{"target", o->name()}})); } void Manager::showEkosOptions() { QWidget * currentWidget = toolsWidget->currentWidget(); if (alignProcess.get() && alignProcess.get() == currentWidget) { KConfigDialog * alignSettings = KConfigDialog::exists("alignsettings"); if (alignSettings) { alignSettings->setEnabled(true); alignSettings->show(); } return; } if (guideProcess.get() && guideProcess.get() == currentWidget) { KConfigDialog::showDialog("guidesettings"); return; } if (ekosOptionsWidget == nullptr) { optionsB->click(); } else if (KConfigDialog::showDialog("settings")) { KConfigDialog * cDialog = KConfigDialog::exists("settings"); cDialog->setCurrentPage(ekosOptionsWidget); } } void Manager::getCurrentProfileTelescopeInfo(double &primaryFocalLength, double &primaryAperture, double &guideFocalLength, double &guideAperture) { ProfileInfo * pi = getCurrentProfile(); if (pi) { int primaryScopeID = 0, guideScopeID = 0; primaryScopeID = pi->primaryscope; guideScopeID = pi->guidescope; if (primaryScopeID > 0 || guideScopeID > 0) { // Get all OAL equipment filter list QList m_scopeList; KStarsData::Instance()->userdb()->GetAllScopes(m_scopeList); foreach(OAL::Scope * oneScope, m_scopeList) { if (oneScope->id().toInt() == primaryScopeID) { primaryFocalLength = oneScope->focalLength(); primaryAperture = oneScope->aperture(); } if (oneScope->id().toInt() == guideScopeID) { guideFocalLength = oneScope->focalLength(); guideAperture = oneScope->aperture(); } } } } } void Manager::updateDebugInterfaces() { KSUtils::Logging::SyncFilterRules(); for (ISD::GDInterface * device : genericDevices) { INDI::Property * debugProp = device->getProperty("DEBUG"); ISwitchVectorProperty * debugSP = nullptr; if (debugProp) debugSP = debugProp->getSwitch(); else continue; // Check if the debug interface matches the driver device class if ( ( opsLogs->getINDIDebugInterface() & device->getBaseDevice()->getDriverInterface() ) && debugSP->sp[0].s != ISS_ON) { debugSP->sp[0].s = ISS_ON; debugSP->sp[1].s = ISS_OFF; device->getDriverInfo()->getClientManager()->sendNewSwitch(debugSP); appendLogText(i18n("Enabling debug logging for %1...", device->getDeviceName())); } else if ( !( opsLogs->getINDIDebugInterface() & device->getBaseDevice()->getDriverInterface() ) && debugSP->sp[0].s != ISS_OFF) { debugSP->sp[0].s = ISS_OFF; debugSP->sp[1].s = ISS_ON; device->getDriverInfo()->getClientManager()->sendNewSwitch(debugSP); appendLogText(i18n("Disabling debug logging for %1...", device->getDeviceName())); } if (opsLogs->isINDISettingsChanged()) device->setConfig(SAVE_CONFIG); } } void Manager::watchDebugProperty(ISwitchVectorProperty * svp) { if (!strcmp(svp->name, "DEBUG")) { ISD::GenericDevice * deviceInterface = qobject_cast(sender()); // We don't process pure general interfaces if (deviceInterface->getBaseDevice()->getDriverInterface() == INDI::BaseDevice::GENERAL_INTERFACE) return; // If debug was turned off, but our logging policy requires it then turn it back on. // We turn on debug logging if AT LEAST one driver interface is selected by the logging settings if (svp->s == IPS_OK && svp->sp[0].s == ISS_OFF && (opsLogs->getINDIDebugInterface() & deviceInterface->getBaseDevice()->getDriverInterface())) { svp->sp[0].s = ISS_ON; svp->sp[1].s = ISS_OFF; deviceInterface->getDriverInfo()->getClientManager()->sendNewSwitch(svp); appendLogText(i18n("Re-enabling debug logging for %1...", deviceInterface->getDeviceName())); } // To turn off debug logging, NONE of the driver interfaces should be enabled in logging settings. // For example, if we have CCD+FilterWheel device and CCD + Filter Wheel logging was turned on in // the log settings, then if the user turns off only CCD logging, the debug logging is NOT // turned off until he turns off Filter Wheel logging as well. else if (svp->s == IPS_OK && svp->sp[0].s == ISS_ON && !(opsLogs->getINDIDebugInterface() & deviceInterface->getBaseDevice()->getDriverInterface())) { svp->sp[0].s = ISS_OFF; svp->sp[1].s = ISS_ON; deviceInterface->getDriverInfo()->getClientManager()->sendNewSwitch(svp); appendLogText(i18n("Re-disabling debug logging for %1...", deviceInterface->getDeviceName())); } } } void Manager::announceEvent(const QString &message, KSNotification::EventType event) { ekosLiveClient.get()->message()->sendEvent(message, event); } void Manager::connectModules() { // Guide <---> Capture connections if (captureProcess.get() && guideProcess.get()) { captureProcess.get()->disconnect(guideProcess.get()); guideProcess.get()->disconnect(captureProcess.get()); // Guide Limits connect(guideProcess.get(), &Ekos::Guide::newStatus, captureProcess.get(), &Ekos::Capture::setGuideStatus, Qt::UniqueConnection); connect(guideProcess.get(), &Ekos::Guide::newAxisDelta, captureProcess.get(), &Ekos::Capture::setGuideDeviation); // Dithering connect(captureProcess.get(), &Ekos::Capture::newStatus, guideProcess.get(), &Ekos::Guide::setCaptureStatus, Qt::UniqueConnection); // Guide Head connect(captureProcess.get(), &Ekos::Capture::suspendGuiding, guideProcess.get(), &Ekos::Guide::suspend, Qt::UniqueConnection); connect(captureProcess.get(), &Ekos::Capture::resumeGuiding, guideProcess.get(), &Ekos::Guide::resume, Qt::UniqueConnection); connect(guideProcess.get(), &Ekos::Guide::guideChipUpdated, captureProcess.get(), &Ekos::Capture::setGuideChip, Qt::UniqueConnection); // Meridian Flip connect(captureProcess.get(), &Ekos::Capture::meridianFlipStarted, guideProcess.get(), &Ekos::Guide::abort, Qt::UniqueConnection); connect(captureProcess.get(), &Ekos::Capture::meridianFlipCompleted, guideProcess.get(), [&]() { if (Options::resetGuideCalibration()) guideProcess->clearCalibration(); guideProcess->guide(); }); } // Guide <---> Mount connections if (guideProcess.get() && mountProcess.get()) { // Parking connect(mountProcess.get(), &Ekos::Mount::newStatus, guideProcess.get(), &Ekos::Guide::setMountStatus, Qt::UniqueConnection); } // Focus <---> Guide connections if (guideProcess.get() && focusProcess.get()) { // Suspend connect(focusProcess.get(), &Ekos::Focus::suspendGuiding, guideProcess.get(), &Ekos::Guide::suspend, Qt::UniqueConnection); connect(focusProcess.get(), &Ekos::Focus::resumeGuiding, guideProcess.get(), &Ekos::Guide::resume, Qt::UniqueConnection); } // Capture <---> Focus connections if (captureProcess.get() && focusProcess.get()) { // Check focus HFR value connect(captureProcess.get(), &Ekos::Capture::checkFocus, focusProcess.get(), &Ekos::Focus::checkFocus, Qt::UniqueConnection); // Reset Focus connect(captureProcess.get(), &Ekos::Capture::resetFocus, focusProcess.get(), &Ekos::Focus::resetFrame, Qt::UniqueConnection); // New Focus Status connect(focusProcess.get(), &Ekos::Focus::newStatus, captureProcess.get(), &Ekos::Capture::setFocusStatus, Qt::UniqueConnection); // New Focus HFR connect(focusProcess.get(), &Ekos::Focus::newHFR, captureProcess.get(), &Ekos::Capture::setHFR, Qt::UniqueConnection); } // Capture <---> Align connections if (captureProcess.get() && alignProcess.get()) { // Alignment flag connect(alignProcess.get(), &Ekos::Align::newStatus, captureProcess.get(), &Ekos::Capture::setAlignStatus, Qt::UniqueConnection); // Solver data connect(alignProcess.get(), &Ekos::Align::newSolverResults, captureProcess.get(), &Ekos::Capture::setAlignResults, Qt::UniqueConnection); // Capture Status connect(captureProcess.get(), &Ekos::Capture::newStatus, alignProcess.get(), &Ekos::Align::setCaptureStatus, Qt::UniqueConnection); } // Capture <---> Mount connections if (captureProcess.get() && mountProcess.get()) { // Meridian Flip states connect(captureProcess.get(), &Ekos::Capture::meridianFlipStarted, mountProcess.get(), &Ekos::Mount::disableAltLimits, Qt::UniqueConnection); connect(captureProcess.get(), &Ekos::Capture::meridianFlipCompleted, mountProcess.get(), &Ekos::Mount::enableAltLimits, Qt::UniqueConnection); connect(captureProcess.get(), &Ekos::Capture::newMeridianFlipStatus, mountProcess.get(), &Ekos::Mount::meridianFlipStatusChanged, Qt::UniqueConnection); connect(mountProcess.get(), &Ekos::Mount::newMeridianFlipStatus, captureProcess.get(), &Ekos::Capture::meridianFlipStatusChanged, Qt::UniqueConnection); // Mount Status connect(mountProcess.get(), &Ekos::Mount::newStatus, captureProcess.get(), &Ekos::Capture::setMountStatus, Qt::UniqueConnection); } // Capture <---> EkosLive connections if (captureProcess.get() && ekosLiveClient.get()) { captureProcess.get()->disconnect(ekosLiveClient.get()->message()); connect(captureProcess.get(), &Ekos::Capture::dslrInfoRequested, ekosLiveClient.get()->message(), &EkosLive::Message::requestDSLRInfo); connect(captureProcess.get(), &Ekos::Capture::sequenceChanged, ekosLiveClient.get()->message(), &EkosLive::Message::sendCaptureSequence); connect(captureProcess.get(), &Ekos::Capture::settingsUpdated, ekosLiveClient.get()->message(), &EkosLive::Message::sendCaptureSettings); } // Focus <---> Align connections if (focusProcess.get() && alignProcess.get()) { connect(focusProcess.get(), &Ekos::Focus::newStatus, alignProcess.get(), &Ekos::Align::setFocusStatus, Qt::UniqueConnection); } // Focus <---> Mount connections if (focusProcess.get() && mountProcess.get()) { connect(mountProcess.get(), &Ekos::Mount::newStatus, focusProcess.get(), &Ekos::Focus::setMountStatus, Qt::UniqueConnection); } // Mount <---> Align connections if (mountProcess.get() && alignProcess.get()) { connect(mountProcess.get(), &Ekos::Mount::newStatus, alignProcess.get(), &Ekos::Align::setMountStatus, Qt::UniqueConnection); } // Mount <---> Guide connections if (mountProcess.get() && guideProcess.get()) { connect(mountProcess.get(), &Ekos::Mount::pierSideChanged, guideProcess.get(), &Ekos::Guide::setPierSide, Qt::UniqueConnection); } // Focus <---> Align connections if (focusProcess.get() && alignProcess.get()) { connect(focusProcess.get(), &Ekos::Focus::newStatus, alignProcess.get(), &Ekos::Align::setFocusStatus, Qt::UniqueConnection); } // Align <--> EkosLive connections if (alignProcess.get() && ekosLiveClient.get()) { alignProcess.get()->disconnect(ekosLiveClient.get()->message()); alignProcess.get()->disconnect(ekosLiveClient.get()->media()); connect(alignProcess.get(), &Ekos::Align::newStatus, ekosLiveClient.get()->message(), &EkosLive::Message::setAlignStatus); connect(alignProcess.get(), &Ekos::Align::newSolution, ekosLiveClient.get()->message(), &EkosLive::Message::setAlignSolution); connect(alignProcess.get(), &Ekos::Align::newPAHStage, ekosLiveClient.get()->message(), &EkosLive::Message::setPAHStage); connect(alignProcess.get(), &Ekos::Align::newPAHMessage, ekosLiveClient.get()->message(), &EkosLive::Message::setPAHMessage); connect(alignProcess.get(), &Ekos::Align::PAHEnabled, ekosLiveClient.get()->message(), &EkosLive::Message::setPAHEnabled); connect(alignProcess.get(), &Ekos::Align::newImage, [&](FITSView * view) { ekosLiveClient.get()->media()->sendPreviewImage(view, QString()); }); connect(alignProcess.get(), &Ekos::Align::newFrame, ekosLiveClient.get()->media(), &EkosLive::Media::sendUpdatedFrame); connect(alignProcess.get(), &Ekos::Align::polarResultUpdated, ekosLiveClient.get()->message(), &EkosLive::Message::setPolarResults); connect(alignProcess.get(), &Ekos::Align::settingsUpdated, ekosLiveClient.get()->message(), &EkosLive::Message::sendAlignSettings); connect(alignProcess.get(), &Ekos::Align::newCorrectionVector, ekosLiveClient.get()->media(), &EkosLive::Media::setCorrectionVector); } } void Manager::setEkosLiveConnected(bool enabled) { ekosLiveClient.get()->setConnected(enabled); } void Manager::setEkosLiveConfig(bool onlineService, bool rememberCredentials, bool autoConnect) { ekosLiveClient.get()->setConfig(onlineService, rememberCredentials, autoConnect); } void Manager::setEkosLiveUser(const QString &username, const QString &password) { ekosLiveClient.get()->setUser(username, password); } bool Manager::ekosLiveStatus() { return ekosLiveClient.get()->isConnected(); } void Manager::syncActiveDevices() { for (auto oneDevice : genericDevices) { uint32_t devInterface = oneDevice->getDriverInterface(); if (devInterface & (INDI::BaseDevice::TELESCOPE_INTERFACE | INDI::BaseDevice::DOME_INTERFACE | INDI::BaseDevice::GPS_INTERFACE | INDI::BaseDevice::FILTER_INTERFACE)) { for (auto otherDevice : genericDevices) { if (otherDevice == oneDevice) continue; ITextVectorProperty *tvp = otherDevice->getBaseDevice()->getText("ACTIVE_DEVICES"); if (tvp) { IText *snoopProperty = nullptr; if (devInterface & INDI::BaseDevice::TELESCOPE_INTERFACE) snoopProperty = IUFindText(tvp, "ACTIVE_TELESCOPE"); else if (devInterface & INDI::BaseDevice::DOME_INTERFACE) snoopProperty = IUFindText(tvp, "ACTIVE_DOME"); else if (devInterface & INDI::BaseDevice::GPS_INTERFACE) snoopProperty = IUFindText(tvp, "ACTIVE_GPS"); else if (devInterface & INDI::BaseDevice::FILTER_INTERFACE) snoopProperty = IUFindText(tvp, "ACTIVE_FILTER"); if (snoopProperty && strcmp(snoopProperty->text, oneDevice->getDeviceName())) { IUSaveText(snoopProperty, oneDevice->getDeviceName()); otherDevice->getDriverInfo()->getClientManager()->sendNewText(tvp); } } } } } } } diff --git a/kstars/ekos/mount/mount.cpp b/kstars/ekos/mount/mount.cpp index 6816280c6..322887b44 100644 --- a/kstars/ekos/mount/mount.cpp +++ b/kstars/ekos/mount/mount.cpp @@ -1,1640 +1,1640 @@ /* Ekos Mount Module Copyright (C) 2015 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 "mount.h" #include #include #include #include #include #include #include "Options.h" #include "ksmessagebox.h" #include "indi/driverinfo.h" #include "indi/indicommon.h" #include "indi/clientmanager.h" #include "indi/indifilter.h" #include "mountadaptor.h" #include "ekos/manager.h" #include "kstars.h" #include "skymapcomposite.h" #include "kspaths.h" #include "dialogs/finddialog.h" #include "kstarsdata.h" #include "ksutils.h" #include #include extern const char *libindi_strings_context; #define UPDATE_DELAY 1000 #define ABORT_DISPATCH_LIMIT 3 namespace Ekos { Mount::Mount() { setupUi(this); new MountAdaptor(this); QDBusConnection::sessionBus().registerObject("/KStars/Ekos/Mount", this); // Set up DBus interfaces QPointer ekosInterface = new QDBusInterface("org.kde.kstars", "/KStars/Ekos", "org.kde.kstars.Ekos", QDBusConnection::sessionBus(), this); qDBusRegisterMetaType(); // 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))); currentTelescope = nullptr; abortDispatch = -1; minAltLimit->setValue(Options::minimumAltLimit()); maxAltLimit->setValue(Options::maximumAltLimit()); connect(minAltLimit, SIGNAL(editingFinished()), this, SLOT(saveLimits())); connect(maxAltLimit, SIGNAL(editingFinished()), this, SLOT(saveLimits())); connect(mountToolBoxB, SIGNAL(clicked()), this, SLOT(toggleMountToolBox())); connect(saveB, SIGNAL(clicked()), this, SLOT(save())); connect(clearAlignmentModelB, &QPushButton::clicked, this, [this]() { resetModel(); }); connect(clearParkingB, &QPushButton::clicked, this, [this]() { if (currentTelescope) currentTelescope->clearParking(); }); connect(purgeConfigB, &QPushButton::clicked, this, [this]() { if (currentTelescope) { if (KMessageBox::questionYesNo(KStars::Instance(), i18n("Are you sure you want to clear all mount configurations?"), i18n("Mount Configuration"), KStandardGuiItem::yes(), KStandardGuiItem::no(), "purge_mount_settings_dialog") == KMessageBox::Yes) { resetModel(); currentTelescope->clearParking(); currentTelescope->setConfig(PURGE_CONFIG); } } }); connect(enableLimitsCheck, SIGNAL(toggled(bool)), this, SLOT(enableAltitudeLimits(bool))); enableLimitsCheck->setChecked(Options::enableAltitudeLimits()); altLimitEnabled = enableLimitsCheck->isChecked(); // meridian flip meridianFlipCheckBox->setChecked(Options::executeMeridianFlip()); // Meridian Flip Unit meridianFlipDegreesR->setChecked(Options::meridianFlipUnitDegrees()); // This is always in hours double offset = Options::meridianFlipOffset(); // Hours --> Degrees if (meridianFlipDegreesR->isChecked()) offset *= 15.0; meridianFlipTimeBox->setValue(offset); connect(meridianFlipCheckBox, &QCheckBox::toggled, this, &Ekos::Mount::meridianFlipSetupChanged); connect(meridianFlipTimeBox, static_cast(&QDoubleSpinBox::valueChanged), this, &Ekos::Mount::meridianFlipSetupChanged); connect(meridianFlipDegreesR, &QRadioButton::toggled, this, [this]() { Options::setMeridianFlipUnitDegrees(meridianFlipDegreesR->isChecked()); // Hours ---> Degrees if (meridianFlipDegreesR->isChecked()) meridianFlipTimeBox->setValue(meridianFlipTimeBox->value() * 15.0); // Degrees --> Hours else meridianFlipTimeBox->setValue(rangeHA(meridianFlipTimeBox->value() / 15.0)); }); updateTimer.setInterval(UPDATE_DELAY); connect(&updateTimer, SIGNAL(timeout()), this, SLOT(updateTelescopeCoords())); everyDayCheck->setChecked(Options::parkEveryDay()); connect(everyDayCheck, &QCheckBox::toggled, this, [](bool toggled) { Options::setParkEveryDay(toggled); }); startupTimeEdit->setTime(QTime::fromString(Options::parkTime())); connect(startupTimeEdit, &QTimeEdit::editingFinished, this, [this]() { Options::setParkTime(startupTimeEdit->time().toString()); }); connect(&autoParkTimer, &QTimer::timeout, this, &Mount::startAutoPark); connect(startTimerB, &QPushButton::clicked, this, &Mount::startParkTimer); connect(stopTimerB, &QPushButton::clicked, this, &Mount::stopParkTimer); stopTimerB->setEnabled(false); if (everyDayCheck->isChecked()) startTimerB->animateClick(); // QML Stuff m_BaseView = new QQuickView(); #if 0 QString MountBox_Location; #if defined(Q_OS_OSX) MountBox_Location = QCoreApplication::applicationDirPath() + "/../Resources/data/ekos/mount/qml/mountbox.qml"; if (!QFileInfo(MountBox_Location).exists()) MountBox_Location = KSPaths::locate(QStandardPaths::AppDataLocation, "ekos/mount/qml/mountbox.qml"); #elif defined(Q_OS_WIN) MountBox_Location = KSPaths::locate(QStandardPaths::GenericDataLocation, "ekos/mount/qml/mountbox.qml"); #else MountBox_Location = KSPaths::locate(QStandardPaths::AppDataLocation, "ekos/mount/qml/mountbox.qml"); #endif m_BaseView->setSource(QUrl::fromLocalFile(MountBox_Location)); #endif m_BaseView->setSource(QUrl("qrc:/qml/mount/mountbox.qml")); m_BaseView->setTitle(i18n("Mount Control")); #ifdef Q_OS_OSX m_BaseView->setFlags(Qt::Tool | Qt::WindowStaysOnTopHint); #else m_BaseView->setFlags(Qt::WindowStaysOnTopHint | Qt::WindowCloseButtonHint); #endif // Theming? m_BaseView->setColor(Qt::black); m_BaseObj = m_BaseView->rootObject(); m_Ctxt = m_BaseView->rootContext(); ///Use instead of KDeclarative m_Ctxt->setContextObject(new KLocalizedContext(m_BaseView)); m_Ctxt->setContextProperty("mount", this); m_BaseView->setMinimumSize(QSize(210, 540)); m_BaseView->setResizeMode(QQuickView::SizeRootObjectToView); m_SpeedSlider = m_BaseObj->findChild("speedSliderObject"); m_SpeedLabel = m_BaseObj->findChild("speedLabelObject"); m_raValue = m_BaseObj->findChild("raValueObject"); m_deValue = m_BaseObj->findChild("deValueObject"); m_azValue = m_BaseObj->findChild("azValueObject"); m_altValue = m_BaseObj->findChild("altValueObject"); m_haValue = m_BaseObj->findChild("haValueObject"); m_zaValue = m_BaseObj->findChild("zaValueObject"); m_targetText = m_BaseObj->findChild("targetTextObject"); m_targetRAText = m_BaseObj->findChild("targetRATextObject"); m_targetDEText = m_BaseObj->findChild("targetDETextObject"); m_J2000Check = m_BaseObj->findChild("j2000CheckObject"); m_JNowCheck = m_BaseObj->findChild("jnowCheckObject"); m_Park = m_BaseObj->findChild("parkButtonObject"); m_Unpark = m_BaseObj->findChild("unparkButtonObject"); m_statusText = m_BaseObj->findChild("statusTextObject"); m_equatorialCheck = m_BaseObj->findChild("equatorialCheckObject"); m_horizontalCheck = m_BaseObj->findChild("horizontalCheckObject"); m_leftRightCheck = m_BaseObj->findChild("leftRightCheckObject"); m_upDownCheck = m_BaseObj->findChild("upDownCheckObject"); m_leftRightCheck->setProperty("checked", Options::leftRightReversed()); m_upDownCheck->setProperty("checked", Options::upDownReversed()); //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); } Mount::~Mount() { delete(m_Ctxt); delete(m_BaseObj); } void Mount::setTelescope(ISD::GDInterface *newTelescope) { if (newTelescope == currentTelescope) { updateTimer.start(); if (enableLimitsCheck->isChecked()) currentTelescope->setAltLimits(minAltLimit->value(), maxAltLimit->value()); syncTelescopeInfo(); return; } if (currentGPS != nullptr) syncGPS(); currentTelescope = static_cast(newTelescope); currentTelescope->disconnect(this); connect(currentTelescope, &ISD::GDInterface::numberUpdated, this, &Mount::updateNumber); connect(currentTelescope, &ISD::GDInterface::switchUpdated, this, &Mount::updateSwitch); connect(currentTelescope, &ISD::GDInterface::textUpdated, this, &Mount::updateText); connect(currentTelescope, &ISD::Telescope::newTarget, this, &Mount::newTarget); connect(currentTelescope, &ISD::Telescope::slewRateChanged, this, &Mount::slewRateChanged); connect(currentTelescope, &ISD::Telescope::pierSideChanged, this, &Mount::pierSideChanged); connect(currentTelescope, &ISD::Telescope::Disconnected, [this]() { updateTimer.stop(); m_BaseView->hide(); }); connect(currentTelescope, &ISD::Telescope::newParkStatus, [&](ISD::ParkStatus status) { m_ParkStatus = status; emit newParkStatus(status); // If mount is unparked AND every day auto-paro check is ON // AND auto park timer is not yet started, we try to initiate it. if (status == ISD::PARK_UNPARKED && everyDayCheck->isChecked() && autoParkTimer.isActive() == false) startTimerB->animateClick(); }); connect(currentTelescope, &ISD::Telescope::ready, this, &Mount::ready); //Disable this for now since ALL INDI drivers now log their messages to verbose output //connect(currentTelescope, SIGNAL(messageUpdated(int)), this, SLOT(updateLog(int)), Qt::UniqueConnection); if (enableLimitsCheck->isChecked()) currentTelescope->setAltLimits(minAltLimit->value(), maxAltLimit->value()); updateTimer.start(); syncTelescopeInfo(); // Send initial status m_Status = currentTelescope->status(); emit newStatus(m_Status); m_ParkStatus = currentTelescope->parkStatus(); emit newParkStatus(m_ParkStatus); } void Mount::removeDevice(ISD::GDInterface *device) { if (currentTelescope && !strcmp(currentTelescope->getDeviceName(), device->getDeviceName())) { currentTelescope->disconnect(this); updateTimer.stop(); m_BaseView->hide(); qCDebug(KSTARS_EKOS_MOUNT) << "Removing mount driver" << device->getDeviceName(); currentTelescope = nullptr; } else if (currentGPS && !strcmp(currentGPS->getDeviceName(), device->getDeviceName())) { currentGPS->disconnect(this); currentGPS = nullptr; } } void Mount::syncTelescopeInfo() { if (currentTelescope == nullptr || currentTelescope->isConnected() == false) return; INumberVectorProperty *nvp = currentTelescope->getBaseDevice()->getNumber("TELESCOPE_INFO"); if (nvp) { primaryScopeGroup->setTitle(currentTelescope->getDeviceName()); guideScopeGroup->setTitle(i18n("%1 guide scope", currentTelescope->getDeviceName())); INumber *np = IUFindNumber(nvp, "TELESCOPE_APERTURE"); if (np && np->value > 0) primaryScopeApertureIN->setValue(np->value); np = IUFindNumber(nvp, "TELESCOPE_FOCAL_LENGTH"); if (np && np->value > 0) primaryScopeFocalIN->setValue(np->value); np = IUFindNumber(nvp, "GUIDER_APERTURE"); if (np && np->value > 0) guideScopeApertureIN->setValue(np->value); np = IUFindNumber(nvp, "GUIDER_FOCAL_LENGTH"); if (np && np->value > 0) guideScopeFocalIN->setValue(np->value); } ISwitchVectorProperty *svp = currentTelescope->getBaseDevice()->getSwitch("TELESCOPE_SLEW_RATE"); if (svp) { int index = IUFindOnSwitchIndex(svp); // QtQuick m_SpeedSlider->setEnabled(true); m_SpeedSlider->setProperty("maximumValue", svp->nsp - 1); m_SpeedSlider->setProperty("value", index); m_SpeedLabel->setProperty("text", i18nc(libindi_strings_context, svp->sp[index].label)); m_SpeedLabel->setEnabled(true); } else { // QtQuick m_SpeedSlider->setEnabled(false); m_SpeedLabel->setEnabled(false); } if (currentTelescope->canPark()) { parkB->setEnabled(!currentTelescope->isParked()); unparkB->setEnabled(currentTelescope->isParked()); connect(parkB, SIGNAL(clicked()), currentTelescope, SLOT(Park()), Qt::UniqueConnection); connect(unparkB, SIGNAL(clicked()), currentTelescope, SLOT(UnPark()), Qt::UniqueConnection); // QtQuick m_Park->setEnabled(!currentTelescope->isParked()); m_Unpark->setEnabled(currentTelescope->isParked()); } else { parkB->setEnabled(false); unparkB->setEnabled(false); disconnect(parkB, SIGNAL(clicked()), currentTelescope, SLOT(Park())); disconnect(unparkB, SIGNAL(clicked()), currentTelescope, SLOT(UnPark())); // QtQuick m_Park->setEnabled(false); m_Unpark->setEnabled(false); } // Configs svp = currentTelescope->getBaseDevice()->getSwitch("APPLY_SCOPE_CONFIG"); if (svp) { scopeConfigCombo->disconnect(); scopeConfigCombo->clear(); for (int i = 0; i < svp->nsp; i++) scopeConfigCombo->addItem(svp->sp[i].label); scopeConfigCombo->setCurrentIndex(IUFindOnSwitchIndex(svp)); connect(scopeConfigCombo, SIGNAL(activated(int)), this, SLOT(setScopeConfig(int))); } // Tracking State svp = currentTelescope->getBaseDevice()->getSwitch("TELESCOPE_TRACK_STATE"); if (svp) { trackingGroup->setEnabled(true); trackOnB->disconnect(); trackOffB->disconnect(); connect(trackOnB, &QPushButton::clicked, [&]() { currentTelescope->setTrackEnabled(true); }); connect(trackOffB, &QPushButton::clicked, [&]() { if (KMessageBox::questionYesNo(KStars::Instance(), i18n("Are you sure you want to turn off mount tracking?"), i18n("Mount Tracking"), KStandardGuiItem::yes(), KStandardGuiItem::no(), "turn_off_mount_tracking_dialog") == KMessageBox::Yes) currentTelescope->setTrackEnabled(false); }); } else { trackOnB->setChecked(false); trackOffB->setChecked(false); trackingGroup->setEnabled(false); } ITextVectorProperty *tvp = currentTelescope->getBaseDevice()->getText("SCOPE_CONFIG_NAME"); if (tvp) scopeConfigNameEdit->setText(tvp->tp[0].text); } void Mount::registerNewModule(const QString &name) { if (name == "Capture") { captureInterface = new QDBusInterface("org.kde.kstars", "/KStars/Ekos/Capture", "org.kde.kstars.Ekos.Capture", QDBusConnection::sessionBus(), this); } } void Mount::updateText(ITextVectorProperty *tvp) { if (!strcmp(tvp->name, "SCOPE_CONFIG_NAME")) { scopeConfigNameEdit->setText(tvp->tp[0].text); } } bool Mount::setScopeConfig(int index) { ISwitchVectorProperty *svp = currentTelescope->getBaseDevice()->getSwitch("APPLY_SCOPE_CONFIG"); if (svp == nullptr) return false; IUResetSwitch(svp); svp->sp[index].s = ISS_ON; // Clear scope config name so that it gets filled by INDI scopeConfigNameEdit->clear(); currentTelescope->getDriverInfo()->getClientManager()->sendNewSwitch(svp); return true; } void Mount::updateTelescopeCoords() { // No need to update coords if we are still parked. if (m_Status == ISD::Telescope::MOUNT_PARKED && m_Status == currentTelescope->status()) return; double ra = 0, dec = 0; if (currentTelescope && currentTelescope->getEqCoords(&ra, &dec)) { if (currentTelescope->isJ2000()) { telescopeCoord.setRA0(ra); telescopeCoord.setDec0(dec); // Get JNow as well telescopeCoord.apparentCoord(static_cast(J2000), KStars::Instance()->data()->ut().djd()); } else { telescopeCoord.setRA(ra); telescopeCoord.setDec(dec); } // Ekos Mount Tab coords are always in JNow raOUT->setText(telescopeCoord.ra().toHMSString()); decOUT->setText(telescopeCoord.dec().toDMSString()); // Mount Control Panel coords depend on the switch if (m_JNowCheck->property("checked").toBool()) { m_raValue->setProperty("text", telescopeCoord.ra().toHMSString()); m_deValue->setProperty("text", telescopeCoord.dec().toDMSString()); } else { // If epoch is already J2000, then we don't need to convert to JNow if (currentTelescope->isJ2000() == false) { SkyPoint J2000Coord(telescopeCoord.ra(), telescopeCoord.dec()); J2000Coord.apparentCoord(KStars::Instance()->data()->ut().djd(), static_cast(J2000)); //J2000Coord.precessFromAnyEpoch(KStars::Instance()->data()->ut().djd(), static_cast(J2000)); telescopeCoord.setRA0(J2000Coord.ra()); telescopeCoord.setDec0(J2000Coord.dec()); } m_raValue->setProperty("text", telescopeCoord.ra0().toHMSString()); m_deValue->setProperty("text", telescopeCoord.dec0().toDMSString()); } // Get horizontal coords telescopeCoord.EquatorialToHorizontal(KStarsData::Instance()->lst(), KStarsData::Instance()->geo()->lat()); azOUT->setText(telescopeCoord.az().toDMSString()); m_azValue->setProperty("text", telescopeCoord.az().toDMSString()); altOUT->setText(telescopeCoord.alt().toDMSString()); m_altValue->setProperty("text", telescopeCoord.alt().toDMSString()); dms lst = KStarsData::Instance()->geo()->GSTtoLST(KStarsData::Instance()->clock()->utc().gst()); dms ha(lst - telescopeCoord.ra()); QChar sgn('+'); if (ha.Hours() > 12.0) { ha.setH(24.0 - ha.Hours()); sgn = '-'; } haOUT->setText(QString("%1%2").arg(sgn).arg(ha.toHMSString())); m_haValue->setProperty("text", haOUT->text()); lstOUT->setText(lst.toHMSString()); double currentAlt = telescopeCoord.altRefracted().Degrees(); m_zaValue->setProperty("text", dms(90 - currentAlt).toDMSString()); if (minAltLimit->isEnabled() && (currentAlt < minAltLimit->value() || currentAlt > maxAltLimit->value())) { if (currentAlt < minAltLimit->value()) { // Only stop if current altitude is less than last altitude indicate worse situation if (currentAlt < lastAlt && (abortDispatch == -1 || (currentTelescope->isInMotion() /* && ++abortDispatch > ABORT_DISPATCH_LIMIT*/))) { appendLogText(i18n("Telescope altitude is below minimum altitude limit of %1. Aborting motion...", QString::number(minAltLimit->value(), 'g', 3))); currentTelescope->Abort(); //KNotification::event( QLatin1String( "OperationFailed" )); KNotification::beep(); abortDispatch++; } } else { // Only stop if current altitude is higher than last altitude indicate worse situation if (currentAlt > lastAlt && (abortDispatch == -1 || (currentTelescope->isInMotion() /* && ++abortDispatch > ABORT_DISPATCH_LIMIT*/))) { appendLogText(i18n("Telescope altitude is above maximum altitude limit of %1. Aborting motion...", QString::number(maxAltLimit->value(), 'g', 3))); currentTelescope->Abort(); //KNotification::event( QLatin1String( "OperationFailed" )); KNotification::beep(); abortDispatch++; } } } else abortDispatch = -1; lastAlt = currentAlt; emit newCoords(raOUT->text(), decOUT->text(), azOUT->text(), altOUT->text()); ISD::Telescope::Status currentStatus = currentTelescope->status(); bool isTracking = (currentStatus == ISD::Telescope::MOUNT_TRACKING); if (m_Status != currentStatus) { qCDebug(KSTARS_EKOS_MOUNT) << "Mount status changed from " << m_Status << " to " << currentStatus; // If we just finished a slew, let's update initialHA and the current target's position if (currentStatus == ISD::Telescope::MOUNT_TRACKING && m_Status == ISD::Telescope::MOUNT_SLEWING) { setInitialHA((sgn == '-' ? -1 : 1) * ha.Hours()); delete currentTargetPosition; currentTargetPosition = new SkyPoint(telescopeCoord.ra(), telescopeCoord.dec()); } m_Status = currentStatus; parkB->setEnabled(!currentTelescope->isParked()); unparkB->setEnabled(currentTelescope->isParked()); m_Park->setEnabled(!currentTelescope->isParked()); m_Unpark->setEnabled(currentTelescope->isParked()); m_statusText->setProperty("text", currentTelescope->getStatusString(currentStatus)); QAction *a = KStars::Instance()->actionCollection()->action("telescope_track"); if (a != nullptr) a->setChecked(currentStatus == ISD::Telescope::MOUNT_TRACKING); emit newStatus(m_Status); } if (trackingGroup->isEnabled()) { trackOnB->setChecked(isTracking); trackOffB->setChecked(!isTracking); } if (currentTelescope->isConnected() == false) updateTimer.stop(); else if (updateTimer.isActive() == false) updateTimer.start(); // Auto Park Timer if (autoParkTimer.isActive()) { QTime remainingTime(0, 0, 0); remainingTime = remainingTime.addMSecs(autoParkTimer.remainingTime()); countdownLabel->setText(remainingTime.toString("hh:mm:ss")); } if (isTracking && checkMeridianFlip(lst)) executeMeridianFlip(); } else updateTimer.stop(); } void Mount::updateNumber(INumberVectorProperty *nvp) { if (!strcmp(nvp->name, "TELESCOPE_INFO")) { if (nvp->s == IPS_ALERT) { QString newMessage; if (primaryScopeApertureIN->value() == 1 || primaryScopeFocalIN->value() == 1) newMessage = i18n("Error syncing telescope info. Please fill telescope aperture and focal length."); else newMessage = i18n("Error syncing telescope info. Check INDI control panel for more details."); if (newMessage != lastNotificationMessage) { appendLogText(newMessage); lastNotificationMessage = newMessage; } } else { syncTelescopeInfo(); QString newMessage = i18n("Telescope info updated successfully."); if (newMessage != lastNotificationMessage) { appendLogText(newMessage); lastNotificationMessage = newMessage; } } } if (currentGPS != nullptr && !strcmp(nvp->device, currentGPS->getDeviceName()) && !strcmp(nvp->name, "GEOGRAPHIC_COORD") && nvp->s == IPS_OK) syncGPS(); } bool Mount::setSlewRate(int index) { if (currentTelescope) return currentTelescope->setSlewRate(index); return false; } void Mount::setUpDownReversed(bool enabled) { Options::setUpDownReversed(enabled); } void Mount::setLeftRightReversed(bool enabled) { Options::setLeftRightReversed(enabled); } void Mount::setMeridianFlipValues(bool activate, double hours) { meridianFlipCheckBox->setChecked(activate); // Hours --> Degrees if (meridianFlipDegreesR->isChecked()) meridianFlipTimeBox->setValue(hours * 15.0); else meridianFlipTimeBox->setValue(hours); Options::setExecuteMeridianFlip(meridianFlipCheckBox->isChecked()); // It is always saved in hours Options::setMeridianFlipOffset(hours); } void Mount::meridianFlipSetupChanged() { if (meridianFlipCheckBox->isChecked() == false) // reset meridian flip setMeridianFlipStatus(FLIP_NONE); Options::setExecuteMeridianFlip(meridianFlipCheckBox->isChecked()); double offset = meridianFlipTimeBox->value(); // Degrees --> Hours if (meridianFlipDegreesR->isChecked()) offset /= 15.0; // It is always saved in hours Options::setMeridianFlipOffset(offset); } void Mount::setMeridianFlipStatus(MeridianFlipStatus status) { if (m_MFStatus != status) { m_MFStatus = status; qCDebug (KSTARS_EKOS_MOUNT) << "Setting meridian flip status to " << status; meridianFlipStatusChanged(status); emit newMeridianFlipStatus(status); } } void Mount::updateSwitch(ISwitchVectorProperty *svp) { if (!strcmp(svp->name, "TELESCOPE_SLEW_RATE")) { int index = IUFindOnSwitchIndex(svp); m_SpeedSlider->setProperty("value", index); m_SpeedLabel->setProperty("text", i18nc(libindi_strings_context, svp->sp[index].label)); } /*else if (!strcmp(svp->name, "TELESCOPE_PARK")) { ISwitch *sp = IUFindSwitch(svp, "PARK"); if (sp) { parkB->setEnabled((sp->s == ISS_OFF)); unparkB->setEnabled((sp->s == ISS_ON)); } }*/ } void Mount::appendLogText(const QString &text) { m_LogText.insert(0, i18nc("log entry; %1 is the date, %2 is the text", "%1 %2", - QDateTime::currentDateTime().toString("yyyy-MM-ddThh:mm:ss"), text)); + KStarsData::Instance()->lt().toString("yyyy-MM-ddThh:mm:ss"), text)); qCInfo(KSTARS_EKOS_MOUNT) << text; emit newLog(text); } void Mount::updateLog(int messageID) { INDI::BaseDevice *dv = currentTelescope->getBaseDevice(); QString message = QString::fromStdString(dv->messageQueue(messageID)); m_LogText.insert(0, i18nc("Message shown in Ekos Mount module", "%1", message)); emit newLog(message); } void Mount::clearLog() { m_LogText.clear(); emit newLog(QString()); } void Mount::motionCommand(int command, int NS, int WE) { if (NS != -1) { if (Options::upDownReversed()) NS = !NS; currentTelescope->MoveNS(static_cast(NS), static_cast(command)); } if (WE != -1) { if (Options::leftRightReversed()) WE = !WE; currentTelescope->MoveWE(static_cast(WE), static_cast(command)); } } void Mount::save() { if (currentTelescope == nullptr) return; if (scopeConfigNameEdit->text().isEmpty() == false) { ITextVectorProperty *tvp = currentTelescope->getBaseDevice()->getText("SCOPE_CONFIG_NAME"); if (tvp) { IUSaveText(&(tvp->tp[0]), scopeConfigNameEdit->text().toLatin1().constData()); currentTelescope->getDriverInfo()->getClientManager()->sendNewText(tvp); } } INumberVectorProperty *nvp = currentTelescope->getBaseDevice()->getNumber("TELESCOPE_INFO"); if (nvp) { primaryScopeGroup->setTitle(currentTelescope->getDeviceName()); guideScopeGroup->setTitle(i18n("%1 guide scope", currentTelescope->getDeviceName())); INumber *np = IUFindNumber(nvp, "TELESCOPE_APERTURE"); if (np) np->value = primaryScopeApertureIN->value(); np = IUFindNumber(nvp, "TELESCOPE_FOCAL_LENGTH"); if (np) np->value = primaryScopeFocalIN->value(); np = IUFindNumber(nvp, "GUIDER_APERTURE"); if (np) np->value = guideScopeApertureIN->value() == 1 ? primaryScopeApertureIN->value() : guideScopeApertureIN->value(); np = IUFindNumber(nvp, "GUIDER_FOCAL_LENGTH"); if (np) np->value = guideScopeFocalIN->value() == 1 ? primaryScopeFocalIN->value() : guideScopeFocalIN->value(); ClientManager *clientManager = currentTelescope->getDriverInfo()->getClientManager(); clientManager->sendNewNumber(nvp); currentTelescope->setConfig(SAVE_CONFIG); //appendLogText(i18n("Saving telescope information...")); } else appendLogText(i18n("Failed to save telescope information.")); } void Mount::saveLimits() { Options::setMinimumAltLimit(minAltLimit->value()); Options::setMaximumAltLimit(maxAltLimit->value()); currentTelescope->setAltLimits(minAltLimit->value(), maxAltLimit->value()); } void Mount::enableAltitudeLimits(bool enable) { Options::setEnableAltitudeLimits(enable); if (enable) { minAltLabel->setEnabled(true); maxAltLabel->setEnabled(true); minAltLimit->setEnabled(true); maxAltLimit->setEnabled(true); if (currentTelescope) currentTelescope->setAltLimits(minAltLimit->value(), maxAltLimit->value()); } else { minAltLabel->setEnabled(false); maxAltLabel->setEnabled(false); minAltLimit->setEnabled(false); maxAltLimit->setEnabled(false); if (currentTelescope) currentTelescope->setAltLimits(-1, -1); } } void Mount::enableAltLimits() { //Only enable if it was already enabled before and the minAltLimit is currently disabled. if (altLimitEnabled && minAltLimit->isEnabled() == false) enableAltitudeLimits(true); } void Mount::disableAltLimits() { altLimitEnabled = enableLimitsCheck->isChecked(); enableAltitudeLimits(false); } QList Mount::altitudeLimits() { QList limits; limits.append(minAltLimit->value()); limits.append(maxAltLimit->value()); return limits; } void Mount::setAltitudeLimits(QList limits) { minAltLimit->setValue(limits[0]); maxAltLimit->setValue(limits[1]); } void Mount::setAltitudeLimitsEnabled(bool enable) { enableLimitsCheck->setChecked(enable); } bool Mount::altitudeLimitsEnabled() { return enableLimitsCheck->isChecked(); } void Mount::setJ2000Enabled(bool enabled) { m_J2000Check->setProperty("checked", enabled); } bool Mount::gotoTarget(const QString &target) { SkyObject *object = KStarsData::Instance()->skyComposite()->findByName(target); if (object != nullptr) return slew(object->ra().Hours(), object->dec().Degrees()); return false; } bool Mount::syncTarget(const QString &target) { SkyObject *object = KStarsData::Instance()->skyComposite()->findByName(target); if (object != nullptr) return sync(object->ra().Hours(), object->dec().Degrees()); return false; } bool Mount::slew(const QString &RA, const QString &DEC) { dms ra, de; if (m_equatorialCheck->property("checked").toBool()) { ra = dms::fromString(RA, false); de = dms::fromString(DEC, true); } else { dms az = dms::fromString(RA, true); dms at = dms::fromString(DEC, true); SkyPoint horizontalTarget; horizontalTarget.setAz(az); horizontalTarget.setAlt(at); horizontalTarget.HorizontalToEquatorial(KStars::Instance()->data()->lst(), KStars::Instance()->data()->geo()->lat()); ra = horizontalTarget.ra(); de = horizontalTarget.dec(); } // If J2000 was checked and the Mount is _not_ already using native J2000 coordinates // then we need to convert J2000 to JNow. Otherwise, we send J2000 as is. if (m_J2000Check->property("checked").toBool() && currentTelescope && currentTelescope->isJ2000() == false) { // J2000 ---> JNow SkyPoint J2000Coord(ra, de); J2000Coord.setRA0(ra); J2000Coord.setDec0(de); J2000Coord.apparentCoord(static_cast(J2000), KStars::Instance()->data()->ut().djd()); ra = J2000Coord.ra(); de = J2000Coord.dec(); } return slew(ra.Hours(), de.Degrees()); } bool Mount::slew(double RA, double DEC) { if (currentTelescope == nullptr || currentTelescope->isConnected() == false) return false; dms lst = KStarsData::Instance()->geo()->GSTtoLST(KStarsData::Instance()->clock()->utc().gst()); double HA = lst.Hours() - RA; HA = rangeHA(HA); // if (HA > 12.0) // HA -= 24.0; setInitialHA(HA); // reset the meridian flip status if the slew is not the meridian flip itself if (m_MFStatus != FLIP_RUNNING) setMeridianFlipStatus(FLIP_NONE); delete currentTargetPosition; currentTargetPosition = new SkyPoint(RA, DEC); qCDebug(KSTARS_EKOS_MOUNT) << "Slewing to RA=" << currentTargetPosition->ra().toHMSString() << "DEC=" << currentTargetPosition->dec().toDMSString(); return currentTelescope->Slew(currentTargetPosition); } bool Mount::checkMeridianFlip(dms lst) { if (currentTelescope == nullptr || currentTelescope->isConnected() == false) { meridianFlipStatusText->setText("Status: inactive"); setMeridianFlipStatus(FLIP_NONE); return false; } if (currentTelescope->isTracking() == false) { meridianFlipStatusText->setText("Status: inactive (not tracking)"); return false; } double offset = meridianFlipTimeBox->value(); // Degrees --> Hours if (meridianFlipDegreesR->isChecked()) offset = rangeHA(offset / 15.0); double deltaHA = offset - lst.Hours() + telescopeCoord.ra().Hours(); deltaHA = rangeHA(deltaHA); int hh = static_cast (deltaHA); int mm = static_cast ((deltaHA - hh) * 60); int ss = static_cast ((deltaHA - hh - mm / 60.0) * 3600); switch (m_MFStatus) { case FLIP_NONE: if (initialHA() >= 0 || meridianFlipCheckBox->isChecked() == false) { meridianFlipStatusText->setText("Status: inactive"); return false; } if (deltaHA > 0) { QString message = QString("Meridian flip in %1h:%2m:%3s") .arg(hh, 2, 10, QChar('0')) .arg(mm, 2, 10, QChar('0')) .arg(ss, 2, 10, QChar('0')); meridianFlipStatusText->setText(message); return false; } else if (initialHA() < 0) { double offset = meridianFlipTimeBox->value(); // Degrees --> Hours if (meridianFlipDegreesR->isChecked()) offset = rangeHA(offset / 15.0); qCDebug(KSTARS_EKOS_MOUNT) << "Meridian flip planned with LST=" << lst.toHMSString() << " scope RA=" << telescopeCoord.ra().toHMSString() << " and meridian diff=" << offset; setMeridianFlipStatus(FLIP_PLANNED); return false; } break; case FLIP_PLANNED: return false; case FLIP_ACCEPTED: return true; case FLIP_RUNNING: if (currentTelescope->isTracking()) { // meridian flip completed setMeridianFlipStatus(FLIP_COMPLETED); } break; case FLIP_COMPLETED: setMeridianFlipStatus(FLIP_NONE); break; default: // do nothing break; } return false; } bool Mount::executeMeridianFlip() { if (initialHA() > 0) // no meridian flip necessary return false; if (currentTelescope->status() != ISD::Telescope::MOUNT_TRACKING) { // no meridian flip necessary return false; } dms lst = KStarsData::Instance()->geo()->GSTtoLST(KStarsData::Instance()->clock()->utc().gst()); double HA = lst.Hours() - currentTargetPosition->ra().Hours(); if (HA > 12.0) // no meridian flip necessary return false; // execute meridian flip qCInfo(KSTARS_EKOS_MOUNT) << "Meridian flip: slewing to RA=" << currentTargetPosition->ra().toHMSString() << "DEC=" << currentTargetPosition->dec().toDMSString(); setMeridianFlipStatus(FLIP_RUNNING); bool result = slew(currentTargetPosition->ra().Hours(), currentTargetPosition->dec().Degrees()); if (result == false) qCWarning(KSTARS_EKOS_MOUNT) << "Meridian flip FAILED: slewing to RA=" << currentTargetPosition->ra().toHMSString() << "DEC=" << currentTargetPosition->dec().toDMSString(); return result; } void Mount::meridianFlipStatusChanged(Mount::MeridianFlipStatus status) { m_MFStatus = status; switch (status) { case FLIP_NONE: meridianFlipStatusText->setText("Status: inactive"); break; case FLIP_PLANNED: meridianFlipStatusText->setText("Meridian flip planned..."); break; case FLIP_WAITING: meridianFlipStatusText->setText("Meridian flip waiting..."); break; case FLIP_ACCEPTED: if (currentTelescope == nullptr || currentTelescope->isTracking() == false) // if the mount is not tracking, we go back one step setMeridianFlipStatus(FLIP_PLANNED); // otherwise do nothing, execution of meridian flip initianted in updateTelescopeCoords() break; case FLIP_RUNNING: meridianFlipStatusText->setText("Meridian flip running..."); break; case FLIP_COMPLETED: meridianFlipStatusText->setText("Meridian flip completed."); break; default: break; } } SkyPoint Mount::currentTarget() { return *currentTargetPosition; } bool Mount::sync(const QString &RA, const QString &DEC) { dms ra, de; ra = dms::fromString(RA, false); de = dms::fromString(DEC, true); if (m_J2000Check->property("checked").toBool()) { // J2000 ---> JNow SkyPoint J2000Coord(ra, de); J2000Coord.setRA0(ra); J2000Coord.setDec0(de); J2000Coord.updateCoordsNow(KStarsData::Instance()->updateNum()); ra = J2000Coord.ra(); de = J2000Coord.dec(); } return sync(ra.Hours(), de.Degrees()); } bool Mount::sync(double RA, double DEC) { if (currentTelescope == nullptr || currentTelescope->isConnected() == false) return false; return currentTelescope->Sync(RA, DEC); } bool Mount::abort() { return currentTelescope->Abort(); } IPState Mount::slewStatus() { if (currentTelescope == nullptr) return IPS_ALERT; return currentTelescope->getState("EQUATORIAL_EOD_COORD"); } QList Mount::equatorialCoords() { double ra, dec; QList coords; currentTelescope->getEqCoords(&ra, &dec); coords.append(ra); coords.append(dec); return coords; } QList Mount::horizontalCoords() { QList coords; coords.append(telescopeCoord.az().Degrees()); coords.append(telescopeCoord.alt().Degrees()); return coords; } double Mount::hourAngle() { dms lst = KStarsData::Instance()->geo()->GSTtoLST(KStarsData::Instance()->clock()->utc().gst()); dms ha(lst.Degrees() - telescopeCoord.ra().Degrees()); double HA = rangeHA(ha.Hours()); return HA; // if (HA > 12.0) // return (HA - 24.0); // else // return HA; } QList Mount::telescopeInfo() { QList info; info.append(primaryScopeFocalIN->value()); info.append(primaryScopeApertureIN->value()); info.append(guideScopeFocalIN->value()); info.append(guideScopeApertureIN->value()); return info; } void Mount::setTelescopeInfo(const QList &info) { if (info[0] > 0) primaryScopeFocalIN->setValue(info[0]); if (info[1] > 0) primaryScopeApertureIN->setValue(info[1]); if (info[2] > 0) guideScopeFocalIN->setValue(info[2]); if (info[3] > 0) guideScopeApertureIN->setValue(info[3]); if (scopeConfigNameEdit->text().isEmpty() == false) appendLogText(i18n("Warning: Overriding %1 configuration.", scopeConfigNameEdit->text())); save(); } bool Mount::canPark() { if (currentTelescope == nullptr) return false; return currentTelescope->canPark(); } bool Mount::park() { if (currentTelescope == nullptr || currentTelescope->canPark() == false) return false; return currentTelescope->Park(); } bool Mount::unpark() { if (currentTelescope == nullptr || currentTelescope->canPark() == false) return false; return currentTelescope->UnPark(); } #if 0 Mount::ParkingStatus Mount::getParkingStatus() { if (currentTelescope == nullptr) return PARKING_ERROR; // In the case mount can't park, return mount is unparked if (currentTelescope->canPark() == false) return UNPARKING_OK; ISwitchVectorProperty *parkSP = currentTelescope->getBaseDevice()->getSwitch("TELESCOPE_PARK"); if (parkSP == nullptr) return PARKING_ERROR; switch (parkSP->s) { case IPS_IDLE: // If mount is unparked on startup, state is OK and switch is UNPARK if (parkSP->sp[1].s == ISS_ON) return UNPARKING_OK; else return PARKING_IDLE; //break; case IPS_OK: if (parkSP->sp[0].s == ISS_ON) return PARKING_OK; else return UNPARKING_OK; //break; case IPS_BUSY: if (parkSP->sp[0].s == ISS_ON) return PARKING_BUSY; else return UNPARKING_BUSY; case IPS_ALERT: // If mount replied with an error to the last un/park request, // assume state did not change in order to return a clear state if (parkSP->sp[0].s == ISS_ON) return PARKING_OK; else return UNPARKING_OK; } return PARKING_ERROR; } #endif void Mount::toggleMountToolBox() { if (m_BaseView->isVisible()) { m_BaseView->hide(); QAction *a = KStars::Instance()->actionCollection()->action("show_mount_box"); if (a) a->setChecked(false); } else { m_BaseView->show(); QAction *a = KStars::Instance()->actionCollection()->action("show_mount_box"); if (a) a->setChecked(true); } } void Mount::findTarget() { KStarsData *data = KStarsData::Instance(); if (FindDialog::Instance()->exec() == QDialog::Accepted) { SkyObject *object = FindDialog::Instance()->targetObject(); if (object != nullptr) { SkyObject *o = object->clone(); o->updateCoords(data->updateNum(), true, data->geo()->lat(), data->lst(), false); m_targetText->setProperty("text", o->name()); if (m_JNowCheck->property("checked").toBool()) { m_targetRAText->setProperty("text", o->ra().toHMSString()); m_targetDEText->setProperty("text", o->dec().toDMSString()); } else { m_targetRAText->setProperty("text", o->ra0().toHMSString()); m_targetDEText->setProperty("text", o->dec0().toDMSString()); } } } } void Mount::centerMount() { if (currentTelescope) currentTelescope->runCommand(INDI_FIND_TELESCOPE); } bool Mount::resetModel() { if (currentTelescope == nullptr) return false; if (currentTelescope->hasAlignmentModel() == false) return false; if (currentTelescope->clearAlignmentModel()) { appendLogText(i18n("Alignment Model cleared.")); return true; } appendLogText(i18n("Failed to clear Alignment Model.")); return false; } void Mount::setGPS(ISD::GDInterface *newGPS) { if (newGPS == currentGPS) return; auto executeSetGPS = [this, newGPS]() { currentGPS = newGPS; connect(newGPS, SIGNAL(numberUpdated(INumberVectorProperty*)), this, SLOT(updateNumber(INumberVectorProperty*)), Qt::UniqueConnection); appendLogText(i18n("GPS driver detected. KStars and mount time and location settings are now synced to the GPS driver.")); syncGPS(); }; if (Options::useGPSSource() == false) { connect(KSMessageBox::Instance(), &KSMessageBox::accepted, this, [this, executeSetGPS]() { //QObject::disconnect(KSMessageBox::Instance(), &KSMessageBox::accepted, this, nullptr); KSMessageBox::Instance()->disconnect(this); Options::setUseKStarsSource(false); Options::setUseMountSource(false); Options::setUseGPSSource(true); executeSetGPS(); }); KSMessageBox::Instance()->questionYesNo(i18n("GPS is detected. Do you want to switch time and location source to GPS?"), i18n("GPS Settings"), 10); } else executeSetGPS(); } void Mount::syncGPS() { // We only update when location is OK INumberVectorProperty *location = currentGPS->getBaseDevice()->getNumber("GEOGRAPHIC_COORD"); if (location == nullptr || location->s != IPS_OK) return; // Sync name if (currentTelescope) { ITextVectorProperty *activeDevices = currentTelescope->getBaseDevice()->getText("ACTIVE_DEVICES"); if (activeDevices) { IText *activeGPS = IUFindText(activeDevices, "ACTIVE_GPS"); if (activeGPS) { if (strcmp(activeGPS->text, currentGPS->getDeviceName())) { IUSaveText(activeGPS, currentGPS->getDeviceName()); currentTelescope->getDriverInfo()->getClientManager()->sendNewText(activeDevices); } } } } // GPS Refresh should only be called once automatically. if (GPSInitialized == false) { ISwitchVectorProperty *refreshGPS = currentGPS->getBaseDevice()->getSwitch("GPS_REFRESH"); if (refreshGPS) { refreshGPS->sp[0].s = ISS_ON; currentGPS->getDriverInfo()->getClientManager()->sendNewSwitch(refreshGPS); GPSInitialized = true; } } } void Mount::setTrackEnabled(bool enabled) { if (enabled) trackOnB->click(); else trackOffB->click(); } int Mount::slewRate() { if (currentTelescope == nullptr) return -1; return currentTelescope->getSlewRate(); } //QJsonArray Mount::getScopes() const //{ // QJsonArray scopes; // if (currentTelescope == nullptr) // return scopes; // QJsonObject primary = // { // {"name", "Primary"}, // {"mount", currentTelescope->getDeviceName()}, // {"aperture", primaryScopeApertureIN->value()}, // {"focalLength", primaryScopeFocalIN->value()}, // }; // scopes.append(primary); // QJsonObject guide = // { // {"name", "Guide"}, // {"mount", currentTelescope->getDeviceName()}, // {"aperture", primaryScopeApertureIN->value()}, // {"focalLength", primaryScopeFocalIN->value()}, // }; // scopes.append(guide); // return scopes; //} void Mount::startParkTimer() { if (currentTelescope == nullptr || m_ParkStatus == ISD::PARK_UNKNOWN) return; if (currentTelescope->isParked()) { appendLogText("Mount already parked."); return; } QTime parkTime = startupTimeEdit->time(); qCDebug(KSTARS_EKOS_MOUNT) << "Parking time is" << parkTime.toString(); QDateTime currentDateTime = KStarsData::Instance()->lt(); QDateTime parkDateTime(currentDateTime); parkDateTime.setTime(parkTime); qint64 parkMilliSeconds = parkDateTime.msecsTo(currentDateTime); qCDebug(KSTARS_EKOS_MOUNT) << "Until parking time:" << parkMilliSeconds << "ms or" << parkMilliSeconds / (60 * 60 * 1000) << "hours"; if (parkMilliSeconds > 0) { qCDebug(KSTARS_EKOS_MOUNT) << "Added a day to parking time..."; parkDateTime = parkDateTime.addDays(1); parkMilliSeconds = parkDateTime.msecsTo(currentDateTime); int hours = parkMilliSeconds / (1000 * 60 * 60); if (hours > 0) { // No need to display warning for every day check if (everyDayCheck->isChecked() == false) appendLogText(i18n("Parking time cannot be in the past.")); return; } else if (std::abs(hours) > 12) { qCDebug(KSTARS_EKOS_MOUNT) << "Parking time is" << hours << "which exceeds 12 hours, auto park is disabled."; return; } } parkMilliSeconds = std::abs(parkMilliSeconds); if (parkMilliSeconds > 24 * 60 * 60 * 1000) { appendLogText(i18n("Parking time must be within 24 hours of current time.")); return; } if (parkMilliSeconds > 12 * 60 * 60 * 1000) appendLogText(i18n("Warning! Parking time is more than 12 hours away.")); appendLogText(i18n("Caution: do not use Auto Park while scheduler is active.")); autoParkTimer.setInterval(parkMilliSeconds); autoParkTimer.start(); startTimerB->setEnabled(false); stopTimerB->setEnabled(true); } void Mount::stopParkTimer() { autoParkTimer.stop(); countdownLabel->setText("00:00:00"); stopTimerB->setEnabled(false); startTimerB->setEnabled(true); } void Mount::startAutoPark() { appendLogText(i18n("Parking timer is up.")); autoParkTimer.stop(); startTimerB->setEnabled(true); stopTimerB->setEnabled(false); countdownLabel->setText("00:00:00"); if (currentTelescope) { if (currentTelescope->isParked() == false) { appendLogText(i18n("Starting auto park...")); park(); } } } } diff --git a/kstars/ekos/observatory/observatory.cpp b/kstars/ekos/observatory/observatory.cpp index 25d5dff1d..30fc9cd34 100644 --- a/kstars/ekos/observatory/observatory.cpp +++ b/kstars/ekos/observatory/observatory.cpp @@ -1,647 +1,649 @@ /* Ekos Observatory Module Copyright (C) Wolfgang Reissenberger 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 "kstarsdata.h" + #include "observatory.h" #include "ekos_observatory_debug.h" namespace Ekos { Observatory::Observatory() { setupUi(this); // status control mObservatoryModel = new ObservatoryModel(); setObseratoryStatusControl(mObservatoryModel->statusControl()); // update UI for status control connect(useDomeCB, &QCheckBox::clicked, this, &Ekos::Observatory::statusControlSettingsChanged); connect(useShutterCB, &QCheckBox::clicked, this, &Ekos::Observatory::statusControlSettingsChanged); connect(useWeatherCB, &QCheckBox::clicked, this, &Ekos::Observatory::statusControlSettingsChanged); connect(mObservatoryModel, &Ekos::ObservatoryModel::newStatus, this, &Ekos::Observatory::observatoryStatusChanged); // ready button deactivated // connect(statusReadyButton, &QPushButton::clicked, mObservatoryModel, &Ekos::ObservatoryModel::makeReady); statusReadyButton->setEnabled(false); setDomeModel(new ObservatoryDomeModel()); setWeatherModel(new ObservatoryWeatherModel()); statusDefinitionBox->setVisible(true); statusDefinitionBox->setEnabled(true); // make invisible, since not implemented yet weatherWarningSchedulerCB->setVisible(false); weatherAlertSchedulerCB->setVisible(false); // initialize the weather sensor data group box sensorDataBoxLayout = new QFormLayout(); sensorDataBox->setLayout(sensorDataBoxLayout); } void Observatory::setObseratoryStatusControl(ObservatoryStatusControl control) { if (mObservatoryModel != nullptr) { useDomeCB->setChecked(control.useDome); useShutterCB->setChecked(control.useShutter); useWeatherCB->setChecked(control.useWeather); } } void Observatory::setDomeModel(ObservatoryDomeModel *model) { mObservatoryModel->setDomeModel(model); if (model != nullptr) { connect(model, &Ekos::ObservatoryDomeModel::ready, this, &Ekos::Observatory::initDome); connect(model, &Ekos::ObservatoryDomeModel::disconnected, this, &Ekos::Observatory::shutdownDome); connect(model, &Ekos::ObservatoryDomeModel::newStatus, this, &Ekos::Observatory::setDomeStatus); connect(model, &Ekos::ObservatoryDomeModel::newParkStatus, this, &Ekos::Observatory::setDomeParkStatus); connect(model, &Ekos::ObservatoryDomeModel::newShutterStatus, this, &Ekos::Observatory::setShutterStatus); connect(model, &Ekos::ObservatoryDomeModel::azimuthPositionChanged, this, &Ekos::Observatory::domeAzimuthChanged); connect(model, &Ekos::ObservatoryDomeModel::newAutoSyncStatus, this, &Ekos::Observatory::showAutoSync); // motion controls connect(motionMoveAbsButton, &QCheckBox::clicked, [this]() { mObservatoryModel->getDomeModel()->setAzimuthPosition(absoluteMotionSB->value()); }); connect(motionMoveRelButton, &QCheckBox::clicked, [this]() { mObservatoryModel->getDomeModel()->setRelativePosition(relativeMotionSB->value()); }); // abort button connect(motionAbortButton, &QPushButton::clicked, model, &ObservatoryDomeModel::abort); // weather controls connect(weatherWarningShutterCB, &QCheckBox::clicked, this, &Observatory::weatherWarningSettingsChanged); connect(weatherWarningDomeCB, &QCheckBox::clicked, this, &Observatory::weatherWarningSettingsChanged); connect(weatherWarningDelaySB, static_cast(&QSpinBox::valueChanged), [this](int i) { Q_UNUSED(i); weatherWarningSettingsChanged(); }); connect(weatherAlertShutterCB, &QCheckBox::clicked, this, &Observatory::weatherAlertSettingsChanged); connect(weatherAlertDomeCB, &QCheckBox::clicked, this, &Observatory::weatherAlertSettingsChanged); connect(weatherAlertDelaySB, static_cast(&QSpinBox::valueChanged), [this](int i) { Q_UNUSED(i); weatherAlertSettingsChanged(); }); } else { shutdownDome(); disconnect(weatherWarningShutterCB, &QCheckBox::clicked, this, &Observatory::weatherWarningSettingsChanged); disconnect(weatherWarningDomeCB, &QCheckBox::clicked, this, &Observatory::weatherWarningSettingsChanged); connect(weatherWarningDelaySB, static_cast(&QSpinBox::valueChanged), [this](int i) { Q_UNUSED(i); weatherWarningSettingsChanged(); }); disconnect(weatherAlertShutterCB, &QCheckBox::clicked, this, &Observatory::weatherAlertSettingsChanged); disconnect(weatherAlertDomeCB, &QCheckBox::clicked, this, &Observatory::weatherAlertSettingsChanged); connect(weatherAlertDelaySB, static_cast(&QSpinBox::valueChanged), [this](int i) { Q_UNUSED(i); weatherWarningSettingsChanged(); }); } } void Observatory::initDome() { domeBox->setEnabled(true); if (getDomeModel() != nullptr) { connect(getDomeModel(), &Ekos::ObservatoryDomeModel::newLog, this, &Ekos::Observatory::appendLogText); // dome motion buttons connect(motionCWButton, &QPushButton::clicked, [ = ](bool checked) { getDomeModel()->moveDome(true, checked); }); connect(motionCCWButton, &QPushButton::clicked, [ = ](bool checked) { getDomeModel()->moveDome(false, checked); }); if (getDomeModel()->canPark()) { connect(domePark, &QPushButton::clicked, getDomeModel(), &Ekos::ObservatoryDomeModel::park); connect(domeUnpark, &QPushButton::clicked, getDomeModel(), &Ekos::ObservatoryDomeModel::unpark); domePark->setEnabled(true); domeUnpark->setEnabled(true); } else { domePark->setEnabled(false); domeUnpark->setEnabled(false); } if (getDomeModel()->isRolloffRoof()) { SlavingBox->setVisible(false); domeAzimuthPosition->setText(i18nc("Not Applicable", "N/A")); enableMotionControl(true); } else { // initialize the dome motion controls domeAzimuthChanged(getDomeModel()->azimuthPosition()); // slaving showAutoSync(getDomeModel()->isAutoSync()); connect(slavingEnableButton, &QPushButton::clicked, this, [this]() { enableAutoSync(true); }); connect(slavingDisableButton, &QPushButton::clicked, this, [this]() { enableAutoSync(false); }); } // shutter handling if (getDomeModel()->hasShutter()) { shutterBox->setVisible(true); shutterBox->setEnabled(true); connect(shutterOpen, &QPushButton::clicked, getDomeModel(), &Ekos::ObservatoryDomeModel::openShutter); connect(shutterClosed, &QPushButton::clicked, getDomeModel(), &Ekos::ObservatoryDomeModel::closeShutter); shutterClosed->setEnabled(true); shutterOpen->setEnabled(true); setShutterStatus(getDomeModel()->shutterStatus()); useShutterCB->setVisible(true); } else { shutterBox->setVisible(false); weatherWarningShutterCB->setVisible(false); weatherAlertShutterCB->setVisible(false); useShutterCB->setVisible(false); } // abort button should always be available motionAbortButton->setEnabled(true); // update the dome status setDomeStatus(getDomeModel()->status()); setDomeParkStatus(getDomeModel()->parkStatus()); } } void Observatory::shutdownDome() { domeBox->setEnabled(false); shutterBox->setEnabled(false); shutterBox->setVisible(false); domePark->setEnabled(false); domeUnpark->setEnabled(false); shutterClosed->setEnabled(false); shutterOpen->setEnabled(false); disconnect(domePark, &QPushButton::clicked, getDomeModel(), &Ekos::ObservatoryDomeModel::park); disconnect(domeUnpark, &QPushButton::clicked, getDomeModel(), &Ekos::ObservatoryDomeModel::unpark); } void Observatory::setDomeStatus(ISD::Dome::Status status) { qCDebug(KSTARS_EKOS_OBSERVATORY) << "Setting dome status to " << status; switch (status) { case ISD::Dome::DOME_ERROR: break; case ISD::Dome::DOME_IDLE: motionCWButton->setChecked(false); motionCWButton->setEnabled(true); motionCCWButton->setChecked(false); motionCCWButton->setEnabled(true); appendLogText(i18n("Dome is idle.")); break; case ISD::Dome::DOME_MOVING_CW: motionCWButton->setChecked(true); motionCCWButton->setEnabled(true); motionCCWButton->setChecked(false); if (getDomeModel()->isRolloffRoof()) { domeAzimuthPosition->setText(i18n("Opening")); toggleButtons(domeUnpark, i18n("Unparking"), domePark, i18n("Park")); appendLogText(i18n("Dome is opening...")); } else { appendLogText(i18n("Dome is moving clockwise...")); } break; case ISD::Dome::DOME_MOVING_CCW: motionCWButton->setChecked(false); motionCWButton->setEnabled(true); motionCCWButton->setChecked(true); if (getDomeModel()->isRolloffRoof()) { domeAzimuthPosition->setText(i18n("Closing")); toggleButtons(domePark, i18n("Parking"), domeUnpark, i18n("Unpark")); appendLogText(i18n("Dome is closing...")); } else { appendLogText(i18n("Dome is moving counter clockwise...")); } break; case ISD::Dome::DOME_PARKED: setDomeParkStatus(ISD::PARK_PARKED); appendLogText(i18n("Dome is parked.")); break; case ISD::Dome::DOME_PARKING: toggleButtons(domePark, i18n("Parking"), domeUnpark, i18n("Unpark")); motionCWButton->setEnabled(true); if (getDomeModel()->isRolloffRoof()) domeAzimuthPosition->setText(i18n("Closing")); else enableMotionControl(false); motionCWButton->setChecked(false); motionCCWButton->setChecked(true); appendLogText(i18n("Dome is parking...")); break; case ISD::Dome::DOME_UNPARKING: toggleButtons(domeUnpark, i18n("Unparking"), domePark, i18n("Park")); motionCCWButton->setEnabled(true); if (getDomeModel()->isRolloffRoof()) domeAzimuthPosition->setText(i18n("Opening")); else enableMotionControl(false); motionCWButton->setChecked(true); motionCCWButton->setChecked(false); appendLogText(i18n("Dome is unparking...")); break; case ISD::Dome::DOME_TRACKING: enableMotionControl(true); motionCWButton->setEnabled(true); motionCCWButton->setChecked(true); appendLogText(i18n("Dome is tracking.")); break; } } void Observatory::setDomeParkStatus(ISD::ParkStatus status) { qCDebug(KSTARS_EKOS_OBSERVATORY) << "Setting dome park status to " << status; switch (status) { case ISD::PARK_UNPARKED: activateButton(domePark, i18n("Park")); buttonPressed(domeUnpark, i18n("Unparked")); motionCWButton->setChecked(false); motionCWButton->setEnabled(false); motionCCWButton->setChecked(false); if (getDomeModel()->isRolloffRoof()) domeAzimuthPosition->setText(i18n("Open")); else enableMotionControl(true); break; case ISD::PARK_PARKED: buttonPressed(domePark, i18n("Parked")); activateButton(domeUnpark, i18n("Unpark")); motionCWButton->setChecked(false); motionCCWButton->setChecked(false); motionCCWButton->setEnabled(false); if (getDomeModel()->isRolloffRoof()) domeAzimuthPosition->setText(i18n("Closed")); else enableMotionControl(false); break; default: break; } } void Observatory::setShutterStatus(ISD::Dome::ShutterStatus status) { qCDebug(KSTARS_EKOS_OBSERVATORY) << "Setting shutter status to " << status; switch (status) { case ISD::Dome::SHUTTER_OPEN: buttonPressed(shutterOpen, i18n("Opened")); activateButton(shutterClosed, i18n("Close")); appendLogText(i18n("Shutter is open.")); break; case ISD::Dome::SHUTTER_OPENING: toggleButtons(shutterOpen, i18n("Opening"), shutterClosed, i18n("Close")); appendLogText(i18n("Shutter is opening...")); break; case ISD::Dome::SHUTTER_CLOSED: buttonPressed(shutterClosed, i18n("Closed")); activateButton(shutterOpen, i18n("Open")); appendLogText(i18n("Shutter is closed.")); break; case ISD::Dome::SHUTTER_CLOSING: toggleButtons(shutterClosed, i18n("Closing"), shutterOpen, i18n("Open")); appendLogText(i18n("Shutter is closing...")); break; default: break; } } void Observatory::setWeatherModel(ObservatoryWeatherModel *model) { mObservatoryModel->setWeatherModel(model); if (model != nullptr) { connect(weatherWarningBox, &QGroupBox::clicked, model, &ObservatoryWeatherModel::setWarningActionsActive); connect(weatherAlertBox, &QGroupBox::clicked, model, &ObservatoryWeatherModel::setAlertActionsActive); connect(model, &Ekos::ObservatoryWeatherModel::ready, this, &Ekos::Observatory::initWeather); connect(model, &Ekos::ObservatoryWeatherModel::newStatus, this, &Ekos::Observatory::setWeatherStatus); connect(model, &Ekos::ObservatoryWeatherModel::disconnected, this, &Ekos::Observatory::shutdownWeather); connect(&weatherStatusTimer, &QTimer::timeout, [this]() { weatherWarningStatusLabel->setText(getWeatherModel()->getWarningActionsStatus()); weatherAlertStatusLabel->setText(getWeatherModel()->getAlertActionsStatus()); }); } else shutdownWeather(); } void Observatory::enableMotionControl(bool enabled) { MotionBox->setEnabled(enabled); // absolute motion controls if (getDomeModel()->canAbsoluteMove()) { motionMoveAbsButton->setEnabled(enabled); absoluteMotionSB->setEnabled(enabled); } else { motionMoveAbsButton->setEnabled(false); absoluteMotionSB->setEnabled(false); } // relative motion controls if (getDomeModel()->canRelativeMove()) { motionMoveRelButton->setEnabled(enabled); relativeMotionSB->setEnabled(enabled); motionCWButton->setEnabled(enabled); motionCCWButton->setEnabled(enabled); } else { motionMoveRelButton->setEnabled(false); relativeMotionSB->setEnabled(false); motionCWButton->setEnabled(false); motionCCWButton->setEnabled(false); } // special case for rolloff roofs if (getDomeModel()->isRolloffRoof()) { motionCWButton->setText(i18n("Open")); motionCCWButton->setText(i18n("Close")); motionCWButton->setEnabled(enabled); motionCCWButton->setEnabled(enabled); motionMoveAbsButton->setVisible(false); motionMoveRelButton->setVisible(false); absoluteMotionSB->setVisible(false); relativeMotionSB->setVisible(false); } } void Observatory::enableAutoSync(bool enabled) { if (getDomeModel() == nullptr) showAutoSync(false); else { getDomeModel()->setAutoSync(enabled); showAutoSync(enabled); } } void Observatory::showAutoSync(bool enabled) { slavingEnableButton->setChecked(enabled); slavingDisableButton->setChecked(! enabled); } void Observatory::initWeather() { weatherBox->setEnabled(true); weatherLabel->setEnabled(true); weatherActionsBox->setVisible(true); weatherActionsBox->setEnabled(true); weatherWarningBox->setChecked(getWeatherModel()->getWarningActionsActive()); weatherAlertBox->setChecked(getWeatherModel()->getAlertActionsActive()); setWeatherStatus(getWeatherModel()->status()); setWarningActions(getWeatherModel()->getWarningActions()); setAlertActions(getWeatherModel()->getAlertActions()); weatherStatusTimer.start(1000); } void Observatory::shutdownWeather() { weatherBox->setEnabled(false); weatherLabel->setEnabled(false); setWeatherStatus(ISD::Weather::WEATHER_IDLE); weatherStatusTimer.stop(); } void Observatory::setWeatherStatus(ISD::Weather::Status status) { std::string label; switch (status) { case ISD::Weather::WEATHER_OK: label = "security-high"; appendLogText(i18n("Weather is OK")); break; case ISD::Weather::WEATHER_WARNING: label = "security-medium"; appendLogText(i18n("Weather Warning")); break; case ISD::Weather::WEATHER_ALERT: label = "security-low"; appendLogText(i18n("Weather Alert")); break; default: label = ""; break; } weatherStatusLabel->setPixmap(QIcon::fromTheme(label.c_str()).pixmap(QSize(48, 48))); std::vector weatherData = getWeatherModel()->getWeatherData(); // update weather sensor data int row_nr = 0; std::vector::iterator it; for (it=weatherData.begin(); it != weatherData.end(); ++it) { if (sensorDataBoxLayout->rowCount() > row_nr) { sensorDataWidgets.value(row_nr)->first->setText(QString(it->label)); sensorDataWidgets.value(row_nr)->second->setText(QString().setNum(it->value, 'f', 2)); } else { QLabel* labelWidget = new QLabel(it->label); QLineEdit* valueWidget = new QLineEdit(QString().setNum(it->value, 'f', 2)); valueWidget->setReadOnly(false); valueWidget->setAlignment(Qt::AlignRight); sensorDataWidgets.append(new QPair(labelWidget, valueWidget)); sensorDataBoxLayout->addRow(labelWidget, valueWidget); } row_nr++; } } void Observatory::weatherWarningSettingsChanged() { struct WeatherActions actions; actions.parkDome = weatherWarningDomeCB->isChecked(); actions.closeShutter = weatherWarningShutterCB->isChecked(); actions.delay = static_cast(weatherWarningDelaySB->value()); getWeatherModel()->setWarningActions(actions); } void Observatory::weatherAlertSettingsChanged() { struct WeatherActions actions; actions.parkDome = weatherAlertDomeCB->isChecked(); actions.closeShutter = weatherAlertShutterCB->isChecked(); actions.delay = static_cast(weatherAlertDelaySB->value()); getWeatherModel()->setAlertActions(actions); } void Observatory::observatoryStatusChanged(bool ready) { // statusReadyButton->setEnabled(!ready); statusReadyButton->setChecked(ready); emit newStatus(ready); } void Observatory::domeAzimuthChanged(double position) { domeAzimuthPosition->setText(QString::number(position, 'f', 2)); } void Observatory::setWarningActions(WeatherActions actions) { weatherWarningDomeCB->setChecked(actions.parkDome); weatherWarningShutterCB->setChecked(actions.closeShutter); weatherWarningDelaySB->setValue(static_cast(actions.delay)); } void Observatory::setAlertActions(WeatherActions actions) { weatherAlertDomeCB->setChecked(actions.parkDome); weatherAlertShutterCB->setChecked(actions.closeShutter); weatherAlertDelaySB->setValue(static_cast(actions.delay)); } void Observatory::toggleButtons(QPushButton *buttonPressed, QString titlePressed, QPushButton *buttonCounterpart, QString titleCounterpart) { buttonPressed->setEnabled(false); buttonPressed->setText(titlePressed); buttonCounterpart->setEnabled(true); buttonCounterpart->setChecked(false); buttonCounterpart->setCheckable(false); buttonCounterpart->setText(titleCounterpart); } void Observatory::activateButton(QPushButton *button, QString title) { button->setEnabled(true); button->setCheckable(false); button->setText(title); } void Observatory::buttonPressed(QPushButton *button, QString title) { button->setEnabled(false); button->setCheckable(true); button->setChecked(true); button->setText(title); } void Observatory::statusControlSettingsChanged() { ObservatoryStatusControl control; control.useDome = useDomeCB->isChecked(); control.useShutter = useShutterCB->isChecked(); control.useWeather = useWeatherCB->isChecked(); mObservatoryModel->setStatusControl(control); } void Observatory::appendLogText(const QString &text) { m_LogText.insert(0, i18nc("log entry; %1 is the date, %2 is the text", "%1 %2", - QDateTime::currentDateTime().toString("yyyy-MM-ddThh:mm:ss"), text)); + KStarsData::Instance()->lt().toString("yyyy-MM-ddThh:mm:ss"), text)); qCInfo(KSTARS_EKOS_OBSERVATORY) << text; emit newLog(text); } void Observatory::clearLog() { m_LogText.clear(); emit newLog(QString()); } } diff --git a/kstars/ekos/scheduler/scheduler.cpp b/kstars/ekos/scheduler/scheduler.cpp index 6181e66b6..d72675bab 100644 --- a/kstars/ekos/scheduler/scheduler.cpp +++ b/kstars/ekos/scheduler/scheduler.cpp @@ -1,7321 +1,7338 @@ /* Ekos Scheduler Module Copyright (C) 2015 Jasem Mutlaq DBus calls from GSoC 2015 Ekos Scheduler project by Daniel Leu 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 "scheduler.h" #include "ksalmanac.h" #include "ksnotification.h" #include "kstars.h" #include "kstarsdata.h" #include "ksutils.h" #include "mosaic.h" #include "Options.h" #include "scheduleradaptor.h" #include "schedulerjob.h" #include "skymapcomposite.h" #include "auxiliary/QProgressIndicator.h" #include "dialogs/finddialog.h" #include "ekos/manager.h" #include "ekos/capture/sequencejob.h" #include "skyobjects/starobject.h" #include #include #include #include #define BAD_SCORE -1000 #define MAX_FAILURE_ATTEMPTS 5 #define UPDATE_PERIOD_MS 1000 #define RESTART_GUIDING_DELAY_MS 5000 #define DEFAULT_CULMINATION_TIME -60 #define DEFAULT_MIN_ALTITUDE 15 #define DEFAULT_MIN_MOON_SEPARATION 0 namespace Ekos { Scheduler::Scheduler() { setupUi(this); qRegisterMetaType("Ekos::SchedulerState"); qDBusRegisterMetaType(); new SchedulerAdaptor(this); QDBusConnection::sessionBus().registerObject("/KStars/Ekos/Scheduler", this); dirPath = QUrl::fromLocalFile(QDir::homePath()); // Get current KStars time and set seconds to zero QDateTime currentDateTime = KStarsData::Instance()->lt(); QTime currentTime = currentDateTime.time(); currentTime.setHMS(currentTime.hour(), currentTime.minute(), 0); currentDateTime.setTime(currentTime); // Set initial time for startup and completion times startupTimeEdit->setDateTime(currentDateTime); completionTimeEdit->setDateTime(currentDateTime); // Set up DBus interfaces QDBusConnection::sessionBus().registerObject("/KStars/Ekos/Scheduler", this); ekosInterface = new QDBusInterface("org.kde.kstars", "/KStars/Ekos", "org.kde.kstars.Ekos", QDBusConnection::sessionBus(), this); // Example of connecting DBus signals //connect(ekosInterface, SIGNAL(indiStatusChanged(Ekos::CommunicationStatus)), this, SLOT(setINDICommunicationStatus(Ekos::CommunicationStatus))); //connect(ekosInterface, SIGNAL(ekosStatusChanged(Ekos::CommunicationStatus)), this, SLOT(setEkosCommunicationStatus(Ekos::CommunicationStatus))); //connect(ekosInterface, SIGNAL(newModule(QString)), this, SLOT(registerNewModule(QString))); QDBusConnection::sessionBus().connect("org.kde.kstars", "/KStars/Ekos", "org.kde.kstars.Ekos", "newModule", this, SLOT(registerNewModule(QString))); QDBusConnection::sessionBus().connect("org.kde.kstars", "/KStars/Ekos", "org.kde.kstars.Ekos", "indiStatusChanged", this, SLOT(setINDICommunicationStatus(Ekos::CommunicationStatus))); QDBusConnection::sessionBus().connect("org.kde.kstars", "/KStars/Ekos", "org.kde.kstars.Ekos", "ekosStatusChanged", this, SLOT(setEkosCommunicationStatus(Ekos::CommunicationStatus))); sleepLabel->setPixmap( QIcon::fromTheme("chronometer").pixmap(QSize(32, 32))); sleepLabel->hide(); connect(&sleepTimer, &QTimer::timeout, this, &Scheduler::wakeUpScheduler); schedulerTimer.setInterval(UPDATE_PERIOD_MS); jobTimer.setInterval(UPDATE_PERIOD_MS); connect(&schedulerTimer, &QTimer::timeout, this, &Scheduler::checkStatus); connect(&jobTimer, &QTimer::timeout, this, &Scheduler::checkJobStage); restartGuidingTimer.setSingleShot(true); restartGuidingTimer.setInterval(RESTART_GUIDING_DELAY_MS); connect(&restartGuidingTimer, &QTimer::timeout, this, [this]() { startGuiding(true); }); pi = new QProgressIndicator(this); bottomLayout->addWidget(pi, 0, nullptr); geo = KStarsData::Instance()->geo(); raBox->setDegType(false); //RA box should be HMS-style /* FIXME: Find a way to have multi-line tooltips in the .ui file, then move the widget configuration there - what about i18n? */ queueTable->setToolTip(i18n("Job scheduler list.\nClick to select a job in the list.\nDouble click to edit a job with the left-hand fields.")); /* Set first button mode to add observation job from left-hand fields */ setJobAddApply(true); removeFromQueueB->setIcon(QIcon::fromTheme("list-remove")); removeFromQueueB->setToolTip(i18n("Remove selected job from the observation list.\nJob properties are copied in the edition fields before removal.")); removeFromQueueB->setAttribute(Qt::WA_LayoutUsesWidgetRect); queueUpB->setIcon(QIcon::fromTheme("go-up")); queueUpB->setToolTip(i18n("Move selected job one line up in the list.\n" "Order only affect observation jobs that are scheduled to start at the same time.\n" "Not available if option \"Sort jobs by Altitude and Priority\" is set.")); queueUpB->setAttribute(Qt::WA_LayoutUsesWidgetRect); queueDownB->setIcon(QIcon::fromTheme("go-down")); queueDownB->setToolTip(i18n("Move selected job one line down in the list.\n" "Order only affect observation jobs that are scheduled to start at the same time.\n" "Not available if option \"Sort jobs by Altitude and Priority\" is set.")); queueDownB->setAttribute(Qt::WA_LayoutUsesWidgetRect); evaluateOnlyB->setIcon(QIcon::fromTheme("system-reboot")); evaluateOnlyB->setToolTip(i18n("Reset state and force reevaluation of all observation jobs.")); evaluateOnlyB->setAttribute(Qt::WA_LayoutUsesWidgetRect); sortJobsB->setIcon(QIcon::fromTheme("transform-move-vertical")); sortJobsB->setToolTip(i18n("Reset state and sort observation jobs per altitude and movement in sky, using the start time of the first job.\n" "This action sorts setting targets before rising targets, and may help scheduling when starting your observation.\n" "Option \"Sort Jobs by Altitude and Priority\" keeps the job list sorted this way, but with current time as reference.\n" "Note the algorithm first calculates all altitudes using the same time, then evaluates jobs.")); sortJobsB->setAttribute(Qt::WA_LayoutUsesWidgetRect); mosaicB->setIcon(QIcon::fromTheme("zoom-draw")); mosaicB->setAttribute(Qt::WA_LayoutUsesWidgetRect); queueSaveAsB->setIcon(QIcon::fromTheme("document-save-as")); queueSaveAsB->setAttribute(Qt::WA_LayoutUsesWidgetRect); queueSaveB->setIcon(QIcon::fromTheme("document-save")); queueSaveB->setAttribute(Qt::WA_LayoutUsesWidgetRect); queueLoadB->setIcon(QIcon::fromTheme("document-open")); queueLoadB->setAttribute(Qt::WA_LayoutUsesWidgetRect); loadSequenceB->setIcon(QIcon::fromTheme("document-open")); loadSequenceB->setAttribute(Qt::WA_LayoutUsesWidgetRect); selectStartupScriptB->setIcon(QIcon::fromTheme("document-open")); selectStartupScriptB->setAttribute(Qt::WA_LayoutUsesWidgetRect); selectShutdownScriptB->setIcon( QIcon::fromTheme("document-open")); selectShutdownScriptB->setAttribute(Qt::WA_LayoutUsesWidgetRect); selectFITSB->setIcon(QIcon::fromTheme("document-open")); selectFITSB->setAttribute(Qt::WA_LayoutUsesWidgetRect); startupB->setIcon( QIcon::fromTheme("media-playback-start")); startupB->setAttribute(Qt::WA_LayoutUsesWidgetRect); shutdownB->setIcon( QIcon::fromTheme("media-playback-start")); shutdownB->setAttribute(Qt::WA_LayoutUsesWidgetRect); connect(startupB, &QPushButton::clicked, this, &Scheduler::runStartupProcedure); connect(shutdownB, &QPushButton::clicked, this, &Scheduler::runShutdownProcedure); selectObjectB->setIcon(QIcon::fromTheme("edit-find")); connect(selectObjectB, &QPushButton::clicked, this, &Scheduler::selectObject); connect(selectFITSB, &QPushButton::clicked, this, &Scheduler::selectFITS); connect(loadSequenceB, &QPushButton::clicked, this, &Scheduler::selectSequence); connect(selectStartupScriptB, &QPushButton::clicked, this, &Scheduler::selectStartupScript); connect(selectShutdownScriptB, &QPushButton::clicked, this, &Scheduler::selectShutdownScript); connect(mosaicB, &QPushButton::clicked, this, &Scheduler::startMosaicTool); connect(addToQueueB, &QPushButton::clicked, this, &Scheduler::addJob); connect(removeFromQueueB, &QPushButton::clicked, this, &Scheduler::removeJob); connect(queueUpB, &QPushButton::clicked, this, &Scheduler::moveJobUp); connect(queueDownB, &QPushButton::clicked, this, &Scheduler::moveJobDown); connect(evaluateOnlyB, &QPushButton::clicked, this, &Scheduler::startJobEvaluation); connect(sortJobsB, &QPushButton::clicked, this, &Scheduler::sortJobsPerAltitude); connect(queueTable->selectionModel(), &QItemSelectionModel::currentRowChanged, this, &Scheduler::queueTableSelectionChanged); connect(queueTable, &QAbstractItemView::clicked, this, &Scheduler::clickQueueTable); connect(queueTable, &QAbstractItemView::doubleClicked, this, &Scheduler::loadJob); startB->setIcon(QIcon::fromTheme("media-playback-start")); startB->setAttribute(Qt::WA_LayoutUsesWidgetRect); pauseB->setIcon(QIcon::fromTheme("media-playback-pause")); pauseB->setAttribute(Qt::WA_LayoutUsesWidgetRect); pauseB->setCheckable(false); connect(startB, &QPushButton::clicked, this, &Scheduler::toggleScheduler); connect(pauseB, &QPushButton::clicked, this, &Scheduler::pause); connect(queueSaveAsB, &QPushButton::clicked, this, &Scheduler::saveAs); connect(queueSaveB, &QPushButton::clicked, this, &Scheduler::save); connect(queueLoadB, &QPushButton::clicked, this, &Scheduler::load); connect(twilightCheck, &QCheckBox::toggled, this, &Scheduler::checkTwilightWarning); + // Connect simulation clock scale + connect(KStarsData::Instance()->clock(), &SimClock::scaleChanged, this, &Scheduler::simClockScaleChanged); + // restore default values for error handling strategy setErrorHandlingStrategy(static_cast(Options::errorHandlingStrategy())); errorHandlingRescheduleErrorsCB->setChecked(Options::rescheduleErrors()); errorHandlingDelaySB->setValue(Options::errorHandlingStrategyDelay()); // save new default values for error handling strategy connect(errorHandlingRescheduleErrorsCB, &QPushButton::clicked, [this](bool checked) { Options::setRescheduleErrors(checked); }); connect(errorHandlingButtonGroup, static_cast(&QButtonGroup::buttonClicked), [this](QAbstractButton * button) { Q_UNUSED(button); Options::setErrorHandlingStrategy(getErrorHandlingStrategy()); }); connect(errorHandlingDelaySB, static_cast(&QSpinBox::valueChanged), [this](int value) { Options::setErrorHandlingStrategyDelay(value); }); connect(KConfigDialog::exists("settings"), &KConfigDialog::settingsChanged, this, &Scheduler::applyConfig); calculateDawnDusk(); loadProfiles(); watchJobChanges(true); } QString Scheduler::getCurrentJobName() { return (currentJob != nullptr ? currentJob->getName() : ""); } void Scheduler::watchJobChanges(bool enable) { /* Don't double watch, this will cause multiple signals to be connected */ if (enable == jobChangesAreWatched) return; /* These are the widgets we want to connect, per signal function, to listen for modifications */ QLineEdit * const lineEdits[] = { nameEdit, raBox, decBox, fitsEdit, sequenceEdit, startupScript, shutdownScript }; QDateTimeEdit * const dateEdits[] = { startupTimeEdit, completionTimeEdit }; QComboBox * const comboBoxes[] = { schedulerProfileCombo }; QButtonGroup * const buttonGroups[] = { stepsButtonGroup, errorHandlingButtonGroup, startupButtonGroup, constraintButtonGroup, completionButtonGroup, startupProcedureButtonGroup, shutdownProcedureGroup }; QAbstractButton * const buttons[] = { errorHandlingRescheduleErrorsCB }; QSpinBox * const spinBoxes[] = { culminationOffset, repeatsSpin, prioritySpin, errorHandlingDelaySB }; QDoubleSpinBox * const dspinBoxes[] = { minMoonSeparation, minAltitude }; if (enable) { /* Connect the relevant signal to setDirty. Note that we are not keeping the connection object: we will * only use that signal once, and there will be no leaks. If we were connecting multiple receiver functions * to the same signal, we would have to be selective when disconnecting. We also use a lambda to absorb the * excess arguments which cannot be passed to setDirty, and limit captured arguments to 'this'. * The main problem with this implementation compared to the macro method is that it is now possible to * stack signal connections. That is, multiple calls to WatchJobChanges will cause multiple signal-to-slot * instances to be registered. As a result, one click will produce N signals, with N*=2 for each call to * WatchJobChanges(true) missing its WatchJobChanges(false) counterpart. */ for (auto * const control : lineEdits) connect(control, &QLineEdit::editingFinished, this, [this]() { setDirty(); }); for (auto * const control : dateEdits) connect(control, &QDateTimeEdit::editingFinished, this, [this]() { setDirty(); }); for (auto * const control : comboBoxes) connect(control, static_cast(&QComboBox::currentIndexChanged), this, [this]() { setDirty(); }); for (auto * const control : buttonGroups) connect(control, static_cast(&QButtonGroup::buttonToggled), this, [this](int, bool) { setDirty(); }); for (auto * const control : buttons) connect(control, static_cast(&QAbstractButton::clicked), this, [this](bool) { setDirty(); }); for (auto * const control : spinBoxes) connect(control, static_cast(&QSpinBox::valueChanged), this, [this]() { setDirty(); }); for (auto * const control : dspinBoxes) connect(control, static_cast(&QDoubleSpinBox::valueChanged), this, [this](double) { setDirty(); }); } else { /* Disconnect the relevant signal from each widget. Actually, this method removes all signals from the widgets, * because we did not take care to keep the connection object when connecting. No problem in our case, we do not * expect other signals to be connected. Because we used a lambda, we cannot use the same function object to * disconnect selectively. */ for (auto * const control : lineEdits) disconnect(control, &QLineEdit::editingFinished, this, nullptr); for (auto * const control : dateEdits) disconnect(control, &QDateTimeEdit::editingFinished, this, nullptr); for (auto * const control : comboBoxes) disconnect(control, static_cast(&QComboBox::currentIndexChanged), this, nullptr); for (auto * const control : buttons) disconnect(control, static_cast(&QAbstractButton::clicked), this, nullptr); for (auto * const control : buttonGroups) disconnect(control, static_cast(&QButtonGroup::buttonToggled), this, nullptr); for (auto * const control : spinBoxes) disconnect(control, static_cast(&QSpinBox::valueChanged), this, nullptr); for (auto * const control : dspinBoxes) disconnect(control, static_cast(&QDoubleSpinBox::valueChanged), this, nullptr); } jobChangesAreWatched = enable; } void Scheduler::appendLogText(const QString &text) { /* FIXME: user settings for log length */ int const max_log_count = 2000; if (m_LogText.size() > max_log_count) m_LogText.removeLast(); m_LogText.prepend(i18nc("log entry; %1 is the date, %2 is the text", "%1 %2", - QDateTime::currentDateTime().toString("yyyy-MM-ddThh:mm:ss"), text)); + KStarsData::Instance()->lt().toString("yyyy-MM-ddThh:mm:ss"), text)); qCInfo(KSTARS_EKOS_SCHEDULER) << text; emit newLog(text); } void Scheduler::clearLog() { m_LogText.clear(); emit newLog(QString()); } void Scheduler::applyConfig() { calculateDawnDusk(); if (SCHEDULER_RUNNING != state) { jobEvaluationOnly = true; evaluateJobs(); } } void Scheduler::selectObject() { if (FindDialog::Instance()->exec() == QDialog::Accepted) { SkyObject *object = FindDialog::Instance()->targetObject(); addObject(object); } } void Scheduler::addObject(SkyObject *object) { if (object != nullptr) { QString finalObjectName(object->name()); if (object->name() == "star") { StarObject *s = dynamic_cast(object); if (s->getHDIndex() != 0) finalObjectName = QString("HD %1").arg(s->getHDIndex()); } nameEdit->setText(finalObjectName); raBox->showInHours(object->ra0()); decBox->showInDegrees(object->dec0()); addToQueueB->setEnabled(sequenceEdit->text().isEmpty() == false); mosaicB->setEnabled(sequenceEdit->text().isEmpty() == false); setDirty(); } } void Scheduler::selectFITS() { fitsURL = QFileDialog::getOpenFileUrl(this, i18n("Select FITS Image"), dirPath, "FITS (*.fits *.fit)"); if (fitsURL.isEmpty()) return; dirPath = QUrl(fitsURL.url(QUrl::RemoveFilename)); fitsEdit->setText(fitsURL.toLocalFile()); if (nameEdit->text().isEmpty()) nameEdit->setText(fitsURL.fileName()); addToQueueB->setEnabled(sequenceEdit->text().isEmpty() == false); mosaicB->setEnabled(sequenceEdit->text().isEmpty() == false); processFITSSelection(); setDirty(); } void Scheduler::processFITSSelection() { const QString filename = fitsEdit->text(); int status = 0; double ra = 0, dec = 0; dms raDMS, deDMS; char comment[128], error_status[512]; fitsfile *fptr = nullptr; if (fits_open_diskfile(&fptr, filename.toLatin1(), READONLY, &status)) { fits_report_error(stderr, status); fits_get_errstatus(status, error_status); qCCritical(KSTARS_EKOS_SCHEDULER) << QString::fromUtf8(error_status); return; } status = 0; if (fits_movabs_hdu(fptr, 1, IMAGE_HDU, &status)) { fits_report_error(stderr, status); fits_get_errstatus(status, error_status); qCCritical(KSTARS_EKOS_SCHEDULER) << QString::fromUtf8(error_status); return; } status = 0; char objectra_str[32] = {0}; if (fits_read_key(fptr, TSTRING, "OBJCTRA", objectra_str, comment, &status)) { if (fits_read_key(fptr, TDOUBLE, "RA", &ra, comment, &status)) { fits_report_error(stderr, status); fits_get_errstatus(status, error_status); appendLogText(i18n("FITS header: cannot find OBJCTRA (%1).", QString(error_status))); return; } raDMS.setD(ra); } else { raDMS = dms::fromString(objectra_str, false); } status = 0; char objectde_str[32] = {0}; if (fits_read_key(fptr, TSTRING, "OBJCTDEC", objectde_str, comment, &status)) { if (fits_read_key(fptr, TDOUBLE, "DEC", &dec, comment, &status)) { fits_report_error(stderr, status); fits_get_errstatus(status, error_status); appendLogText(i18n("FITS header: cannot find OBJCTDEC (%1).", QString(error_status))); return; } deDMS.setD(dec); } else { deDMS = dms::fromString(objectde_str, true); } raBox->setDMS(raDMS.toHMSString()); decBox->setDMS(deDMS.toDMSString()); char object_str[256] = {0}; if (fits_read_key(fptr, TSTRING, "OBJECT", object_str, comment, &status)) { QFileInfo info(filename); nameEdit->setText(info.baseName()); } else { nameEdit->setText(object_str); } } void Scheduler::selectSequence() { sequenceURL = QFileDialog::getOpenFileUrl(this, i18n("Select Sequence Queue"), dirPath, i18n("Ekos Sequence Queue (*.esq)")); if (sequenceURL.isEmpty()) return; dirPath = QUrl(sequenceURL.url(QUrl::RemoveFilename)); sequenceEdit->setText(sequenceURL.toLocalFile()); // For object selection, all fields must be filled if ((raBox->isEmpty() == false && decBox->isEmpty() == false && nameEdit->text().isEmpty() == false) // For FITS selection, only the name and fits URL should be filled. || (nameEdit->text().isEmpty() == false && fitsURL.isEmpty() == false)) { addToQueueB->setEnabled(true); mosaicB->setEnabled(true); } setDirty(); } void Scheduler::selectStartupScript() { startupScriptURL = QFileDialog::getOpenFileUrl(this, i18n("Select Startup Script"), dirPath, i18n("Script (*)")); if (startupScriptURL.isEmpty()) return; dirPath = QUrl(startupScriptURL.url(QUrl::RemoveFilename)); mDirty = true; startupScript->setText(startupScriptURL.toLocalFile()); } void Scheduler::selectShutdownScript() { shutdownScriptURL = QFileDialog::getOpenFileUrl(this, i18n("Select Shutdown Script"), dirPath, i18n("Script (*)")); if (shutdownScriptURL.isEmpty()) return; dirPath = QUrl(shutdownScriptURL.url(QUrl::RemoveFilename)); mDirty = true; shutdownScript->setText(shutdownScriptURL.toLocalFile()); } void Scheduler::addJob() { if (0 <= jobUnderEdit) { /* If a job is being edited, reset edition mode as all fields are already transferred to the job */ resetJobEdit(); } else { /* If a job is being added, save fields into a new job */ saveJob(); /* There is now an evaluation for each change, so don't duplicate the evaluation now */ // jobEvaluationOnly = true; // evaluateJobs(); } } void Scheduler::saveJob() { if (state == SCHEDULER_RUNNING) { appendLogText(i18n("Warning: You cannot add or modify a job while the scheduler is running.")); return; } if (nameEdit->text().isEmpty()) { appendLogText(i18n("Warning: Target name is required.")); return; } if (sequenceEdit->text().isEmpty()) { appendLogText(i18n("Warning: Sequence file is required.")); return; } // Coordinates are required unless it is a FITS file if ((raBox->isEmpty() || decBox->isEmpty()) && fitsURL.isEmpty()) { appendLogText(i18n("Warning: Target coordinates are required.")); return; } bool raOk = false, decOk = false; dms /*const*/ ra(raBox->createDms(false, &raOk)); //false means expressed in hours dms /*const*/ dec(decBox->createDms(true, &decOk)); if (raOk == false) { appendLogText(i18n("Warning: RA value %1 is invalid.", raBox->text())); return; } if (decOk == false) { appendLogText(i18n("Warning: DEC value %1 is invalid.", decBox->text())); return; } watchJobChanges(false); /* Create or Update a scheduler job */ int currentRow = queueTable->currentRow(); SchedulerJob * job = nullptr; /* If no row is selected for insertion, append at end of list. */ if (currentRow < 0) currentRow = queueTable->rowCount(); /* Add job to queue only if it is new, else reuse current row. * Make sure job is added at the right index, now that queueTable may have a line selected without being edited. */ if (0 <= jobUnderEdit) { /* FIXME: jobUnderEdit is a parallel variable that may cause issues if it desyncs from queueTable->currentRow(). */ if (jobUnderEdit != currentRow) qCWarning(KSTARS_EKOS_SCHEDULER) << "BUG: the observation job under edit does not match the selected row in the job table."; /* Use the job in the row currently edited */ job = jobs.at(currentRow); } else { /* Instantiate a new job, insert it in the job list and add a row in the table for it just after the row currently selected. */ job = new SchedulerJob(); jobs.insert(currentRow, job); queueTable->insertRow(currentRow); } /* Configure or reconfigure the observation job */ job->setName(nameEdit->text()); job->setPriority(prioritySpin->value()); job->setTargetCoords(ra, dec); job->setDateTimeDisplayFormat(startupTimeEdit->displayFormat()); /* Consider sequence file is new, and clear captured frames map */ job->setCapturedFramesMap(SchedulerJob::CapturedFramesMap()); job->setSequenceFile(sequenceURL); fitsURL = QUrl::fromLocalFile(fitsEdit->text()); job->setFITSFile(fitsURL); // #1 Startup conditions if (asapConditionR->isChecked()) { job->setStartupCondition(SchedulerJob::START_ASAP); } else if (culminationConditionR->isChecked()) { job->setStartupCondition(SchedulerJob::START_CULMINATION); job->setCulminationOffset(culminationOffset->value()); } else { job->setStartupCondition(SchedulerJob::START_AT); job->setStartupTime(startupTimeEdit->dateTime()); } /* Store the original startup condition */ job->setFileStartupCondition(job->getStartupCondition()); job->setFileStartupTime(job->getStartupTime()); // #2 Constraints // Do we have minimum altitude constraint? if (altConstraintCheck->isChecked()) job->setMinAltitude(minAltitude->value()); else job->setMinAltitude(-90); // Do we have minimum moon separation constraint? if (moonSeparationCheck->isChecked()) job->setMinMoonSeparation(minMoonSeparation->value()); else job->setMinMoonSeparation(-1); // Check enforce weather constraints job->setEnforceWeather(weatherCheck->isChecked()); // twilight constraints job->setEnforceTwilight(twilightCheck->isChecked()); /* Verifications */ /* FIXME: perhaps use a method more visible to the end-user */ if (SchedulerJob::START_AT == job->getFileStartupCondition()) { /* Warn if appending a job which startup time doesn't allow proper score */ if (calculateJobScore(job, job->getStartupTime()) < 0) appendLogText(i18n("Warning: job '%1' has startup time %2 resulting in a negative score, and will be marked invalid when processed.", job->getName(), job->getStartupTime().toString(job->getDateTimeDisplayFormat()))); } // #3 Completion conditions if (sequenceCompletionR->isChecked()) { job->setCompletionCondition(SchedulerJob::FINISH_SEQUENCE); } else if (repeatCompletionR->isChecked()) { job->setCompletionCondition(SchedulerJob::FINISH_REPEAT); job->setRepeatsRequired(repeatsSpin->value()); job->setRepeatsRemaining(repeatsSpin->value()); } else if (loopCompletionR->isChecked()) { job->setCompletionCondition(SchedulerJob::FINISH_LOOP); } else { job->setCompletionCondition(SchedulerJob::FINISH_AT); job->setCompletionTime(completionTimeEdit->dateTime()); } // Job steps job->setStepPipeline(SchedulerJob::USE_NONE); if (trackStepCheck->isChecked()) job->setStepPipeline(static_cast(job->getStepPipeline() | SchedulerJob::USE_TRACK)); if (focusStepCheck->isChecked()) job->setStepPipeline(static_cast(job->getStepPipeline() | SchedulerJob::USE_FOCUS)); if (alignStepCheck->isChecked()) job->setStepPipeline(static_cast(job->getStepPipeline() | SchedulerJob::USE_ALIGN)); if (guideStepCheck->isChecked()) job->setStepPipeline(static_cast(job->getStepPipeline() | SchedulerJob::USE_GUIDE)); /* Reset job state to evaluate the changes */ job->reset(); // Warn user if a duplicated job is in the list - same target, same sequence // FIXME: Those duplicated jobs are not necessarily processed in the order they appear in the list! foreach (SchedulerJob *a_job, jobs) { if (a_job == job) { break; } else if (a_job->getName() == job->getName()) { int const a_job_row = a_job->getNameCell() ? a_job->getNameCell()->row() + 1 : 0; /* FIXME: Warning about duplicate jobs only checks the target name, doing it properly would require checking storage for each sequence job of each scheduler job. */ appendLogText(i18n("Warning: job '%1' at row %2 has a duplicate target at row %3, " "the scheduler may consider the same storage for captures.", job->getName(), currentRow, a_job_row)); /* Warn the user in case the two jobs are really identical */ if (a_job->getSequenceFile() == job->getSequenceFile()) { if (a_job->getRepeatsRequired() == job->getRepeatsRequired() && Options::rememberJobProgress()) appendLogText(i18n("Warning: jobs '%1' at row %2 and %3 probably require a different repeat count " "as currently they will complete simultaneously after %4 batches (or disable option 'Remember job progress')", job->getName(), currentRow, a_job_row, job->getRepeatsRequired())); if (a_job->getStartupTime() == a_job->getStartupTime() && a_job->getPriority() == job->getPriority()) appendLogText(i18n("Warning: job '%1' at row %2 might require a specific startup time or a different priority, " "as currently they will start in order of insertion in the table", job->getName(), currentRow)); } } } if (-1 == jobUnderEdit) { QTableWidgetItem *nameCell = new QTableWidgetItem(); queueTable->setItem(currentRow, static_cast(SCHEDCOL_NAME), nameCell); nameCell->setTextAlignment(Qt::AlignHCenter | Qt::AlignVCenter); nameCell->setFlags(Qt::ItemIsSelectable | Qt::ItemIsEnabled); QTableWidgetItem *statusCell = new QTableWidgetItem(); queueTable->setItem(currentRow, static_cast(SCHEDCOL_STATUS), statusCell); statusCell->setTextAlignment(Qt::AlignHCenter | Qt::AlignVCenter); statusCell->setFlags(Qt::ItemIsSelectable | Qt::ItemIsEnabled); QTableWidgetItem *captureCount = new QTableWidgetItem(); queueTable->setItem(currentRow, static_cast(SCHEDCOL_CAPTURES), captureCount); captureCount->setTextAlignment(Qt::AlignHCenter | Qt::AlignVCenter); captureCount->setFlags(Qt::ItemIsSelectable | Qt::ItemIsEnabled); QTableWidgetItem *scoreValue = new QTableWidgetItem(); queueTable->setItem(currentRow, static_cast(SCHEDCOL_SCORE), scoreValue); scoreValue->setTextAlignment(Qt::AlignHCenter | Qt::AlignVCenter); scoreValue->setFlags(Qt::ItemIsSelectable | Qt::ItemIsEnabled); QTableWidgetItem *startupCell = new QTableWidgetItem(); queueTable->setItem(currentRow, static_cast(SCHEDCOL_STARTTIME), startupCell); startupCell->setTextAlignment(Qt::AlignHCenter | Qt::AlignVCenter); startupCell->setFlags(Qt::ItemIsSelectable | Qt::ItemIsEnabled); QTableWidgetItem *altitudeCell = new QTableWidgetItem(); queueTable->setItem(currentRow, static_cast(SCHEDCOL_ALTITUDE), altitudeCell); altitudeCell->setTextAlignment(Qt::AlignHCenter | Qt::AlignVCenter); altitudeCell->setFlags(Qt::ItemIsSelectable | Qt::ItemIsEnabled); QTableWidgetItem *completionCell = new QTableWidgetItem(); queueTable->setItem(currentRow, static_cast(SCHEDCOL_ENDTIME), completionCell); completionCell->setTextAlignment(Qt::AlignHCenter | Qt::AlignVCenter); completionCell->setFlags(Qt::ItemIsSelectable | Qt::ItemIsEnabled); QTableWidgetItem *estimatedTimeCell = new QTableWidgetItem(); queueTable->setItem(currentRow, static_cast(SCHEDCOL_DURATION), estimatedTimeCell); estimatedTimeCell->setTextAlignment(Qt::AlignHCenter | Qt::AlignVCenter); estimatedTimeCell->setFlags(Qt::ItemIsSelectable | Qt::ItemIsEnabled); QTableWidgetItem *leadTimeCell = new QTableWidgetItem(); queueTable->setItem(currentRow, static_cast(SCHEDCOL_LEADTIME), leadTimeCell); leadTimeCell->setTextAlignment(Qt::AlignHCenter | Qt::AlignVCenter); leadTimeCell->setFlags(Qt::ItemIsSelectable | Qt::ItemIsEnabled); } setJobStatusCells(currentRow); /* We just added or saved a job, so we have a job in the list - enable relevant buttons */ queueSaveAsB->setEnabled(true); queueSaveB->setEnabled(true); startB->setEnabled(true); evaluateOnlyB->setEnabled(true); setJobManipulation(!Options::sortSchedulerJobs(), true); qCDebug(KSTARS_EKOS_SCHEDULER) << QString("Job '%1' at row #%2 was saved.").arg(job->getName()).arg(currentRow + 1); watchJobChanges(true); if (SCHEDULER_LOADING != state) { jobEvaluationOnly = true; evaluateJobs(); } } void Scheduler::syncGUIToJob(SchedulerJob *job) { nameEdit->setText(job->getName()); prioritySpin->setValue(job->getPriority()); raBox->showInHours(job->getTargetCoords().ra0()); decBox->showInDegrees(job->getTargetCoords().dec0()); if (job->getFITSFile().isEmpty() == false) fitsEdit->setText(job->getFITSFile().toLocalFile()); else fitsEdit->clear(); sequenceEdit->setText(job->getSequenceFile().toLocalFile()); trackStepCheck->setChecked(job->getStepPipeline() & SchedulerJob::USE_TRACK); focusStepCheck->setChecked(job->getStepPipeline() & SchedulerJob::USE_FOCUS); alignStepCheck->setChecked(job->getStepPipeline() & SchedulerJob::USE_ALIGN); guideStepCheck->setChecked(job->getStepPipeline() & SchedulerJob::USE_GUIDE); switch (job->getFileStartupCondition()) { case SchedulerJob::START_ASAP: asapConditionR->setChecked(true); culminationOffset->setValue(DEFAULT_CULMINATION_TIME); break; case SchedulerJob::START_CULMINATION: culminationConditionR->setChecked(true); culminationOffset->setValue(job->getCulminationOffset()); break; case SchedulerJob::START_AT: startupTimeConditionR->setChecked(true); startupTimeEdit->setDateTime(job->getStartupTime()); culminationOffset->setValue(DEFAULT_CULMINATION_TIME); break; } if (-90 < job->getMinAltitude()) { altConstraintCheck->setChecked(true); minAltitude->setValue(job->getMinAltitude()); } else { altConstraintCheck->setChecked(false); minAltitude->setValue(DEFAULT_MIN_ALTITUDE); } if (job->getMinMoonSeparation() >= 0) { moonSeparationCheck->setChecked(true); minMoonSeparation->setValue(job->getMinMoonSeparation()); } else { moonSeparationCheck->setChecked(false); minMoonSeparation->setValue(DEFAULT_MIN_MOON_SEPARATION); } weatherCheck->setChecked(job->getEnforceWeather()); twilightCheck->blockSignals(true); twilightCheck->setChecked(job->getEnforceTwilight()); twilightCheck->blockSignals(false); switch (job->getCompletionCondition()) { case SchedulerJob::FINISH_SEQUENCE: sequenceCompletionR->setChecked(true); break; case SchedulerJob::FINISH_REPEAT: repeatCompletionR->setChecked(true); repeatsSpin->setValue(job->getRepeatsRequired()); break; case SchedulerJob::FINISH_LOOP: loopCompletionR->setChecked(true); break; case SchedulerJob::FINISH_AT: timeCompletionR->setChecked(true); completionTimeEdit->setDateTime(job->getCompletionTime()); break; } setJobManipulation(!Options::sortSchedulerJobs(), true); } void Scheduler::loadJob(QModelIndex i) { if (jobUnderEdit == i.row()) return; if (state == SCHEDULER_RUNNING) { appendLogText(i18n("Warning: you cannot add or modify a job while the scheduler is running.")); return; } SchedulerJob * const job = jobs.at(i.row()); if (job == nullptr) return; watchJobChanges(false); //job->setState(SchedulerJob::JOB_IDLE); //job->setStage(SchedulerJob::STAGE_IDLE); syncGUIToJob(job); if (job->getFITSFile().isEmpty() == false) fitsURL = job->getFITSFile(); else fitsURL = QUrl(); sequenceURL = job->getSequenceFile(); /* Turn the add button into an apply button */ setJobAddApply(false); /* Disable scheduler start/evaluate buttons */ startB->setEnabled(false); evaluateOnlyB->setEnabled(false); /* Don't let the end-user remove a job being edited */ setJobManipulation(false, false); jobUnderEdit = i.row(); qCDebug(KSTARS_EKOS_SCHEDULER) << QString("Job '%1' at row #%2 is currently edited.").arg(job->getName()).arg(jobUnderEdit + 1); watchJobChanges(true); } void Scheduler::queueTableSelectionChanged(QModelIndex current, QModelIndex previous) { Q_UNUSED(previous); // prevent selection when not idle if (state != SCHEDULER_IDLE) return; if (current.row() < 0 || (current.row() + 1) > jobs.size()) return; SchedulerJob * const job = jobs.at(current.row()); if (job == nullptr) return; resetJobEdit(); syncGUIToJob(job); } void Scheduler::clickQueueTable(QModelIndex index) { setJobManipulation(!Options::sortSchedulerJobs() && index.isValid(), index.isValid()); } void Scheduler::setJobAddApply(bool add_mode) { if (add_mode) { addToQueueB->setIcon(QIcon::fromTheme("list-add")); addToQueueB->setToolTip(i18n("Use edition fields to create a new job in the observation list.")); //addToQueueB->setStyleSheet(QString()); addToQueueB->setAttribute(Qt::WA_LayoutUsesWidgetRect); } else { addToQueueB->setIcon(QIcon::fromTheme("dialog-ok-apply")); addToQueueB->setToolTip(i18n("Apply job changes.")); //addToQueueB->setStyleSheet("background-color:orange;}"); addToQueueB->setEnabled(true); } } void Scheduler::setJobManipulation(bool can_reorder, bool can_delete) { bool can_edit = (state == SCHEDULER_IDLE); if (can_reorder) { int const currentRow = queueTable->currentRow(); queueUpB->setEnabled(can_edit && 0 < currentRow); queueDownB->setEnabled(can_edit && currentRow < queueTable->rowCount() - 1); } else { queueUpB->setEnabled(false); queueDownB->setEnabled(false); } sortJobsB->setEnabled(can_edit && can_reorder); removeFromQueueB->setEnabled(can_edit && can_delete); } bool Scheduler::reorderJobs(QList reordered_sublist) { /* Add jobs not reordered at the end of the list, in initial order */ foreach (SchedulerJob* job, jobs) if (!reordered_sublist.contains(job)) reordered_sublist.append(job); if (jobs != reordered_sublist) { /* Remember job currently selected */ int const selectedRow = queueTable->currentRow(); SchedulerJob * const selectedJob = 0 <= selectedRow ? jobs.at(selectedRow) : nullptr; /* Reassign list */ jobs = reordered_sublist; /* Reassign status cells for all jobs, and reset them */ for (int row = 0; row < jobs.size(); row++) setJobStatusCells(row); /* Reselect previously selected job */ if (nullptr != selectedJob) queueTable->selectRow(jobs.indexOf(selectedJob)); return true; } else return false; } void Scheduler::moveJobUp() { /* No move if jobs are sorted automatically */ if (Options::sortSchedulerJobs()) return; int const rowCount = queueTable->rowCount(); int const currentRow = queueTable->currentRow(); int const destinationRow = currentRow - 1; /* No move if no job selected, if table has one line or less or if destination is out of table */ if (currentRow < 0 || rowCount <= 1 || destinationRow < 0) return; /* Swap jobs in the list */ jobs.swap(currentRow, destinationRow); /* Reassign status cells */ setJobStatusCells(currentRow); setJobStatusCells(destinationRow); /* Move selection to destination row */ queueTable->selectRow(destinationRow); setJobManipulation(!Options::sortSchedulerJobs(), true); /* Jobs are now sorted, so reset all later jobs */ for (int row = destinationRow; row < jobs.size(); row++) jobs.at(row)->reset(); /* Make list modified and evaluate jobs */ mDirty = true; jobEvaluationOnly = true; evaluateJobs(); } void Scheduler::moveJobDown() { /* No move if jobs are sorted automatically */ if (Options::sortSchedulerJobs()) return; int const rowCount = queueTable->rowCount(); int const currentRow = queueTable->currentRow(); int const destinationRow = currentRow + 1; /* No move if no job selected, if table has one line or less or if destination is out of table */ if (currentRow < 0 || rowCount <= 1 || destinationRow == rowCount) return; /* Swap jobs in the list */ jobs.swap(currentRow, destinationRow); /* Reassign status cells */ setJobStatusCells(currentRow); setJobStatusCells(destinationRow); /* Move selection to destination row */ queueTable->selectRow(destinationRow); setJobManipulation(!Options::sortSchedulerJobs(), true); /* Jobs are now sorted, so reset all later jobs */ for (int row = currentRow; row < jobs.size(); row++) jobs.at(row)->reset(); /* Make list modified and evaluate jobs */ mDirty = true; jobEvaluationOnly = true; evaluateJobs(); } void Scheduler::setJobStatusCells(int row) { if (row < 0 || jobs.size() <= row) return; SchedulerJob * const job = jobs.at(row); job->setNameCell(queueTable->item(row, static_cast(SCHEDCOL_NAME))); job->setStatusCell(queueTable->item(row, static_cast(SCHEDCOL_STATUS))); job->setCaptureCountCell(queueTable->item(row, static_cast(SCHEDCOL_CAPTURES))); job->setScoreCell(queueTable->item(row, static_cast(SCHEDCOL_SCORE))); job->setAltitudeCell(queueTable->item(row, static_cast(SCHEDCOL_ALTITUDE))); job->setStartupCell(queueTable->item(row, static_cast(SCHEDCOL_STARTTIME))); job->setCompletionCell(queueTable->item(row, static_cast(SCHEDCOL_ENDTIME))); job->setEstimatedTimeCell(queueTable->item(row, static_cast(SCHEDCOL_DURATION))); job->setLeadTimeCell(queueTable->item(row, static_cast(SCHEDCOL_LEADTIME))); job->updateJobCells(); } void Scheduler::resetJobEdit() { if (jobUnderEdit < 0) return; SchedulerJob * const job = jobs.at(jobUnderEdit); Q_ASSERT_X(job != nullptr, __FUNCTION__, "Edited job must be valid"); qCDebug(KSTARS_EKOS_SCHEDULER) << QString("Job '%1' at row #%2 is not longer edited.").arg(job->getName()).arg(jobUnderEdit + 1); jobUnderEdit = -1; watchJobChanges(false); /* Revert apply button to add */ setJobAddApply(true); /* Refresh state of job manipulation buttons */ setJobManipulation(!Options::sortSchedulerJobs(), true); /* Restore scheduler operation buttons */ evaluateOnlyB->setEnabled(true); startB->setEnabled(true); Q_ASSERT_X(jobUnderEdit == -1, __FUNCTION__, "No more edited/selected job after exiting edit mode"); } void Scheduler::removeJob() { int currentRow = queueTable->currentRow(); /* Don't remove a row that is not selected */ if (currentRow < 0) return; /* Grab the job currently selected */ SchedulerJob * const job = jobs.at(currentRow); qCDebug(KSTARS_EKOS_SCHEDULER) << QString("Job '%1' at row #%2 is being deleted.").arg(job->getName()).arg(currentRow + 1); /* Remove the job from the table */ queueTable->removeRow(currentRow); /* If there are no job rows left, update UI buttons */ if (queueTable->rowCount() == 0) { setJobManipulation(false, false); evaluateOnlyB->setEnabled(false); queueSaveAsB->setEnabled(false); queueSaveB->setEnabled(false); startB->setEnabled(false); pauseB->setEnabled(false); } /* Else update the selection */ else { if (currentRow > queueTable->rowCount()) currentRow = queueTable->rowCount() - 1; loadJob(queueTable->currentIndex()); queueTable->selectRow(currentRow); } /* If needed, reset edit mode to clean up UI */ if (jobUnderEdit >= 0) resetJobEdit(); /* And remove the job object */ jobs.removeOne(job); delete (job); mDirty = true; jobEvaluationOnly = true; evaluateJobs(); } void Scheduler::toggleScheduler() { if (state == SCHEDULER_RUNNING) { preemptiveShutdown = false; stop(); } else start(); } void Scheduler::stop() { if (state != SCHEDULER_RUNNING) return; qCInfo(KSTARS_EKOS_SCHEDULER) << "Scheduler is stopping..."; // Stop running job and abort all others // in case of soft shutdown we skip this if (preemptiveShutdown == false) { bool wasAborted = false; foreach (SchedulerJob *job, jobs) { if (job == currentJob) stopCurrentJobAction(); if (job->getState() <= SchedulerJob::JOB_BUSY) { appendLogText(i18n("Job '%1' has not been processed upon scheduler stop, marking aborted.", job->getName())); job->setState(SchedulerJob::JOB_ABORTED); wasAborted = true; } } if (wasAborted) KNotification::event(QLatin1String("SchedulerAborted"), i18n("Scheduler aborted.")); } schedulerTimer.stop(); jobTimer.stop(); restartGuidingTimer.stop(); state = SCHEDULER_IDLE; emit newStatus(state); ekosState = EKOS_IDLE; indiState = INDI_IDLE; parkWaitState = PARKWAIT_IDLE; // Only reset startup state to idle if the startup procedure was interrupted before it had the chance to complete. // Or if we're doing a soft shutdown if (startupState != STARTUP_COMPLETE || preemptiveShutdown) { if (startupState == STARTUP_SCRIPT) { scriptProcess.disconnect(); scriptProcess.terminate(); } startupState = STARTUP_IDLE; } // Reset startup state to unparking phase (dome -> mount -> cap) // We do not want to run the startup script again but unparking should be checked // whenever the scheduler is running again. else if (startupState == STARTUP_COMPLETE) { if (unparkDomeCheck->isChecked()) startupState = STARTUP_UNPARK_DOME; else if (unparkMountCheck->isChecked()) startupState = STARTUP_UNPARK_MOUNT; else if (uncapCheck->isChecked()) startupState = STARTUP_UNPARK_CAP; } shutdownState = SHUTDOWN_IDLE; setCurrentJob(nullptr); captureBatch = 0; indiConnectFailureCount = 0; ekosConnectFailureCount = 0; focusFailureCount = 0; guideFailureCount = 0; alignFailureCount = 0; captureFailureCount = 0; jobEvaluationOnly = false; loadAndSlewProgress = false; autofocusCompleted = false; startupB->setEnabled(true); shutdownB->setEnabled(true); // If soft shutdown, we return for now if (preemptiveShutdown) { sleepLabel->setToolTip(i18n("Scheduler is in shutdown until next job is ready")); sleepLabel->show(); return; } // Clear target name in capture interface upon stopping if (captureInterface.isNull() == false) captureInterface->setProperty("targetName", QString()); if (scriptProcess.state() == QProcess::Running) scriptProcess.terminate(); sleepTimer.stop(); //sleepTimer.disconnect(); sleepLabel->hide(); pi->stopAnimation(); startB->setIcon(QIcon::fromTheme("media-playback-start")); startB->setToolTip(i18n("Start Scheduler")); pauseB->setEnabled(false); //startB->setText("Start Scheduler"); queueLoadB->setEnabled(true); addToQueueB->setEnabled(true); setJobManipulation(false, false); mosaicB->setEnabled(true); evaluateOnlyB->setEnabled(true); } void Scheduler::start() { switch (state) { case SCHEDULER_IDLE: /* FIXME: Manage the non-validity of the startup script earlier, and make it a warning only when the scheduler starts */ startupScriptURL = QUrl::fromUserInput(startupScript->text()); if (!startupScript->text().isEmpty() && !startupScriptURL.isValid()) { appendLogText(i18n("Warning: startup script URL %1 is not valid.", startupScript->text())); return; } /* FIXME: Manage the non-validity of the shutdown script earlier, and make it a warning only when the scheduler starts */ shutdownScriptURL = QUrl::fromUserInput(shutdownScript->text()); if (!shutdownScript->text().isEmpty() && !shutdownScriptURL.isValid()) { appendLogText(i18n("Warning: shutdown script URL %1 is not valid.", shutdownScript->text())); return; } qCInfo(KSTARS_EKOS_SCHEDULER) << "Scheduler is starting..."; /* Update UI to reflect startup */ pi->startAnimation(); sleepLabel->hide(); startB->setIcon(QIcon::fromTheme("media-playback-stop")); startB->setToolTip(i18n("Stop Scheduler")); pauseB->setEnabled(true); pauseB->setChecked(false); /* Disable edit-related buttons */ queueLoadB->setEnabled(false); addToQueueB->setEnabled(false); setJobManipulation(false, false); mosaicB->setEnabled(false); evaluateOnlyB->setEnabled(false); startupB->setEnabled(false); shutdownB->setEnabled(false); /* Reset and re-evaluate all scheduler jobs, then start the Scheduler */ startJobEvaluation(); state = SCHEDULER_RUNNING; emit newStatus(state); schedulerTimer.start(); appendLogText(i18n("Scheduler started.")); qCDebug(KSTARS_EKOS_SCHEDULER) << "Scheduler started."; break; case SCHEDULER_PAUSED: /* Update UI to reflect resume */ startB->setIcon(QIcon::fromTheme("media-playback-stop")); startB->setToolTip(i18n("Stop Scheduler")); pauseB->setEnabled(true); pauseB->setCheckable(false); pauseB->setChecked(false); /* Edit-related buttons are still disabled */ /* The end-user cannot update the schedule, don't re-evaluate jobs. Timer schedulerTimer is already running. */ state = SCHEDULER_RUNNING; emit newStatus(state); schedulerTimer.start(); appendLogText(i18n("Scheduler resuming.")); qCDebug(KSTARS_EKOS_SCHEDULER) << "Scheduler resuming."; break; default: break; } } void Scheduler::pause() { state = SCHEDULER_PAUSED; emit newStatus(state); appendLogText(i18n("Scheduler pause planned...")); pauseB->setEnabled(false); startB->setIcon(QIcon::fromTheme("media-playback-start")); startB->setToolTip(i18n("Resume Scheduler")); } void Scheduler::setPaused() { pauseB->setCheckable(true); pauseB->setChecked(true); schedulerTimer.stop(); appendLogText(i18n("Scheduler paused.")); } void Scheduler::setCurrentJob(SchedulerJob *job) { /* Reset job widgets */ if (currentJob) { currentJob->setStageLabel(nullptr); } /* Set current job */ currentJob = job; /* Reassign job widgets, or reset to defaults */ if (currentJob) { currentJob->setStageLabel(jobStatus); queueTable->selectRow(jobs.indexOf(currentJob)); } else { jobStatus->setText(i18n("No job running")); //queueTable->clearSelection(); } } void Scheduler::evaluateJobs() { /* Don't evaluate if list is empty */ if (jobs.isEmpty()) return; /* FIXME: it is possible to evaluate jobs while KStars has a time offset, so warn the user about this */ QDateTime const now = KStarsData::Instance()->lt(); /* Start by refreshing the number of captures already present - unneeded if not remembering job progress */ if (Options::rememberJobProgress()) updateCompletedJobsCount(); /* Update dawn and dusk astronomical times - unconditionally in case date changed */ calculateDawnDusk(); /* First, filter out non-schedulable jobs */ /* FIXME: jobs in state JOB_ERROR should not be in the list, reorder states */ QList sortedJobs = jobs; /* Then enumerate SchedulerJobs to consolidate imaging time */ foreach (SchedulerJob *job, sortedJobs) { /* Let aborted jobs be rescheduled later instead of forgetting them */ switch (job->getState()) { case SchedulerJob::JOB_SCHEDULED: /* If job is scheduled, keep it for evaluation against others */ break; case SchedulerJob::JOB_INVALID: case SchedulerJob::JOB_COMPLETE: /* If job is invalid or complete, bypass evaluation */ continue; case SchedulerJob::JOB_BUSY: /* If job is busy, edge case, bypass evaluation */ continue; case SchedulerJob::JOB_ERROR: case SchedulerJob::JOB_ABORTED: /* If job is in error or aborted and we're running, keep its evaluation until there is nothing else to do */ if (state == SCHEDULER_RUNNING) continue; /* Fall through */ case SchedulerJob::JOB_IDLE: case SchedulerJob::JOB_EVALUATION: default: /* If job is idle, re-evaluate completely */ job->setEstimatedTime(-1); break; } switch (job->getCompletionCondition()) { case SchedulerJob::FINISH_AT: /* Job is complete if its fixed completion time is passed */ if (job->getCompletionTime().isValid() && job->getCompletionTime() < now) { job->setState(SchedulerJob::JOB_COMPLETE); continue; } break; case SchedulerJob::FINISH_REPEAT: // In case of a repeating jobs, let's make sure we have more runs left to go // If we don't, re-estimate imaging time for the scheduler job before concluding if (job->getRepeatsRemaining() == 0) { appendLogText(i18n("Job '%1' has no more batches remaining.", job->getName())); if (Options::rememberJobProgress()) { job->setEstimatedTime(-1); } else { job->setState(SchedulerJob::JOB_COMPLETE); job->setEstimatedTime(0); continue; } } break; default: break; } // -1 = Job is not estimated yet // -2 = Job is estimated but time is unknown // > 0 Job is estimated and time is known if (job->getEstimatedTime() == -1) { if (estimateJobTime(job) == false) { job->setState(SchedulerJob::JOB_INVALID); continue; } } if (job->getEstimatedTime() == 0) { job->setRepeatsRemaining(0); job->setState(SchedulerJob::JOB_COMPLETE); continue; } // In any other case, evaluate job->setState(SchedulerJob::JOB_EVALUATION); } /* * At this step, we prepare scheduling of jobs. * We filter out jobs that won't run now, and make sure jobs are not all starting at the same time. */ updatePreDawn(); /* This predicate matches jobs not being evaluated and not aborted */ auto neither_evaluated_nor_aborted = [](SchedulerJob const * const job) { SchedulerJob::JOBStatus const s = job->getState(); return SchedulerJob::JOB_EVALUATION != s && SchedulerJob::JOB_ABORTED != s; }; /* This predicate matches jobs neither being evaluated nor aborted nor in error state */ auto neither_evaluated_nor_aborted_nor_error = [](SchedulerJob const * const job) { SchedulerJob::JOBStatus const s = job->getState(); return SchedulerJob::JOB_EVALUATION != s && SchedulerJob::JOB_ABORTED != s && SchedulerJob::JOB_ERROR != s; }; /* This predicate matches jobs that aborted, or completed for whatever reason */ auto finished_or_aborted = [](SchedulerJob const * const job) { SchedulerJob::JOBStatus const s = job->getState(); return SchedulerJob::JOB_ERROR <= s || SchedulerJob::JOB_ABORTED == s; }; bool nea = std::all_of(sortedJobs.begin(), sortedJobs.end(), neither_evaluated_nor_aborted); bool neae = std::all_of(sortedJobs.begin(), sortedJobs.end(), neither_evaluated_nor_aborted_nor_error); /* If there are no jobs left to run in the filtered list, stop evaluation */ if (sortedJobs.isEmpty() || (!errorHandlingRescheduleErrorsCB->isChecked() && nea) || (errorHandlingRescheduleErrorsCB->isChecked() && neae)) { appendLogText(i18n("No jobs left in the scheduler queue.")); setCurrentJob(nullptr); jobEvaluationOnly = false; return; } /* If there are only aborted jobs that can run, reschedule those */ if (std::all_of(sortedJobs.begin(), sortedJobs.end(), finished_or_aborted) && errorHandlingDontRestartButton->isChecked() == false) { appendLogText(i18n("Only %1 jobs left in the scheduler queue, rescheduling those.", errorHandlingRescheduleErrorsCB->isChecked() ? "aborted or error" : "aborted")); // set aborted and error jobs to evaluation state for (int index = 0; index < sortedJobs.size(); index++) { SchedulerJob * const job = sortedJobs.at(index); if (SchedulerJob::JOB_ABORTED == job->getState() || (errorHandlingRescheduleErrorsCB->isChecked() && SchedulerJob::JOB_ERROR == job->getState())) job->setState(SchedulerJob::JOB_EVALUATION); } if (errorHandlingRestartAfterAllButton->isChecked()) { // interrupt regular status checks during the sleep time schedulerTimer.stop(); // but before we restart them, we wait for the given delay. appendLogText(i18n("All jobs aborted. Waiting %1 seconds to re-schedule.", errorHandlingDelaySB->value())); // wait the given delay until the jobs will be evaluated again - sleepTimer.setInterval(( errorHandlingDelaySB->value() * 1000)); + sleepTimer.setInterval(std::lround((errorHandlingDelaySB->value() * 1000) / KStarsData::Instance()->clock()->scale())); sleepTimer.start(); sleepLabel->setToolTip(i18n("Scheduler waits for a retry.")); sleepLabel->show(); // we continue to determine which job should be running, when the delay is over } } /* If option says so, reorder by altitude and priority before sequencing */ /* FIXME: refactor so all sorts are using the same predicates */ /* FIXME: use std::stable_sort as qStableSort is deprecated */ /* FIXME: dissociate altitude and priority, it's difficult to choose which predicate to use first */ qCInfo(KSTARS_EKOS_SCHEDULER) << "Option to sort jobs based on priority and altitude is" << Options::sortSchedulerJobs(); if (Options::sortSchedulerJobs()) { using namespace std::placeholders; std::stable_sort(sortedJobs.begin(), sortedJobs.end(), std::bind(SchedulerJob::decreasingAltitudeOrder, _1, _2, KStarsData::Instance()->lt())); std::stable_sort(sortedJobs.begin(), sortedJobs.end(), SchedulerJob::increasingPriorityOrder); } /* The first reordered job has no lead time - this could also be the delay from now to startup */ sortedJobs.first()->setLeadTime(0); /* The objective of the following block is to make sure jobs are sequential in the list filtered previously. * * The algorithm manages overlap between jobs by stating that scheduled jobs that start sooner are non-movable. * If the completion time of the previous job overlaps the current job, we offset the startup of the current job. * Jobs that have no valid startup time when evaluated (ASAP jobs) are assigned an immediate startup time. * The lead time from the Options registry is used as a buffer between jobs. * * Note about the situation where the current job overlaps the next job, and the next job is not movable: * - If we mark the current job invalid, it will not be processed at all. Dropping is not satisfactory. * - If we move the current job after the fixed job, we need to restart evaluation with a new list, and risk an * infinite loop eventually. This means swapping schedules, and is incompatible with altitude/priority sort. * - If we mark the current job aborted, it will be re-evaluated each time a job is complete to see if it can fit. * Although puzzling for the end-user, this solution is dynamic: the aborted job might or might not be scheduled * at the planned time slot. But as the end-user did not enforce the start time, this is acceptable. Moreover, the * schedule will be altered by external events during the execution. * * Here are the constraints that have an effect on the job being examined, and indirectly on all subsequent jobs: * - Twilight constraint moves jobs to the next dark sky interval. * - Altitude constraint, currently linked with Moon separation, moves jobs to the next acceptable altitude time. * - Culmination constraint moves jobs to the next transit time, with arbitrary offset. * - Fixed startup time moves jobs to a fixed time, essentially making them non-movable, or invalid if in the past. * * Here are the constraints that have an effect on jobs following the job being examined: * - Repeats requirement increases the duration of the current job, pushing subsequent jobs. * - Looping requirement causes subsequent jobs to become invalid (until dynamic priority is implemented). * - Fixed completion makes subsequent jobs start after that boundary time. * * However, we need a way to inform the end-user about failed schedules clearly in the UI. * The message to get through is that if jobs are not sorted by altitude/priority, the aborted or invalid jobs * should be modified or manually moved to a better position. If jobs are sorted automatically, aborted jobs will * be processed when possible, probably not at the expected moment. */ // Make sure no two jobs have the same scheduled time or overlap with other jobs for (int index = 0; index < sortedJobs.size(); index++) { SchedulerJob * const currentJob = sortedJobs.at(index); // Bypass jobs that are not marked for evaluation - we did not remove them to preserve schedule order if (SchedulerJob::JOB_EVALUATION != currentJob->getState()) continue; // At this point, a job with no valid start date is a problem, so consider invalid startup time is now if (!currentJob->getStartupTime().isValid()) currentJob->setStartupTime(now); // Locate the previous scheduled job, so that a full schedule plan may be actually consolidated SchedulerJob const * previousJob = nullptr; for (int i = index - 1; 0 <= i; i--) { SchedulerJob const * const a_job = sortedJobs.at(i); if (SchedulerJob::JOB_SCHEDULED == a_job->getState()) { previousJob = a_job; break; } } Q_ASSERT_X(nullptr == previousJob || previousJob != currentJob, __FUNCTION__, "Previous job considered for schedule is either undefined or not equal to current."); // Locate the next job - nothing special required except end of list check SchedulerJob const * const nextJob = index + 1 < sortedJobs.size() ? sortedJobs.at(index + 1) : nullptr; Q_ASSERT_X(nullptr == nextJob || nextJob != currentJob, __FUNCTION__, "Next job considered for schedule is either undefined or not equal to current."); // We're attempting to schedule the job 10 times before making it invalid for (int attempt = 1; attempt < 11; attempt++) { qCDebug(KSTARS_EKOS_SCHEDULER) << QString("Schedule attempt #%1 for %2-second job '%3' on row #%4 starting at %5, completing at %6.") .arg(attempt) .arg(static_cast(currentJob->getEstimatedTime())) .arg(currentJob->getName()) .arg(index + 1) .arg(currentJob->getStartupTime().toString(currentJob->getDateTimeDisplayFormat())) .arg(currentJob->getCompletionTime().toString(currentJob->getDateTimeDisplayFormat())); // ----- #1 Should we reject the current job because of its fixed startup time? // // A job with fixed startup time must be processed at the time of startup, and may be late up to leadTime. // When such a job repeats, its startup time is reinitialized to prevent abort - see completion algorithm. // If such a job requires night time, minimum altitude or Moon separation, the consolidated startup time is checked for errors. // If all restrictions are complied with, we bypass the rest of the verifications as the job cannot be moved. if (SchedulerJob::START_AT == currentJob->getFileStartupCondition()) { // Check whether the current job is too far in the past to be processed - if job is repeating, its startup time is already now if (currentJob->getStartupTime().addSecs(static_cast (ceil(Options::leadTime() * 60))) < now) { currentJob->setState(SchedulerJob::JOB_INVALID); appendLogText(i18n("Warning: job '%1' has fixed startup time %2 set in the past, marking invalid.", currentJob->getName(), currentJob->getStartupTime().toString(currentJob->getDateTimeDisplayFormat()))); break; } // Check whether the current job has a positive dark sky score at the time of startup else if (true == currentJob->getEnforceTwilight() && getDarkSkyScore(currentJob->getStartupTime()) < 0) { currentJob->setState(SchedulerJob::JOB_INVALID); appendLogText(i18n("Warning: job '%1' has a fixed start time incompatible with its twilight restriction, marking invalid.", currentJob->getName())); break; } // Check whether the current job has a positive altitude score at the time of startup else if (-90 < currentJob->getMinAltitude() && currentJob->getAltitudeScore(currentJob->getStartupTime()) < 0) { currentJob->setState(SchedulerJob::JOB_INVALID); appendLogText(i18n("Warning: job '%1' has a fixed start time incompatible with its altitude restriction, marking invalid.", currentJob->getName())); break; } // Check whether the current job has a positive Moon separation score at the time of startup else if (0 < currentJob->getMinMoonSeparation() && currentJob->getMoonSeparationScore(currentJob->getStartupTime()) < 0) { currentJob->setState(SchedulerJob::JOB_INVALID); appendLogText(i18n("Warning: job '%1' has a fixed start time incompatible with its Moon separation restriction, marking invalid.", currentJob->getName())); break; } // Check whether a previous job overlaps the current job if (nullptr != previousJob && previousJob->getCompletionTime().isValid()) { // Calculate time we should be at after finishing the previous job QDateTime const previousCompletionTime = previousJob->getCompletionTime().addSecs(static_cast (ceil(Options::leadTime() * 60.0))); // Make this job invalid if startup time is not achievable because a START_AT job is non-movable if (currentJob->getStartupTime() < previousCompletionTime) { currentJob->setState(SchedulerJob::JOB_INVALID); appendLogText(i18n("Warning: job '%1' has fixed startup time %2 unachievable due to the completion time of its previous sibling, marking invalid.", currentJob->getName(), currentJob->getStartupTime().toString(currentJob->getDateTimeDisplayFormat()))); break; } currentJob->setLeadTime(previousJob->getCompletionTime().secsTo(currentJob->getStartupTime())); } // This job is non-movable, we're done currentJob->setScore(calculateJobScore(currentJob, now)); currentJob->setState(SchedulerJob::JOB_SCHEDULED); qCDebug(KSTARS_EKOS_SCHEDULER) << QString("Job '%1' is scheduled to start at %2, in compliance with fixed startup time requirement.") .arg(currentJob->getName()) .arg(currentJob->getStartupTime().toString(currentJob->getDateTimeDisplayFormat())); break; } // ----- #2 Should we delay the current job because it overlaps the previous job? // // The previous job is considered non-movable, and its completion, plus lead time, is the origin for the current job. // If no previous job exists, or if all prior jobs in the list are rejected, there is no overlap. // If there is a previous job, the current job is simply delayed to avoid an eventual overlap. // IF there is a previous job but it never finishes, the current job is rejected. // This scheduling obviously relies on imaging time estimation: because errors stack up, future startup times are less and less reliable. if (nullptr != previousJob) { if (previousJob->getCompletionTime().isValid()) { // Calculate time we should be at after finishing the previous job QDateTime const previousCompletionTime = previousJob->getCompletionTime().addSecs(static_cast (ceil(Options::leadTime() * 60.0))); // Delay the current job to completion of its previous sibling if needed - this updates the completion time automatically if (currentJob->getStartupTime() < previousCompletionTime) { currentJob->setStartupTime(previousCompletionTime); qCDebug(KSTARS_EKOS_SCHEDULER) << QString("Job '%1' is scheduled to start at %2, %3 seconds after %4, in compliance with previous job completion requirement.") .arg(currentJob->getName()) .arg(currentJob->getStartupTime().toString(currentJob->getDateTimeDisplayFormat())) .arg(previousJob->getCompletionTime().secsTo(currentJob->getStartupTime())) .arg(previousJob->getCompletionTime().toString(previousJob->getDateTimeDisplayFormat())); // If the job is repeating or looping, re-estimate imaging duration - error case may be a bug if (SchedulerJob::FINISH_SEQUENCE != currentJob->getCompletionCondition()) if (false == estimateJobTime(currentJob)) currentJob->setState(SchedulerJob::JOB_INVALID); continue; } } else { currentJob->setState(SchedulerJob::JOB_INVALID); appendLogText(i18n("Warning: Job '%1' cannot start because its previous sibling has no completion time, marking invalid.", currentJob->getName())); break; } currentJob->setLeadTime(previousJob->getCompletionTime().secsTo(currentJob->getStartupTime())); // Lead time can be zero, so completion may equal startup Q_ASSERT_X(previousJob->getCompletionTime() <= currentJob->getStartupTime(), __FUNCTION__, "Previous and current jobs do not overlap."); } // ----- #3 Should we delay the current job because it overlaps daylight? // // Pre-dawn time rules whether a job may be started before dawn, or delayed to next night. // Note that the case of START_AT jobs is considered earlier in the algorithm, thus may be omitted here. // In addition to be hardcoded currently, the imaging duration is not reliable enough to start a short job during pre-dawn. // However, completion time during daylight only causes a warning, as this case will be processed as the job runs. if (currentJob->getEnforceTwilight()) { // During that check, we don't verify the current job can actually complete before dawn. // If the job is interrupted while running, it will be aborted and rescheduled at a later time. // We wouldn't start observation 30 mins (default) before dawn. // FIXME: Refactor duplicated dawn/dusk calculations double const earlyDawn = Dawn - Options::preDawnTime() / (60.0 * 24.0); // Compute dawn time for the startup date of the job // FIXME: Use KAlmanac to find the real dawn/dusk time for the day the job is supposed to be processed QDateTime const dawnDateTime(currentJob->getStartupTime().date(), QTime(0, 0).addSecs(earlyDawn * 24 * 3600)); // Check if the job starts after dawn if (dawnDateTime < currentJob->getStartupTime()) { // Compute dusk time for the startup date of the job - no lead time on dusk QDateTime duskDateTime(currentJob->getStartupTime().date(), QTime(0, 0).addSecs(Dusk * 24 * 3600)); // Near summer solstice, dusk may happen before dawn on the same day, shift dusk by one day in that case if (duskDateTime < dawnDateTime) duskDateTime = duskDateTime.addDays(1); // Check if the job starts before dusk if (currentJob->getStartupTime() < duskDateTime) { // Delay job to next dusk - we will check other requirements later on currentJob->setStartupTime(duskDateTime); qCDebug(KSTARS_EKOS_SCHEDULER) << QString("Job '%1' is scheduled to start at %2, in compliance with night time requirement.") .arg(currentJob->getName()) .arg(currentJob->getStartupTime().toString(currentJob->getDateTimeDisplayFormat())); continue; } } // Compute dawn time for the day following the startup time, but disregard the pre-dawn offset as we'll consider completion // FIXME: Use KAlmanac to find the real dawn/dusk time for the day next to the day the job is supposed to be processed QDateTime const nextDawnDateTime(currentJob->getStartupTime().date().addDays(1), QTime(0, 0).addSecs(Dawn * 24 * 3600)); // Check if the completion date overlaps the next dawn, and issue a warning if so if (nextDawnDateTime < currentJob->getCompletionTime()) { appendLogText(i18n("Warning: job '%1' execution overlaps daylight, it will be interrupted at dawn and rescheduled on next night time.", currentJob->getName())); } Q_ASSERT_X(0 <= getDarkSkyScore(currentJob->getStartupTime()), __FUNCTION__, "Consolidated startup time results in a positive dark sky score."); } // ----- #4 Should we delay the current job because of its target culmination? // // Culmination uses the transit time, and fixes the startup time of the job to a particular offset around this transit time. // This restriction may be used to start a job at the least air mass, or after a meridian flip. // Culmination is scheduled before altitude restriction because it is normally more restrictive for the resulting startup time. // It may happen that a target cannot rise enough to comply with the altitude restriction, but a culmination time is always valid. if (SchedulerJob::START_CULMINATION == currentJob->getFileStartupCondition()) { // Consolidate the culmination time, with offset, of the current job QDateTime const nextCulminationTime = currentJob->calculateCulmination(currentJob->getStartupTime()); if (nextCulminationTime.isValid()) // Guaranteed { if (currentJob->getStartupTime() < nextCulminationTime) { currentJob->setStartupTime(nextCulminationTime); qCDebug(KSTARS_EKOS_SCHEDULER) << QString("Job '%1' is scheduled to start at %2, in compliance with culmination requirements.") .arg(currentJob->getName()) .arg(currentJob->getStartupTime().toString(currentJob->getDateTimeDisplayFormat())); continue; } } else { currentJob->setState(SchedulerJob::JOB_INVALID); appendLogText(i18n("Warning: job '%1' requires culmination offset of %2 minutes, not achievable, marking invalid.", currentJob->getName(), QString("%L1").arg(currentJob->getCulminationOffset()))); break; } // Don't test altitude here, because we will push the job during the next check step // Q_ASSERT_X(0 <= getAltitudeScore(currentJob, currentJob->getStartupTime()), __FUNCTION__, "Consolidated altitude time results in a positive altitude score."); } // ----- #5 Should we delay the current job because its altitude is incorrect? // // Altitude time ensures the job is assigned a startup time when its target is high enough. // As other restrictions, the altitude is only considered for startup time, completion time is managed while the job is running. // Because a target setting down is a problem for the schedule, a cutoff altitude is added in the case the job target is past the meridian at startup time. // FIXME: though arguable, Moon separation is also considered in that restriction check - move it to a separate case. if (-90 < currentJob->getMinAltitude()) { // Consolidate a new altitude time from the startup time of the current job QDateTime const nextAltitudeTime = currentJob->calculateAltitudeTime(currentJob->getStartupTime()); if (nextAltitudeTime.isValid()) { if (currentJob->getStartupTime() < nextAltitudeTime) { currentJob->setStartupTime(nextAltitudeTime); qCDebug(KSTARS_EKOS_SCHEDULER) << QString("Job '%1' is scheduled to start at %2, in compliance with altitude and Moon separation requirements.") .arg(currentJob->getName()) .arg(currentJob->getStartupTime().toString(currentJob->getDateTimeDisplayFormat())); continue; } } else { currentJob->setState(SchedulerJob::JOB_INVALID); appendLogText(i18n("Warning: job '%1' requires minimum altitude %2 and Moon separation %3, not achievable, marking invalid.", currentJob->getName(), QString("%L1").arg(static_cast(currentJob->getMinAltitude()), 0, 'f', minAltitude->decimals()), 0.0 < currentJob->getMinMoonSeparation() ? QString("%L1").arg(static_cast(currentJob->getMinMoonSeparation()), 0, 'f', minMoonSeparation->decimals()) : QString("-"))); break; } Q_ASSERT_X(0 <= currentJob->getAltitudeScore(currentJob->getStartupTime()), __FUNCTION__, "Consolidated altitude time results in a positive altitude score."); } // ----- #6 Should we reject the current job because it overlaps the next job and that next job is not movable? // // If we have a blocker next to the current job, we compare the completion time of the current job and the startup time of this next job, taking lead time into account. // This verification obviously relies on the imaging time to be reliable, but there's not much we can do at this stage of the implementation. if (nullptr != nextJob && SchedulerJob::START_AT == nextJob->getFileStartupCondition()) { // In the current implementation, it is not possible to abort a running job when the next job is supposed to start. // Movable jobs after this one will be delayed, but non-movable jobs are considered blockers. // Calculate time we have between the end of the current job and the next job double const timeToNext = static_cast (currentJob->getCompletionTime().secsTo(nextJob->getStartupTime())); // If that time is overlapping the next job, abort the current job if (timeToNext < Options::leadTime() * 60) { currentJob->setState(SchedulerJob::JOB_ABORTED); appendLogText(i18n("Warning: job '%1' is constrained by the start time of the next job, and cannot finish in time, marking aborted.", currentJob->getName())); break; } Q_ASSERT_X(currentJob->getCompletionTime().addSecs(Options::leadTime() * 60) < nextJob->getStartupTime(), __FUNCTION__, "No overlap "); } // ----- #7 Should we reject the current job because it exceeded its fixed completion time? // // This verification simply checks that because of previous jobs, the startup time of the current job doesn't exceed its fixed completion time. // Its main objective is to catch wrong dates in the FINISH_AT configuration. if (SchedulerJob::FINISH_AT == currentJob->getCompletionCondition()) { if (currentJob->getCompletionTime() < currentJob->getStartupTime()) { appendLogText(i18n("Job '%1' completion time (%2) could not be achieved before start up time (%3)", currentJob->getName(), currentJob->getCompletionTime().toString(currentJob->getDateTimeDisplayFormat()), currentJob->getStartupTime().toString(currentJob->getDateTimeDisplayFormat()))); currentJob->setState(SchedulerJob::JOB_INVALID); break; } } // ----- #8 Should we reject the current job because of weather? // // That verification is left for runtime // // if (false == isWeatherOK(currentJob)) //{ // currentJob->setState(SchedulerJob::JOB_ABORTED); // // appendLogText(i18n("Job '%1' cannot run now because of bad weather, marking aborted.", currentJob->getName())); //} // ----- #9 Update score for current time and mark evaluating jobs as scheduled currentJob->setScore(calculateJobScore(currentJob, now)); currentJob->setState(SchedulerJob::JOB_SCHEDULED); qCDebug(KSTARS_EKOS_SCHEDULER) << QString("Job '%1' on row #%2 passed all checks after %3 attempts, will proceed at %4 for approximately %5 seconds, marking scheduled") .arg(currentJob->getName()) .arg(index + 1) .arg(attempt) .arg(currentJob->getStartupTime().toString(currentJob->getDateTimeDisplayFormat())) .arg(currentJob->getEstimatedTime()); break; } // Check if job was successfully scheduled, else reject it if (SchedulerJob::JOB_EVALUATION == currentJob->getState()) { currentJob->setState(SchedulerJob::JOB_INVALID); //appendLogText(i18n("Warning: job '%1' on row #%2 could not be scheduled during evaluation and is marked invalid, please review your plan.", // currentJob->getName(), // index + 1)); #if 0 // Advices if (-90 < currentJob->getMinAltitude()) appendLogText(i18n("Job '%1' may require relaxing the current altitude requirement of %2 degrees.", currentJob->getName(), QString("%L1").arg(static_cast(currentJob->getMinAltitude()), 0, 'f', minAltitude->decimals))); if (SchedulerJob::START_CULMINATION == currentJob->getFileStartupCondition() && Options::leadTime() < 5) appendLogText(i18n("Job '%1' may require increasing the current lead time of %2 minutes to make transit time calculation stable.", currentJob->getName(), Options::leadTime())); #endif } } /* Apply sorting to queue table, and mark it for saving if it changes */ mDirty = reorderJobs(sortedJobs) | mDirty; if (jobEvaluationOnly || state != SCHEDULER_RUNNING) { qCInfo(KSTARS_EKOS_SCHEDULER) << "Ekos finished evaluating jobs, no job selection required."; jobEvaluationOnly = false; return; } /* * At this step, we finished evaluating jobs. * We select the first job that has to be run, per schedule. */ /* This predicate matches jobs that are neither scheduled to run nor aborted */ auto neither_scheduled_nor_aborted = [](SchedulerJob const * const job) { SchedulerJob::JOBStatus const s = job->getState(); return SchedulerJob::JOB_SCHEDULED != s && SchedulerJob::JOB_ABORTED != s; }; /* If there are no jobs left to run in the filtered list, stop evaluation */ if (sortedJobs.isEmpty() || std::all_of(sortedJobs.begin(), sortedJobs.end(), neither_scheduled_nor_aborted)) { appendLogText(i18n("No jobs left in the scheduler queue after evaluating.")); setCurrentJob(nullptr); jobEvaluationOnly = false; return; } /* If there are only aborted jobs that can run, reschedule those and let Scheduler restart one loop */ else if (std::all_of(sortedJobs.begin(), sortedJobs.end(), finished_or_aborted) && errorHandlingDontRestartButton->isChecked() == false) { appendLogText(i18n("Only aborted jobs left in the scheduler queue after evaluating, rescheduling those.")); std::for_each(sortedJobs.begin(), sortedJobs.end(), [](SchedulerJob * job) { if (SchedulerJob::JOB_ABORTED == job->getState()) job->setState(SchedulerJob::JOB_EVALUATION); }); jobEvaluationOnly = false; return; } /* The job to run is the first scheduled, locate it in the list */ QList::iterator job_to_execute_iterator = std::find_if(sortedJobs.begin(), sortedJobs.end(), [](SchedulerJob * const job) { return SchedulerJob::JOB_SCHEDULED == job->getState(); }); /* If there is no scheduled job anymore (because the restriction loop made them invalid, for instance), bail out */ if (sortedJobs.end() == job_to_execute_iterator) { appendLogText(i18n("No jobs left in the scheduler queue after schedule cleanup.")); setCurrentJob(nullptr); jobEvaluationOnly = false; return; } /* Check if job can be processed right now */ SchedulerJob * const job_to_execute = *job_to_execute_iterator; if (job_to_execute->getFileStartupCondition() == SchedulerJob::START_ASAP) if( 0 <= calculateJobScore(job_to_execute, now)) job_to_execute->setStartupTime(now); qCDebug(KSTARS_EKOS_SCHEDULER) << QString("Job '%1' is selected for next observation with priority #%2 and score %3.") .arg(job_to_execute->getName()) .arg(job_to_execute->getPriority()) .arg(job_to_execute->getScore()); // Set the current job, and let the status timer execute it when ready setCurrentJob(job_to_execute); } void Scheduler::wakeUpScheduler() { sleepLabel->hide(); sleepTimer.stop(); if (preemptiveShutdown) { preemptiveShutdown = false; appendLogText(i18n("Scheduler is awake.")); start(); } else { if (state == SCHEDULER_RUNNING) appendLogText(i18n("Scheduler is awake. Jobs shall be started when ready...")); else appendLogText(i18n("Scheduler is awake. Jobs shall be started when scheduler is resumed.")); schedulerTimer.start(); } } int16_t Scheduler::getWeatherScore() const { if (weatherCheck->isEnabled() == false || weatherCheck->isChecked() == false) return 0; if (weatherStatus == ISD::Weather::WEATHER_WARNING) return BAD_SCORE / 2; else if (weatherStatus == ISD::Weather::WEATHER_ALERT) return BAD_SCORE; return 0; } int16_t Scheduler::getDarkSkyScore(QDateTime const &when) const { double const secsPerDay = 24.0 * 3600.0; double const minsPerDay = 24.0 * 60.0; // Dark sky score is calculated based on distance to today's dawn and next dusk. // Option "Pre-dawn Time" avoids executing a job when dawn is approaching, and is a value in minutes. // - If observation is between option "Pre-dawn Time" and dawn, score is BAD_SCORE/50. // - If observation is before dawn today, score is fraction of the day from beginning of observation to dawn time, as percentage. // - If observation is after dusk, score is fraction of the day from dusk to beginning of observation, as percentage. // - If observation is between dawn and dusk, score is BAD_SCORE. // // If observation time is invalid, the score is calculated for the current day time. // Note exact dusk time is considered valid in terms of night time, and will return a positive, albeit null, score. // FIXME: Dark sky score should consider the middle of the local night as best value. // FIXME: Current algorithm uses the dawn and dusk of today, instead of the day of the observation. int const earlyDawnSecs = static_cast ((Dawn - static_cast (Options::preDawnTime()) / minsPerDay) * secsPerDay); int const dawnSecs = static_cast (Dawn * secsPerDay); int const duskSecs = static_cast (Dusk * secsPerDay); int const obsSecs = (when.isValid() ? when : KStarsData::Instance()->lt()).time().msecsSinceStartOfDay() / 1000; int16_t score = 0; if (earlyDawnSecs <= obsSecs && obsSecs < dawnSecs) { score = BAD_SCORE / 50; //qCDebug(KSTARS_EKOS_SCHEDULER) << QString("Dark sky score at %1 is %2 (between pre-dawn and dawn).") // .arg(observationDateTime.toString()) // .arg(QString::asprintf("%+d", score)); } else if (obsSecs < dawnSecs) { score = static_cast ((dawnSecs - obsSecs) / secsPerDay) * 100; //qCDebug(KSTARS_EKOS_SCHEDULER) << QString("Dark sky score at %1 is %2 (before dawn).") // .arg(observationDateTime.toString()) // .arg(QString::asprintf("%+d", score)); } else if (duskSecs <= obsSecs) { score = static_cast ((obsSecs - duskSecs) / secsPerDay) * 100; //qCDebug(KSTARS_EKOS_SCHEDULER) << QString("Dark sky score at %1 is %2 (after dusk).") // .arg(observationDateTime.toString()) // .arg(QString::asprintf("%+d", score)); } else { score = BAD_SCORE; //qCDebug(KSTARS_EKOS_SCHEDULER) << QString("Dark sky score at %1 is %2 (during daylight).") // .arg(observationDateTime.toString()) // .arg(QString::asprintf("%+d", score)); } return score; } int16_t Scheduler::calculateJobScore(SchedulerJob const *job, QDateTime const &when) const { if (nullptr == job) return BAD_SCORE; /* Only consolidate the score if light frames are required, calibration frames can run whenever needed */ if (!job->getLightFramesRequired()) return 1000; int16_t total = 0; /* As soon as one score is negative, it's a no-go and other scores are unneeded */ if (job->getEnforceTwilight()) { int16_t const darkSkyScore = getDarkSkyScore(when); qCDebug(KSTARS_EKOS_SCHEDULER) << QString("Job '%1' dark sky score is %2 at %3") .arg(job->getName()) .arg(QString::asprintf("%+d", darkSkyScore)) .arg(when.toString(job->getDateTimeDisplayFormat())); total += darkSkyScore; } /* We still enforce altitude if the job is neither required to track nor guide, because this is too confusing for the end-user. * If we bypass calculation here, it must also be bypassed when checking job constraints in checkJobStage. */ if (0 <= total /*&& ((job->getStepPipeline() & SchedulerJob::USE_TRACK) || (job->getStepPipeline() & SchedulerJob::USE_GUIDE))*/) { int16_t const altitudeScore = job->getAltitudeScore(when); qCDebug(KSTARS_EKOS_SCHEDULER) << QString("Job '%1' altitude score is %2 at %3") .arg(job->getName()) .arg(QString::asprintf("%+d", altitudeScore)) .arg(when.toString(job->getDateTimeDisplayFormat())); total += altitudeScore; } if (0 <= total) { int16_t const moonSeparationScore = job->getMoonSeparationScore(when); qCDebug(KSTARS_EKOS_SCHEDULER) << QString("Job '%1' Moon separation score is %2 at %3") .arg(job->getName()) .arg(QString::asprintf("%+d", moonSeparationScore)) .arg(when.toString(job->getDateTimeDisplayFormat())); total += moonSeparationScore; } qCInfo(KSTARS_EKOS_SCHEDULER) << QString("Job '%1' has a total score of %2 at %3.") .arg(job->getName()) .arg(QString::asprintf("%+d", total)) .arg(when.toString(job->getDateTimeDisplayFormat())); return total; } void Scheduler::calculateDawnDusk() { KSAlmanac ksal; Dawn = ksal.getDawnAstronomicalTwilight() + Options::dawnOffset() / 24.0; Dusk = ksal.getDuskAstronomicalTwilight() + Options::duskOffset() / 24.0; QTime const dawn = QTime(0, 0, 0).addSecs(Dawn * 24 * 3600); QTime const dusk = QTime(0, 0, 0).addSecs(Dusk * 24 * 3600); duskDateTime.setDate(KStars::Instance()->data()->lt().date()); duskDateTime.setTime(dusk); nightTime->setText(i18n("%1 - %2", dusk.toString("hh:mm"), dawn.toString("hh:mm"))); } void Scheduler::executeJob(SchedulerJob *job) { // Some states have executeJob called after current job is cancelled - checkStatus does this if (job == nullptr) return; // Don't execute the current job if it is already busy if (currentJob == job && SchedulerJob::JOB_BUSY == currentJob->getState()) return; setCurrentJob(job); int index = jobs.indexOf(job); if (index >= 0) queueTable->selectRow(index); QDateTime const now = KStarsData::Instance()->lt(); // If we already started, we check when the next object is scheduled at. // If it is more than 30 minutes in the future, we park the mount if that is supported // and we unpark when it is due to start. //int const nextObservationTime = now.secsTo(currentJob->getStartupTime()); // If the time to wait is greater than the lead time (5 minutes by default) // then we sleep, otherwise we wait. It's the same thing, just different labels. if (shouldSchedulerSleep(currentJob)) return; // If job schedule isn't now, wait - continuing to execute would cancel a parking attempt else if (0 < KStarsData::Instance()->lt().secsTo(currentJob->getStartupTime())) return; // From this point job can be executed now if (job->getCompletionCondition() == SchedulerJob::FINISH_SEQUENCE && Options::rememberJobProgress()) { captureInterface->setProperty("targetName", job->getName().replace(' ', "")); } updatePreDawn(); // Reset autofocus so that focus step is applied properly when checked // When the focus step is not checked, the capture module will eventually run focus periodically autofocusCompleted = false; qCInfo(KSTARS_EKOS_SCHEDULER) << "Executing Job " << currentJob->getName(); currentJob->setState(SchedulerJob::JOB_BUSY); KNotification::event(QLatin1String("EkosSchedulerJobStart"), i18n("Ekos job started (%1)", currentJob->getName())); // No need to continue evaluating jobs as we already have one. schedulerTimer.stop(); jobTimer.start(); } bool Scheduler::checkEkosState() { if (state == SCHEDULER_PAUSED) return false; switch (ekosState) { case EKOS_IDLE: { if (m_EkosCommunicationStatus == Ekos::Success) { ekosState = EKOS_READY; return true; } else { ekosInterface->call(QDBus::AutoDetect, "start"); ekosState = EKOS_STARTING; currentOperationTime.start(); qCInfo(KSTARS_EKOS_SCHEDULER) << "Ekos communication status is" << m_EkosCommunicationStatus << "Starting Ekos..."; return false; } } case EKOS_STARTING: { if (m_EkosCommunicationStatus == Ekos::Success) { appendLogText(i18n("Ekos started.")); ekosConnectFailureCount = 0; ekosState = EKOS_READY; return true; } else if (m_EkosCommunicationStatus == Ekos::Error) { if (ekosConnectFailureCount++ < MAX_FAILURE_ATTEMPTS) { appendLogText(i18n("Starting Ekos failed. Retrying...")); ekosInterface->call(QDBus::AutoDetect, "start"); return false; } appendLogText(i18n("Starting Ekos failed.")); stop(); return false; } else if (m_EkosCommunicationStatus == Ekos::Idle) return false; // If a minute passed, give up else if (currentOperationTime.elapsed() > (60 * 1000)) { if (ekosConnectFailureCount++ < MAX_FAILURE_ATTEMPTS) { appendLogText(i18n("Starting Ekos timed out. Retrying...")); ekosInterface->call(QDBus::AutoDetect, "stop"); QTimer::singleShot(1000, this, [&]() { ekosInterface->call(QDBus::AutoDetect, "start"); currentOperationTime.restart(); }); return false; } appendLogText(i18n("Starting Ekos timed out.")); stop(); return false; } } break; case EKOS_STOPPING: { if (m_EkosCommunicationStatus == Ekos::Idle) { appendLogText(i18n("Ekos stopped.")); ekosState = EKOS_IDLE; return true; } } break; case EKOS_READY: return true; } return false; } bool Scheduler::isINDIConnected() { return (m_INDICommunicationStatus == Ekos::Success); } bool Scheduler::checkINDIState() { if (state == SCHEDULER_PAUSED) return false; //qCDebug(KSTARS_EKOS_SCHEDULER) << "Checking INDI State" << indiState; switch (indiState) { case INDI_IDLE: { if (m_INDICommunicationStatus == Ekos::Success) { indiState = INDI_PROPERTY_CHECK; indiConnectFailureCount = 0; qCDebug(KSTARS_EKOS_SCHEDULER) << "Checking INDI Properties..."; } else { qCDebug(KSTARS_EKOS_SCHEDULER) << "Connecting INDI devices..."; ekosInterface->call(QDBus::AutoDetect, "connectDevices"); indiState = INDI_CONNECTING; currentOperationTime.start(); } } break; case INDI_CONNECTING: { if (m_INDICommunicationStatus == Ekos::Success) { appendLogText(i18n("INDI devices connected.")); indiState = INDI_PROPERTY_CHECK; } else if (m_INDICommunicationStatus == Ekos::Error) { if (indiConnectFailureCount++ < MAX_FAILURE_ATTEMPTS) { appendLogText(i18n("One or more INDI devices failed to connect. Retrying...")); ekosInterface->call(QDBus::AutoDetect, "connectDevices"); } else { appendLogText(i18n("One or more INDI devices failed to connect. Check INDI control panel for details.")); stop(); } } // If 30 seconds passed, we retry else if (currentOperationTime.elapsed() > (30 * 1000)) { if (indiConnectFailureCount++ < MAX_FAILURE_ATTEMPTS) { appendLogText(i18n("One or more INDI devices timed out. Retrying...")); ekosInterface->call(QDBus::AutoDetect, "connectDevices"); currentOperationTime.restart(); } else { appendLogText(i18n("One or more INDI devices timed out. Check INDI control panel for details.")); stop(); } } } break; case INDI_DISCONNECTING: { if (m_INDICommunicationStatus == Ekos::Idle) { appendLogText(i18n("INDI devices disconnected.")); indiState = INDI_IDLE; return true; } } break; case INDI_PROPERTY_CHECK: { qCDebug(KSTARS_EKOS_SCHEDULER) << "Checking INDI properties."; // If dome unparking is required then we wait for dome interface if (unparkDomeCheck->isChecked() && m_DomeReady == false) { if (currentOperationTime.elapsed() > (30 * 1000)) { currentOperationTime.restart(); appendLogText(i18n("Warning: dome device not ready after timeout, attempting to recover...")); disconnectINDI(); stopEkos(); } qCDebug(KSTARS_EKOS_SCHEDULER) << "Dome unpark required but dome is not yet ready."; return false; } // If mount unparking is required then we wait for mount interface if (unparkMountCheck->isChecked() && m_MountReady == false) { if (currentOperationTime.elapsed() > (30 * 1000)) { currentOperationTime.restart(); appendLogText(i18n("Warning: mount device not ready after timeout, attempting to recover...")); disconnectINDI(); stopEkos(); } qCDebug(KSTARS_EKOS_SCHEDULER) << "Mount unpark required but mount is not yet ready."; return false; } // If cap unparking is required then we wait for cap interface if (uncapCheck->isChecked() && m_CapReady == false) { if (currentOperationTime.elapsed() > (30 * 1000)) { currentOperationTime.restart(); appendLogText(i18n("Warning: cap device not ready after timeout, attempting to recover...")); disconnectINDI(); stopEkos(); } qCDebug(KSTARS_EKOS_SCHEDULER) << "Cap unpark required but cap is not yet ready."; return false; } // capture interface is required at all times to proceed. if (captureInterface.isNull()) return false; if (m_CaptureReady == false) { QVariant hasCoolerControl = captureInterface->property("coolerControl"); if (hasCoolerControl.isValid()) { warmCCDCheck->setEnabled(hasCoolerControl.toBool()); m_CaptureReady = true; } else qCWarning(KSTARS_EKOS_SCHEDULER) << "Capture module is not ready yet..."; } indiState = INDI_READY; indiConnectFailureCount = 0; return true; #if 0 // Check if mount and dome support parking or not. QDBusReply boolReply = mountInterface->call(QDBus::AutoDetect, "canPark"); unparkMountCheck->setEnabled(boolReply.value()); parkMountCheck->setEnabled(boolReply.value()); //qDebug() << "Mount can park " << boolReply.value(); boolReply = domeInterface->call(QDBus::AutoDetect, "canPark"); unparkDomeCheck->setEnabled(boolReply.value()); parkDomeCheck->setEnabled(boolReply.value()); boolReply = captureInterface->call(QDBus::AutoDetect, "hasCoolerControl"); warmCCDCheck->setEnabled(boolReply.value()); QDBusReply updateReply = weatherInterface->call(QDBus::AutoDetect, "getUpdatePeriod"); if (updateReply.error().type() == QDBusError::NoError) { weatherCheck->setEnabled(true); if (updateReply.value() > 0) { weatherTimer.setInterval(updateReply.value() * 1000); connect(&weatherTimer, &QTimer::timeout, this, &Scheduler::checkWeather); weatherTimer.start(); // Check weather initially checkWeather(); } } else weatherCheck->setEnabled(false); QDBusReply capReply = capInterface->call(QDBus::AutoDetect, "canPark"); if (capReply.error().type() == QDBusError::NoError) { capCheck->setEnabled(capReply.value()); uncapCheck->setEnabled(capReply.value()); } else { capCheck->setEnabled(false); uncapCheck->setEnabled(false); } indiState = INDI_READY; return true; #endif } case INDI_READY: return true; } return false; } bool Scheduler::checkStartupState() { if (state == SCHEDULER_PAUSED) return false; qCDebug(KSTARS_EKOS_SCHEDULER) << QString("Checking Startup State (%1)...").arg(startupState); switch (startupState) { case STARTUP_IDLE: { KNotification::event(QLatin1String("ObservatoryStartup"), i18n("Observatory is in the startup process")); qCDebug(KSTARS_EKOS_SCHEDULER) << "Startup Idle. Starting startup process..."; // If Ekos is already started, we skip the script and move on to dome unpark step // unless we do not have light frames, then we skip all //QDBusReply isEkosStarted; //isEkosStarted = ekosInterface->call(QDBus::AutoDetect, "getEkosStartingStatus"); //if (isEkosStarted.value() == Ekos::Success) if (m_EkosCommunicationStatus == Ekos::Success) { if (startupScriptURL.isEmpty() == false) appendLogText(i18n("Ekos is already started, skipping startup script...")); if (currentJob->getLightFramesRequired()) startupState = STARTUP_UNPARK_DOME; else startupState = STARTUP_COMPLETE; return true; } if (schedulerProfileCombo->currentText() != i18n("Default")) { QList profile; profile.append(schedulerProfileCombo->currentText()); ekosInterface->callWithArgumentList(QDBus::AutoDetect, "setProfile", profile); } if (startupScriptURL.isEmpty() == false) { startupState = STARTUP_SCRIPT; executeScript(startupScriptURL.toString(QUrl::PreferLocalFile)); return false; } startupState = STARTUP_UNPARK_DOME; return false; } case STARTUP_SCRIPT: return false; case STARTUP_UNPARK_DOME: // If there is no job in case of manual startup procedure, // or if the job requires light frames, let's proceed with // unparking the dome, otherwise startup process is complete. if (currentJob == nullptr || currentJob->getLightFramesRequired()) { if (unparkDomeCheck->isEnabled() && unparkDomeCheck->isChecked()) unParkDome(); else startupState = STARTUP_UNPARK_MOUNT; } else { startupState = STARTUP_COMPLETE; return true; } break; case STARTUP_UNPARKING_DOME: checkDomeParkingStatus(); break; case STARTUP_UNPARK_MOUNT: if (unparkMountCheck->isEnabled() && unparkMountCheck->isChecked()) unParkMount(); else startupState = STARTUP_UNPARK_CAP; break; case STARTUP_UNPARKING_MOUNT: checkMountParkingStatus(); break; case STARTUP_UNPARK_CAP: if (uncapCheck->isEnabled() && uncapCheck->isChecked()) unParkCap(); else startupState = STARTUP_COMPLETE; break; case STARTUP_UNPARKING_CAP: checkCapParkingStatus(); break; case STARTUP_COMPLETE: return true; case STARTUP_ERROR: stop(); return true; } return false; } bool Scheduler::checkShutdownState() { if (state == SCHEDULER_PAUSED) return false; qCDebug(KSTARS_EKOS_SCHEDULER) << "Checking shutdown state..."; switch (shutdownState) { case SHUTDOWN_IDLE: KNotification::event(QLatin1String("ObservatoryShutdown"), i18n("Observatory is in the shutdown process")); qCInfo(KSTARS_EKOS_SCHEDULER) << "Starting shutdown process..."; // weatherTimer.stop(); // weatherTimer.disconnect(); weatherLabel->hide(); jobTimer.stop(); setCurrentJob(nullptr); if (state == SCHEDULER_RUNNING) schedulerTimer.start(); if (preemptiveShutdown == false) { sleepTimer.stop(); //sleepTimer.disconnect(); } if (warmCCDCheck->isEnabled() && warmCCDCheck->isChecked()) { appendLogText(i18n("Warming up CCD...")); // Turn it off //QVariant arg(false); //captureInterface->call(QDBus::AutoDetect, "setCoolerControl", arg); captureInterface->setProperty("coolerControl", false); } // The following steps require a connection to the INDI server if (isINDIConnected()) { if (capCheck->isEnabled() && capCheck->isChecked()) { shutdownState = SHUTDOWN_PARK_CAP; return false; } if (parkMountCheck->isEnabled() && parkMountCheck->isChecked()) { shutdownState = SHUTDOWN_PARK_MOUNT; return false; } if (parkDomeCheck->isEnabled() && parkDomeCheck->isChecked()) { shutdownState = SHUTDOWN_PARK_DOME; return false; } } else appendLogText(i18n("Warning: Bypassing parking procedures, no INDI connection.")); if (shutdownScriptURL.isEmpty() == false) { shutdownState = SHUTDOWN_SCRIPT; return false; } shutdownState = SHUTDOWN_COMPLETE; return true; case SHUTDOWN_PARK_CAP: if (!isINDIConnected()) { qCInfo(KSTARS_EKOS_SCHEDULER) << "Bypassing shutdown step 'park cap', no INDI connection."; shutdownState = SHUTDOWN_SCRIPT; } else if (capCheck->isEnabled() && capCheck->isChecked()) parkCap(); else shutdownState = SHUTDOWN_PARK_MOUNT; break; case SHUTDOWN_PARKING_CAP: checkCapParkingStatus(); break; case SHUTDOWN_PARK_MOUNT: if (!isINDIConnected()) { qCInfo(KSTARS_EKOS_SCHEDULER) << "Bypassing shutdown step 'park cap', no INDI connection."; shutdownState = SHUTDOWN_SCRIPT; } else if (parkMountCheck->isEnabled() && parkMountCheck->isChecked()) parkMount(); else shutdownState = SHUTDOWN_PARK_DOME; break; case SHUTDOWN_PARKING_MOUNT: checkMountParkingStatus(); break; case SHUTDOWN_PARK_DOME: if (!isINDIConnected()) { qCInfo(KSTARS_EKOS_SCHEDULER) << "Bypassing shutdown step 'park cap', no INDI connection."; shutdownState = SHUTDOWN_SCRIPT; } else if (parkDomeCheck->isEnabled() && parkDomeCheck->isChecked()) parkDome(); else shutdownState = SHUTDOWN_SCRIPT; break; case SHUTDOWN_PARKING_DOME: checkDomeParkingStatus(); break; case SHUTDOWN_SCRIPT: if (shutdownScriptURL.isEmpty() == false) { // Need to stop Ekos now before executing script if it happens to stop INDI if (ekosState != EKOS_IDLE && Options::shutdownScriptTerminatesINDI()) { stopEkos(); return false; } shutdownState = SHUTDOWN_SCRIPT_RUNNING; executeScript(shutdownScriptURL.toString(QUrl::PreferLocalFile)); } else shutdownState = SHUTDOWN_COMPLETE; break; case SHUTDOWN_SCRIPT_RUNNING: return false; case SHUTDOWN_COMPLETE: return true; case SHUTDOWN_ERROR: stop(); return true; } return false; } bool Scheduler::checkParkWaitState() { if (state == SCHEDULER_PAUSED) return false; qCDebug(KSTARS_EKOS_SCHEDULER) << "Checking Park Wait State..."; switch (parkWaitState) { case PARKWAIT_IDLE: return true; case PARKWAIT_PARK: parkMount(); break; case PARKWAIT_PARKING: checkMountParkingStatus(); break; case PARKWAIT_PARKED: return true; case PARKWAIT_UNPARK: unParkMount(); break; case PARKWAIT_UNPARKING: checkMountParkingStatus(); break; case PARKWAIT_UNPARKED: return true; case PARKWAIT_ERROR: appendLogText(i18n("park/unpark wait procedure failed, aborting...")); stop(); return true; } return false; } void Scheduler::executeScript(const QString &filename) { appendLogText(i18n("Executing script %1...", filename)); connect(&scriptProcess, &QProcess::readyReadStandardOutput, this, &Scheduler::readProcessOutput); connect(&scriptProcess, static_cast(&QProcess::finished), this, [this](int exitCode, QProcess::ExitStatus) { checkProcessExit(exitCode); }); scriptProcess.start(filename); } void Scheduler::readProcessOutput() { appendLogText(scriptProcess.readAllStandardOutput().simplified()); } void Scheduler::checkProcessExit(int exitCode) { scriptProcess.disconnect(); if (exitCode == 0) { if (startupState == STARTUP_SCRIPT) startupState = STARTUP_UNPARK_DOME; else if (shutdownState == SHUTDOWN_SCRIPT_RUNNING) shutdownState = SHUTDOWN_COMPLETE; return; } if (startupState == STARTUP_SCRIPT) { appendLogText(i18n("Startup script failed, aborting...")); startupState = STARTUP_ERROR; } else if (shutdownState == SHUTDOWN_SCRIPT_RUNNING) { appendLogText(i18n("Shutdown script failed, aborting...")); shutdownState = SHUTDOWN_ERROR; } } bool Scheduler::checkStatus() { if (state == SCHEDULER_PAUSED) { if (currentJob == nullptr) { setPaused(); return false; } switch (currentJob->getState()) { case SchedulerJob::JOB_BUSY: // do nothing break; case SchedulerJob::JOB_COMPLETE: // start finding next job before pausing break; default: // in all other cases pause setPaused(); break; } } // #1 If no current job selected, let's check if we need to shutdown or evaluate jobs if (currentJob == nullptr) { // #2.1 If shutdown is already complete or in error, we need to stop if (shutdownState == SHUTDOWN_COMPLETE || shutdownState == SHUTDOWN_ERROR) { // If INDI is not done disconnecting, try again later if (indiState == INDI_DISCONNECTING && checkINDIState() == false) return false; // Disconnect INDI if required first if (indiState != INDI_IDLE && Options::stopEkosAfterShutdown()) { disconnectINDI(); return false; } // If Ekos is not done stopping, try again later if (ekosState == EKOS_STOPPING && checkEkosState() == false) return false; // Stop Ekos if required. if (ekosState != EKOS_IDLE && Options::stopEkosAfterShutdown()) { stopEkos(); return false; } if (shutdownState == SHUTDOWN_COMPLETE) appendLogText(i18n("Shutdown complete.")); else appendLogText(i18n("Shutdown procedure failed, aborting...")); // Stop Scheduler stop(); return true; } // #2.2 Check if shutdown is in progress if (shutdownState > SHUTDOWN_IDLE) { // If Ekos is not done stopping, try again later if (ekosState == EKOS_STOPPING && checkEkosState() == false) return false; checkShutdownState(); return false; } // #2.3 Check if park wait procedure is in progress if (checkParkWaitState() == false) return false; // #2.4 If not in shutdown state, evaluate the jobs evaluateJobs(); // #2.5 If there is no current job after evaluation, shutdown if (nullptr == currentJob) { checkShutdownState(); return false; } } // JM 2018-12-07: Check if we need to sleep else if (shouldSchedulerSleep(currentJob) == false) { // #3 Check if startup procedure has failed. if (startupState == STARTUP_ERROR) { // Stop Scheduler stop(); return true; } // #4 Check if startup procedure Phase #1 is complete (Startup script) if ((startupState == STARTUP_IDLE && checkStartupState() == false) || startupState == STARTUP_SCRIPT) return false; // #5 Check if Ekos is started if (checkEkosState() == false) return false; // #6 Check if INDI devices are connected. if (checkINDIState() == false) return false; // #6.1 Check if park wait procedure is in progress - in the case we're waiting for a distant job if (checkParkWaitState() == false) return false; // #7 Check if startup procedure Phase #2 is complete (Unparking phase) if (startupState > STARTUP_SCRIPT && startupState < STARTUP_ERROR && checkStartupState() == false) return false; // #8 Check it it already completed (should only happen starting a paused job) // Find the next job in this case, otherwise execute the current one if (currentJob->getState() == SchedulerJob::JOB_COMPLETE) findNextJob(); else executeJob(currentJob); } return true; } void Scheduler::checkJobStage() { Q_ASSERT_X(currentJob, __FUNCTION__, "Actual current job is required to check job stage"); if (!currentJob) return; qCDebug(KSTARS_EKOS_SCHEDULER) << "Checking job stage for" << currentJob->getName() << "startup" << currentJob->getStartupCondition() << currentJob->getStartupTime().toString(currentJob->getDateTimeDisplayFormat()) << "state" << currentJob->getState(); QDateTime const now = KStarsData::Instance()->lt(); /* Refresh the score of the current job */ /* currentJob->setScore(calculateJobScore(currentJob, now)); */ /* If current job is scheduled and has not started yet, wait */ if (SchedulerJob::JOB_SCHEDULED == currentJob->getState()) if (now < currentJob->getStartupTime()) return; // #1 Check if we need to stop at some point if (currentJob->getCompletionCondition() == SchedulerJob::FINISH_AT && currentJob->getState() == SchedulerJob::JOB_BUSY) { // If the job reached it COMPLETION time, we stop it. if (now.secsTo(currentJob->getCompletionTime()) <= 0) { appendLogText(i18n("Job '%1' reached completion time %2, stopping.", currentJob->getName(), currentJob->getCompletionTime().toString(currentJob->getDateTimeDisplayFormat()))); currentJob->setState(SchedulerJob::JOB_COMPLETE); stopCurrentJobAction(); findNextJob(); return; } } // #2 Check if altitude restriction still holds true if (-90 < currentJob->getMinAltitude()) { SkyPoint p = currentJob->getTargetCoords(); p.EquatorialToHorizontal(KStarsData::Instance()->lst(), geo->lat()); /* FIXME: find a way to use altitude cutoff here, because the job can be scheduled when evaluating, then aborted when running */ if (p.alt().Degrees() < currentJob->getMinAltitude()) { // Only terminate job due to altitude limitation if mount is NOT parked. if (isMountParked() == false) { appendLogText(i18n("Job '%1' current altitude (%2 degrees) crossed minimum constraint altitude (%3 degrees), " "marking aborted.", currentJob->getName(), QString("%L1").arg(p.alt().Degrees(), 0, 'f', minAltitude->decimals()), QString("%L1").arg(currentJob->getMinAltitude(), 0, 'f', minAltitude->decimals()))); currentJob->setState(SchedulerJob::JOB_COMPLETE); stopCurrentJobAction(); findNextJob(); return; } } } // #3 Check if moon separation is still valid if (currentJob->getMinMoonSeparation() > 0) { SkyPoint p = currentJob->getTargetCoords(); p.EquatorialToHorizontal(KStarsData::Instance()->lst(), geo->lat()); double moonSeparation = currentJob->getCurrentMoonSeparation(); if (moonSeparation < currentJob->getMinMoonSeparation()) { // Only terminate job due to moon separation limitation if mount is NOT parked. if (isMountParked() == false) { appendLogText(i18n("Job '%2' current moon separation (%1 degrees) is lower than minimum constraint (%3 " "degrees), marking aborted.", moonSeparation, currentJob->getName(), currentJob->getMinMoonSeparation())); currentJob->setState(SchedulerJob::JOB_COMPLETE); stopCurrentJobAction(); findNextJob(); return; } } } // #4 Check if we're not at dawn if (currentJob->getEnforceTwilight() && now > KStarsDateTime(preDawnDateTime)) { // If either mount or dome are not parked, we shutdown if we approach dawn if (isMountParked() == false || (parkDomeCheck->isEnabled() && isDomeParked() == false)) { // Minute is a DOUBLE value, do not use i18np appendLogText(i18n( "Job '%3' is now approaching astronomical twilight rise limit at %1 (%2 minutes safety margin), marking aborted.", preDawnDateTime.toString(), Options::preDawnTime(), currentJob->getName())); currentJob->setState(SchedulerJob::JOB_COMPLETE); stopCurrentJobAction(); findNextJob(); return; } } // #5 Check system status to improve robustness // This handles external events such as disconnections or end-user manipulating INDI panel if (!checkStatus()) return; // #6 Check each stage is processing properly // FIXME: Vanishing property should trigger a call to its event callback switch (currentJob->getStage()) { case SchedulerJob::STAGE_IDLE: getNextAction(); break; case SchedulerJob::STAGE_ALIGNING: // Let's make sure align module does not become unresponsive if (currentOperationTime.elapsed() > ALIGN_INACTIVITY_TIMEOUT) { QVariant const status = alignInterface->property("status"); Ekos::AlignState alignStatus = static_cast(status.toInt()); if (alignStatus == Ekos::ALIGN_IDLE) { if (alignFailureCount++ < MAX_FAILURE_ATTEMPTS) { qCDebug(KSTARS_EKOS_SCHEDULER) << "Align module timed out. Restarting request..."; startAstrometry(); } else { appendLogText(i18n("Warning: job '%1' alignment procedure failed, marking aborted.", currentJob->getName())); currentJob->setState(SchedulerJob::JOB_ABORTED); findNextJob(); } } else currentOperationTime.restart(); } break; case SchedulerJob::STAGE_CAPTURING: // Let's make sure capture module does not become unresponsive if (currentOperationTime.elapsed() > CAPTURE_INACTIVITY_TIMEOUT) { QVariant const status = captureInterface->property("status"); Ekos::CaptureState captureStatus = static_cast(status.toInt()); if (captureStatus == Ekos::CAPTURE_IDLE) { if (captureFailureCount++ < MAX_FAILURE_ATTEMPTS) { qCDebug(KSTARS_EKOS_SCHEDULER) << "capture module timed out. Restarting request..."; startCapture(); } else { appendLogText(i18n("Warning: job '%1' capture procedure failed, marking aborted.", currentJob->getName())); currentJob->setState(SchedulerJob::JOB_ABORTED); findNextJob(); } } else currentOperationTime.restart(); } break; case SchedulerJob::STAGE_FOCUSING: // Let's make sure focus module does not become unresponsive if (currentOperationTime.elapsed() > FOCUS_INACTIVITY_TIMEOUT) { QVariant const status = focusInterface->property("status"); Ekos::FocusState focusStatus = static_cast(status.toInt()); if (focusStatus == Ekos::FOCUS_IDLE || focusStatus == Ekos::FOCUS_WAITING) { if (focusFailureCount++ < MAX_FAILURE_ATTEMPTS) { qCDebug(KSTARS_EKOS_SCHEDULER) << "Focus module timed out. Restarting request..."; startFocusing(); } else { appendLogText(i18n("Warning: job '%1' focusing procedure failed, marking aborted.", currentJob->getName())); currentJob->setState(SchedulerJob::JOB_ABORTED); findNextJob(); } } else currentOperationTime.restart(); } break; case SchedulerJob::STAGE_GUIDING: // Let's make sure guide module does not become unresponsive if (currentOperationTime.elapsed() > GUIDE_INACTIVITY_TIMEOUT) { GuideState guideStatus = getGuidingStatus(); if (guideStatus == Ekos::GUIDE_IDLE || guideStatus == Ekos::GUIDE_CONNECTED || guideStatus == Ekos::GUIDE_DISCONNECTED) { if (guideFailureCount++ < MAX_FAILURE_ATTEMPTS) { qCDebug(KSTARS_EKOS_SCHEDULER) << "guide module timed out. Restarting request..."; startGuiding(); } else { appendLogText(i18n("Warning: job '%1' guiding procedure failed, marking aborted.", currentJob->getName())); currentJob->setState(SchedulerJob::JOB_ABORTED); findNextJob(); } } else currentOperationTime.restart(); } break; case SchedulerJob::STAGE_SLEWING: case SchedulerJob::STAGE_RESLEWING: // While slewing or re-slewing, check slew status can still be obtained { QVariant const slewStatus = mountInterface->property("status"); if (slewStatus.isValid()) { // Send the slew status periodically to avoid the situation where the mount is already at location and does not send any event // FIXME: in that case, filter TRACKING events only? ISD::Telescope::Status const status = static_cast(slewStatus.toInt()); setMountStatus(status); } else { appendLogText(i18n("Warning: job '%1' lost connection to the mount, attempting to reconnect.", currentJob->getName())); if (!manageConnectionLoss()) currentJob->setState(SchedulerJob::JOB_ERROR); return; } } break; case SchedulerJob::STAGE_SLEW_COMPLETE: case SchedulerJob::STAGE_RESLEWING_COMPLETE: // When done slewing or re-slewing and we use a dome, only shift to the next action when the dome is done moving if (m_DomeReady) { QVariant const isDomeMoving = domeInterface->property("isMoving"); if (!isDomeMoving.isValid()) { appendLogText(i18n("Warning: job '%1' lost connection to the dome, attempting to reconnect.", currentJob->getName())); if (!manageConnectionLoss()) currentJob->setState(SchedulerJob::JOB_ERROR); return; } if (!isDomeMoving.value()) getNextAction(); } else getNextAction(); break; #if 0 case SchedulerJob::STAGE_FOCUSING: { QDBusReply focusReply = focusInterface->call(QDBus::AutoDetect, "getStatus"); if (focusReply.error().type() == QDBusError::UnknownObject) { appendLogText(i18n("Warning: job '%1' lost connection to INDI server while focusing, attempting to reconnect.", currentJob->getName())); if (!manageConnectionLoss()) currentJob->setState(SchedulerJob::JOB_ERROR); return; } qCDebug(KSTARS_EKOS_SCHEDULER) << "Focus stage..."; Ekos::FocusState focusStatus = static_cast(focusReply.value()); // Is focus complete? if (focusStatus == Ekos::FOCUS_COMPLETE) { appendLogText(i18n("Job '%1' focusing is complete.", currentJob->getName())); autofocusCompleted = true; currentJob->setStage(SchedulerJob::STAGE_FOCUS_COMPLETE); getNextAction(); } else if (focusStatus == Ekos::FOCUS_FAILED || focusStatus == Ekos::FOCUS_ABORTED) { appendLogText(i18n("Warning: job '%1' focusing failed.", currentJob->getName())); if (focusFailureCount++ < MAX_FAILURE_ATTEMPTS) { appendLogText(i18n("Job '%1' is restarting its focusing procedure.", currentJob->getName())); // Reset frame to original size. focusInterface->call(QDBus::AutoDetect, "resetFrame"); // Restart focusing startFocusing(); } else { appendLogText(i18n("Warning: job '%1' focusing procedure failed, marking terminated due to errors.", currentJob->getName())); currentJob->setState(SchedulerJob::JOB_ERROR); findNextJob(); } } } break; #endif /*case SchedulerJob::STAGE_POSTALIGN_FOCUSING: focusInterface->call(QDBus::AutoDetect,"resetFrame"); currentJob->setStage(SchedulerJob::STAGE_POSTALIGN_FOCUSING_COMPLETE); getNextAction(); break;*/ #if 0 case SchedulerJob::STAGE_ALIGNING: { QDBusReply alignReply; qCDebug(KSTARS_EKOS_SCHEDULER) << "Alignment stage..."; alignReply = alignInterface->call(QDBus::AutoDetect, "getStatus"); if (alignReply.error().type() == QDBusError::UnknownObject) { appendLogText(i18n("Warning: job '%1' lost connection to INDI server while aligning, attempting to reconnect.", currentJob->getName())); if (!manageConnectionLoss()) currentJob->setState(SchedulerJob::JOB_ERROR); return; } Ekos::AlignState alignStatus = static_cast(alignReply.value()); // Is solver complete? if (alignStatus == Ekos::ALIGN_COMPLETE) { appendLogText(i18n("Job '%1' alignment is complete.", currentJob->getName())); alignFailureCount = 0; currentJob->setStage(SchedulerJob::STAGE_ALIGN_COMPLETE); getNextAction(); } else if (alignStatus == Ekos::ALIGN_FAILED || alignStatus == Ekos::ALIGN_ABORTED) { appendLogText(i18n("Warning: job '%1' alignment failed.", currentJob->getName())); if (alignFailureCount++ < MAX_FAILURE_ATTEMPTS) { if (Options::resetMountModelOnAlignFail() && MAX_FAILURE_ATTEMPTS - 1 < alignFailureCount) { appendLogText(i18n("Warning: job '%1' forcing mount model reset after failing alignment #%2.", currentJob->getName(), alignFailureCount)); mountInterface->call(QDBus::AutoDetect, "resetModel"); } appendLogText(i18n("Restarting %1 alignment procedure...", currentJob->getName())); startAstrometry(); } else { appendLogText(i18n("Warning: job '%1' alignment procedure failed, aborting job.", currentJob->getName())); currentJob->setState(SchedulerJob::JOB_ABORTED); findNextJob(); } } } break; #endif #if 0 case SchedulerJob::STAGE_GUIDING: { QDBusReply guideReply = guideInterface->call(QDBus::AutoDetect, "getStatus"); qCDebug(KSTARS_EKOS_SCHEDULER) << "Calibration & Guide stage..."; if (guideReply.error().type() == QDBusError::UnknownObject) { appendLogText(i18n("Warning: job '%1' lost connection to INDI server while guiding, attempting to reconnect.", currentJob->getName())); if (!manageConnectionLoss()) currentJob->setState(SchedulerJob::JOB_ERROR); return; } Ekos::GuideState guideStatus = static_cast(guideReply.value()); // If calibration stage complete? if (guideStatus == Ekos::GUIDE_GUIDING) { appendLogText(i18n("Job '%1' guiding is in progress.", currentJob->getName())); guideFailureCount = 0; currentJob->setStage(SchedulerJob::STAGE_GUIDING_COMPLETE); getNextAction(); } // JM 2018-07-30: GUIDE_IDLE is also a failure else if (guideStatus == Ekos::GUIDE_CALIBRATION_ERROR || guideStatus == Ekos::GUIDE_ABORTED) { if (guideStatus == Ekos::GUIDE_ABORTED) appendLogText(i18n("Warning: job '%1' guiding failed.", currentJob->getName())); else appendLogText(i18n("Warning: job '%1' calibration failed.", currentJob->getName())); if (guideFailureCount++ < MAX_FAILURE_ATTEMPTS) { if (guideStatus == Ekos::GUIDE_CALIBRATION_ERROR && Options::realignAfterCalibrationFailure()) { appendLogText(i18n("Restarting %1 alignment procedure...", currentJob->getName())); // JM: We have to go back to startSlew() since if we just call startAstrometry() // It would captureAndSolve at the _current_ coords which could be way off center if the calibration // process took a wild ride search for a suitable guide star and then failed. So startSlew() would ensure // we're back on our target and then it proceed to alignment (focus is skipped since it is done if it was checked anyway). startSlew(); } else { appendLogText(i18n("Job '%1' is guiding, and is restarting its guiding procedure.", currentJob->getName())); startGuiding(true); } } else { appendLogText(i18n("Warning: job '%1' guiding procedure failed, marking terminated due to errors.", currentJob->getName())); currentJob->setState(SchedulerJob::JOB_ERROR); findNextJob(); } } } break; #endif #if 0 case SchedulerJob::STAGE_CAPTURING: { QDBusReply captureReply = captureInterface->call(QDBus::AutoDetect, "getSequenceQueueStatus"); if (captureReply.error().type() == QDBusError::UnknownObject) { appendLogText(i18n("Warning: job '%1' lost connection to INDI server while capturing, attempting to reconnect.", currentJob->getName())); if (!manageConnectionLoss()) currentJob->setState(SchedulerJob::JOB_ERROR); } else if (captureReply.value().toStdString() == "Aborted" || captureReply.value().toStdString() == "Error") { appendLogText(i18n("Warning: job '%1' failed to capture target (%2).", currentJob->getName(), captureReply.value())); if (captureFailureCount++ < MAX_FAILURE_ATTEMPTS) { // If capture failed due to guiding error, let's try to restart that if (currentJob->getStepPipeline() & SchedulerJob::USE_GUIDE) { // Check if it is guiding related. QDBusReply guideReply = guideInterface->call(QDBus::AutoDetect, "getStatus"); if (guideReply.value() == Ekos::GUIDE_ABORTED || guideReply.value() == Ekos::GUIDE_CALIBRATION_ERROR || guideReply.value() == GUIDE_DITHERING_ERROR) // If guiding failed, let's restart it //if(guideReply.value() == false) { appendLogText(i18n("Job '%1' is capturing, and is restarting its guiding procedure.", currentJob->getName())); //currentJob->setStage(SchedulerJob::STAGE_GUIDING); startGuiding(true); return; } } /* FIXME: it's not clear whether it is actually possible to continue capturing when capture fails this way */ appendLogText(i18n("Warning: job '%1' failed its capture procedure, restarting capture.", currentJob->getName())); startCapture(); } else { /* FIXME: it's not clear whether this situation can be recovered at all */ appendLogText(i18n("Warning: job '%1' failed its capture procedure, marking aborted.", currentJob->getName())); currentJob->setState(SchedulerJob::JOB_ABORTED); findNextJob(); } } else if (captureReply.value().toStdString() == "Complete") { KNotification::event(QLatin1String("EkosScheduledImagingFinished"), i18n("Ekos job (%1) - Capture finished", currentJob->getName())); captureInterface->call(QDBus::AutoDetect, "clearSequenceQueue"); currentJob->setState(SchedulerJob::JOB_COMPLETE); findNextJob(); } else { captureFailureCount = 0; /* currentJob->setCompletedCount(currentJob->getCompletedCount() + 1); */ } } break; #endif default: break; } } void Scheduler::getNextAction() { qCDebug(KSTARS_EKOS_SCHEDULER) << "Get next action..."; switch (currentJob->getStage()) { case SchedulerJob::STAGE_IDLE: if (currentJob->getLightFramesRequired()) { if (currentJob->getStepPipeline() & SchedulerJob::USE_TRACK) startSlew(); else if (currentJob->getStepPipeline() & SchedulerJob::USE_FOCUS && autofocusCompleted == false) startFocusing(); else if (currentJob->getStepPipeline() & SchedulerJob::USE_ALIGN) startAstrometry(); else if (currentJob->getStepPipeline() & SchedulerJob::USE_GUIDE) if (getGuidingStatus() == GUIDE_GUIDING) { appendLogText(i18n("Guiding already running, directly start capturing.")); startCapture(); } else startGuiding(); else startCapture(); } else { if (currentJob->getStepPipeline()) appendLogText( i18n("Job '%1' is proceeding directly to capture stage because only calibration frames are pending.", currentJob->getName())); startCapture(); } break; case SchedulerJob::STAGE_SLEW_COMPLETE: if (currentJob->getStepPipeline() & SchedulerJob::USE_FOCUS && autofocusCompleted == false) startFocusing(); else if (currentJob->getStepPipeline() & SchedulerJob::USE_ALIGN) startAstrometry(); else if (currentJob->getStepPipeline() & SchedulerJob::USE_GUIDE) startGuiding(); else startCapture(); break; case SchedulerJob::STAGE_FOCUS_COMPLETE: if (currentJob->getStepPipeline() & SchedulerJob::USE_ALIGN) startAstrometry(); else if (currentJob->getStepPipeline() & SchedulerJob::USE_GUIDE) startGuiding(); else startCapture(); break; case SchedulerJob::STAGE_ALIGN_COMPLETE: currentJob->setStage(SchedulerJob::STAGE_RESLEWING); break; case SchedulerJob::STAGE_RESLEWING_COMPLETE: // If we have in-sequence-focus in the sequence file then we perform post alignment focusing so that the focus // frame is ready for the capture module in-sequence-focus procedure. if ((currentJob->getStepPipeline() & SchedulerJob::USE_FOCUS) && currentJob->getInSequenceFocus()) // Post alignment re-focusing startFocusing(); else if (currentJob->getStepPipeline() & SchedulerJob::USE_GUIDE) startGuiding(); else startCapture(); break; case SchedulerJob::STAGE_POSTALIGN_FOCUSING_COMPLETE: if (currentJob->getStepPipeline() & SchedulerJob::USE_GUIDE) startGuiding(); else startCapture(); break; case SchedulerJob::STAGE_GUIDING_COMPLETE: startCapture(); break; default: break; } } void Scheduler::stopCurrentJobAction() { if (nullptr != currentJob) { qCDebug(KSTARS_EKOS_SCHEDULER) << "Job '" << currentJob->getName() << "' is stopping current action..." << currentJob->getStage(); switch (currentJob->getStage()) { case SchedulerJob::STAGE_IDLE: break; case SchedulerJob::STAGE_SLEWING: mountInterface->call(QDBus::AutoDetect, "abort"); break; case SchedulerJob::STAGE_FOCUSING: focusInterface->call(QDBus::AutoDetect, "abort"); break; case SchedulerJob::STAGE_ALIGNING: alignInterface->call(QDBus::AutoDetect, "abort"); break; case SchedulerJob::STAGE_CAPTURING: captureInterface->call(QDBus::AutoDetect, "abort"); break; default: break; } /* Reset interrupted job stage */ currentJob->setStage(SchedulerJob::STAGE_IDLE); } /* Guiding being a parallel process, check to stop it */ stopGuiding(); } bool Scheduler::manageConnectionLoss() { if (SCHEDULER_RUNNING != state) return false; // Don't manage loss if Ekos is actually down in the state machine switch (ekosState) { case EKOS_IDLE: case EKOS_STOPPING: return false; default: break; } // Don't manage loss if INDI is actually down in the state machine switch (indiState) { case INDI_IDLE: case INDI_DISCONNECTING: return false; default: break; } // If Ekos is assumed to be up, check its state //QDBusReply const isEkosStarted = ekosInterface->call(QDBus::AutoDetect, "getEkosStartingStatus"); if (m_EkosCommunicationStatus == Ekos::Success) { qCDebug(KSTARS_EKOS_SCHEDULER) << QString("Ekos is currently connected, checking INDI before mitigating connection loss."); // If INDI is assumed to be up, check its state if (isINDIConnected()) { // If both Ekos and INDI are assumed up, and are actually up, no mitigation needed, this is a DBus interface error qCDebug(KSTARS_EKOS_SCHEDULER) << QString("INDI is currently connected, no connection loss mitigation needed."); return false; } } // Stop actions of the current job stopCurrentJobAction(); // Acknowledge INDI and Ekos disconnections disconnectINDI(); stopEkos(); // Let the Scheduler attempt to connect INDI again return true; } void Scheduler::load() { QUrl fileURL = QFileDialog::getOpenFileUrl(this, i18n("Open Ekos Scheduler List"), dirPath, "Ekos Scheduler List (*.esl)"); 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)); /* Run a job idle evaluation after a successful load */ if (loadScheduler(fileURL.toLocalFile())) startJobEvaluation(); } bool Scheduler::loadScheduler(const QString &fileURL) { SchedulerState const old_state = state; state = SCHEDULER_LOADING; QFile sFile; sFile.setFileName(fileURL); if (!sFile.open(QIODevice::ReadOnly)) { QString message = i18n("Unable to open file %1", fileURL); KSNotification::sorry(message, i18n("Could Not Open File")); state = old_state; return false; } if (jobUnderEdit >= 0) resetJobEdit(); while (queueTable->rowCount() > 0) queueTable->removeRow(0); qDeleteAll(jobs); jobs.clear(); LilXML *xmlParser = newLilXML(); char errmsg[MAXRBUF]; XMLEle *root = nullptr; XMLEle *ep = nullptr; XMLEle *subEP = 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) { for (ep = nextXMLEle(root, 1); ep != nullptr; ep = nextXMLEle(root, 0)) { const char *tag = tagXMLEle(ep); if (!strcmp(tag, "Job")) processJobInfo(ep); else if (!strcmp(tag, "Profile")) { schedulerProfileCombo->setCurrentText(pcdataXMLEle(ep)); } else if (!strcmp(tag, "ErrorHandlingStrategy")) { setErrorHandlingStrategy(static_cast(cLocale.toInt(findXMLAttValu(ep, "value")))); subEP = findXMLEle(ep, "delay"); if (subEP) { errorHandlingDelaySB->setValue(cLocale.toInt(pcdataXMLEle(subEP))); } subEP = findXMLEle(ep, "RescheduleErrors"); errorHandlingRescheduleErrorsCB->setChecked(subEP != nullptr); } else if (!strcmp(tag, "StartupProcedure")) { XMLEle *procedure; startupScript->clear(); unparkDomeCheck->setChecked(false); unparkMountCheck->setChecked(false); uncapCheck->setChecked(false); for (procedure = nextXMLEle(ep, 1); procedure != nullptr; procedure = nextXMLEle(ep, 0)) { const char *proc = pcdataXMLEle(procedure); if (!strcmp(proc, "StartupScript")) { startupScript->setText(findXMLAttValu(procedure, "value")); startupScriptURL = QUrl::fromUserInput(startupScript->text()); } else if (!strcmp(proc, "UnparkDome")) unparkDomeCheck->setChecked(true); else if (!strcmp(proc, "UnparkMount")) unparkMountCheck->setChecked(true); else if (!strcmp(proc, "UnparkCap")) uncapCheck->setChecked(true); } } else if (!strcmp(tag, "ShutdownProcedure")) { XMLEle *procedure; shutdownScript->clear(); warmCCDCheck->setChecked(false); parkDomeCheck->setChecked(false); parkMountCheck->setChecked(false); capCheck->setChecked(false); for (procedure = nextXMLEle(ep, 1); procedure != nullptr; procedure = nextXMLEle(ep, 0)) { const char *proc = pcdataXMLEle(procedure); if (!strcmp(proc, "ShutdownScript")) { shutdownScript->setText(findXMLAttValu(procedure, "value")); shutdownScriptURL = QUrl::fromUserInput(shutdownScript->text()); } else if (!strcmp(proc, "ParkDome")) parkDomeCheck->setChecked(true); else if (!strcmp(proc, "ParkMount")) parkMountCheck->setChecked(true); else if (!strcmp(proc, "ParkCap")) capCheck->setChecked(true); else if (!strcmp(proc, "WarmCCD")) warmCCDCheck->setChecked(true); } } } delXMLEle(root); } else if (errmsg[0]) { appendLogText(QString(errmsg)); delLilXML(xmlParser); state = old_state; return false; } } schedulerURL = QUrl::fromLocalFile(fileURL); mosaicB->setEnabled(true); mDirty = false; delLilXML(xmlParser); state = old_state; return true; } bool Scheduler::processJobInfo(XMLEle *root) { XMLEle *ep; XMLEle *subEP; altConstraintCheck->setChecked(false); moonSeparationCheck->setChecked(false); weatherCheck->setChecked(false); twilightCheck->blockSignals(true); twilightCheck->setChecked(false); twilightCheck->blockSignals(false); minAltitude->setValue(minAltitude->minimum()); minMoonSeparation->setValue(minMoonSeparation->minimum()); // We expect all data read from the XML to be in the C locale - QLocale::c() QLocale cLocale = QLocale::c(); for (ep = nextXMLEle(root, 1); ep != nullptr; ep = nextXMLEle(root, 0)) { if (!strcmp(tagXMLEle(ep), "Name")) nameEdit->setText(pcdataXMLEle(ep)); else if (!strcmp(tagXMLEle(ep), "Priority")) prioritySpin->setValue(atoi(pcdataXMLEle(ep))); else if (!strcmp(tagXMLEle(ep), "Coordinates")) { subEP = findXMLEle(ep, "J2000RA"); if (subEP) { dms ra; ra.setH(cLocale.toDouble(pcdataXMLEle(subEP))); raBox->showInHours(ra); } subEP = findXMLEle(ep, "J2000DE"); if (subEP) { dms de; de.setD(cLocale.toDouble(pcdataXMLEle(subEP))); decBox->showInDegrees(de); } } else if (!strcmp(tagXMLEle(ep), "Sequence")) { sequenceEdit->setText(pcdataXMLEle(ep)); sequenceURL = QUrl::fromUserInput(sequenceEdit->text()); } else if (!strcmp(tagXMLEle(ep), "FITS")) { fitsEdit->setText(pcdataXMLEle(ep)); fitsURL.setPath(fitsEdit->text()); } else if (!strcmp(tagXMLEle(ep), "StartupCondition")) { for (subEP = nextXMLEle(ep, 1); subEP != nullptr; subEP = nextXMLEle(ep, 0)) { if (!strcmp("ASAP", pcdataXMLEle(subEP))) asapConditionR->setChecked(true); else if (!strcmp("Culmination", pcdataXMLEle(subEP))) { culminationConditionR->setChecked(true); culminationOffset->setValue(cLocale.toDouble(findXMLAttValu(subEP, "value"))); } else if (!strcmp("At", pcdataXMLEle(subEP))) { startupTimeConditionR->setChecked(true); startupTimeEdit->setDateTime(QDateTime::fromString(findXMLAttValu(subEP, "value"), Qt::ISODate)); } } } else if (!strcmp(tagXMLEle(ep), "Constraints")) { for (subEP = nextXMLEle(ep, 1); subEP != nullptr; subEP = nextXMLEle(ep, 0)) { if (!strcmp("MinimumAltitude", pcdataXMLEle(subEP))) { altConstraintCheck->setChecked(true); minAltitude->setValue(cLocale.toDouble(findXMLAttValu(subEP, "value"))); } else if (!strcmp("MoonSeparation", pcdataXMLEle(subEP))) { moonSeparationCheck->setChecked(true); minMoonSeparation->setValue(cLocale.toDouble(findXMLAttValu(subEP, "value"))); } else if (!strcmp("EnforceWeather", pcdataXMLEle(subEP))) weatherCheck->setChecked(true); else if (!strcmp("EnforceTwilight", pcdataXMLEle(subEP))) twilightCheck->setChecked(true); } } else if (!strcmp(tagXMLEle(ep), "CompletionCondition")) { for (subEP = nextXMLEle(ep, 1); subEP != nullptr; subEP = nextXMLEle(ep, 0)) { if (!strcmp("Sequence", pcdataXMLEle(subEP))) sequenceCompletionR->setChecked(true); else if (!strcmp("Repeat", pcdataXMLEle(subEP))) { repeatCompletionR->setChecked(true); repeatsSpin->setValue(cLocale.toInt(findXMLAttValu(subEP, "value"))); } else if (!strcmp("Loop", pcdataXMLEle(subEP))) loopCompletionR->setChecked(true); else if (!strcmp("At", pcdataXMLEle(subEP))) { timeCompletionR->setChecked(true); completionTimeEdit->setDateTime(QDateTime::fromString(findXMLAttValu(subEP, "value"), Qt::ISODate)); } } } else if (!strcmp(tagXMLEle(ep), "Steps")) { XMLEle *module; trackStepCheck->setChecked(false); focusStepCheck->setChecked(false); alignStepCheck->setChecked(false); guideStepCheck->setChecked(false); for (module = nextXMLEle(ep, 1); module != nullptr; module = nextXMLEle(ep, 0)) { const char *proc = pcdataXMLEle(module); if (!strcmp(proc, "Track")) trackStepCheck->setChecked(true); else if (!strcmp(proc, "Focus")) focusStepCheck->setChecked(true); else if (!strcmp(proc, "Align")) alignStepCheck->setChecked(true); else if (!strcmp(proc, "Guide")) guideStepCheck->setChecked(true); } } } addToQueueB->setEnabled(true); saveJob(); return true; } void Scheduler::saveAs() { schedulerURL.clear(); save(); } void Scheduler::save() { QUrl backupCurrent = schedulerURL; if (schedulerURL.toLocalFile().startsWith(QLatin1String("/tmp/")) || schedulerURL.toLocalFile().contains("/Temp")) schedulerURL.clear(); // If no changes made, return. if (mDirty == false && !schedulerURL.isEmpty()) return; if (schedulerURL.isEmpty()) { schedulerURL = QFileDialog::getSaveFileUrl(this, i18n("Save Ekos Scheduler List"), dirPath, "Ekos Scheduler List (*.esl)"); // if user presses cancel if (schedulerURL.isEmpty()) { schedulerURL = backupCurrent; return; } dirPath = QUrl(schedulerURL.url(QUrl::RemoveFilename)); if (schedulerURL.toLocalFile().contains('.') == 0) schedulerURL.setPath(schedulerURL.toLocalFile() + ".esl"); } if (schedulerURL.isValid()) { if ((saveScheduler(schedulerURL)) == false) { KSNotification::error(i18n("Failed to save scheduler list"), i18n("Save")); return; } mDirty = false; } else { QString message = i18n("Invalid URL: %1", schedulerURL.url()); KSNotification::sorry(message, i18n("Invalid URL")); } } bool Scheduler::saveScheduler(const QUrl &fileURL) { QFile file; file.setFileName(fileURL.toLocalFile()); if (!file.open(QIODevice::WriteOnly)) { QString message = i18n("Unable to write to file %1", fileURL.toLocalFile()); 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; outstream << "" << schedulerProfileCombo->currentText() << "" << endl; foreach (SchedulerJob *job, jobs) { outstream << "" << endl; outstream << "" << job->getName() << "" << endl; outstream << "" << job->getPriority() << "" << endl; outstream << "" << endl; outstream << "" << cLocale.toString(job->getTargetCoords().ra0().Hours()) << "" << endl; outstream << "" << cLocale.toString(job->getTargetCoords().dec0().Degrees()) << "" << endl; outstream << "" << endl; if (job->getFITSFile().isValid() && job->getFITSFile().isEmpty() == false) outstream << "" << job->getFITSFile().toLocalFile() << "" << endl; outstream << "" << job->getSequenceFile().toLocalFile() << "" << endl; outstream << "" << endl; if (job->getFileStartupCondition() == SchedulerJob::START_ASAP) outstream << "ASAP" << endl; else if (job->getFileStartupCondition() == SchedulerJob::START_CULMINATION) outstream << "Culmination" << endl; else if (job->getFileStartupCondition() == SchedulerJob::START_AT) outstream << "At" << endl; outstream << "" << endl; outstream << "" << endl; if (-90 < job->getMinAltitude()) outstream << "MinimumAltitude" << endl; if (job->getMinMoonSeparation() > 0) outstream << "MoonSeparation" << endl; if (job->getEnforceWeather()) outstream << "EnforceWeather" << endl; if (job->getEnforceTwilight()) outstream << "EnforceTwilight" << endl; outstream << "" << endl; outstream << "" << endl; if (job->getCompletionCondition() == SchedulerJob::FINISH_SEQUENCE) outstream << "Sequence" << endl; else if (job->getCompletionCondition() == SchedulerJob::FINISH_REPEAT) outstream << "Repeat" << endl; else if (job->getCompletionCondition() == SchedulerJob::FINISH_LOOP) outstream << "Loop" << endl; else if (job->getCompletionCondition() == SchedulerJob::FINISH_AT) outstream << "At" << endl; outstream << "" << endl; outstream << "" << endl; if (job->getStepPipeline() & SchedulerJob::USE_TRACK) outstream << "Track" << endl; if (job->getStepPipeline() & SchedulerJob::USE_FOCUS) outstream << "Focus" << endl; if (job->getStepPipeline() & SchedulerJob::USE_ALIGN) outstream << "Align" << endl; if (job->getStepPipeline() & SchedulerJob::USE_GUIDE) outstream << "Guide" << endl; outstream << "" << endl; outstream << "" << endl; } outstream << "" << endl; if (errorHandlingRescheduleErrorsCB->isChecked()) outstream << "" << endl; outstream << "" << errorHandlingDelaySB->value() << "" << endl; outstream << "" << endl; outstream << "" << endl; if (startupScript->text().isEmpty() == false) outstream << "StartupScript" << endl; if (unparkDomeCheck->isChecked()) outstream << "UnparkDome" << endl; if (unparkMountCheck->isChecked()) outstream << "UnparkMount" << endl; if (uncapCheck->isChecked()) outstream << "UnparkCap" << endl; outstream << "" << endl; outstream << "" << endl; if (warmCCDCheck->isChecked()) outstream << "WarmCCD" << endl; if (capCheck->isChecked()) outstream << "ParkCap" << endl; if (parkMountCheck->isChecked()) outstream << "ParkMount" << endl; if (parkDomeCheck->isChecked()) outstream << "ParkDome" << endl; if (shutdownScript->text().isEmpty() == false) outstream << "ShutdownScript" << endl; outstream << "" << endl; outstream << "" << endl; appendLogText(i18n("Scheduler list saved to %1", fileURL.toLocalFile())); file.close(); return true; } void Scheduler::startSlew() { Q_ASSERT_X(nullptr != currentJob, __FUNCTION__, "Job starting slewing must be valid"); // If the mount was parked by a pause or the end-user, unpark if (isMountParked()) { parkWaitState = PARKWAIT_UNPARK; return; } if (Options::resetMountModelBeforeJob()) mountInterface->call(QDBus::AutoDetect, "resetModel"); SkyPoint target = currentJob->getTargetCoords(); QList telescopeSlew; telescopeSlew.append(target.ra().Hours()); telescopeSlew.append(target.dec().Degrees()); QDBusReply const slewModeReply = mountInterface->callWithArgumentList(QDBus::AutoDetect, "slew", telescopeSlew); if (slewModeReply.error().type() != QDBusError::NoError) { qCCritical(KSTARS_EKOS_SCHEDULER) << QString("Warning: job '%1' slew request received DBUS error: %2").arg(currentJob->getName(), QDBusError::errorString(slewModeReply.error().type())); if (!manageConnectionLoss()) currentJob->setState(SchedulerJob::JOB_ERROR); } else { currentJob->setStage(SchedulerJob::STAGE_SLEWING); appendLogText(i18n("Job '%1' is slewing to target.", currentJob->getName())); } } void Scheduler::startFocusing() { Q_ASSERT_X(nullptr != currentJob, __FUNCTION__, "Job starting focusing must be valid"); // 2017-09-30 Jasem: We're skipping post align focusing now as it can be performed // when first focus request is made in capture module if (currentJob->getStage() == SchedulerJob::STAGE_RESLEWING_COMPLETE || currentJob->getStage() == SchedulerJob::STAGE_POSTALIGN_FOCUSING) { // Clear the HFR limit value set in the capture module captureInterface->call(QDBus::AutoDetect, "clearAutoFocusHFR"); // Reset Focus frame so that next frame take a full-resolution capture first. focusInterface->call(QDBus::AutoDetect, "resetFrame"); currentJob->setStage(SchedulerJob::STAGE_POSTALIGN_FOCUSING_COMPLETE); getNextAction(); return; } // Check if autofocus is supported QDBusReply focusModeReply; focusModeReply = focusInterface->call(QDBus::AutoDetect, "canAutoFocus"); if (focusModeReply.error().type() != QDBusError::NoError) { qCCritical(KSTARS_EKOS_SCHEDULER) << QString("Warning: job '%1' canAutoFocus request received DBUS error: %2").arg(currentJob->getName(), QDBusError::errorString(focusModeReply.error().type())); if (!manageConnectionLoss()) currentJob->setState(SchedulerJob::JOB_ERROR); return; } if (focusModeReply.value() == false) { appendLogText(i18n("Warning: job '%1' is unable to proceed with autofocus, not supported.", currentJob->getName())); currentJob->setStepPipeline( static_cast(currentJob->getStepPipeline() & ~SchedulerJob::USE_FOCUS)); currentJob->setStage(SchedulerJob::STAGE_FOCUS_COMPLETE); getNextAction(); return; } // Clear the HFR limit value set in the capture module captureInterface->call(QDBus::AutoDetect, "clearAutoFocusHFR"); QDBusMessage reply; // We always need to reset frame first if ((reply = focusInterface->call(QDBus::AutoDetect, "resetFrame")).type() == QDBusMessage::ErrorMessage) { qCCritical(KSTARS_EKOS_SCHEDULER) << QString("Warning: job '%1' resetFrame request received DBUS error: %2").arg(currentJob->getName(), reply.errorMessage()); if (!manageConnectionLoss()) currentJob->setState(SchedulerJob::JOB_ERROR); return; } // Set autostar if full field option is false if (Options::focusUseFullField() == false) { QList autoStar; autoStar.append(true); if ((reply = focusInterface->callWithArgumentList(QDBus::AutoDetect, "setAutoStarEnabled", autoStar)).type() == QDBusMessage::ErrorMessage) { qCCritical(KSTARS_EKOS_SCHEDULER) << QString("Warning: job '%1' setAutoFocusStar request received DBUS error: %1").arg(currentJob->getName(), reply.errorMessage()); if (!manageConnectionLoss()) currentJob->setState(SchedulerJob::JOB_ERROR); return; } } // Start auto-focus if ((reply = focusInterface->call(QDBus::AutoDetect, "start")).type() == QDBusMessage::ErrorMessage) { qCCritical(KSTARS_EKOS_SCHEDULER) << QString("Warning: job '%1' startFocus request received DBUS error: %2").arg(currentJob->getName(), reply.errorMessage()); if (!manageConnectionLoss()) currentJob->setState(SchedulerJob::JOB_ERROR); return; } /*if (currentJob->getStage() == SchedulerJob::STAGE_RESLEWING_COMPLETE || currentJob->getStage() == SchedulerJob::STAGE_POSTALIGN_FOCUSING) { currentJob->setStage(SchedulerJob::STAGE_POSTALIGN_FOCUSING); appendLogText(i18n("Post-alignment focusing for %1 ...", currentJob->getName())); } else { currentJob->setStage(SchedulerJob::STAGE_FOCUSING); appendLogText(i18n("Focusing %1 ...", currentJob->getName())); }*/ currentJob->setStage(SchedulerJob::STAGE_FOCUSING); appendLogText(i18n("Job '%1' is focusing.", currentJob->getName())); currentOperationTime.restart(); } void Scheduler::findNextJob() { if (state == SCHEDULER_PAUSED) { // everything finished, we can pause setPaused(); return; } Q_ASSERT_X(currentJob->getState() == SchedulerJob::JOB_ERROR || currentJob->getState() == SchedulerJob::JOB_ABORTED || currentJob->getState() == SchedulerJob::JOB_COMPLETE, __FUNCTION__, "Finding next job requires current to be in error, aborted or complete"); jobTimer.stop(); // Reset failed count alignFailureCount = guideFailureCount = focusFailureCount = captureFailureCount = 0; /* FIXME: Other debug logs in that function probably */ qCDebug(KSTARS_EKOS_SCHEDULER) << "Find next job..."; if (currentJob->getState() == SchedulerJob::JOB_ERROR || currentJob->getState() == SchedulerJob::JOB_ABORTED) { captureBatch = 0; // Stop Guiding if it was used stopGuiding(); if (currentJob->getState() == SchedulerJob::JOB_ERROR) appendLogText(i18n("Job '%1' is terminated due to errors.", currentJob->getName())); else appendLogText(i18n("Job '%1' is aborted.", currentJob->getName())); // Always reset job stage currentJob->setStage(SchedulerJob::STAGE_IDLE); // restart aborted jobs immediately, if error handling strategy is set to "restart immediately" if (errorHandlingRestartImmediatelyButton->isChecked() && (currentJob->getState() == SchedulerJob::JOB_ABORTED || (currentJob->getState() == SchedulerJob::JOB_ERROR && errorHandlingRescheduleErrorsCB->isChecked()))) { // reset the state so that it will be restarted currentJob->setState(SchedulerJob::JOB_SCHEDULED); appendLogText(i18n("Waiting %1 seconds to restart job '%2'.", errorHandlingDelaySB->value(), currentJob->getName())); // wait the given delay until the jobs will be evaluated again - sleepTimer.setInterval(( errorHandlingDelaySB->value() * 1000)); + sleepTimer.setInterval(std::lround((errorHandlingDelaySB->value() * 1000) / KStarsData::Instance()->clock()->scale())); sleepTimer.start(); sleepLabel->setToolTip(i18n("Scheduler waits for a retry.")); sleepLabel->show(); return; } // otherwise start re-evaluation setCurrentJob(nullptr); schedulerTimer.start(); } // Job is complete, so check completion criteria to optimize processing // In any case, we're done whether the job completed successfully or not. else if (currentJob->getCompletionCondition() == SchedulerJob::FINISH_SEQUENCE) { /* If we remember job progress, mark the job idle as well as all its duplicates for re-evaluation */ if (Options::rememberJobProgress()) { foreach(SchedulerJob *a_job, jobs) if (a_job == currentJob || a_job->isDuplicateOf(currentJob)) a_job->setState(SchedulerJob::JOB_IDLE); } captureBatch = 0; // Stop Guiding if it was used stopGuiding(); appendLogText(i18n("Job '%1' is complete.", currentJob->getName())); // Always reset job stage currentJob->setStage(SchedulerJob::STAGE_IDLE); setCurrentJob(nullptr); schedulerTimer.start(); } else if (currentJob->getCompletionCondition() == SchedulerJob::FINISH_REPEAT) { /* If the job is about to repeat, decrease its repeat count and reset its start time */ if (0 < currentJob->getRepeatsRemaining()) { currentJob->setRepeatsRemaining(currentJob->getRepeatsRemaining() - 1); currentJob->setStartupTime(QDateTime()); } /* Mark the job idle as well as all its duplicates for re-evaluation */ foreach(SchedulerJob *a_job, jobs) if (a_job == currentJob || a_job->isDuplicateOf(currentJob)) a_job->setState(SchedulerJob::JOB_IDLE); /* Re-evaluate all jobs, without selecting a new job */ jobEvaluationOnly = true; evaluateJobs(); /* If current job is actually complete because of previous duplicates, prepare for next job */ if (currentJob == nullptr || currentJob->getRepeatsRemaining() == 0) { stopCurrentJobAction(); if (currentJob != nullptr) { appendLogText(i18np("Job '%1' is complete after #%2 batch.", "Job '%1' is complete after #%2 batches.", currentJob->getName(), currentJob->getRepeatsRequired())); setCurrentJob(nullptr); } schedulerTimer.start(); } /* If job requires more work, continue current observation */ else { /* FIXME: raise priority to allow other jobs to schedule in-between */ executeJob(currentJob); /* If we are guiding, continue capturing */ if (currentJob->getStepPipeline() & SchedulerJob::USE_GUIDE) { currentJob->setStage(SchedulerJob::STAGE_CAPTURING); startCapture(); } /* If we are not guiding, but using alignment, realign */ else if (currentJob->getStepPipeline() & SchedulerJob::USE_ALIGN) { currentJob->setStage(SchedulerJob::STAGE_ALIGNING); startAstrometry(); } /* Else if we are neither guiding nor using alignment, slew back to target */ else if (currentJob->getStepPipeline() & SchedulerJob::USE_TRACK) { currentJob->setStage(SchedulerJob::STAGE_SLEWING); startSlew(); } /* Else just start capturing */ else { currentJob->setStage(SchedulerJob::STAGE_CAPTURING); startCapture(); } appendLogText(i18np("Job '%1' is repeating, #%2 batch remaining.", "Job '%1' is repeating, #%2 batches remaining.", currentJob->getName(), currentJob->getRepeatsRemaining())); /* currentJob remains the same */ jobTimer.start(); } } else if (currentJob->getCompletionCondition() == SchedulerJob::FINISH_LOOP) { executeJob(currentJob); currentJob->setStage(SchedulerJob::STAGE_CAPTURING); captureBatch++; startCapture(); appendLogText(i18n("Job '%1' is repeating, looping indefinitely.", currentJob->getName())); /* currentJob remains the same */ jobTimer.start(); } else if (currentJob->getCompletionCondition() == SchedulerJob::FINISH_AT) { if (KStarsData::Instance()->lt().secsTo(currentJob->getCompletionTime()) <= 0) { /* Mark the job idle as well as all its duplicates for re-evaluation */ foreach(SchedulerJob *a_job, jobs) if (a_job == currentJob || a_job->isDuplicateOf(currentJob)) a_job->setState(SchedulerJob::JOB_IDLE); stopCurrentJobAction(); captureBatch = 0; appendLogText(i18np("Job '%1' stopping, reached completion time with #%2 batch done.", "Job '%1' stopping, reached completion time with #%2 batches done.", currentJob->getName(), captureBatch + 1)); // Always reset job stage currentJob->setStage(SchedulerJob::STAGE_IDLE); setCurrentJob(nullptr); schedulerTimer.start(); } else { executeJob(currentJob); currentJob->setStage(SchedulerJob::STAGE_CAPTURING); captureBatch++; startCapture(); appendLogText(i18np("Job '%1' completed #%2 batch before completion time, restarted.", "Job '%1' completed #%2 batches before completion time, restarted.", currentJob->getName(), captureBatch)); /* currentJob remains the same */ jobTimer.start(); } } else { /* Unexpected situation, mitigate by resetting the job and restarting the scheduler timer */ qCDebug(KSTARS_EKOS_SCHEDULER) << "BUGBUG! Job '" << currentJob->getName() << "' timer elapsed, but no action to be taken."; // Always reset job stage currentJob->setStage(SchedulerJob::STAGE_IDLE); setCurrentJob(nullptr); schedulerTimer.start(); } } void Scheduler::startAstrometry() { Q_ASSERT_X(nullptr != currentJob, __FUNCTION__, "Job starting aligning must be valid"); QDBusMessage reply; setSolverAction(Align::GOTO_SLEW); // Always turn update coords on //QVariant arg(true); //alignInterface->call(QDBus::AutoDetect, "setUpdateCoords", arg); // If FITS file is specified, then we use load and slew if (currentJob->getFITSFile().isEmpty() == false) { QList solveArgs; solveArgs.append(currentJob->getFITSFile().toString(QUrl::PreferLocalFile)); if ((reply = alignInterface->callWithArgumentList(QDBus::AutoDetect, "loadAndSlew", solveArgs)).type() == QDBusMessage::ErrorMessage) { qCCritical(KSTARS_EKOS_SCHEDULER) << QString("Warning: job '%1' loadAndSlew request received DBUS error: %2").arg(currentJob->getName(), reply.errorMessage()); if (!manageConnectionLoss()) currentJob->setState(SchedulerJob::JOB_ERROR); return; } loadAndSlewProgress = true; appendLogText(i18n("Job '%1' is plate solving %2.", currentJob->getName(), currentJob->getFITSFile().fileName())); } else { if ((reply = alignInterface->call(QDBus::AutoDetect, "captureAndSolve")).type() == QDBusMessage::ErrorMessage) { qCCritical(KSTARS_EKOS_SCHEDULER) << QString("Warning: job '%1' captureAndSolve request received DBUS error: %2").arg(currentJob->getName(), reply.errorMessage()); if (!manageConnectionLoss()) currentJob->setState(SchedulerJob::JOB_ERROR); return; } appendLogText(i18n("Job '%1' is capturing and plate solving.", currentJob->getName())); } /* FIXME: not supposed to modify the job */ currentJob->setStage(SchedulerJob::STAGE_ALIGNING); currentOperationTime.restart(); } void Scheduler::startGuiding(bool resetCalibration) { Q_ASSERT_X(nullptr != currentJob, __FUNCTION__, "Job starting guiding must be valid"); // avoid starting the guider twice if (resetCalibration == false && getGuidingStatus() == GUIDE_GUIDING) { appendLogText(i18n("Guiding already running for %1 ...", currentJob->getName())); currentJob->setStage(SchedulerJob::STAGE_GUIDING); currentOperationTime.restart(); return; } // Connect Guider guideInterface->call(QDBus::AutoDetect, "connectGuider"); // Set Auto Star to true QVariant arg(true); guideInterface->call(QDBus::AutoDetect, "setCalibrationAutoStar", arg); // Only reset calibration on trouble // and if we are allowed to reset calibration (true by default) if (resetCalibration && Options::resetGuideCalibration()) guideInterface->call(QDBus::AutoDetect, "clearCalibration"); guideInterface->call(QDBus::AutoDetect, "guide"); currentJob->setStage(SchedulerJob::STAGE_GUIDING); appendLogText(i18n("Starting guiding procedure for %1 ...", currentJob->getName())); currentOperationTime.restart(); } void Scheduler::startCapture(bool restart) { Q_ASSERT_X(nullptr != currentJob, __FUNCTION__, "Job starting capturing must be valid"); captureInterface->setProperty("targetName", currentJob->getName().replace(' ', "")); QString url = currentJob->getSequenceFile().toLocalFile(); if (restart == false) { QList dbusargs; dbusargs.append(url); captureInterface->callWithArgumentList(QDBus::AutoDetect, "loadSequenceQueue", dbusargs); } switch (currentJob->getCompletionCondition()) { case SchedulerJob::FINISH_LOOP: case SchedulerJob::FINISH_AT: // In these cases, we leave the captured frames map empty // to ensure, that the capture sequence is executed in any case. break; default: // Scheduler always sets captured frame map when starting a sequence - count may be different, robustness, dynamic priority #if 0 // JM 2018-09-24: If job is looping, no need to set captured frame maps. if (currentJob->getCompletionCondition() != SchedulerJob::FINISH_SEQUENCE) break; #endif // hand over the map of captured frames so that the capture // process knows about existing frames SchedulerJob::CapturedFramesMap fMap = currentJob->getCapturedFramesMap(); for (auto &e : fMap.keys()) { QList dbusargs; QDBusMessage reply; dbusargs.append(e); dbusargs.append(fMap.value(e)); if ((reply = captureInterface->callWithArgumentList(QDBus::AutoDetect, "setCapturedFramesMap", dbusargs)).type() == QDBusMessage::ErrorMessage) { qCCritical(KSTARS_EKOS_SCHEDULER) << QString("Warning: job '%1' setCapturedFramesCount request received DBUS error: %1").arg(currentJob->getName()).arg(reply.errorMessage()); if (!manageConnectionLoss()) currentJob->setState(SchedulerJob::JOB_ERROR); return; } } break; } // Never ignore sequence history in the Capture module, it is unrelated to storage #if 0 // If sequence is a loop, ignore sequence history // FIXME: set, but never used. if (currentJob->getCompletionCondition() != SchedulerJob::FINISH_SEQUENCE) captureInterface->call(QDBus::AutoDetect, "ignoreSequenceHistory"); #endif // Start capture process captureInterface->call(QDBus::AutoDetect, "start"); currentJob->setStage(SchedulerJob::STAGE_CAPTURING); KNotification::event(QLatin1String("EkosScheduledImagingStart"), i18n("Ekos job (%1) - Capture started", currentJob->getName())); if (captureBatch > 0) appendLogText(i18n("Job '%1' capture is in progress (batch #%2)...", currentJob->getName(), captureBatch + 1)); else appendLogText(i18n("Job '%1' capture is in progress...", currentJob->getName())); currentOperationTime.restart(); } void Scheduler::stopGuiding() { // Tell guider to abort if the current job requires guiding - end-user may enable guiding manually before observation if (nullptr != currentJob && (currentJob->getStepPipeline() & SchedulerJob::USE_GUIDE)) { qCInfo(KSTARS_EKOS_SCHEDULER) << QString("Job '%1' is stopping guiding...").arg(currentJob->getName()); guideInterface->call(QDBus::AutoDetect, "abort"); guideFailureCount = 0; } // In any case, stop the automatic guider restart if (restartGuidingTimer.isActive()) restartGuidingTimer.stop(); } void Scheduler::setSolverAction(Align::GotoMode mode) { QVariant gotoMode(static_cast(mode)); alignInterface->call(QDBus::AutoDetect, "setSolverAction", gotoMode); } void Scheduler::disconnectINDI() { qCInfo(KSTARS_EKOS_SCHEDULER) << "Disconnecting INDI..."; indiState = INDI_DISCONNECTING; ekosInterface->call(QDBus::AutoDetect, "disconnectDevices"); } void Scheduler::stopEkos() { qCInfo(KSTARS_EKOS_SCHEDULER) << "Stopping Ekos..."; ekosState = EKOS_STOPPING; ekosConnectFailureCount = 0; ekosInterface->call(QDBus::AutoDetect, "stop"); m_MountReady = m_CapReady = m_CaptureReady = m_DomeReady = false; } void Scheduler::setDirty() { mDirty = true; if (sender() == startupProcedureButtonGroup || sender() == shutdownProcedureGroup) return; if (0 <= jobUnderEdit && state != SCHEDULER_RUNNING && 0 <= queueTable->currentRow()) { // Now that jobs are sorted, reset jobs that are later than the edited one for re-evaluation for (int row = jobUnderEdit; row < jobs.size(); row++) jobs.at(row)->reset(); saveJob(); } // For object selection, all fields must be filled bool const nameSelectionOK = !raBox->isEmpty() && !decBox->isEmpty() && !nameEdit->text().isEmpty(); // For FITS selection, only the name and fits URL should be filled. bool const fitsSelectionOK = !nameEdit->text().isEmpty() && !fitsURL.isEmpty(); // Sequence selection is required bool const seqSelectionOK = !sequenceEdit->text().isEmpty(); // Finally, adding is allowed upon object/FITS and sequence selection bool const addingOK = (nameSelectionOK || fitsSelectionOK) && seqSelectionOK; addToQueueB->setEnabled(addingOK); mosaicB->setEnabled(addingOK); } void Scheduler::updateCompletedJobsCount(bool forced) { /* Use a temporary map in order to limit the number of file searches */ SchedulerJob::CapturedFramesMap newFramesCount; /* FIXME: Capture storage cache is refreshed too often, feature requires rework. */ /* Check if one job is idle or requires evaluation - if so, force refresh */ forced |= std::any_of(jobs.begin(), jobs.end(), [](SchedulerJob * oneJob) -> bool { SchedulerJob::JOBStatus const state = oneJob->getState(); return state == SchedulerJob::JOB_IDLE || state == SchedulerJob::JOB_EVALUATION;}); /* If update is forced, clear the frame map */ if (forced) capturedFramesCount.clear(); /* Enumerate SchedulerJobs to count captures that are already stored */ for (SchedulerJob *oneJob : jobs) { QList seqjobs; bool hasAutoFocus = false; //oneJob->setLightFramesRequired(false); /* Look into the sequence requirements, bypass if invalid */ if (loadSequenceQueue(oneJob->getSequenceFile().toLocalFile(), oneJob, seqjobs, hasAutoFocus) == false) { appendLogText(i18n("Warning: job '%1' has inaccessible sequence '%2', marking invalid.", oneJob->getName(), oneJob->getSequenceFile().toLocalFile())); oneJob->setState(SchedulerJob::JOB_INVALID); continue; } /* Enumerate the SchedulerJob's SequenceJobs to count captures stored for each */ for (SequenceJob *oneSeqJob : seqjobs) { /* Only consider captures stored on client (Ekos) side */ /* FIXME: ask the remote for the file count */ if (oneSeqJob->getUploadMode() == ISD::CCD::UPLOAD_LOCAL) continue; /* FIXME: this signature path is incoherent when there is no filter wheel on the setup - bugfix should be elsewhere though */ QString const signature = oneSeqJob->getSignature(); /* If signature was processed during this run, keep it */ if (newFramesCount.constEnd() != newFramesCount.constFind(signature)) continue; /* If signature was processed during an earlier run, use the earlier count */ QMap::const_iterator const earlierRunIterator = capturedFramesCount.constFind(signature); if (capturedFramesCount.constEnd() != earlierRunIterator) { newFramesCount[signature] = earlierRunIterator.value(); continue; } /* Else recount captures already stored */ newFramesCount[signature] = getCompletedFiles(signature, oneSeqJob->getFullPrefix()); } // determine whether we need to continue capturing, depending on captured frames bool lightFramesRequired = false; switch (oneJob->getCompletionCondition()) { case SchedulerJob::FINISH_SEQUENCE: case SchedulerJob::FINISH_REPEAT: for (SequenceJob *oneSeqJob : seqjobs) { QString const signature = oneSeqJob->getSignature(); /* If frame is LIGHT, how hany do we have left? */ if (oneSeqJob->getFrameType() == FRAME_LIGHT && oneSeqJob->getCount()*oneJob->getRepeatsRequired() > newFramesCount[signature]) lightFramesRequired = true; } break; default: // in all other cases it does not depend on the number of captured frames lightFramesRequired = true; } oneJob->setLightFramesRequired(lightFramesRequired); } capturedFramesCount = newFramesCount; //if (forced) { qCDebug(KSTARS_EKOS_SCHEDULER) << "Frame map summary:"; QMap::const_iterator it = capturedFramesCount.constBegin(); for (; it != capturedFramesCount.constEnd(); it++) qCDebug(KSTARS_EKOS_SCHEDULER) << " " << it.key() << ':' << it.value(); } } bool Scheduler::estimateJobTime(SchedulerJob *schedJob) { /* updateCompletedJobsCount(); */ // Load the sequence job associated with the argument scheduler job. QList seqJobs; bool hasAutoFocus = false; if (loadSequenceQueue(schedJob->getSequenceFile().toLocalFile(), schedJob, seqJobs, hasAutoFocus) == false) { qCWarning(KSTARS_EKOS_SCHEDULER) << QString("Warning: Failed estimating the duration of job '%1', its sequence file is invalid.").arg(schedJob->getSequenceFile().toLocalFile()); return false; } // FIXME: setting in-sequence focus should be done in XML processing. schedJob->setInSequenceFocus(hasAutoFocus); if (hasAutoFocus && !(schedJob->getStepPipeline() & SchedulerJob::USE_FOCUS)) appendLogText(i18n("Warning: Job '%1' has its focus step disabled, periodic and/or HFR procedures currently set in its sequence will not occur.", schedJob->getName())); /* This is the map of captured frames for this scheduler job, keyed per storage signature. * It will be forwarded to the Capture module in order to capture only what frames are required. * If option "Remember Job Progress" is disabled, this map will be empty, and the Capture module will process all requested captures unconditionally. */ SchedulerJob::CapturedFramesMap capture_map; bool const rememberJobProgress = Options::rememberJobProgress(); int totalSequenceCount = 0, totalCompletedCount = 0; double totalImagingTime = 0; // Determine number of captures in the scheduler job int capturesPerRepeat = 0; foreach (SequenceJob *seqJob, seqJobs) capturesPerRepeat += seqJob->getCount(); // Loop through sequence jobs to calculate the number of required frames and estimate duration. foreach (SequenceJob *seqJob, seqJobs) { // FIXME: find a way to actually display the filter name. QString seqName = i18n("Job '%1' %2x%3\" %4", schedJob->getName(), seqJob->getCount(), seqJob->getExposure(), seqJob->getFilterName()); if (seqJob->getUploadMode() == ISD::CCD::UPLOAD_LOCAL) { qCInfo(KSTARS_EKOS_SCHEDULER) << QString("%1 duration cannot be estimated time since the sequence saves the files remotely.").arg(seqName); schedJob->setEstimatedTime(-2); qDeleteAll(seqJobs); return true; } // Note that looping jobs will have zero repeats required. int const captures_required = seqJob->getCount() * schedJob->getRepeatsRequired(); int captures_completed = 0; if (rememberJobProgress) { /* Enumerate sequence jobs associated to this scheduler job, and assign them a completed count. * * The objective of this block is to fill the storage map of the scheduler job with completed counts for each capture storage. * * Sequence jobs capture to a storage folder, and are given a count of captures to store at that location. * The tricky part is to make sure the repeat count of the scheduler job is properly transferred to each sequence job. * * For instance, a scheduler job repeated three times must execute the full list of sequence jobs three times, thus * has to tell each sequence job it misses all captures, three times. It cannot tell the sequence job three captures are * missing, first because that's not how the sequence job is designed (completed count, not required count), and second * because this would make the single sequence job repeat three times, instead of repeating the full list of sequence * jobs three times. * * The consolidated storage map will be assigned to each sequence job based on their signature when the scheduler job executes them. * * For instance, consider a RGBL sequence of single captures. The map will store completed captures for R, G, B and L storages. * If R and G have 1 file each, and B and L have no files, map[storage(R)] = map[storage(G)] = 1 and map[storage(B)] = map[storage(L)] = 0. * When that scheduler job executes, only B and L captures will be processed. * * In the case of a RGBLRGB sequence of single captures, the second R, G and B map items will count one less capture than what is really in storage. * If R and G have 1 file each, and B and L have no files, map[storage(R1)] = map[storage(B1)] = 1, and all others will be 0. * When that scheduler job executes, B1, L, R2, G2 and B2 will be processed. * * This doesn't handle the case of duplicated scheduler jobs, that is, scheduler jobs with the same storage for capture sets. * Those scheduler jobs will all change state to completion at the same moment as they all target the same storage. * This is why it is important to manage the repeat count of the scheduler job, as stated earlier. */ // Retrieve cached count of completed captures for the output folder of this seqJob QString const signature = seqJob->getSignature(); QString const signature_path = QFileInfo(signature).path(); captures_completed = capturedFramesCount[signature]; qCInfo(KSTARS_EKOS_SCHEDULER) << QString("%1 sees %2 captures in output folder '%3'.").arg(seqName).arg(captures_completed).arg(signature_path); // Enumerate sequence jobs to check how many captures are completed overall in the same storage as the current one foreach (SequenceJob *prevSeqJob, seqJobs) { // Enumerate seqJobs up to the current one if (seqJob == prevSeqJob) break; // If the previous sequence signature matches the current, reduce completion count to take duplicates into account if (!signature.compare(prevSeqJob->getLocalDir() + prevSeqJob->getDirectoryPostfix())) { // Note that looping jobs will have zero repeats required. int const previous_captures_required = prevSeqJob->getCount() * schedJob->getRepeatsRequired(); qCInfo(KSTARS_EKOS_SCHEDULER) << QString("%1 has a previous duplicate sequence job requiring %2 captures.").arg(seqName).arg(previous_captures_required); captures_completed -= previous_captures_required; } // Now completed count can be needlessly negative for this job, so clamp to zero if (captures_completed < 0) captures_completed = 0; // And break if no captures remain, this job has to execute if (captures_completed == 0) break; } // Finally we're only interested in the number of captures required for this sequence item if (0 < captures_required && captures_required < captures_completed) captures_completed = captures_required; qCInfo(KSTARS_EKOS_SCHEDULER) << QString("%1 has completed %2/%3 of its required captures in output folder '%4'.").arg(seqName).arg(captures_completed).arg(captures_required).arg(signature_path); // Update the completion count for this signature in the frame map if we still have captures to take. // That frame map will be transferred to the Capture module, for which the sequence is a single batch of the scheduler job. // For instance, consider a scheduler job repeated 3 times and using a 3xLum sequence, so we want 9xLum in the end. // - If no captures are already processed, the frame map contains Lum=0 // - If 1xLum are already processed, the frame map contains Lum=0 when the batch executes, so that 3xLum may be taken. // - If 3xLum are already processed, the frame map contains Lum=0 when the batch executes, as we still need more than what the sequence provides. // - If 7xLum are already processed, the frame map contains Lum=1 when the batch executes, because we now only need 2xLum to finish the job. // Therefore we need to specify a number of existing captures only for the last batch of the scheduler job. // In the last batch, we only need the remainder of frames to get to the required total. if (captures_completed < captures_required) { if (captures_required - captures_completed < seqJob->getCount()) capture_map[signature] = captures_completed % seqJob->getCount(); else capture_map[signature] = 0; } else capture_map[signature] = captures_required; // From now on, 'captures_completed' is the number of frames completed for the *current* sequence job } // Else rely on the captures done during this session else captures_completed = schedJob->getCompletedCount() / capturesPerRepeat * seqJob->getCount(); // Check if we still need any light frames. Because light frames changes the flow of the observatory startup // Without light frames, there is no need to do focusing, alignment, guiding...etc // We check if the frame type is LIGHT and if either the number of captures_completed frames is less than required // OR if the completion condition is set to LOOP so it is never complete due to looping. // Note that looping jobs will have zero repeats required. // FIXME: As it is implemented now, FINISH_LOOP may loop over a capture-complete, therefore inoperant, scheduler job. bool const areJobCapturesComplete = !(captures_completed < captures_required || 0 == captures_required); if (seqJob->getFrameType() == FRAME_LIGHT) { if(areJobCapturesComplete) { qCInfo(KSTARS_EKOS_SCHEDULER) << QString("%1 completed its sequence of %2 light frames.").arg(seqName).arg(captures_required); } } else { qCInfo(KSTARS_EKOS_SCHEDULER) << QString("%1 captures calibration frames.").arg(seqName); } totalSequenceCount += captures_required; totalCompletedCount += captures_completed; /* If captures are not complete, we have imaging time left */ if (!areJobCapturesComplete) { /* if looping, consider we always have one capture left */ unsigned int const captures_to_go = 0 < captures_required ? captures_required - captures_completed : 1; totalImagingTime += fabs((seqJob->getExposure() + seqJob->getDelay()) * captures_to_go); /* If we have light frames to process, add focus/dithering delay */ if (seqJob->getFrameType() == FRAME_LIGHT) { // If inSequenceFocus is true if (hasAutoFocus) { // Wild guess that each in sequence auto focus takes an average of 30 seconds. It can take any where from 2 seconds to 2+ minutes. // FIXME: estimating one focus per capture is probably not realistic. qCInfo(KSTARS_EKOS_SCHEDULER) << QString("%1 requires a focus procedure.").arg(seqName); totalImagingTime += captures_to_go * 30; } // If we're dithering after each exposure, that's another 10-20 seconds if (schedJob->getStepPipeline() & SchedulerJob::USE_GUIDE && Options::ditherEnabled()) { qCInfo(KSTARS_EKOS_SCHEDULER) << QString("%1 requires a dither procedure.").arg(seqName); totalImagingTime += (captures_to_go * 15) / Options::ditherFrames(); } } } } schedJob->setCapturedFramesMap(capture_map); schedJob->setSequenceCount(totalSequenceCount); // only in case we remember the job progress, we change the completion count if (rememberJobProgress) schedJob->setCompletedCount(totalCompletedCount); qDeleteAll(seqJobs); // FIXME: Move those ifs away to the caller in order to avoid estimating in those situations! // We can't estimate times that do not finish when sequence is done if (schedJob->getCompletionCondition() == SchedulerJob::FINISH_LOOP) { // We can't know estimated time if it is looping indefinitely schedJob->setEstimatedTime(-2); qCDebug(KSTARS_EKOS_SCHEDULER) << QString("Job '%1' is configured to loop until Scheduler is stopped manually, has undefined imaging time.") .arg(schedJob->getName()); } // If we know startup and finish times, we can estimate time right away else if (schedJob->getStartupCondition() == SchedulerJob::START_AT && schedJob->getCompletionCondition() == SchedulerJob::FINISH_AT) { // FIXME: SchedulerJob is probably doing this already qint64 const diff = schedJob->getStartupTime().secsTo(schedJob->getCompletionTime()); schedJob->setEstimatedTime(diff); qCDebug(KSTARS_EKOS_SCHEDULER) << QString("Job '%1' has a startup time and fixed completion time, will run for %2.") .arg(schedJob->getName()) .arg(dms(diff * 15.0 / 3600.0f).toHMSString()); } // If we know finish time only, we can roughly estimate the time considering the job starts now else if (schedJob->getStartupCondition() != SchedulerJob::START_AT && schedJob->getCompletionCondition() == SchedulerJob::FINISH_AT) { qint64 const diff = KStarsData::Instance()->lt().secsTo(schedJob->getCompletionTime()); schedJob->setEstimatedTime(diff); qCDebug(KSTARS_EKOS_SCHEDULER) << QString("Job '%1' has no startup time but fixed completion time, will run for %2 if started now.") .arg(schedJob->getName()) .arg(dms(diff * 15.0 / 3600.0f).toHMSString()); } // Rely on the estimated imaging time to determine whether this job is complete or not - this makes the estimated time null else if (totalImagingTime <= 0) { schedJob->setEstimatedTime(0); qCDebug(KSTARS_EKOS_SCHEDULER) << QString("Job '%1' will not run, complete with %2/%3 captures.") .arg(schedJob->getName()).arg(totalCompletedCount).arg(totalSequenceCount); } // Else consolidate with step durations else { if (schedJob->getLightFramesRequired()) { /* FIXME: estimation should base on actual measure of each step, eventually with preliminary data as what it used now */ // Are we doing tracking? It takes about 30 seconds if (schedJob->getStepPipeline() & SchedulerJob::USE_TRACK) totalImagingTime += 30; // Are we doing initial focusing? That can take about 2 minutes if (schedJob->getStepPipeline() & SchedulerJob::USE_FOCUS) totalImagingTime += 120; // Are we doing astrometry? That can take about 60 seconds if (schedJob->getStepPipeline() & SchedulerJob::USE_ALIGN) { totalImagingTime += 60; } // Are we doing guiding? if (schedJob->getStepPipeline() & SchedulerJob::USE_GUIDE) { // Looping, finding guide star, settling takes 15 sec totalImagingTime += 15; // Add guiding settle time from dither setting (used by phd2::guide()) totalImagingTime += Options::ditherSettle(); // Add guiding settle time from ekos sccheduler setting totalImagingTime += Options::guidingSettle(); // If calibration always cleared // then calibration process can take about 2 mins if(Options::resetGuideCalibration()) totalImagingTime += 120; } } dms const estimatedTime(totalImagingTime * 15.0 / 3600.0); schedJob->setEstimatedTime(totalImagingTime); qCInfo(KSTARS_EKOS_SCHEDULER) << QString("Job '%1' estimated to take %2 to complete.").arg(schedJob->getName(), estimatedTime.toHMSString()); } return true; } void Scheduler::parkMount() { QVariant parkingStatus = mountInterface->property("parkStatus"); if (parkingStatus.isValid() == false) { qCCritical(KSTARS_EKOS_SCHEDULER) << QString("Warning: mount parkStatus request received DBUS error: %1").arg(mountInterface->lastError().type()); if (!manageConnectionLoss()) parkWaitState = PARKWAIT_ERROR; } ISD::ParkStatus status = static_cast(parkingStatus.toInt()); switch (status) { case ISD::PARK_PARKED: if (shutdownState == SHUTDOWN_PARK_MOUNT) shutdownState = SHUTDOWN_PARK_DOME; parkWaitState = PARKWAIT_PARKED; appendLogText(i18n("Mount already parked.")); break; case ISD::PARK_UNPARKING: //case Mount::UNPARKING_BUSY: /* FIXME: Handle the situation where we request parking but an unparking procedure is running. */ // case Mount::PARKING_IDLE: // case Mount::UNPARKING_OK: case ISD::PARK_ERROR: case ISD::PARK_UNKNOWN: case ISD::PARK_UNPARKED: { QDBusReply const mountReply = mountInterface->call(QDBus::AutoDetect, "park"); if (mountReply.error().type() != QDBusError::NoError) { qCCritical(KSTARS_EKOS_SCHEDULER) << QString("Warning: mount park request received DBUS error: %1").arg(QDBusError::errorString(mountReply.error().type())); if (!manageConnectionLoss()) parkWaitState = PARKWAIT_ERROR; } else currentOperationTime.start(); } // Fall through case ISD::PARK_PARKING: //case Mount::PARKING_BUSY: if (shutdownState == SHUTDOWN_PARK_MOUNT) shutdownState = SHUTDOWN_PARKING_MOUNT; parkWaitState = PARKWAIT_PARKING; appendLogText(i18n("Parking mount in progress...")); break; // All cases covered above so no need for default //default: // qCWarning(KSTARS_EKOS_SCHEDULER) << QString("BUG: Parking state %1 not managed while parking mount.").arg(mountReply.value()); } } void Scheduler::unParkMount() { if (mountInterface.isNull()) return; QVariant parkingStatus = mountInterface->property("parkStatus"); if (parkingStatus.isValid() == false) { qCCritical(KSTARS_EKOS_SCHEDULER) << QString("Warning: mount parkStatus request received DBUS error: %1").arg(mountInterface->lastError().type()); if (!manageConnectionLoss()) parkWaitState = PARKWAIT_ERROR; } ISD::ParkStatus status = static_cast(parkingStatus.toInt()); switch (status) { //case Mount::UNPARKING_OK: case ISD::PARK_UNPARKED: if (startupState == STARTUP_UNPARK_MOUNT) startupState = STARTUP_UNPARK_CAP; parkWaitState = PARKWAIT_UNPARKED; appendLogText(i18n("Mount already unparked.")); break; //case Mount::PARKING_BUSY: case ISD::PARK_PARKING: /* FIXME: Handle the situation where we request unparking but a parking procedure is running. */ // case Mount::PARKING_IDLE: // case Mount::PARKING_OK: // case Mount::PARKING_ERROR: case ISD::PARK_ERROR: case ISD::PARK_UNKNOWN: case ISD::PARK_PARKED: { QDBusReply const mountReply = mountInterface->call(QDBus::AutoDetect, "unpark"); if (mountReply.error().type() != QDBusError::NoError) { qCCritical(KSTARS_EKOS_SCHEDULER) << QString("Warning: mount unpark request received DBUS error: %1").arg(QDBusError::errorString(mountReply.error().type())); if (!manageConnectionLoss()) parkWaitState = PARKWAIT_ERROR; } else currentOperationTime.start(); } // Fall through //case Mount::UNPARKING_BUSY: case ISD::PARK_UNPARKING: if (startupState == STARTUP_UNPARK_MOUNT) startupState = STARTUP_UNPARKING_MOUNT; parkWaitState = PARKWAIT_UNPARKING; qCInfo(KSTARS_EKOS_SCHEDULER) << "Unparking mount in progress..."; break; // All cases covered above //default: // qCWarning(KSTARS_EKOS_SCHEDULER) << QString("BUG: Parking state %1 not managed while unparking mount.").arg(mountReply.value()); } } void Scheduler::checkMountParkingStatus() { if (mountInterface.isNull()) return; static int parkingFailureCount = 0; QVariant parkingStatus = mountInterface->property("parkStatus"); if (parkingStatus.isValid() == false) { qCCritical(KSTARS_EKOS_SCHEDULER) << QString("Warning: mount parkStatus request received DBUS error: %1").arg(mountInterface->lastError().type()); if (!manageConnectionLoss()) parkWaitState = PARKWAIT_ERROR; } ISD::ParkStatus status = static_cast(parkingStatus.toInt()); switch (status) { //case Mount::PARKING_OK: case ISD::PARK_PARKED: // If we are starting up, we will unpark the mount in checkParkWaitState soon // If we are shutting down and mount is parked, proceed to next step if (shutdownState == SHUTDOWN_PARKING_MOUNT) shutdownState = SHUTDOWN_PARK_DOME; // Update parking engine state if (parkWaitState == PARKWAIT_PARKING) parkWaitState = PARKWAIT_PARKED; appendLogText(i18n("Mount parked.")); parkingFailureCount = 0; break; //case Mount::UNPARKING_OK: case ISD::PARK_UNPARKED: // If we are starting up and mount is unparked, proceed to next step // If we are shutting down, we will park the mount in checkParkWaitState soon if (startupState == STARTUP_UNPARKING_MOUNT) startupState = STARTUP_UNPARK_CAP; // Update parking engine state if (parkWaitState == PARKWAIT_UNPARKING) parkWaitState = PARKWAIT_UNPARKED; appendLogText(i18n("Mount unparked.")); parkingFailureCount = 0; break; // FIXME: Create an option for the parking/unparking timeout. //case Mount::UNPARKING_BUSY: case ISD::PARK_UNPARKING: if (currentOperationTime.elapsed() > (60 * 1000)) { if (++parkingFailureCount < MAX_FAILURE_ATTEMPTS) { appendLogText(i18n("Warning: mount unpark operation timed out on attempt %1/%2. Restarting operation...", parkingFailureCount, MAX_FAILURE_ATTEMPTS)); unParkMount(); } else { appendLogText(i18n("Warning: mount unpark operation timed out on last attempt.")); parkWaitState = PARKWAIT_ERROR; } } else qCInfo(KSTARS_EKOS_SCHEDULER) << "Unparking mount in progress..."; break; //case Mount::PARKING_BUSY: case ISD::PARK_PARKING: if (currentOperationTime.elapsed() > (60 * 1000)) { if (++parkingFailureCount < MAX_FAILURE_ATTEMPTS) { appendLogText(i18n("Warning: mount park operation timed out on attempt %1/%2. Restarting operation...", parkingFailureCount, MAX_FAILURE_ATTEMPTS)); parkMount(); } else { appendLogText(i18n("Warning: mount park operation timed out on last attempt.")); parkWaitState = PARKWAIT_ERROR; } } else qCInfo(KSTARS_EKOS_SCHEDULER) << "Parking mount in progress..."; break; //case Mount::PARKING_ERROR: case ISD::PARK_ERROR: if (startupState == STARTUP_UNPARKING_MOUNT) { appendLogText(i18n("Mount unparking error.")); startupState = STARTUP_ERROR; } else if (shutdownState == SHUTDOWN_PARKING_MOUNT) { appendLogText(i18n("Mount parking error.")); shutdownState = SHUTDOWN_ERROR; } else if (parkWaitState == PARKWAIT_PARKING) { appendLogText(i18n("Mount parking error.")); parkWaitState = PARKWAIT_ERROR; } else if (parkWaitState == PARKWAIT_UNPARKING) { appendLogText(i18n("Mount unparking error.")); parkWaitState = PARKWAIT_ERROR; } parkingFailureCount = 0; break; //case Mount::PARKING_IDLE: // FIXME Does this work as intended? check! case ISD::PARK_UNKNOWN: // Last parking action did not result in an action, so proceed to next step if (shutdownState == SHUTDOWN_PARKING_MOUNT) shutdownState = SHUTDOWN_PARK_DOME; // Last unparking action did not result in an action, so proceed to next step if (startupState == STARTUP_UNPARKING_MOUNT) startupState = STARTUP_UNPARK_CAP; // Update parking engine state if (parkWaitState == PARKWAIT_PARKING) parkWaitState = PARKWAIT_PARKED; else if (parkWaitState == PARKWAIT_UNPARKING) parkWaitState = PARKWAIT_UNPARKED; parkingFailureCount = 0; break; // All cases covered above //default: // qCWarning(KSTARS_EKOS_SCHEDULER) << QString("BUG: Parking state %1 not managed while checking progress.").arg(mountReply.value()); } } bool Scheduler::isMountParked() { if (mountInterface.isNull()) return false; // First check if the mount is able to park - if it isn't, getParkingStatus will reply PARKING_ERROR and status won't be clear //QDBusReply const parkCapableReply = mountInterface->call(QDBus::AutoDetect, "canPark"); QVariant canPark = mountInterface->property("canPark"); if (canPark.isValid() == false) { qCCritical(KSTARS_EKOS_SCHEDULER) << QString("Warning: mount canPark request received DBUS error: %1").arg(mountInterface->lastError().type()); manageConnectionLoss(); return false; } else if (canPark.toBool() == true) { // If it is able to park, obtain its current status //QDBusReply const mountReply = mountInterface->call(QDBus::AutoDetect, "getParkingStatus"); QVariant parkingStatus = mountInterface->property("parkStatus"); if (parkingStatus.isValid() == false) { qCCritical(KSTARS_EKOS_SCHEDULER) << QString("Warning: mount parking status property is invalid %1.").arg(mountInterface->lastError().type()); manageConnectionLoss(); return false; } // Deduce state of mount - see getParkingStatus in mount.cpp switch (static_cast(parkingStatus.toInt())) { // case Mount::PARKING_OK: // INDI switch ok, and parked // case Mount::PARKING_IDLE: // INDI switch idle, and parked case ISD::PARK_PARKED: return true; // case Mount::UNPARKING_OK: // INDI switch idle or ok, and unparked // case Mount::PARKING_ERROR: // INDI switch error // case Mount::PARKING_BUSY: // INDI switch busy // case Mount::UNPARKING_BUSY: // INDI switch busy default: return false; } } // If the mount is not able to park, consider it not parked return false; } void Scheduler::parkDome() { if (domeInterface.isNull()) return; //QDBusReply const domeReply = domeInterface->call(QDBus::AutoDetect, "getParkingStatus"); //Dome::ParkingStatus status = static_cast(domeReply.value()); QVariant parkingStatus = domeInterface->property("parkStatus"); if (parkingStatus.isValid() == false) { qCCritical(KSTARS_EKOS_SCHEDULER) << QString("Warning: dome parkStatus request received DBUS error: %1").arg(mountInterface->lastError().type()); if (!manageConnectionLoss()) parkingStatus = ISD::PARK_ERROR; } ISD::ParkStatus status = static_cast(parkingStatus.toInt()); if (status != ISD::PARK_PARKED) { shutdownState = SHUTDOWN_PARKING_DOME; domeInterface->call(QDBus::AutoDetect, "park"); appendLogText(i18n("Parking dome...")); currentOperationTime.start(); } else { appendLogText(i18n("Dome already parked.")); shutdownState = SHUTDOWN_SCRIPT; } } void Scheduler::unParkDome() { if (domeInterface.isNull()) return; QVariant parkingStatus = domeInterface->property("parkStatus"); if (parkingStatus.isValid() == false) { qCCritical(KSTARS_EKOS_SCHEDULER) << QString("Warning: dome parkStatus request received DBUS error: %1").arg(mountInterface->lastError().type()); if (!manageConnectionLoss()) parkingStatus = ISD::PARK_ERROR; } if (static_cast(parkingStatus.toInt()) != ISD::PARK_UNPARKED) { startupState = STARTUP_UNPARKING_DOME; domeInterface->call(QDBus::AutoDetect, "unpark"); appendLogText(i18n("Unparking dome...")); currentOperationTime.start(); } else { appendLogText(i18n("Dome already unparked.")); startupState = STARTUP_UNPARK_MOUNT; } } void Scheduler::checkDomeParkingStatus() { if (domeInterface.isNull()) return; /* FIXME: move this elsewhere */ static int parkingFailureCount = 0; QVariant parkingStatus = domeInterface->property("parkStatus"); if (parkingStatus.isValid() == false) { qCCritical(KSTARS_EKOS_SCHEDULER) << QString("Warning: dome parkStatus request received DBUS error: %1").arg(mountInterface->lastError().type()); if (!manageConnectionLoss()) parkWaitState = PARKWAIT_ERROR; } ISD::ParkStatus status = static_cast(parkingStatus.toInt()); switch (status) { case ISD::PARK_PARKED: if (shutdownState == SHUTDOWN_PARKING_DOME) { appendLogText(i18n("Dome parked.")); shutdownState = SHUTDOWN_SCRIPT; } parkingFailureCount = 0; break; case ISD::PARK_UNPARKED: if (startupState == STARTUP_UNPARKING_DOME) { startupState = STARTUP_UNPARK_MOUNT; appendLogText(i18n("Dome unparked.")); } parkingFailureCount = 0; break; case ISD::PARK_PARKING: case ISD::PARK_UNPARKING: // TODO make the timeouts configurable by the user if (currentOperationTime.elapsed() > (120 * 1000)) { if (parkingFailureCount++ < MAX_FAILURE_ATTEMPTS) { appendLogText(i18n("Operation timeout. Restarting operation...")); if (status == ISD::PARK_PARKING) parkDome(); else unParkDome(); break; } } break; case ISD::PARK_ERROR: if (shutdownState == SHUTDOWN_PARKING_DOME) { appendLogText(i18n("Dome parking error.")); shutdownState = SHUTDOWN_ERROR; } else if (startupState == STARTUP_UNPARKING_DOME) { appendLogText(i18n("Dome unparking error.")); startupState = STARTUP_ERROR; } parkingFailureCount = 0; break; default: break; } } bool Scheduler::isDomeParked() { if (domeInterface.isNull()) return false; QVariant parkingStatus = domeInterface->property("parkStatus"); if (parkingStatus.isValid() == false) { qCCritical(KSTARS_EKOS_SCHEDULER) << QString("Warning: dome parkStatus request received DBUS error: %1").arg(mountInterface->lastError().type()); if (!manageConnectionLoss()) parkingStatus = ISD::PARK_ERROR; } ISD::ParkStatus status = static_cast(parkingStatus.toInt()); return status == ISD::PARK_PARKED; } void Scheduler::parkCap() { if (capInterface.isNull()) return; QVariant parkingStatus = capInterface->property("parkStatus"); if (parkingStatus.isValid() == false) { qCCritical(KSTARS_EKOS_SCHEDULER) << QString("Warning: cap parkStatus request received DBUS error: %1").arg(mountInterface->lastError().type()); if (!manageConnectionLoss()) parkingStatus = ISD::PARK_ERROR; } ISD::ParkStatus status = static_cast(parkingStatus.toInt()); if (status != ISD::PARK_PARKED) { shutdownState = SHUTDOWN_PARKING_CAP; capInterface->call(QDBus::AutoDetect, "park"); appendLogText(i18n("Parking Cap...")); currentOperationTime.start(); } else { appendLogText(i18n("Cap already parked.")); shutdownState = SHUTDOWN_PARK_MOUNT; } } void Scheduler::unParkCap() { if (capInterface.isNull()) return; QVariant parkingStatus = capInterface->property("parkStatus"); if (parkingStatus.isValid() == false) { qCCritical(KSTARS_EKOS_SCHEDULER) << QString("Warning: cap parkStatus request received DBUS error: %1").arg(mountInterface->lastError().type()); if (!manageConnectionLoss()) parkingStatus = ISD::PARK_ERROR; } ISD::ParkStatus status = static_cast(parkingStatus.toInt()); if (status != ISD::PARK_UNPARKED) { startupState = STARTUP_UNPARKING_CAP; capInterface->call(QDBus::AutoDetect, "unpark"); appendLogText(i18n("Unparking cap...")); currentOperationTime.start(); } else { appendLogText(i18n("Cap already unparked.")); startupState = STARTUP_COMPLETE; } } void Scheduler::checkCapParkingStatus() { if (capInterface.isNull()) return; /* FIXME: move this elsewhere */ static int parkingFailureCount = 0; QVariant parkingStatus = capInterface->property("parkStatus"); if (parkingStatus.isValid() == false) { qCCritical(KSTARS_EKOS_SCHEDULER) << QString("Warning: cap parkStatus request received DBUS error: %1").arg(mountInterface->lastError().type()); if (!manageConnectionLoss()) parkingStatus = ISD::PARK_ERROR; } ISD::ParkStatus status = static_cast(parkingStatus.toInt()); switch (status) { case ISD::PARK_PARKED: if (shutdownState == SHUTDOWN_PARKING_CAP) { appendLogText(i18n("Cap parked.")); shutdownState = SHUTDOWN_PARK_MOUNT; } parkingFailureCount = 0; break; case ISD::PARK_UNPARKED: if (startupState == STARTUP_UNPARKING_CAP) { startupState = STARTUP_COMPLETE; appendLogText(i18n("Cap unparked.")); } parkingFailureCount = 0; break; case ISD::PARK_PARKING: case ISD::PARK_UNPARKING: // TODO make the timeouts configurable by the user if (currentOperationTime.elapsed() > (60 * 1000)) { if (parkingFailureCount++ < MAX_FAILURE_ATTEMPTS) { appendLogText(i18n("Operation timeout. Restarting operation...")); if (status == ISD::PARK_PARKING) parkCap(); else unParkCap(); break; } } break; case ISD::PARK_ERROR: if (shutdownState == SHUTDOWN_PARKING_CAP) { appendLogText(i18n("Cap parking error.")); shutdownState = SHUTDOWN_ERROR; } else if (startupState == STARTUP_UNPARKING_CAP) { appendLogText(i18n("Cap unparking error.")); startupState = STARTUP_ERROR; } parkingFailureCount = 0; break; default: break; } } void Scheduler::startJobEvaluation() { // Reset current job setCurrentJob(nullptr); // Reset ALL scheduler jobs to IDLE and force-reset their completed count - no effect when progress is kept for (SchedulerJob * job : jobs) { job->reset(); job->setCompletedCount(0); } // Unconditionally update the capture storage updateCompletedJobsCount(true); // And evaluate all pending jobs per the conditions set in each jobEvaluationOnly = true; evaluateJobs(); } void Scheduler::sortJobsPerAltitude() { // We require a first job to sort, so bail out if list is empty if (jobs.isEmpty()) return; // Don't reset current job // setCurrentJob(nullptr); // Don't reset scheduler jobs startup times before sorting - we need the first job startup time // Sort by startup time, using the first job time as reference for altitude calculations using namespace std::placeholders; QList sortedJobs = jobs; std::stable_sort(sortedJobs.begin() + 1, sortedJobs.end(), std::bind(SchedulerJob::decreasingAltitudeOrder, _1, _2, jobs.first()->getStartupTime())); // If order changed, reset and re-evaluate if (reorderJobs(sortedJobs)) { for (SchedulerJob * job : jobs) job->reset(); jobEvaluationOnly = true; evaluateJobs(); } } void Scheduler::updatePreDawn() { double earlyDawn = Dawn - Options::preDawnTime() / (60.0 * 24.0); int dayOffset = 0; QTime dawn = QTime(0, 0, 0).addSecs(Dawn * 24 * 3600); if (KStarsData::Instance()->lt().time() >= dawn) dayOffset = 1; preDawnDateTime.setDate(KStarsData::Instance()->lt().date().addDays(dayOffset)); preDawnDateTime.setTime(QTime::fromMSecsSinceStartOfDay(earlyDawn * 24 * 3600 * 1000)); } bool Scheduler::isWeatherOK(SchedulerJob *job) { if (weatherStatus == ISD::Weather::WEATHER_OK || weatherCheck->isChecked() == false) return true; else if (weatherStatus == ISD::Weather::WEATHER_IDLE) { if (indiState == INDI_READY) appendLogText(i18n("Weather information is pending...")); return true; } // Temporary BUSY is ALSO accepted for now // TODO Figure out how to exactly handle this if (weatherStatus == ISD::Weather::WEATHER_WARNING) return true; if (weatherStatus == ISD::Weather::WEATHER_ALERT) { job->setState(SchedulerJob::JOB_ABORTED); appendLogText(i18n("Job '%1' suffers from bad weather, marking aborted.", job->getName())); } /*else if (weatherStatus == IPS_BUSY) { appendLogText(i18n("%1 observation job delayed due to bad weather.", job->getName())); schedulerTimer.stop(); connect(this, &Scheduler::weatherChanged, this, &Scheduler::resumeCheckStatus); }*/ return false; } void Scheduler::resumeCheckStatus() { disconnect(this, &Scheduler::weatherChanged, this, &Scheduler::resumeCheckStatus); schedulerTimer.start(); } Scheduler::ErrorHandlingStrategy Scheduler::getErrorHandlingStrategy() { // The UI holds the state if (errorHandlingRestartAfterAllButton->isChecked()) return ERROR_RESTART_AFTER_TERMINATION; else if (errorHandlingRestartImmediatelyButton->isChecked()) return ERROR_RESTART_IMMEDIATELY; else return ERROR_DONT_RESTART; } void Scheduler::setErrorHandlingStrategy(Scheduler::ErrorHandlingStrategy strategy) { errorHandlingDelaySB->setEnabled(strategy != ERROR_DONT_RESTART); switch (strategy) { case ERROR_RESTART_AFTER_TERMINATION: errorHandlingRestartAfterAllButton->setChecked(true); break; case ERROR_RESTART_IMMEDIATELY: errorHandlingRestartImmediatelyButton->setChecked(true); break; default: errorHandlingDontRestartButton->setChecked(true); break; } } void Scheduler::startMosaicTool() { bool raOk = false, decOk = false; dms ra(raBox->createDms(false, &raOk)); //false means expressed in hours dms dec(decBox->createDms(true, &decOk)); if (raOk == false) { appendLogText(i18n("Warning: RA value %1 is invalid.", raBox->text())); return; } if (decOk == false) { appendLogText(i18n("Warning: DEC value %1 is invalid.", decBox->text())); return; } Mosaic mosaicTool; SkyPoint center; center.setRA0(ra); center.setDec0(dec); mosaicTool.setCenter(center); mosaicTool.calculateFOV(); mosaicTool.adjustSize(); if (mosaicTool.exec() == QDialog::Accepted) { // #1 Edit Sequence File ---> Not needed as of 2016-09-12 since Scheduler can send Target Name to Capture module it will append it to root dir // #1.1 Set prefix to Target-Part# // #1.2 Set directory to output/Target-Part# // #2 Save all sequence files in Jobs dir // #3 Set as current Sequence file // #4 Change Target name to Target-Part# // #5 Update J2000 coords // #6 Repeat and save Ekos Scheduler List in the output directory qCDebug(KSTARS_EKOS_SCHEDULER) << "Job accepted with # " << mosaicTool.getJobs().size() << " jobs and fits dir " << mosaicTool.getJobsDir(); QString outputDir = mosaicTool.getJobsDir(); QString targetName = nameEdit->text().simplified().remove(' '); int batchCount = 1; XMLEle *root = getSequenceJobRoot(); if (root == nullptr) return; // Delete any prior jobs before saving if (!jobs.empty()) { if (KMessageBox::questionYesNo(nullptr, i18n("Do you want to keep the existing jobs in the mosaic schedule?")) == KMessageBox::No) { qDeleteAll(jobs); jobs.clear(); while (queueTable->rowCount() > 0) queueTable->removeRow(0); } } foreach (OneTile *oneJob, mosaicTool.getJobs()) { QString prefix = QString("%1-Part%2").arg(targetName).arg(batchCount++); prefix.replace(' ', '-'); nameEdit->setText(prefix); if (createJobSequence(root, prefix, outputDir) == false) return; QString filename = QString("%1/%2.esq").arg(outputDir, prefix); sequenceEdit->setText(filename); sequenceURL = QUrl::fromLocalFile(filename); raBox->showInHours(oneJob->skyCenter.ra0()); decBox->showInDegrees(oneJob->skyCenter.dec0()); saveJob(); } delXMLEle(root); QUrl mosaicURL = QUrl::fromLocalFile((QString("%1/%2_mosaic.esl").arg(outputDir, targetName))); if (saveScheduler(mosaicURL)) { appendLogText(i18n("Mosaic file %1 saved successfully.", mosaicURL.toLocalFile())); } else { appendLogText(i18n("Error saving mosaic file %1. Please reload job.", mosaicURL.toLocalFile())); } } } XMLEle *Scheduler::getSequenceJobRoot() { QFile sFile; sFile.setFileName(sequenceURL.toLocalFile()); if (!sFile.open(QIODevice::ReadOnly)) { KSNotification::sorry(i18n("Unable to open file %1", sFile.fileName()), i18n("Could Not Open File")); return nullptr; } LilXML *xmlParser = newLilXML(); char errmsg[MAXRBUF]; XMLEle *root = nullptr; char c; while (sFile.getChar(&c)) { root = readXMLEle(xmlParser, c, errmsg); if (root) break; } delLilXML(xmlParser); sFile.close(); return root; } bool Scheduler::createJobSequence(XMLEle *root, const QString &prefix, const QString &outputDir) { QFile sFile; sFile.setFileName(sequenceURL.toLocalFile()); if (!sFile.open(QIODevice::ReadOnly)) { KSNotification::sorry(i18n("Unable to open sequence file %1", sFile.fileName()), i18n("Could Not Open File")); return false; } XMLEle *ep = nullptr; XMLEle *subEP = nullptr; for (ep = nextXMLEle(root, 1); ep != nullptr; ep = nextXMLEle(root, 0)) { if (!strcmp(tagXMLEle(ep), "Job")) { for (subEP = nextXMLEle(ep, 1); subEP != nullptr; subEP = nextXMLEle(ep, 0)) { if (!strcmp(tagXMLEle(subEP), "Prefix")) { XMLEle *rawPrefix = findXMLEle(subEP, "RawPrefix"); if (rawPrefix) { editXMLEle(rawPrefix, prefix.toLatin1().constData()); } } else if (!strcmp(tagXMLEle(subEP), "FITSDirectory")) { editXMLEle(subEP, QString("%1/%2").arg(outputDir, prefix).toLatin1().constData()); } } } } QDir().mkpath(outputDir); QString filename = QString("%1/%2.esq").arg(outputDir, prefix); FILE *outputFile = fopen(filename.toLatin1().constData(), "w"); if (outputFile == nullptr) { QString message = i18n("Unable to write to file %1", filename); KSNotification::sorry(message, i18n("Could Not Open File")); return false; } fprintf(outputFile, ""); prXMLEle(outputFile, root, 0); fclose(outputFile); return true; } void Scheduler::resetAllJobs() { if (state == SCHEDULER_RUNNING) return; // Reset capture count of all jobs before re-evaluating foreach (SchedulerJob *job, jobs) job->setCompletedCount(0); // Evaluate all jobs, this refreshes storage and resets job states startJobEvaluation(); } void Scheduler::checkTwilightWarning(bool enabled) { if (enabled) return; if (KMessageBox::warningContinueCancel( nullptr, i18n("Turning off astronomial twilight check may cause the observatory " "to run during daylight. This can cause irreversible damage to your equipment!"), i18n("Astronomial Twilight Warning"), KStandardGuiItem::cont(), KStandardGuiItem::cancel(), "astronomical_twilight_warning") == KMessageBox::Cancel) { twilightCheck->setChecked(true); } } void Scheduler::checkStartupProcedure() { if (checkStartupState() == false) QTimer::singleShot(1000, this, SLOT(checkStartupProcedure())); else { if (startupState == STARTUP_COMPLETE) appendLogText(i18n("Manual startup procedure completed successfully.")); else if (startupState == STARTUP_ERROR) appendLogText(i18n("Manual startup procedure terminated due to errors.")); startupB->setIcon( QIcon::fromTheme("media-playback-start")); } } void Scheduler::runStartupProcedure() { if (startupState == STARTUP_IDLE || startupState == STARTUP_ERROR || startupState == STARTUP_COMPLETE) { /* FIXME: Probably issue a warning only, in case the user wants to run the startup script alone */ if (indiState == INDI_IDLE) { KSNotification::sorry(i18n("Cannot run startup procedure while INDI devices are not online.")); return; } if (KMessageBox::questionYesNo( nullptr, i18n("Are you sure you want to execute the startup procedure manually?")) == KMessageBox::Yes) { appendLogText(i18n("Warning: executing startup procedure manually...")); startupB->setIcon( QIcon::fromTheme("media-playback-stop")); startupState = STARTUP_IDLE; checkStartupState(); QTimer::singleShot(1000, this, SLOT(checkStartupProcedure())); } } else { switch (startupState) { case STARTUP_IDLE: break; case STARTUP_SCRIPT: scriptProcess.terminate(); break; case STARTUP_UNPARK_DOME: break; case STARTUP_UNPARKING_DOME: domeInterface->call(QDBus::AutoDetect, "abort"); break; case STARTUP_UNPARK_MOUNT: break; case STARTUP_UNPARKING_MOUNT: mountInterface->call(QDBus::AutoDetect, "abort"); break; case STARTUP_UNPARK_CAP: break; case STARTUP_UNPARKING_CAP: break; case STARTUP_COMPLETE: break; case STARTUP_ERROR: break; } startupState = STARTUP_IDLE; appendLogText(i18n("Startup procedure terminated.")); } } void Scheduler::checkShutdownProcedure() { // If shutdown procedure is not finished yet, let's check again in 1 second. if (checkShutdownState() == false) QTimer::singleShot(1000, this, SLOT(checkShutdownProcedure())); else { if (shutdownState == SHUTDOWN_COMPLETE) { appendLogText(i18n("Manual shutdown procedure completed successfully.")); // Stop Ekos if (Options::stopEkosAfterShutdown()) stopEkos(); } else if (shutdownState == SHUTDOWN_ERROR) appendLogText(i18n("Manual shutdown procedure terminated due to errors.")); shutdownState = SHUTDOWN_IDLE; shutdownB->setIcon( QIcon::fromTheme("media-playback-start")); } } void Scheduler::runShutdownProcedure() { if (shutdownState == SHUTDOWN_IDLE || shutdownState == SHUTDOWN_ERROR || shutdownState == SHUTDOWN_COMPLETE) { if (KMessageBox::questionYesNo( nullptr, i18n("Are you sure you want to execute the shutdown procedure manually?")) == KMessageBox::Yes) { appendLogText(i18n("Warning: executing shutdown procedure manually...")); shutdownB->setIcon( QIcon::fromTheme("media-playback-stop")); shutdownState = SHUTDOWN_IDLE; checkShutdownState(); QTimer::singleShot(1000, this, SLOT(checkShutdownProcedure())); } } else { switch (shutdownState) { case SHUTDOWN_IDLE: break; case SHUTDOWN_SCRIPT: break; case SHUTDOWN_SCRIPT_RUNNING: scriptProcess.terminate(); break; case SHUTDOWN_PARK_DOME: break; case SHUTDOWN_PARKING_DOME: domeInterface->call(QDBus::AutoDetect, "abort"); break; case SHUTDOWN_PARK_MOUNT: break; case SHUTDOWN_PARKING_MOUNT: mountInterface->call(QDBus::AutoDetect, "abort"); break; case SHUTDOWN_PARK_CAP: break; case SHUTDOWN_PARKING_CAP: break; case SHUTDOWN_COMPLETE: break; case SHUTDOWN_ERROR: break; } shutdownState = SHUTDOWN_IDLE; appendLogText(i18n("Shutdown procedure terminated.")); } } void Scheduler::loadProfiles() { QString currentProfile = schedulerProfileCombo->currentText(); QDBusReply profiles = ekosInterface->call(QDBus::AutoDetect, "getProfiles"); if (profiles.error().type() == QDBusError::NoError) { schedulerProfileCombo->blockSignals(true); schedulerProfileCombo->clear(); schedulerProfileCombo->addItem(i18n("Default")); schedulerProfileCombo->addItems(profiles); schedulerProfileCombo->setCurrentText(currentProfile); schedulerProfileCombo->blockSignals(false); } } bool Scheduler::loadSequenceQueue(const QString &fileURL, SchedulerJob *schedJob, QList &jobs, bool &hasAutoFocus) { QFile sFile; sFile.setFileName(fileURL); if (!sFile.open(QIODevice::ReadOnly)) { QString message = i18n("Unable to open sequence queue file '%1'", fileURL); KSNotification::sorry(message, i18n("Could Not Open File")); return false; } LilXML *xmlParser = newLilXML(); char errmsg[MAXRBUF]; XMLEle *root = nullptr; XMLEle *ep = nullptr; char c; while (sFile.getChar(&c)) { root = readXMLEle(xmlParser, c, errmsg); if (root) { for (ep = nextXMLEle(root, 1); ep != nullptr; ep = nextXMLEle(root, 0)) { if (!strcmp(tagXMLEle(ep), "Autofocus")) hasAutoFocus = (!strcmp(findXMLAttValu(ep, "enabled"), "true")); else if (!strcmp(tagXMLEle(ep), "Job")) jobs.append(processJobInfo(ep, schedJob)); } delXMLEle(root); } else if (errmsg[0]) { appendLogText(QString(errmsg)); delLilXML(xmlParser); qDeleteAll(jobs); return false; } } return true; } SequenceJob *Scheduler::processJobInfo(XMLEle *root, SchedulerJob *schedJob) { XMLEle *ep = nullptr; XMLEle *subEP = nullptr; const QMap frameTypes = { { "Light", FRAME_LIGHT }, { "Dark", FRAME_DARK }, { "Bias", FRAME_BIAS }, { "Flat", FRAME_FLAT } }; SequenceJob *job = new SequenceJob(); QString rawPrefix, frameType, filterType; double exposure = 0; bool filterEnabled = false, expEnabled = false, tsEnabled = false; /* Reset light frame presence flag before enumerating */ // JM 2018-09-14: If last sequence job is not LIGHT // then scheduler job light frame is set to whatever last sequence job is // so if it was non-LIGHT, this value is set to false which is wrong. //if (nullptr != schedJob) // schedJob->setLightFramesRequired(false); for (ep = nextXMLEle(root, 1); ep != nullptr; ep = nextXMLEle(root, 0)) { if (!strcmp(tagXMLEle(ep), "Exposure")) { exposure = atof(pcdataXMLEle(ep)); job->setExposure(exposure); } else if (!strcmp(tagXMLEle(ep), "Filter")) { filterType = QString(pcdataXMLEle(ep)); } else if (!strcmp(tagXMLEle(ep), "Type")) { frameType = QString(pcdataXMLEle(ep)); /* Record frame type and mark presence of light frames for this sequence */ CCDFrameType const frameEnum = frameTypes[frameType]; job->setFrameType(frameEnum); if (FRAME_LIGHT == frameEnum && nullptr != schedJob) schedJob->setLightFramesRequired(true); } else if (!strcmp(tagXMLEle(ep), "Prefix")) { subEP = findXMLEle(ep, "RawPrefix"); if (subEP) rawPrefix = QString(pcdataXMLEle(subEP)); subEP = findXMLEle(ep, "FilterEnabled"); if (subEP) filterEnabled = !strcmp("1", pcdataXMLEle(subEP)); subEP = findXMLEle(ep, "ExpEnabled"); if (subEP) expEnabled = (!strcmp("1", pcdataXMLEle(subEP))); subEP = findXMLEle(ep, "TimeStampEnabled"); if (subEP) tsEnabled = (!strcmp("1", pcdataXMLEle(subEP))); job->setPrefixSettings(rawPrefix, filterEnabled, expEnabled, tsEnabled); } else if (!strcmp(tagXMLEle(ep), "Count")) { job->setCount(atoi(pcdataXMLEle(ep))); } else if (!strcmp(tagXMLEle(ep), "Delay")) { job->setDelay(atoi(pcdataXMLEle(ep))); } else if (!strcmp(tagXMLEle(ep), "FITSDirectory")) { job->setLocalDir(pcdataXMLEle(ep)); } else if (!strcmp(tagXMLEle(ep), "RemoteDirectory")) { job->setRemoteDir(pcdataXMLEle(ep)); } else if (!strcmp(tagXMLEle(ep), "UploadMode")) { job->setUploadMode(static_cast(atoi(pcdataXMLEle(ep)))); } } // Make full prefix QString imagePrefix = rawPrefix; if (imagePrefix.isEmpty() == false) imagePrefix += '_'; imagePrefix += frameType; if (filterEnabled && filterType.isEmpty() == false && (job->getFrameType() == FRAME_LIGHT || job->getFrameType() == FRAME_FLAT)) { imagePrefix += '_'; imagePrefix += filterType; } if (expEnabled) { imagePrefix += '_'; imagePrefix += QString::number(exposure, 'd', 0) + QString("_secs"); } job->setFullPrefix(imagePrefix); QString targetName = schedJob->getName().remove(' '); // Directory postfix QString directoryPostfix; /* FIXME: Refactor directoryPostfix assignment, whose code is duplicated in capture.cpp */ if (targetName.isEmpty()) directoryPostfix = QLatin1String("/") + frameType; else directoryPostfix = QLatin1String("/") + targetName + QLatin1String("/") + frameType; if ((job->getFrameType() == FRAME_LIGHT || job->getFrameType() == FRAME_FLAT) && filterType.isEmpty() == false) directoryPostfix += QLatin1String("/") + filterType; job->setDirectoryPostfix(directoryPostfix); return job; } int Scheduler::getCompletedFiles(const QString &path, const QString &seqPrefix) { int seqFileCount = 0; QFileInfo const path_info(path); QString const sig_dir(path_info.dir().path()); QString const sig_file(path_info.baseName()); qCDebug(KSTARS_EKOS_SCHEDULER) << QString("Searching in path '%1', files '%2*' for prefix '%3'...").arg(sig_dir, sig_file, seqPrefix); QDirIterator it(sig_dir, QDir::Files); /* FIXME: this counts all files with prefix in the storage location, not just captures. DSS analysis files are counted in, for instance. */ while (it.hasNext()) { QString const fileName = QFileInfo(it.next()).baseName(); if (fileName.startsWith(seqPrefix)) { qCDebug(KSTARS_EKOS_SCHEDULER) << QString("> Found '%1'").arg(fileName); seqFileCount++; } } return seqFileCount; } void Scheduler::setINDICommunicationStatus(Ekos::CommunicationStatus status) { qCDebug(KSTARS_EKOS_SCHEDULER) << "Scheduler INDI status is" << status; m_INDICommunicationStatus = status; } void Scheduler::setEkosCommunicationStatus(Ekos::CommunicationStatus status) { qCDebug(KSTARS_EKOS_SCHEDULER) << "Scheduler Ekos status is" << status; m_EkosCommunicationStatus = status; } +void Scheduler::simClockScaleChanged(float newScale) +{ + if (sleepTimer.isActive()) + { + QTime const remainingTimeMs = QTime::fromMSecsSinceStartOfDay(std::lround((double) sleepTimer.remainingTime() + * KStarsData::Instance()->clock()->scale() + / newScale)); + appendLogText(i18n("Sleeping for %2 on simulation clock update until observation job %1 is ready...", currentJob->getName(), + remainingTimeMs.toString("hh:mm:ss"))); + sleepTimer.stop(); + sleepTimer.start(remainingTimeMs.msecsSinceStartOfDay()); + } +} + void Scheduler::registerNewModule(const QString &name) { qCDebug(KSTARS_EKOS_SCHEDULER) << "Registering new Module (" << name << ")"; if (name == "Focus") { delete focusInterface; focusInterface = new QDBusInterface("org.kde.kstars", "/KStars/Ekos/Focus", "org.kde.kstars.Ekos.Focus", QDBusConnection::sessionBus(), this); connect(focusInterface, SIGNAL(newStatus(Ekos::FocusState)), this, SLOT(setFocusStatus(Ekos::FocusState)), Qt::UniqueConnection); } else if (name == "Capture") { delete captureInterface; captureInterface = new QDBusInterface("org.kde.kstars", "/KStars/Ekos/Capture", "org.kde.kstars.Ekos.Capture", QDBusConnection::sessionBus(), this); connect(captureInterface, SIGNAL(ready()), this, SLOT(syncProperties())); connect(captureInterface, SIGNAL(newStatus(Ekos::CaptureState)), this, SLOT(setCaptureStatus(Ekos::CaptureState)), Qt::UniqueConnection); } else if (name == "Mount") { delete mountInterface; mountInterface = new QDBusInterface("org.kde.kstars", "/KStars/Ekos/Mount", "org.kde.kstars.Ekos.Mount", QDBusConnection::sessionBus(), this); connect(mountInterface, SIGNAL(ready()), this, SLOT(syncProperties())); connect(mountInterface, SIGNAL(newStatus(ISD::Telescope::Status)), this, SLOT(setMountStatus(ISD::Telescope::Status)), Qt::UniqueConnection); } else if (name == "Align") { delete alignInterface; alignInterface = new QDBusInterface("org.kde.kstars", "/KStars/Ekos/Align", "org.kde.kstars.Ekos.Align", QDBusConnection::sessionBus(), this); connect(alignInterface, SIGNAL(newStatus(Ekos::AlignState)), this, SLOT(setAlignStatus(Ekos::AlignState)), Qt::UniqueConnection); } else if (name == "Guide") { delete guideInterface; guideInterface = new QDBusInterface("org.kde.kstars", "/KStars/Ekos/Guide", "org.kde.kstars.Ekos.Guide", QDBusConnection::sessionBus(), this); connect(guideInterface, SIGNAL(newStatus(Ekos::GuideState)), this, SLOT(setGuideStatus(Ekos::GuideState)), Qt::UniqueConnection); } else if (name == "Dome") { delete domeInterface; domeInterface = new QDBusInterface("org.kde.kstars", "/KStars/Ekos/Dome", "org.kde.kstars.Ekos.Dome", QDBusConnection::sessionBus(), this); connect(domeInterface, SIGNAL(ready()), this, SLOT(syncProperties())); } else if (name == "Weather") { delete weatherInterface; weatherInterface = new QDBusInterface("org.kde.kstars", "/KStars/Ekos/Weather", "org.kde.kstars.Ekos.Weather", QDBusConnection::sessionBus(), this); connect(weatherInterface, SIGNAL(ready()), this, SLOT(syncProperties())); connect(weatherInterface, SIGNAL(newStatus(ISD::Weather::Status)), this, SLOT(setWeatherStatus(ISD::Weather::Status))); } else if (name == "DustCap") { delete capInterface; capInterface = new QDBusInterface("org.kde.kstars", "/KStars/Ekos/DustCap", "org.kde.kstars.Ekos.DustCap", QDBusConnection::sessionBus(), this); connect(capInterface, SIGNAL(ready()), this, SLOT(syncProperties()), Qt::UniqueConnection); } } void Scheduler::syncProperties() { QDBusInterface *iface = qobject_cast(sender()); if (iface == mountInterface) { QVariant canMountPark = mountInterface->property("canPark"); unparkMountCheck->setEnabled(canMountPark.toBool()); parkMountCheck->setEnabled(canMountPark.toBool()); m_MountReady = true; } else if (iface == capInterface) { QVariant canCapPark = capInterface->property("canPark"); if (canCapPark.isValid()) { capCheck->setEnabled(canCapPark.toBool()); uncapCheck->setEnabled(canCapPark.toBool()); m_CapReady = true; } else { capCheck->setEnabled(false); uncapCheck->setEnabled(false); } } else if (iface == weatherInterface) { QVariant updatePeriod = weatherInterface->property("updatePeriod"); if (updatePeriod.isValid()) { weatherCheck->setEnabled(true); QVariant status = weatherInterface->property("status"); setWeatherStatus(static_cast(status.toInt())); // if (updatePeriod.toInt() > 0) // { // weatherTimer.setInterval(updatePeriod.toInt() * 1000); // connect(&weatherTimer, &QTimer::timeout, this, &Scheduler::checkWeather, Qt::UniqueConnection); // weatherTimer.start(); // // Check weather initially // checkWeather(); // } } else weatherCheck->setEnabled(true); } else if (iface == domeInterface) { QVariant canDomePark = domeInterface->property("canPark"); unparkDomeCheck->setEnabled(canDomePark.toBool()); parkDomeCheck->setEnabled(canDomePark.toBool()); m_DomeReady = true; } else if (iface == captureInterface) { QVariant hasCoolerControl = captureInterface->property("coolerControl"); warmCCDCheck->setEnabled(hasCoolerControl.toBool()); m_CaptureReady = true; } } void Scheduler::setAlignStatus(Ekos::AlignState status) { if (state == SCHEDULER_PAUSED || currentJob == nullptr) return; qCDebug(KSTARS_EKOS_SCHEDULER) << "Align State" << Ekos::getAlignStatusString(status); /* If current job is scheduled and has not started yet, wait */ if (SchedulerJob::JOB_SCHEDULED == currentJob->getState()) { QDateTime const now = KStarsData::Instance()->lt(); if (now < currentJob->getStartupTime()) return; } if (currentJob->getStage() == SchedulerJob::STAGE_ALIGNING) { // Is solver complete? if (status == Ekos::ALIGN_COMPLETE) { appendLogText(i18n("Job '%1' alignment is complete.", currentJob->getName())); alignFailureCount = 0; currentJob->setStage(SchedulerJob::STAGE_ALIGN_COMPLETE); getNextAction(); } else if (status == Ekos::ALIGN_FAILED || status == Ekos::ALIGN_ABORTED) { appendLogText(i18n("Warning: job '%1' alignment failed.", currentJob->getName())); if (alignFailureCount++ < MAX_FAILURE_ATTEMPTS) { if (Options::resetMountModelOnAlignFail() && MAX_FAILURE_ATTEMPTS - 1 < alignFailureCount) { appendLogText(i18n("Warning: job '%1' forcing mount model reset after failing alignment #%2.", currentJob->getName(), alignFailureCount)); mountInterface->call(QDBus::AutoDetect, "resetModel"); } appendLogText(i18n("Restarting %1 alignment procedure...", currentJob->getName())); startAstrometry(); } else { appendLogText(i18n("Warning: job '%1' alignment procedure failed, marking aborted.", currentJob->getName())); currentJob->setState(SchedulerJob::JOB_ABORTED); findNextJob(); } } } } void Scheduler::setGuideStatus(Ekos::GuideState status) { if (state == SCHEDULER_PAUSED || currentJob == nullptr) return; qCDebug(KSTARS_EKOS_SCHEDULER) << "Guide State" << Ekos::getGuideStatusString(status); /* If current job is scheduled and has not started yet, wait */ if (SchedulerJob::JOB_SCHEDULED == currentJob->getState()) { QDateTime const now = KStarsData::Instance()->lt(); if (now < currentJob->getStartupTime()) return; } if (currentJob->getStage() == SchedulerJob::STAGE_GUIDING) { qCDebug(KSTARS_EKOS_SCHEDULER) << "Calibration & Guide stage..."; // If calibration stage complete? if (status == Ekos::GUIDE_GUIDING) { appendLogText(i18n("Job '%1' guiding is in progress.", currentJob->getName())); guideFailureCount = 0; // if guiding recovered while we are waiting, abort the restart restartGuidingTimer.stop(); currentJob->setStage(SchedulerJob::STAGE_GUIDING_COMPLETE); getNextAction(); } else if (status == Ekos::GUIDE_CALIBRATION_ERROR || status == Ekos::GUIDE_ABORTED) { if (status == Ekos::GUIDE_ABORTED) appendLogText(i18n("Warning: job '%1' guiding failed.", currentJob->getName())); else appendLogText(i18n("Warning: job '%1' calibration failed.", currentJob->getName())); // if the timer for restarting the guiding is already running, we do nothing and // wait for the action triggered by the timer. This way we avoid that a small guiding problem // abort the scheduler job if (restartGuidingTimer.isActive()) return; if (guideFailureCount++ < MAX_FAILURE_ATTEMPTS) { if (status == Ekos::GUIDE_CALIBRATION_ERROR && Options::realignAfterCalibrationFailure()) { appendLogText(i18n("Restarting %1 alignment procedure...", currentJob->getName())); // JM: We have to go back to startSlew() since if we just call startAstrometry() // It would captureAndSolve at the _current_ coords which could be way off center if the calibration // process took a wild ride search for a suitable guide star and then failed. So startSlew() would ensure // we're back on our target and then it proceed to alignment (focus is skipped since it is done if it was checked anyway). startSlew(); } else { appendLogText(i18n("Job '%1' is guiding, guiding procedure will be restarted in %2 seconds.", currentJob->getName(), (RESTART_GUIDING_DELAY_MS * guideFailureCount) / 1000)); restartGuidingTimer.start(RESTART_GUIDING_DELAY_MS * guideFailureCount); } } else { appendLogText(i18n("Warning: job '%1' guiding procedure failed, marking aborted.", currentJob->getName())); currentJob->setState(SchedulerJob::JOB_ABORTED); findNextJob(); } } } } GuideState Scheduler::getGuidingStatus() { QVariant guideStatus = guideInterface->property("status"); Ekos::GuideState gStatus = static_cast(guideStatus.toInt()); return gStatus; } void Scheduler::setCaptureStatus(Ekos::CaptureState status) { if (currentJob == nullptr) return; qCDebug(KSTARS_EKOS_SCHEDULER) << "Capture State" << Ekos::getCaptureStatusString(status); /* If current job is scheduled and has not started yet, wait */ if (SchedulerJob::JOB_SCHEDULED == currentJob->getState()) { QDateTime const now = KStarsData::Instance()->lt(); if (now < currentJob->getStartupTime()) return; } if (currentJob->getStage() == SchedulerJob::STAGE_CAPTURING) { if (status == Ekos::CAPTURE_ABORTED) { appendLogText(i18n("Warning: job '%1' failed to capture target.", currentJob->getName())); if (captureFailureCount++ < MAX_FAILURE_ATTEMPTS) { // If capture failed due to guiding error, let's try to restart that if (currentJob->getStepPipeline() & SchedulerJob::USE_GUIDE) { // Check if it is guiding related. Ekos::GuideState gStatus = getGuidingStatus(); if (gStatus == Ekos::GUIDE_ABORTED || gStatus == Ekos::GUIDE_CALIBRATION_ERROR || gStatus == GUIDE_DITHERING_ERROR) { appendLogText(i18n("Job '%1' is capturing, is restarting its guiding procedure (attempt #%2 of %3).", currentJob->getName(), captureFailureCount, MAX_FAILURE_ATTEMPTS)); startGuiding(true); return; } } /* FIXME: it's not clear whether it is actually possible to continue capturing when capture fails this way */ appendLogText(i18n("Warning: job '%1' failed its capture procedure, restarting capture.", currentJob->getName())); startCapture(true); } else { /* FIXME: it's not clear whether this situation can be recovered at all */ appendLogText(i18n("Warning: job '%1' failed its capture procedure, marking aborted.", currentJob->getName())); currentJob->setState(SchedulerJob::JOB_ABORTED); findNextJob(); } } else if (status == Ekos::CAPTURE_COMPLETE) { KNotification::event(QLatin1String("EkosScheduledImagingFinished"), i18n("Ekos job (%1) - Capture finished", currentJob->getName())); currentJob->setState(SchedulerJob::JOB_COMPLETE); findNextJob(); } else if (status == Ekos::CAPTURE_IMAGE_RECEIVED) { // We received a new image, but we don't know precisely where so update the storage map and re-estimate job times. // FIXME: rework this once capture storage is reworked if (Options::rememberJobProgress()) { updateCompletedJobsCount(true); for (SchedulerJob * job : jobs) estimateJobTime(job); } // Else if we don't remember the progress on jobs, increase the completed count for the current job only - no cross-checks else currentJob->setCompletedCount(currentJob->getCompletedCount() + 1); captureFailureCount = 0; } } } void Scheduler::setFocusStatus(Ekos::FocusState status) { if (state == SCHEDULER_PAUSED || currentJob == nullptr) return; qCDebug(KSTARS_EKOS_SCHEDULER) << "Focus State" << Ekos::getFocusStatusString(status); /* If current job is scheduled and has not started yet, wait */ if (SchedulerJob::JOB_SCHEDULED == currentJob->getState()) { QDateTime const now = KStarsData::Instance()->lt(); if (now < currentJob->getStartupTime()) return; } if (currentJob->getStage() == SchedulerJob::STAGE_FOCUSING) { // Is focus complete? if (status == Ekos::FOCUS_COMPLETE) { appendLogText(i18n("Job '%1' focusing is complete.", currentJob->getName())); autofocusCompleted = true; currentJob->setStage(SchedulerJob::STAGE_FOCUS_COMPLETE); getNextAction(); } else if (status == Ekos::FOCUS_FAILED || status == Ekos::FOCUS_ABORTED) { appendLogText(i18n("Warning: job '%1' focusing failed.", currentJob->getName())); if (focusFailureCount++ < MAX_FAILURE_ATTEMPTS) { appendLogText(i18n("Job '%1' is restarting its focusing procedure.", currentJob->getName())); // Reset frame to original size. focusInterface->call(QDBus::AutoDetect, "resetFrame"); // Restart focusing startFocusing(); } else { appendLogText(i18n("Warning: job '%1' focusing procedure failed, marking aborted.", currentJob->getName())); currentJob->setState(SchedulerJob::JOB_ABORTED); findNextJob(); } } } } void Scheduler::setMountStatus(ISD::Telescope::Status status) { if (state == SCHEDULER_PAUSED || currentJob == nullptr) return; qCDebug(KSTARS_EKOS_SCHEDULER) << "Mount State changed to" << status; /* If current job is scheduled and has not started yet, wait */ if (SchedulerJob::JOB_SCHEDULED == currentJob->getState()) if (static_cast(KStarsData::Instance()->lt()) < currentJob->getStartupTime()) return; switch (currentJob->getStage()) { case SchedulerJob::STAGE_SLEWING: { qCDebug(KSTARS_EKOS_SCHEDULER) << "Slewing stage..."; if (status == ISD::Telescope::MOUNT_TRACKING) { appendLogText(i18n("Job '%1' slew is complete.", currentJob->getName())); currentJob->setStage(SchedulerJob::STAGE_SLEW_COMPLETE); /* getNextAction is deferred to checkJobStage for dome support */ } else if (status == ISD::Telescope::MOUNT_ERROR) { appendLogText(i18n("Warning: job '%1' slew failed, marking terminated due to errors.", currentJob->getName())); currentJob->setState(SchedulerJob::JOB_ERROR); findNextJob(); } else if (status == ISD::Telescope::MOUNT_IDLE) { appendLogText(i18n("Warning: job '%1' found not slewing, restarting.", currentJob->getName())); currentJob->setStage(SchedulerJob::STAGE_IDLE); getNextAction(); } } break; case SchedulerJob::STAGE_RESLEWING: { qCDebug(KSTARS_EKOS_SCHEDULER) << "Re-slewing stage..."; if (status == ISD::Telescope::MOUNT_TRACKING) { appendLogText(i18n("Job '%1' repositioning is complete.", currentJob->getName())); currentJob->setStage(SchedulerJob::STAGE_RESLEWING_COMPLETE); /* getNextAction is deferred to checkJobStage for dome support */ } else if (status == ISD::Telescope::MOUNT_ERROR) { appendLogText(i18n("Warning: job '%1' repositioning failed, marking terminated due to errors.", currentJob->getName())); currentJob->setState(SchedulerJob::JOB_ERROR); findNextJob(); } else if (status == ISD::Telescope::MOUNT_IDLE) { appendLogText(i18n("Warning: job '%1' found not repositioning, restarting.", currentJob->getName())); currentJob->setStage(SchedulerJob::STAGE_IDLE); getNextAction(); } } break; default: break; } } void Scheduler::setWeatherStatus(ISD::Weather::Status status) { ISD::Weather::Status newStatus = status; QString statusString; switch (newStatus) { case ISD::Weather::WEATHER_OK: statusString = i18n("Weather conditions are OK."); break; case ISD::Weather::WEATHER_WARNING: statusString = i18n("Warning: weather conditions are in the WARNING zone."); break; case ISD::Weather::WEATHER_ALERT: statusString = i18n("Caution: weather conditions are in the DANGER zone!"); break; default: break; } if (newStatus != weatherStatus) { weatherStatus = newStatus; qCDebug(KSTARS_EKOS_SCHEDULER) << statusString; if (weatherStatus == ISD::Weather::WEATHER_OK) weatherLabel->setPixmap( QIcon::fromTheme("security-high") .pixmap(QSize(32, 32))); else if (weatherStatus == ISD::Weather::WEATHER_WARNING) { weatherLabel->setPixmap( QIcon::fromTheme("security-medium") .pixmap(QSize(32, 32))); KNotification::event(QLatin1String("WeatherWarning"), i18n("Weather conditions in warning zone")); } else if (weatherStatus == ISD::Weather::WEATHER_ALERT) { weatherLabel->setPixmap( QIcon::fromTheme("security-low") .pixmap(QSize(32, 32))); KNotification::event(QLatin1String("WeatherAlert"), i18n("Weather conditions are critical. Observatory shutdown is imminent")); } else weatherLabel->setPixmap(QIcon::fromTheme("chronometer") .pixmap(QSize(32, 32))); weatherLabel->show(); weatherLabel->setToolTip(statusString); appendLogText(statusString); emit weatherChanged(weatherStatus); } // Shutdown scheduler if it was started and not already in shutdown if (weatherStatus == ISD::Weather::WEATHER_ALERT && state != Ekos::SCHEDULER_IDLE && state != Ekos::SCHEDULER_SHUTDOWN) { appendLogText(i18n("Starting shutdown procedure due to severe weather.")); if (currentJob) { currentJob->setState(SchedulerJob::JOB_ABORTED); stopCurrentJobAction(); jobTimer.stop(); } checkShutdownState(); //connect(KStars::Instance()->data()->clock(), SIGNAL(timeAdvanced()), this, SLOT(checkStatus()), &Scheduler::Qt::UniqueConnection); } } bool Scheduler::shouldSchedulerSleep(SchedulerJob *currentJob) { if (currentJob->getLightFramesRequired() == false) return false; QDateTime const now = KStarsData::Instance()->lt(); int const nextObservationTime = now.secsTo(currentJob->getStartupTime()); // If start up procedure is complete and the user selected pre-emptive shutdown, let us check if the next observation time exceed // the pre-emptive shutdown time in hours (default 2). If it exceeds that, we perform complete shutdown until next job is ready if (startupState == STARTUP_COMPLETE && Options::preemptiveShutdown() && nextObservationTime > (Options::preemptiveShutdownTime() * 3600)) { appendLogText(i18n( "Job '%1' scheduled for execution at %2. " "Observatory scheduled for shutdown until next job is ready.", currentJob->getName(), currentJob->getStartupTime().toString(currentJob->getDateTimeDisplayFormat()))); preemptiveShutdown = true; weatherCheck->setEnabled(false); weatherLabel->hide(); checkShutdownState(); //schedulerTimer.stop(); // Wake up when job is due. // FIXME: Implement waking up periodically before job is due for weather check. // int const nextWakeup = nextObservationTime < 60 ? nextObservationTime : 60; - sleepTimer.setInterval( (nextObservationTime + 1) * 1000); + sleepTimer.setInterval(std::lround(((nextObservationTime + 1) * 1000) / KStarsData::Instance()->clock()->scale())); sleepTimer.start(); return true; } // Otherwise, sleep until job is ready /* FIXME: if not parking, stop tracking maybe? this would prevent crashes or scheduler stops from leaving the mount to track and bump the pier */ // If start up procedure is already complete, and we didn't issue any parking commands before and parking is checked and enabled // Then we park the mount until next job is ready. But only if the job uses TRACK as its first step, otherwise we cannot get into position again. // This is also only performed if next job is due more than the default lead time (5 minutes). // If job is due sooner than that is not worth parking and we simply go into sleep or wait modes. else if (nextObservationTime > Options::leadTime() * 60 && startupState == STARTUP_COMPLETE && parkWaitState == PARKWAIT_IDLE && (currentJob->getStepPipeline() & SchedulerJob::USE_TRACK) && parkMountCheck->isEnabled() && parkMountCheck->isChecked()) { appendLogText(i18n( "Job '%1' scheduled for execution at %2. " "Parking the mount until the job is ready.", currentJob->getName(), currentJob->getStartupTime().toString())); parkWaitState = PARKWAIT_PARK; return false; } else if (nextObservationTime > Options::leadTime() * 60) { appendLogText(i18n("Sleeping until observation job %1 is ready at %2...", currentJob->getName(), now.addSecs(nextObservationTime + 1).toString())); sleepLabel->setToolTip(i18n("Scheduler is in sleep mode")); sleepLabel->show(); // Warn the user if the next job is really far away - 60/5 = 12 times the lead time if (nextObservationTime > Options::leadTime() * 60 * 12) { dms delay(static_cast(nextObservationTime * 15.0 / 3600.0)); appendLogText(i18n( "Warning: Job '%1' is %2 away from now, you may want to enable Preemptive Shutdown.", currentJob->getName(), delay.toHMSString())); } /* FIXME: stop tracking now */ schedulerTimer.stop(); // Wake up when job is due. // FIXME: Implement waking up periodically before job is due for weather check. // int const nextWakeup = nextObservationTime < 60 ? nextObservationTime : 60; - sleepTimer.setInterval(( (nextObservationTime + 1) * 1000)); + sleepTimer.setInterval(std::lround(((nextObservationTime + 1) * 1000) / KStarsData::Instance()->clock()->scale())); sleepTimer.start(); return true; } return false; } } diff --git a/kstars/ekos/scheduler/scheduler.h b/kstars/ekos/scheduler/scheduler.h index 77f9ba883..bf2950343 100644 --- a/kstars/ekos/scheduler/scheduler.h +++ b/kstars/ekos/scheduler/scheduler.h @@ -1,796 +1,798 @@ /* Ekos Scheduler Module Copyright (C) 2015 Jasem Mutlaq DBus calls from GSoC 2015 Ekos Scheduler project by Daniel Leu 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_scheduler.h" #include "ekos/align/align.h" #include "indi/indiweather.h" #include #include #include #include #include #include #include class QProgressIndicator; class GeoLocation; class SchedulerJob; class SkyObject; class KConfigDialog; namespace Ekos { class SequenceJob; /** * @brief The Ekos scheduler is a simple scheduler class to orchestrate automated multi object observation jobs. * @author Jasem Mutlaq * @version 1.2 */ class Scheduler : public QWidget, public Ui::Scheduler { Q_OBJECT Q_CLASSINFO("D-Bus Interface", "org.kde.kstars.Ekos.Scheduler") Q_PROPERTY(Ekos::SchedulerState status READ status NOTIFY newStatus) Q_PROPERTY(QStringList logText READ logText NOTIFY newLog) Q_PROPERTY(QString profile READ profile WRITE setProfile) public: typedef enum { EKOS_IDLE, EKOS_STARTING, EKOS_STOPPING, EKOS_READY } EkosState; typedef enum { INDI_IDLE, INDI_CONNECTING, INDI_DISCONNECTING, INDI_PROPERTY_CHECK, INDI_READY } INDIState; typedef enum { STARTUP_IDLE, STARTUP_SCRIPT, STARTUP_UNPARK_DOME, STARTUP_UNPARKING_DOME, STARTUP_UNPARK_MOUNT, STARTUP_UNPARKING_MOUNT, STARTUP_UNPARK_CAP, STARTUP_UNPARKING_CAP, STARTUP_ERROR, STARTUP_COMPLETE } StartupState; typedef enum { SHUTDOWN_IDLE, SHUTDOWN_PARK_CAP, SHUTDOWN_PARKING_CAP, SHUTDOWN_PARK_MOUNT, SHUTDOWN_PARKING_MOUNT, SHUTDOWN_PARK_DOME, SHUTDOWN_PARKING_DOME, SHUTDOWN_SCRIPT, SHUTDOWN_SCRIPT_RUNNING, SHUTDOWN_ERROR, SHUTDOWN_COMPLETE } ShutdownState; typedef enum { PARKWAIT_IDLE, PARKWAIT_PARK, PARKWAIT_PARKING, PARKWAIT_PARKED, PARKWAIT_UNPARK, PARKWAIT_UNPARKING, PARKWAIT_UNPARKED, PARKWAIT_ERROR } ParkWaitStatus; /** @brief options what should happen if an error or abort occurs */ typedef enum { ERROR_DONT_RESTART, ERROR_RESTART_AFTER_TERMINATION, ERROR_RESTART_IMMEDIATELY } ErrorHandlingStrategy; /** @brief Columns, in the same order as UI. */ typedef enum { SCHEDCOL_NAME = 0, SCHEDCOL_STATUS, SCHEDCOL_CAPTURES, SCHEDCOL_ALTITUDE, SCHEDCOL_SCORE, SCHEDCOL_STARTTIME, SCHEDCOL_ENDTIME, SCHEDCOL_DURATION, SCHEDCOL_LEADTIME, SCHEDCOL_COUNT } SchedulerColumns; Scheduler(); ~Scheduler() = default; QString getCurrentJobName(); void appendLogText(const QString &); QStringList logText() { return m_LogText; } QString getLogText() { return m_LogText.join("\n"); } void clearLog(); void applyConfig(); void addObject(SkyObject *object); /** * @brief startSlew DBus call for initiating slew */ void startSlew(); /** * @brief startFocusing DBus call for feeding ekos the specified settings and initiating focus operation */ void startFocusing(); /** * @brief startAstrometry initiation of the capture and solve operation. We change the job state * after solver is started */ void startAstrometry(); /** * @brief startGuiding After ekos is fed the calibration options, we start the guiging process * @param resetCalibration By default calibration is not reset until it is explicitly requested */ void startGuiding(bool resetCalibration = false); /** * @brief startCapture The current job file name is solved to an url which is fed to ekos. We then start the capture process * @param restart Set to true if the goal to restart an existing sequence. The only difference is that when a sequence is restarted, sequence file * is not loaded from disk again since that results in erasing all the history of the capture process. */ void startCapture(bool restart = false); /** * @brief getNextAction Checking for the next appropriate action regarding the current state of the scheduler and execute it */ void getNextAction(); /** * @brief disconnectINDI disconnect all INDI devices from server. */ void disconnectINDI(); /** * @brief stopEkos shutdown Ekos completely */ void stopEkos(); /** * @brief stopGuiding After guiding is done we need to stop the process */ void stopGuiding(); /** * @brief setSolverAction set the GOTO mode for the solver * @param mode 0 For Sync, 1 for SlewToTarget, 2 for Nothing */ void setSolverAction(Align::GotoMode mode); /** @defgroup SchedulerDBusInterface Ekos DBus Interface - Scheduler Module * Ekos::Align interface provides primary functions to run and stop the scheduler. */ /*@{*/ /** DBUS interface function. * @brief Start the scheduler main loop and evaluate jobs and execute them accordingly. */ Q_SCRIPTABLE Q_NOREPLY void start(); /** DBUS interface function. * @brief Stop the scheduler. */ Q_SCRIPTABLE Q_NOREPLY void stop(); /** DBUS interface function. * @brief Loads the Ekos Scheduler List (.esl) file. * @param fileURL path to a file * @return true if loading file is successful, false otherwise. */ Q_SCRIPTABLE bool loadScheduler(const QString &fileURL); /** DBUS interface function. * @brief Resets all jobs to IDLE */ Q_SCRIPTABLE void resetAllJobs(); /** DBUS interface function. * @brief Resets all jobs to IDLE */ Q_SCRIPTABLE void sortJobsPerAltitude(); Ekos::SchedulerState status() { return state; } void setProfile(const QString &profile) { schedulerProfileCombo->setCurrentText(profile); } QString profile() { return schedulerProfileCombo->currentText(); } /** * @brief retrieve the error handling strategy from the UI */ ErrorHandlingStrategy getErrorHandlingStrategy(); /** * @brief select the error handling strategy (no restart, restart after all terminated, restart immediately) */ void setErrorHandlingStrategy (ErrorHandlingStrategy strategy); /** @}*/ /** @{ */ private: /** @internal Safeguard flag to avoid registering signals from widgets multiple times. */ bool jobChangesAreWatched { false }; protected: /** @internal Enables signal watch on SchedulerJob form values in order to apply changes to current job. * @param enable is the toggle flag, true to watch for changes, false to ignore them. */ void watchJobChanges(bool enable); /** @internal Marks the currently selected SchedulerJob as modified change. * * This triggers job re-evaluation. * Next time save button is invoked, the complete content is written to disk. */ void setDirty(); /** @} */ protected: /** @internal Associate job table cells on a row to the corresponding SchedulerJob. * @param row is an integer indexing the row to associate cells from, and also the index of the job in the job list.. */ void setJobStatusCells(int row); protected slots: /** * @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); /** * @brief syncProperties Sync startup properties from the various device to enable/disable features in the scheduler * like the ability to park/unpark..etc */ void syncProperties(); void setAlignStatus(Ekos::AlignState status); void setGuideStatus(Ekos::GuideState status); void setCaptureStatus(Ekos::CaptureState status); void setFocusStatus(Ekos::FocusState status); void setMountStatus(ISD::Telescope::Status status); void setWeatherStatus(ISD::Weather::Status status); /** * @brief select object from KStars's find dialog. */ void selectObject(); /** * @brief Selects FITS file for solving. */ void selectFITS(); /** * @brief Selects sequence queue. */ void selectSequence(); /** * @brief Selects sequence queue. */ void selectStartupScript(); /** * @brief Selects sequence queue. */ void selectShutdownScript(); /** * @brief addToQueue Construct a SchedulerJob and add it to the queue or save job settings from current form values. * jobUnderEdit determines whether to add or edit */ void saveJob(); /** * @brief addJob Add a new job from form values */ void addJob(); /** * @brief editJob Edit an observation job * @param i index model in queue table */ void loadJob(QModelIndex i); /** * @brief removeJob Remove a job from the currently selected row. If no row is selected, it remove the last job in the queue. */ void removeJob(); /** * @brief setJobAddApply Set first button state to add new job or apply changes. */ void setJobAddApply(bool add_mode); /** * @brief setJobManipulation Enable or disable job manipulation buttons. */ void setJobManipulation(bool can_reorder, bool can_delete); /** * @brief set all GUI fields to the values of the given scheduler job */ void syncGUIToJob(SchedulerJob *job); /** * @brief jobSelectionChanged Update UI state when the job list is clicked once. */ void clickQueueTable(QModelIndex index); /** * @brief Update scheduler parameters to the currently selected scheduler job * @param current table position * @param previous table position */ void queueTableSelectionChanged(QModelIndex current, QModelIndex previous); /** * @brief reorderJobs Change the order of jobs in the UI based on a subset of its jobs. */ bool reorderJobs(QList reordered_sublist); /** * @brief moveJobUp Move the selected job up in the job list. */ void moveJobUp(); /** * @brief moveJobDown Move the selected job down in the list. */ void moveJobDown(); /** * @brief shouldSchedulerSleep Check if the scheduler needs to sleep until the job is ready * @param currentJob Job to check * @return True if we set the scheduler to sleep mode. False, if not required and we need to execute now */ bool shouldSchedulerSleep(SchedulerJob *currentJob); void toggleScheduler(); void pause(); void setPaused(); void save(); void saveAs(); void load(); void resetJobEdit(); /** * @brief checkJobStatus Check the overall state of the scheduler, Ekos, and INDI. When all is OK, it calls evaluateJobs() when no job is current or executeJob() if a job is selected. * @return False if this function needs to be called again later, true if situation is stable and operations may continue. */ bool checkStatus(); /** * @brief checkJobStage Check the progress of the job states and make DBUS call to start the next stage until the job is complete. */ void checkJobStage(); /** * @brief findNextJob Check if the job met the completion criteria, and if it did, then it search for next job candidate. If no jobs are found, it starts the shutdown stage. */ void findNextJob(); /** * @brief stopCurrentJobAction Stop whatever action taking place in the current job (eg. capture, guiding...etc). */ void stopCurrentJobAction(); /** * @brief manageConnectionLoss Mitigate loss of connection with the INDI server. * @return true if connection to Ekos/INDI should be attempted again, false if not mitigation is available or needed. */ bool manageConnectionLoss(); /** * @brief readProcessOutput read running script process output and display it in Ekos */ void readProcessOutput(); /** * @brief checkProcessExit Check script process exist status. This is called when the process exists either normally or abnormally. * @param exitCode exit code from the script process. Depending on the exist code, the status of startup/shutdown procedure is set accordingly. */ void checkProcessExit(int exitCode); /** * @brief resumeCheckStatus If the scheduler primary loop was suspended due to weather or sleep event, resume it again. */ void resumeCheckStatus(); /** * @brief checkWeather Check weather status and act accordingly depending on the current status of the scheduler and running jobs. */ //void checkWeather(); /** * @brief wakeUpScheduler Wake up scheduler from sleep state */ void wakeUpScheduler(); /** * @brief startJobEvaluation Start job evaluation only without starting the scheduler process itself. Display the result to the user. */ void startJobEvaluation(); /** * @brief startMosaicTool Start Mosaic tool and create jobs if necessary. */ void startMosaicTool(); /** * @brief displayTwilightWarning Display twilight warning to user if it is unchecked. */ void checkTwilightWarning(bool enabled); void runStartupProcedure(); void checkStartupProcedure(); void runShutdownProcedure(); void checkShutdownProcedure(); void setINDICommunicationStatus(Ekos::CommunicationStatus status); void setEkosCommunicationStatus(Ekos::CommunicationStatus status); + void simClockScaleChanged(float); + signals: void newLog(const QString &text); void newStatus(Ekos::SchedulerState state); void weatherChanged(ISD::Weather::Status state); void newTarget(const QString &); private: /** * @brief evaluateJobs evaluates the current state of each objects and gives each one a score based on the constraints. * Given that score, the scheduler will decide which is the best job that needs to be executed. */ void evaluateJobs(); /** * @brief executeJob After the best job is selected, we call this in order to start the process that will execute the job. * checkJobStatus slot will be connected in order to figure the exact state of the current job each second * @param value */ void executeJob(SchedulerJob *job); void executeScript(const QString &filename); /** * @brief getDarkSkyScore Get the dark sky score of a date and time. The further from dawn the better. * @param when date and time to check the dark sky score, now if omitted * @return Dark sky score. Daylight get bad score, as well as pre-dawn to dawn. */ int16_t getDarkSkyScore(QDateTime const &when = QDateTime()) const; /** * @brief calculateJobScore Calculate job dark sky score, altitude score, and moon separation scores and returns the sum. * @param job Target * @param when date and time to evaluate constraints, now if omitted. * @return Total score */ int16_t calculateJobScore(SchedulerJob const *job, QDateTime const &when = QDateTime()) const; /** * @brief getWeatherScore Get current weather condition score. * @return If weather condition OK, return score 0, else bad score. */ int16_t getWeatherScore() const; /** * @brief calculateDawnDusk Get dawn and dusk times for today */ void calculateDawnDusk(); /** * @brief checkEkosState Check ekos startup stages and take whatever action necessary to get Ekos up and running * @return True if Ekos is running, false if Ekos start up is in progress. */ bool checkEkosState(); /** * @brief isINDIConnected Determines the status of the INDI connection. * @return True if INDI connection is up and usable, else false. */ bool isINDIConnected(); /** * @brief checkINDIState Check INDI startup stages and take whatever action necessary to get INDI devices connected. * @return True if INDI devices are connected, false if it is under progress. */ bool checkINDIState(); /** * @brief checkStartupState Check startup procedure stages and make sure all stages are complete. * @return True if startup is complete, false otherwise. */ bool checkStartupState(); /** * @brief checkShutdownState Check shutdown procedure stages and make sure all stages are complete. * @return */ bool checkShutdownState(); /** * @brief checkParkWaitState Check park wait state. * @return If parking/unparking in progress, return false. If parking/unparking complete, return true. */ bool checkParkWaitState(); /** * @brief parkMount Park mount */ void parkMount(); /** * @brief unParkMount Unpark mount */ void unParkMount(); /** * @return True if mount is parked */ bool isMountParked(); /** * @brief parkDome Park dome */ void parkDome(); /** * @brief unParkDome Unpark dome */ void unParkDome(); /** * @return True if dome is parked */ bool isDomeParked(); /** * @brief parkCap Close dust cover */ void parkCap(); /** * @brief unCap Open dust cover */ void unParkCap(); /** * @brief checkMountParkingStatus check mount parking status and updating corresponding states accordingly. */ void checkMountParkingStatus(); /** * @brief checkDomeParkingStatus check dome parking status and updating corresponding states accordingly. */ void checkDomeParkingStatus(); /** * @brief checkDomeParkingStatus check dome parking status and updating corresponding states accordingly. */ void checkCapParkingStatus(); /** * @brief saveScheduler Save scheduler jobs to a file * @param path path of a file * @return true on success, false on failure. */ bool saveScheduler(const QUrl &fileURL); /** * @brief processJobInfo Process the job information from a scheduler file and populate jobs accordingly * @param root XML root element of JOB * @return true on success, false on failure. */ bool processJobInfo(XMLEle *root); /** * @brief updatePreDawn Update predawn time depending on current time and user offset */ void updatePreDawn(); /** * @brief estimateJobTime Estimates the time the job takes to complete based on the sequence file and what modules to utilize during the observation run. * @param job target job * @return Estimated time in seconds. */ bool estimateJobTime(SchedulerJob *schedJob); /** * @brief createJobSequence Creates a job sequence for the mosaic tool given the prefix and output dir. The currently selected sequence file is modified * and a new version given the supplied parameters are saved to the output directory * @param prefix Prefix to set for the job sequence * @param outputDir Output dir to set for the job sequence * @return True if new file is saved, false otherwise */ bool createJobSequence(XMLEle *root, const QString &prefix, const QString &outputDir); /** @internal Change the current job, updating associated widgets. * @param job is an existing SchedulerJob to set as current, or nullptr. */ void setCurrentJob(SchedulerJob *job); /** * @brief processFITSSelection When a FITS file is selected, open it and try to guess * the object name, and its J2000 RA/DE to fill the UI with such info automatically. */ void processFITSSelection(); void loadProfiles(); XMLEle *getSequenceJobRoot(); bool isWeatherOK(SchedulerJob *job); /** * @brief updateCompletedJobsCount For each scheduler job, examine sequence job storage and count captures. * @param forced forces recounting captures unconditionally if true, else only IDLE, EVALUATION or new jobs are examined. */ void updateCompletedJobsCount(bool forced = false); SequenceJob *processJobInfo(XMLEle *root, SchedulerJob *schedJob); bool loadSequenceQueue(const QString &fileURL, SchedulerJob *schedJob, QList &jobs, bool &hasAutoFocus); int getCompletedFiles(const QString &path, const QString &seqPrefix); // retrieve the guiding status GuideState getGuidingStatus(); Ekos::Scheduler *ui { nullptr }; //DBus interfaces QPointer focusInterface { nullptr }; QPointer ekosInterface { nullptr }; QPointer captureInterface { nullptr }; QPointer mountInterface { nullptr }; QPointer alignInterface { nullptr }; QPointer guideInterface { nullptr }; QPointer domeInterface { nullptr }; QPointer weatherInterface { nullptr }; QPointer capInterface { nullptr }; // Scheduler and job state and stages SchedulerState state { SCHEDULER_IDLE }; EkosState ekosState { EKOS_IDLE }; INDIState indiState { INDI_IDLE }; StartupState startupState { STARTUP_IDLE }; ShutdownState shutdownState { SHUTDOWN_IDLE }; ParkWaitStatus parkWaitState { PARKWAIT_IDLE }; Ekos::CommunicationStatus m_EkosCommunicationStatus { Ekos::Idle }; Ekos::CommunicationStatus m_INDICommunicationStatus { Ekos::Idle }; /// List of all jobs as entered by the user or file QList jobs; /// Active job SchedulerJob *currentJob { nullptr }; /// URL to store the scheduler file QUrl schedulerURL; /// URL for Ekos Sequence QUrl sequenceURL; /// FITS URL to solve QUrl fitsURL; /// Startup script URL QUrl startupScriptURL; /// Shutdown script URL QUrl shutdownScriptURL; /// Store all log strings QStringList m_LogText; /// Busy indicator widget QProgressIndicator *pi { nullptr }; /// Are we editing a job right now? Job row index int jobUnderEdit { -1 }; /// Pointer to Geographic location GeoLocation *geo { nullptr }; /// How many repeated job batches did we complete thus far? uint16_t captureBatch { 0 }; /// Startup and Shutdown scripts process QProcess scriptProcess; /// Store day fraction of dawn to calculate dark skies range double Dawn { -1 }; /// Store day fraction of dusk to calculate dark skies range double Dusk { -1 }; /// Pre-dawn is where we stop all jobs, it is a user-configurable value before Dawn. QDateTime preDawnDateTime; /// Dusk date time QDateTime duskDateTime; /// Was job modified and needs saving? bool mDirty { false }; /// Keep watch of weather status ISD::Weather::Status weatherStatus { ISD::Weather::WEATHER_IDLE }; /// Keep track of how many times we didn't receive weather updates uint8_t noWeatherCounter { 0 }; /// Are we shutting down until later? bool preemptiveShutdown { false }; /// Only run job evaluation bool jobEvaluationOnly { false }; /// Keep track of Load & Slew operation bool loadAndSlewProgress { false }; /// Check if initial autofocus is completed and do not run autofocus until there is a change is telescope position/alignment. bool autofocusCompleted { false }; /// Keep track of INDI connection failures uint8_t indiConnectFailureCount { 0 }; /// Keep track of Ekos connection failures uint8_t ekosConnectFailureCount { 0 }; /// Keep track of Ekos focus module failures uint8_t focusFailureCount { 0 }; /// Keep track of Ekos guide module failures uint8_t guideFailureCount { 0 }; /// Keep track of Ekos align module failures uint8_t alignFailureCount { 0 }; /// Keep track of Ekos capture module failures uint8_t captureFailureCount { 0 }; /// Call checkWeather when weatherTimer time expires. It is equal to the UpdatePeriod time in INDI::Weather device. //QTimer weatherTimer; /// Timer to put the scheduler into sleep mode until a job is ready QTimer sleepTimer; /// To call checkStatus QTimer schedulerTimer; /// To call checkJobStage QTimer jobTimer; /// Delay for restarting the guider QTimer restartGuidingTimer; /// Generic time to track timeout of current operation in progress QTime currentOperationTime; QUrl dirPath; QMap capturedFramesCount; bool m_MountReady { false }; bool m_CaptureReady { false }; bool m_DomeReady { false }; bool m_CapReady { false }; // When a module is commanded to perform an action, wait this many milliseconds // before check its state again. If State is still IDLE, then it either didn't received the command // or there is another problem. static const uint32_t ALIGN_INACTIVITY_TIMEOUT = 120000; static const uint32_t FOCUS_INACTIVITY_TIMEOUT = 120000; static const uint32_t CAPTURE_INACTIVITY_TIMEOUT = 120000; static const uint16_t GUIDE_INACTIVITY_TIMEOUT = 60000; }; } diff --git a/kstars/time/simclock.cpp b/kstars/time/simclock.cpp index ca00b6db6..47e8447e5 100644 --- a/kstars/time/simclock.cpp +++ b/kstars/time/simclock.cpp @@ -1,194 +1,194 @@ /*************************************************************************** simclock.cpp - description ------------------- begin : Mon Feb 18 2002 copyright : (C) 2002 by Mark Hollomon email : mhh@mindspring.com ***************************************************************************/ /*************************************************************************** * * * This program 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 "simclock.h" #ifndef KSTARS_LITE #include "kstars.h" #include "simclockadaptor.h" #endif #include int SimClock::TimerInterval = 100; //msec SimClock::SimClock(QObject *parent, const KStarsDateTime &when) : QObject(parent), tmr(this) { #ifndef KSTARS_LITE new SimClockAdaptor(this); QDBusConnection::sessionBus().registerObject("/KStars/SimClock", this); #endif if (!when.isValid()) tmr.stop(); setUTC(when); julianmark = UTC.djd(); QObject::connect(&tmr, SIGNAL(timeout()), this, SLOT(tick())); } void SimClock::tick() { if (!ManualMode) //only tick if ManualMode is false { long mselapsed = sysmark.elapsed(); if (mselapsed < lastelapsed) { // The sysmark timer has wrapped after 24 hours back to 0 ms. // Reset sysmark and julianmark julianmark = UTC.djd(); sysmark.start(); lastelapsed = 0; } else { lastelapsed = mselapsed; } long double scaledsec = static_cast(mselapsed) * static_cast(Scale) / 1000.0; UTC.setDJD(julianmark + scaledsec / (24. * 3600.)); // qDebug() << "tick() : JD = " << QLocale().toString( UTC.djd(), 7 ) << // " mselapsed = " << mselapsed << " scale = " << Scale << // " scaledsec = " << double(scaledsec) << endl; emit timeAdvanced(); } } void SimClock::setManualMode(bool on) { if (on) { //Turn on manual ticking. ManualActive = tmr.isActive(); tmr.stop(); } else { //Turn off manual ticking. If the Manual clock was active, start the timer. if (isActive()) { sysmark.start(); julianmark = UTC.djd(); lastelapsed = 0; tmr.start(TimerInterval); } } ManualMode = on; } void SimClock::manualTick(bool force, bool backward) { if (force || (ManualMode && ManualActive)) { //The single shot timer is needed because otherwise the animation is happening so frequently //that the kstars interface becomes too unresponsive. //QTimer::singleShot(1, [this,backward] { setUTC(UTC.addSecs(static_castScale * (backward ? -1 : 1))); }); setUTC(UTC.addSecs(static_cast(Scale) * (backward ? -1 : 1))); } else if (!ManualMode) tick(); } bool SimClock::isActive() { if (ManualMode) return ManualActive; else return tmr.isActive(); } void SimClock::stop() { if (ManualMode && ManualActive) { ManualActive = false; emit clockToggled(true); } if (!ManualMode && tmr.isActive()) { qCDebug(KSTARS) << "Stopping the timer"; tmr.stop(); emit clockToggled(true); } } void SimClock::start() { if (ManualMode && !ManualActive) { ManualActive = true; sysmark.start(); julianmark = UTC.djd(); lastelapsed = 0; emit clockToggled(false); //emit timeChanged() in order to restart calls to updateTime() emit timeChanged(); } else if (!ManualMode && !tmr.isActive()) { qCDebug(KSTARS) << "Starting the timer"; sysmark.start(); julianmark = UTC.djd(); lastelapsed = 0; tmr.start(TimerInterval); emit clockToggled(false); } } void SimClock::setUTC(const KStarsDateTime &newtime) { //DEBUG //qDebug() << newtime.toString(); //qDebug() << "is dateTime valid? " << newtime.isValid(); if (newtime.isValid()) { UTC = newtime; if (tmr.isActive()) { julianmark = UTC.djd(); sysmark.start(); lastelapsed = 0; } // N.B. Too much log spam when in manual mode //qCInfo(KSTARS) << QString("Setting clock: UTC: %1 JD: %2").arg(UTC.toString(), QLocale().toString((double)UTC.djd(), 'f', 2)); emit timeChanged(); } else { qCWarning(KSTARS) << "Cannot set SimClock: Invalid Date/Time."; } } void SimClock::setClockScale(float s) { if (Scale != s) { qCInfo(KSTARS) << "New clock scale: " << s << " sec"; + emit scaleChanged(s); Scale = s; if (tmr.isActive()) { julianmark = UTC.djd(); sysmark.start(); lastelapsed = 0; } - emit scaleChanged(s); } }