diff --git a/kstars/ekos/align/align.cpp b/kstars/ekos/align/align.cpp index 5dad4ec1f..bc4d56a5c 100644 --- a/kstars/ekos/align/align.cpp +++ b/kstars/ekos/align/align.cpp @@ -1,6312 +1,6315 @@ /* 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 "astapastrometryparser.h" #include "opsalign.h" #include "opsastap.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 #include #define PAH_CUTOFF_FOV 10 // Minimum FOV width in arcminutes for PAH to work #define MAXIMUM_SOLVER_ITERATIONS 10 #define CAPTURE_RETRY_DELAY 10000 #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(); KStarsData::Instance()->clearTransientFOVs(); //loadSlewMode = false; solverFOV.reset(new FOV()); solverFOV->setName(i18n("Solver FOV")); solverFOV->setObjectName("solver_fov"); solverFOV->setLockCelestialPole(true); solverFOV->setColor(KStars::Instance()->data()->colorScheme()->colorNamed("SolverFOVColor").name()); solverFOV->setProperty("visible", false); KStarsData::Instance()->addTransientFOV(solverFOV); sensorFOV.reset(new FOV()); sensorFOV->setObjectName("sensor_fov"); sensorFOV->setLockCelestialPole(true); sensorFOV->setProperty("visible", Options::showSensorFOV()); KStarsData::Instance()->addTransientFOV(sensorFOV); 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::setDefaultAlignFilterWheel(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); }); PAHFlipVectorC->setChecked(Options::pAHFlipCorrectionVector()); PAHFlipVectorC2->setChecked(Options::pAHFlipCorrectionVector()); connect(PAHFlipVectorC2, &QCheckBox::toggled, [&] { PAHFlipVectorC->setChecked(PAHFlipVectorC2->isChecked());}); connect(PAHFlipVectorC, &QCheckBox::toggled, [&]() { Options::setPAHFlipCorrectionVector(PAHFlipVectorC->isChecked()); PAHFlipVectorC2->blockSignals(true); PAHFlipVectorC2->setChecked(PAHFlipVectorC->isChecked()); PAHFlipVectorC2->blockSignals(false); syncCorrectionVector(); }); 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(CAPTURE_RETRY_DELAY); 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()) { appendLogText(i18n("Capturing still running, Retrying in %1 seconds...", m_CaptureTimer.interval() / 500)); 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 opsASTAP = new OpsASTAP(this); page = dialog->addPage(opsASTAP, i18n("ASTAP")); page->setIcon(QIcon(":/icons/astap.ico")); 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(); rememberMeridianFlip = Options::executeMeridianFlip(); solverBackendGroup->setId(astapSolverR, SOLVER_ASTAP); solverBackendGroup->setId(astrometrySolverR, SOLVER_ASTROMETRYNET); // JM 2019-11-10: solver type was 3 in previous version (online, offline, remote) // But they are now two choices (ASTAP and ASTROMETERY.NET) so we need to accommodate that. if (Options::solverBackend() > SOLVER_ASTROMETRYNET) { Options::setSolverBackend(SOLVER_ASTROMETRYNET); } solverBackendGroup->button(Options::solverBackend())->setChecked(true); connect(solverBackendGroup, static_cast(&QButtonGroup::buttonClicked), this, &Align::setSolverBackend); astrometryTypeCombo->addItem(i18n("Online")); #ifndef Q_OS_WIN astrometryTypeCombo->addItem(i18n("Offline")); #endif astrometryTypeCombo->addItem(i18n("Remote")); astrometryTypeCombo->setCurrentIndex(Options::astrometrySolverType()); connect(astrometryTypeCombo, static_cast(&QComboBox::activated), this, &Ekos::Align::setAstrometrySolverType); setSolverBackend(solverBackendGroup->checkedId()); // Which telescope info to use for FOV calculations FOVScopeCombo->setCurrentIndex(Options::solverScopeType()); connect(FOVScopeCombo, static_cast(&QComboBox::currentIndexChanged), this, &Ekos::Align::updateTelescopeType); 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->horizontalHeader()->setSectionResizeMode(QHeaderView::ResizeToContents); 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()); 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 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(); std::sort(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; 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() { Q_ASSERT_X(parser, __FUNCTION__, "Astrometry parser is not valid."); 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(); int currentRow = solutionTable->rowCount() - 1; solutionTable->setCellWidget(currentRow, 3, new QWidget()); QTableWidgetItem *statusReport = new QTableWidgetItem(); statusReport->setIcon(QIcon(":/icons/timedout.svg")); statusReport->setFlags(Qt::ItemIsSelectable); solutionTable->setItem(currentRow, 3, statusReport); captureAndSolve(); } // TODO must also account for loadAndSlew. Retain file name } void Align::setSolverBackend(int type) { if (sender() == nullptr && type >= 0 && type <= 1) { solverBackendGroup->button(type)->setChecked(true); } // Astrometry solver if (type == SOLVER_ASTROMETRYNET) { astrometryTypeCombo->setEnabled(true); setAstrometrySolverType(Options::astrometrySolverType()); } // ASTAP solver else { if (!QFile::exists(Options::aSTAPExecutable())) { // Set to Astrometry for now. type = SOLVER_ASTROMETRYNET; astrometryTypeCombo->setEnabled(true); setAstrometrySolverType(Options::astrometrySolverType()); KSMessageBox::Instance()->error( i18n("No valid ASTAP installation found. Install ASTAP and select the path to ASTAP executable in options.")); } else { if (astapParser.get() != nullptr) parser = astapParser.get(); else { astapParser.reset(new Ekos::ASTAPAstrometryParser()); parser = astapParser.get(); } 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(); astrometryTypeCombo->setEnabled(false); } } Options::setSolverBackend(type); generateArgs(); } void Align::setAstrometrySolverType(int type) { if (sender() == nullptr && type >= 0 && type <= 2) { astrometryTypeCombo->setCurrentIndex(type); } // For Windows, we only have two items in the combo box (Online & Remote) // When Remote is clicked, type = 1 is sent which is SOLVER_OFFLINE // We need to change that to SOLVER_REMOTE. #ifdef Q_OS_WIN if (type == SOLVER_OFFLINE) type = SOLVER_REMOTE; #endif syncSettings(); Options::setAstrometrySolverType(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 (solverBackendGroup->checkedId() == SOLVER_REMOTE && remoteParser.get() != nullptr) (dynamic_cast(remoteParser.get()))->setCCD(currentCCD->getDeviceName()); syncCCDInfo(); 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::switchUpdated, this, &Ekos::Align::processSwitch, 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 && currentTelescope->getDeviceName() == device->getDeviceName()) { currentTelescope = nullptr; m_isRateSynced = false; } else if (currentDome && currentDome->getDeviceName() == device->getDeviceName()) { currentDome = nullptr; } else if (currentRotator && 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; CCDCaptureCombo->setCurrentIndex(-1); } else CCDCaptureCombo->setCurrentIndex(0); 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; FilterDevicesCombo->setCurrentIndex(-1); } else FilterDevicesCombo->setCurrentIndex(0); 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); 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; } QList Align::cameraInfo() { QList result; result << ccd_width << ccd_height << ccd_hor_pixel << ccd_ver_pixel; return result; } QList Align::telescopeInfo() { QList result; result << focal_length << aperture; 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; // Put FOV upper limit as 180 degrees if (fov_x < 1 || fov_x > 60 * 180 || fov_y < 1 || fov_y > 60 * 180) { appendLogText( i18n("Warning! The calculated field of view is out of bounds. Ensure the telescope focal length and camera pixel size are correct.")); return; } double calculated_fov_x = fov_x; double calculated_fov_y = fov_y; QString calculatedFOV = (QString("%1' x %2'").arg(fov_x, 0, 'f', 1).arg(fov_y, 0, 'f', 1)); // 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)); fov_x = calculated_fov_x; fov_y = calculated_fov_y; m_EffectiveFOVPending = true; } else { m_EffectiveFOVPending = false; FOVOut->setToolTip(i18n("

Effective field of view size in arcminutes.

")); } 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, 'f', 1).arg(fov_y, 0, 'f', 1)); 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, uint8_t solverType) { QStringList solver_args; // -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 if (solverType == SOLVER_ASTROMETRYNET) { // 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()); // JM 2020-05-23 This should ONLY apply to offline astrometry if(Options::useSextractor() && Options::astrometrySolverType() == SOLVER_OFFLINE) { //Sextractor needs all these parameters in order to solve an xylist of stars if (optionsMap.contains("image_width")) solver_args << "--width" << QString::number(optionsMap.value("image_width").toInt()); if (optionsMap.contains("image_height")) solver_args << "--height" << QString::number(optionsMap.value("image_height").toInt()); solver_args << "--x-column" << "X_IMAGE"; solver_args << "--y-column" << "Y_IMAGE"; solver_args << "--sort-column" << "MAG_AUTO"; solver_args << "--sort-ascending"; //Note This set of items is NOT NEEDED for Sextractor, it is needed to avoid python usage //This may need to be changed later, but since the goal for using sextractor is to avoid python, this is placed here. solver_args << "--no-remove-lines"; solver_args << "--uniformize" << "0"; } // 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(); } else { // Radius if (optionsMap.contains("radius")) solver_args << "-r" << QString::number(optionsMap.value("radius").toDouble()); // downsample if (optionsMap.contains("downsample")) solver_args << "-z" << QString::number(optionsMap.value("downsample", 0).toInt()); // Speed if (optionsMap.contains("speed")) solver_args << "-speed" << optionsMap.value("speed").toString(); if (optionsMap.contains("update")) solver_args << "-update"; } 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, QString &fov_low, QString &fov_high, double tolerance) { // This sets the percentage we search outside the lower and upper boundary limits // by default, we stretch the limits by 5% (tolerance = 0.05) double lower_boundary = 1.0 - tolerance; double upper_boundary = 1.0 + tolerance; // let's stretch the boundaries by 5% // fov_lower = ((fov_h < fov_v) ? (fov_h * lower_boundary) : (fov_v * lower_boundary)); // fov_upper = ((fov_h > fov_v) ? (fov_h * upper_boundary) : (fov_v * upper_boundary)); // JM 2019-10-20: The bounds consider image width only, not height. double fov_lower = fov_h * lower_boundary; double fov_upper = fov_h * upper_boundary; //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() { QVariantMap optionsMap; if (solverBackendGroup->checkedId() == SOLVER_ASTROMETRYNET) { // -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 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(); } //Options needed for Sextractor int bin = Options::solverBinningIndex() + 1; optionsMap["image_width"] = ccd_width / bin; optionsMap["image_height"] = ccd_height / bin; 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; } // If effective FOV is pending, let's set a wider tolerance range generateFOVBounds(fov_w, fov_low, fov_high, m_EffectiveFOVPending ? 0.3 : 0.05); 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(); } // ASTAP else { if (Options::aSTAPSearchRadius()) optionsMap["radius"] = Options::aSTAPSearchRadiusValue(); if (Options::aSTAPDownSample() && Options::aSTAPDownSampleValue() > 0) optionsMap["downsample"] = Options::aSTAPDownSampleValue(); optionsMap["speed"] = Options::aSTAPLargeSearchWindow() ? "slow" : "auto"; if (Options::aSTAPUpdateFITS()) optionsMap["update"] = true; } QStringList solverArgs = generateOptions(optionsMap, solverBackendGroup->checkedId()); QString options = solverArgs.join(" "); solverOptions->setText(options); solverOptions->setToolTip(options); } bool Align::captureAndSolve() { m_AlignTimer.stop(); m_CaptureTimer.stop(); #ifdef Q_OS_OSX if(solverBackendGroup->checkedId() == SOLVER_OFFLINE) { if(!Options::useSextractor()) { 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) + { + appendLogText(i18n("Error: No camera detected.")); return false; + } if (currentCCD->isConnected() == false) { - appendLogText(i18n("Error: lost connection to CCD.")); + appendLogText(i18n("Error: lost connection to camera.")); 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; // Disabling the autofocus policy for align. filterManager->setFilterPosition( targetPosition, FilterManager::NO_AUTOFOCUS_POLICY); state = ALIGN_PROGRESS; return true; } } if (currentCCD->getDriverInfo()->getClientManager()->getBLOBMode(currentCCD->getDeviceName().toLatin1().constData(), "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().toLatin1().constData(), "CCD1"); currentCCD->getDriverInfo()->getClientManager()->setBLOBMode(B_ONLY, currentCCD->getDeviceName().toLatin1().constData(), "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 %1 seconds...", CAPTURE_RETRY_DELAY / 1000)); m_CaptureTimer.start(CAPTURE_RETRY_DELAY); return false; } if (targetChip->isCapturing()) { appendLogText(i18n("Cannot capture while CCD exposure is in progress. Retrying in %1 seconds...", CAPTURE_RETRY_DELAY / 1000)); m_CaptureTimer.start(CAPTURE_RETRY_DELAY); 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 (solverBackendGroup->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(); } 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); solverFOV->setProperty("visible", true); // 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 (solverBackendGroup->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 (solverBackendGroup->checkedId() == SOLVER_ASTROMETRYNET && astrometryTypeCombo->currentIndex() == 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; } } solverFOV->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(); //Options needed for Sextractor int bin = Options::solverBinningIndex() + 1; optionsMap["image_width"] = ccd_width / bin; optionsMap["image_height"] = ccd_height / bin; solverArgs = generateOptions(optionsMap, solverBackendGroup->checkedId()); } 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::setSolverAccuracyThreshold(accuracySpin->value()); Options::setAlignDarkFrame(alignDarkFrameCheck->isChecked()); Options::setSolverGotoOption(currentGotoMode); 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 (solverBackendGroup->checkedId() == SOLVER_ASTROMETRYNET && astrometryTypeCombo->currentIndex() == 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, 'f', 5), QString::number(dec, 'f', 5), QString::number(orientation, 'f', 5), QString::number(pixscale, 'f', 5))); if ( (fov_x == 0 || m_EffectiveFOVPending) && pixscale > 0) { double newFOVW = ccd_width * pixscale / binx / 60.0; double newFOVH = ccd_height * pixscale / biny / 60.0; saveNewEffectiveFOV(newFOVW, newFOVH); m_EffectiveFOVPending = false; } alignCoord.setRA0(ra / 15.0); alignCoord.setDec0(dec); RotOut->setText(QString::number(orientation, 'f', 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().deltaAngle(targetCoord.ra())).Degrees() * 3600; double deDiff = (alignCoord.dec().deltaAngle(targetCoord.dec())).Degrees() * 3600; dms RADiff(fabs(raDiff) / 3600.0), DEDiff(deDiff / 3600.0); QString dRAText = QString("%1%2").arg((raDiff > 0 ? "+" : "-"), RADiff.toHMSString()); QString dDEText = DEDiff.toDMSString(true); pixScaleOut->setText(QString::number(pixscale, 'f', 2)); targetDiff = sqrt(raDiff * raDiff + deDiff * deDiff); errOut->setText(QString("%1 arcsec. RA:%2 DE:%3").arg( QString::number(targetDiff, 'f', 0), QString::number(raDiff, 'f', 0), QString::number(deDiff, 'f', 0))); if (targetDiff <= static_cast(accuracySpin->value())) errOut->setStyleSheet("color:green"); else if (targetDiff < 1.5 * accuracySpin->value()) errOut->setStyleSheet("color:yellow"); else errOut->setStyleSheet("color:red"); double solverPA = orientation; // TODO 2019-11-06 JM: KStars needs to support "upside-down" displays since this is a hack. // Because astrometry reads image upside-down (bottom to top), the orientation is rotated 180 degrees when compared to PA // PA = Orientation + 180 double solverFlippedPA = orientation + 180; // Limit PA to -180 to +180 if (solverFlippedPA > 180) solverFlippedPA -= 360; if (solverFlippedPA < -180) solverFlippedPA += 360; solverFOV->setCenter(alignCoord); solverFOV->setPA(solverFlippedPA); solverFOV->setImageDisplay(Options::astrometrySolverOverlay()); // Sensor FOV as well sensorFOV->setPA(solverFlippedPA); 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->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); //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 = range360(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 = range360((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", dRAText}, {"dDE", dDEText}, {"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; solverFOV->setProperty("visible", true); 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; loadSlewState = IPS_IDLE; solverIterations = 0; m_CaptureErrorCounter = 0; m_CaptureTimeoutCounter = 0; m_SlewErrorCounter = 0; state = ALIGN_FAILED; emit newStatus(state); solverFOV->setProperty("visible", false); 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() { m_CaptureTimer.stop(); 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; loadSlewState = IPS_IDLE; solverIterations = 0; m_CaptureErrorCounter = 0; m_CaptureTimeoutCounter = 0; m_SlewErrorCounter = 0; m_AlignTimer.stop(); 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", 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 handleMountStatus(); } } else if ((!strcmp(svp->name, "TELESCOPE_MOTION_NS") || !strcmp(svp->name, "TELESCOPE_MOTION_WE"))) switch (svp->s) { case IPS_BUSY: // react upon mount motion handleMountMotion(); m_wasSlewStarted = true; break; default: qCDebug(KSTARS_EKOS_ALIGN) << "Mount motion finished."; handleMountStatus(); break; } } 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; } 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 (m_wasSlewStarted == false) { // If mount has not started slewing yet, then skip //qCDebug(KSTARS_EKOS_ALIGN) << "Mount slew planned, but not started slewing yet..."; break; } //qCDebug(KSTARS_EKOS_ALIGN) << "Mount slew completed."; 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...")); m_CaptureTimer.start(delaySpin->value()); 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...")); m_CaptureTimer.start(delaySpin->value()); 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) << "Mount slew running."; m_wasSlewStarted = true; handleMountMotion(); } 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...")); m_CaptureTimer.start(delaySpin->value()); } // 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...")); m_CaptureTimer.start(delaySpin->value()); } // 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::handleMountMotion() { if (state == ALIGN_PROGRESS) { if (pahStage == PAH_IDLE) { // whoops, mount slews during alignment appendLogText(i18n("Slew detected, aborting solving...")); abort(); // reset the state to busy so that solving restarts after slewing finishes loadSlewState = IPS_BUSY; // if mount model is running, retry the current alignment point if (mountModelRunning) { appendLogText(i18n("Restarting alignment point %1", currentAlignmentPoint + 1)); if (currentAlignmentPoint > 0) currentAlignmentPoint--; } } state = ALIGN_SLEWING; } } void Align::handleMountStatus() { 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::executeGOTO() { if (loadSlewState == IPS_BUSY) { 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 (Ekos::Manager::Instance()->getCurrentJobName().isEmpty()) { KSNotification::event(QLatin1String("EkosSchedulerTelescopeSynced"), i18n("Ekos job (%1) - Telescope synced", Ekos::Manager::Instance()->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...")); m_CaptureTimer.start(delaySpin->value()); 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...")); m_CaptureTimer.start(delaySpin->value()); 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()); 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(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) { #ifdef Q_OS_OSX if(solverBackendGroup->checkedId() == SOLVER_OFFLINE) { if(!Options::useSextractor()) { 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); QString newFileURL = QDir::tempPath() + "/" + fileInfo.fileName().remove(" "); QFile::copy(fileURL, newFileURL); QFileInfo newFileInfo(newFileURL); dirPath = fileInfo.absolutePath(); differentialSlewingActivated = false; loadSlewState = IPS_BUSY; stopPAHProcess(); slewR->setChecked(true); currentGotoMode = GOTO_SLEW; solveB->setEnabled(false); stopB->setEnabled(true); pi->startAnimation(); startSolving(newFileURL, 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); } void Align::addFilter(ISD::GDInterface *newFilter) { for (auto filter : Filters) { if (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)); int filterWheelIndex = 1; if (Options::defaultAlignFilterWheel().isEmpty() == false) filterWheelIndex = FilterDevicesCombo->findText(Options::defaultAlignFilterWheel()); if (filterWheelIndex < 1) filterWheelIndex = 1; checkFilter(filterWheelIndex); FilterDevicesCombo->setCurrentIndex(filterWheelIndex); } 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) { QVariantMap optionsMap; // For ASTAP, we just default settings if (solverBackendGroup->checkedId() == SOLVER_ASTAP) { if (Options::aSTAPSearchRadius()) optionsMap["radius"] = Options::aSTAPSearchRadiusValue(); if (Options::aSTAPDownSample() && Options::aSTAPDownSampleValue() > 0) optionsMap["downsample"] = Options::aSTAPDownSampleValue(); optionsMap["speed"] = Options::aSTAPLargeSearchWindow() ? "slow" : "auto"; if (Options::aSTAPUpdateFITS()) optionsMap["update"] = true; return generateOptions(optionsMap, solverBackendGroup->checkedId()); } 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; 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, solverBackendGroup->checkedId()); status = 0; // 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, SOLVER_ASTROMETRYNET); } //Needed for Sextractor, let us figure out the image size and regenerate options if(Options::useSextractor()) { optionsMap["image_width"] = fits_ccd_width; optionsMap["image_height"] = fits_ccd_height; solver_args = generateOptions(optionsMap, SOLVER_ASTROMETRYNET); } 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 && 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."); } m_CaptureTimer.start(Options::settlingTime()); 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); 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(); rememberMeridianFlip = Options::executeMeridianFlip(); Options::setAutoWCS(false); Options::setAstrometrySolverWCS(true); Options::setExecuteMeridianFlip(false); 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 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(); RACenterPoint.setX(RACircle.x()); RACenterPoint.setY(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()); correctionVector.setP1(Options::pAHFlipCorrectionVector() ? RACenterPoint : celestialPolePoint); correctionVector.setP2(Options::pAHFlipCorrectionVector() ? celestialPolePoint : RACenterPoint); 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::syncCorrectionVector() { correctionVector.setP1(Options::pAHFlipCorrectionVector() ? RACenterPoint : celestialPolePoint); correctionVector.setP2(Options::pAHFlipCorrectionVector() ? celestialPolePoint : RACenterPoint); emit newCorrectionVector(correctionVector); alignView->setCorrectionParams(correctionVector); } 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); Options::setExecuteMeridianFlip(rememberMeridianFlip); 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...")); m_CaptureTimer.start(delaySpin->value()); } 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); Options::setExecuteMeridianFlip(rememberMeridianFlip); stopPAHProcess(); } void Align::processPAHStage(double orientation, double ra, double dec, double pixscale) { //QString newWCSFile = QDir::tempPath() + QString("/fitswcs%1").arg(QUuid::createUuid().toString().remove(QRegularExpression("[-{}]"))); FITSData *imageData = alignView->getImageData(); 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()); // J2000 to JNow solution->skyCenter.apparentCoord(J2000, KStarsData::Instance()->ut().djd()); //solution->ts = KStarsData::Instance()->ut(); solution->ts = imageData->getDateTime(); 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->injectWCS(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()); // J2000 to JNow solution->skyCenter.apparentCoord(J2000, KStarsData::Instance()->ut().djd()); //solution->ts = KStarsData::Instance()->ut(); solution->ts = imageData->getDateTime(); 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->injectWCS(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()); // J2000 to JNow solution->skyCenter.apparentCoord(J2000, KStarsData::Instance()->ut().djd()); //solution->ts = KStarsData::Instance()->ut(); solution->ts = imageData->getDateTime(); 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->injectWCS(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; } // 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)) { // JM 2019-11-15: This creates more problems at times, better leave it off #if 0 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 #endif 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; // Because we are measuing the coordinates in the THIRD frame // The JNow RA/DE coordinates of the first two frames _already_ rotated by some amount since time passed. // So if we try to measure their cartesian coordinates now in the 3rd frame, they would be the coordinates // of the ROTATED coordinates, and not the original coordinates. Therefore, we subtract the time from RA // to compensate for this which is equivelent to de-rotating the points. double hoursSinceFirstFrame = (pahImageInfos[2]->ts.secsTo(pahImageInfos[0]->ts)) / 3600.0; pahImageInfos[0]->skyCenter.setRA(pahImageInfos[0]->skyCenter.ra().Hours() + hoursSinceFirstFrame); double hoursSinceSecondFrame = (pahImageInfos[2]->ts.secsTo(pahImageInfos[1]->ts)) / 3600.0; pahImageInfos[1]->skyCenter.setRA(pahImageInfos[1]->skyCenter.ra().Hours() + hoursSinceSecondFrame); // Now reset ra0,de0 to JNow before calling wcsToPixel since that function checks ra0,dec0 for (int i = 0; i < 3; i++) { pahImageInfos[i]->skyCenter.setRA0(pahImageInfos[i]->skyCenter.ra()); pahImageInfos[i]->skyCenter.setDec0(pahImageInfos[i]->skyCenter.dec()); } // Now find pixel locations for all recorded center coordinates in the 3rd frame reference if (!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)) { appendLogText(i18n("WCS transformation failed: %1", imageData->getLastError())); stopPAHProcess(); return; } 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(); 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; 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() { solverFOV->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: { const int filterComboIndex = filterManager->getTargetFilterPosition() - 1; if (filterComboIndex >= 0 && filterComboIndex < FilterPosCombo->count()) appendLogText(i18n("Changing filter to %1...", FilterPosCombo->itemText(filterComboIndex))); } 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 solverBackendGroup->checkedId(); } QString Align::getPAHMessage() const { switch (pahStage) { case PAH_IDLE: case PAH_FIND_CP: 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(); } return QString(); } 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", qMax(1, binningCombo->currentIndex() + 1)); settings.insert("solverAction", gotoModeButtonGroup->checkedId()); settings.insert("solverBackend", solverBackendGroup->checkedId()); settings.insert("solverType", astrometryTypeCombo->currentIndex()); 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(); int solverBackend = settings["solverBackend"].toInt(1); int solverType = settings["solverType"].toInt(1); if (solverBackend == SOLVER_ASTROMETRYNET) { Options::setAstrometrySolverType(solverType); astrometryTypeCombo->setCurrentIndex(solverType); solverBackendGroup->button(SOLVER_ASTROMETRYNET)->animateClick(); } else { solverBackendGroup->button(SOLVER_ASTAP)->animateClick(); } 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/focus/focus.cpp b/kstars/ekos/focus/focus.cpp index 182c0a182..2838056cf 100644 --- a/kstars/ekos/focus/focus.cpp +++ b/kstars/ekos/focus/focus.cpp @@ -1,3807 +1,3807 @@ /* 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 "focusalgorithms.h" #include "polynomialfit.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) { for (auto &oneFilter : Filters) { if (oneFilter->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)); int filterWheelIndex = 1; if (Options::defaultFocusFilterWheel().isEmpty() == false) filterWheelIndex = FilterDevicesCombo->findText(Options::defaultFocusFilterWheel()); if (filterWheelIndex < 1) filterWheelIndex = 1; checkFilter(filterWheelIndex); FilterDevicesCombo->setCurrentIndex(filterWheelIndex); } 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); } hasDeviation = currentFocuser->hasDeviation(); 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(); // In case we have a timer-based focuser and using the linear focus algorithm, // we pretend it is an absolute focuser with initial point set at 50,000. // These variables don't have in impact on timer-based focusers if the algorithm // is not the linear focus algorithm. if (!canAbsMove && !canRelMove && canTimerMove) { currentPosition = 50000; absMotionMax = 100000; absMotionMin = 0; } 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); } getCurrentFocuserTemperature(); 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::getCurrentFocuserTemperature() { INumberVectorProperty *focuserTemperature = currentFocuser->getBaseDevice()->getNumber("FOCUS_TEMPERATURE"); if (focuserTemperature && focuserTemperature->s != IPS_ALERT) { currentTemperature = focuserTemperature->np[0].value; qCDebug(KSTARS_EKOS_FOCUS) << QString("Setting current focuser temperature: %1").arg(currentTemperature, 0, 'f', 2); } else { currentTemperature = INVALID_VALUE; qCDebug(KSTARS_EKOS_FOCUS) << QString("Focuser temperature is not available"); } } void Focus::start() { if (currentCCD == nullptr) { appendLogText(i18n("No CCD connected.")); return; } lastFocusDirection = FOCUS_NONE; polySolutionFound = 0; waitStarSelectTimer.stop(); starsHFR.clear(); lastHFR = 0; // Forget last focus temperature, reset temperature delta lastFocusTemperature = INVALID_VALUE; emit newFocusTemperatureDelta(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(); absIterations = 0; absMotionMax = 100000; absMotionMin = 0; 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; focuserAdditionalMovement = 0; 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() << " Gaussian Sigma: " << gaussianSigmaSpin->value() << " Gaussian Kernel size: " << gaussianKernelSizeSpin->value() << " Multi row average: " << multiRowAverageSpin->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")); // Used for all the focuser types. if (focusAlgorithm == FOCUS_LINEAR) { const int position = static_cast(currentPosition); FocusAlgorithmInterface::FocusParams params( maxTravelIN->value(), stepIN->value(), position, absMotionMin, absMotionMax, MAXIMUM_ABS_ITERATIONS, toleranceIN->value() / 100.0, filter()); linearFocuser.reset(MakeLinearFocuser(params)); linearRequestedPosition = linearFocuser->initialPosition(); const int newPosition = adjustLinearPosition(position, linearRequestedPosition); if (newPosition != position) { if (!changeFocus(newPosition - position)) { abort(); setAutoFocusResult(false); } // Avoid the capture below. return; } } capture(); } int Focus::adjustLinearPosition(int position, int newPosition) { if (newPosition > position) { constexpr int extraMotionSteps = 5; int adjustment = extraMotionSteps * stepIN->value(); if (newPosition + adjustment > absMotionMax) adjustment = static_cast(absMotionMax) - newPosition; focuserAdditionalMovement = adjustment; qCDebug(KSTARS_EKOS_FOCUS) << QString("LinearFocuser: extending outward movement by %1").arg(adjustment); return newPosition + adjustment; } return newPosition; } 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) << "Stopping Focus"; captureTimeout.stop(); ISD::CCDChip *targetChip = currentCCD->getChip(ISD::CCDChip::PRIMARY_CCD); inAutoFocus = false; focuserAdditionalMovement = 0; 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; captureFailureCounter = 0; minimumRequiredHFR = -1; noStarCount = 0; HFRFrames.clear(); //maxHFR=1; disconnect(currentCCD, &ISD::CCD::BLOBUpdated, this, &Ekos::Focus::newFITS); disconnect(currentCCD, &ISD::CCD::captureFailed, this, &Ekos::Focus::processCaptureFailure); 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.")); + appendLogText(i18n("Error: No camera detected.")); + return; + } + + if (currentCCD->isConnected() == false) + { + appendLogText(i18n("Error: Lost connection to camera.")); 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); connect(currentCCD, &ISD::CCD::captureFailed, this, &Ekos::Focus::processCaptureFailure); 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 (ms == -1) ms = stepIN->value(); return changeFocus(-ms); } bool Focus::focusOut(int ms) { if (ms == -1) ms = stepIN->value(); return changeFocus(ms); } // If amount > 0 we focus out, otherwise in. bool Focus::changeFocus(int amount) { if (currentFocuser == nullptr) return false; // This needs to be re-thought. Just returning does not set the timer // and the algorithm ends in limbo. // Ignore zero // if (amount == 0) // return true; if (currentFocuser->isConnected() == false) { appendLogText(i18n("Error: Lost connection to Focuser.")); return false; } const int absAmount = abs(amount); const bool focusingOut = amount > 0; const QString dirStr = focusingOut ? i18n("outward") : i18n("inward"); lastFocusDirection = focusingOut ? FOCUS_OUT : FOCUS_IN; qCDebug(KSTARS_EKOS_FOCUS) << "Focus " << dirStr << " (" << absAmount << ")"; if (focusingOut) currentFocuser->focusOut(); else currentFocuser->focusIn(); if (canAbsMove) { currentFocuser->moveAbs(currentPosition + amount); appendLogText(i18n("Focusing %2 by %1 steps...", absAmount, dirStr)); } else if (canRelMove) { currentFocuser->moveRel(absAmount); appendLogText(i18np("Focusing %2 by %1 step...", "Focusing %2 by %1 steps...", absAmount, dirStr)); } else { currentFocuser->moveByTimer(absAmount); appendLogText(i18n("Focusing %2 by %1 ms...", absAmount, dirStr)); } 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); disconnect(currentCCD, &ISD::CCD::captureFailed, this, &Ekos::Focus::processCaptureFailure); 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(); resetButtons(); }); 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(); resetButtons(); } double Focus::analyzeSources(FITSData *image_data) { // 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()) { focusView->setTrackingBoxEnabled(false); 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(); // Get the average HFR of the whole frame return image_data->getHFR(HFR_AVERAGE); } else { // If star is already selected then use whatever algorithm currently selected. if (starSelected) { focusView->findStars(focusDetection); return 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); // Get maximum HFR in the frame return image_data->getHFR(HFR_MAX); } } } bool Focus::appendHFR(double newHFR) { // Add new HFR to existing values, even if invalid HFRFrames.append(newHFR); // Prepare a work vector with valid HFR values QVector samples(HFRFrames); samples.erase(std::remove_if(samples.begin(), samples.end(), [](const double HFR) { return HFR == -1; }), samples.end()); // Perform simple sigma clipping if more than a few samples if (samples.count() > 3) { // Sort all HFRs and extract the median std::sort(samples.begin(), samples.end()); const auto median = ((samples.size() % 2) ? samples[samples.size() / 2] : (static_cast(samples[samples.size() / 2 - 1]) + samples[samples.size() / 2]) * .5); // Extract the mean const auto mean = std::accumulate(samples.begin(), samples.end(), .0) / samples.size(); // Extract the variance double variance = 0; foreach (auto val, samples) variance += (val - mean) * (val - mean); // Deduce the standard deviation const double stddev = sqrt(variance / samples.size()); // Reject those 2 sigma away from median const double sigmaHigh = median + stddev * 2; const double sigmaLow = median - stddev * 2; // FIXME: why is the first value not considered? // FIXME: what if there are less than 3 samples after clipping? QMutableVectorIterator i(samples); while (i.hasNext()) { auto val = i.next(); if (val > sigmaHigh || val < sigmaLow) i.remove(); } } // Consolidate the average HFR currentHFR = samples.isEmpty() ? -1 : std::accumulate(samples.begin(), samples.end(), .0) / samples.size(); // Return whether we need more frame based on user requirement return HFRFrames.count() < focusFramesSpin->value(); } void Focus::setCaptureComplete() { DarkLibrary::Instance()->disconnect(this); // 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 (captureInProgress && inFocusLoop == false && inAutoFocus == 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) { currentHFR = analyzeSources(image_data); focusView->updateFrame(); } // 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()); // Take the new HFR into account, eventually continue to stack samples if (appendHFR(currentHFR)) { capture(); return; } else HFRFrames.clear(); // 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); graphPolynomialFunction(); 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 inAutoFocus is true without canAbsMove and without canRelMove, canTimerMove must be true. // We'd only want to execute this if the focus linear algorithm is not being used, as that // algorithm simulates a position-based system even for timer-based focusers. if (inFocusLoop || (inAutoFocus && canAbsMove == false && canRelMove == false && focusAlgorithm != FOCUS_LINEAR)) { 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; } // Get target chip ISD::CCDChip *targetChip = currentCCD->getChip(ISD::CCDChip::PRIMARY_CCD); // Get target chip binning int subBinX = 1, subBinY = 1; if (!targetChip->getBinning(&subBinX, &subBinY)) qCDebug(KSTARS_EKOS_FOCUS) << "Warning: target chip is reporting no binning property, using 1x1."; // 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(); defaultScale = static_cast(filterCombo->currentIndex()); // 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(); return; } // 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(); 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/" + 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_" + 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 if (focusAlgorithm == FOCUS_LINEAR) autoFocusLinear(); else if (canAbsMove || canRelMove) // Position-based algorithms autoFocusAbs(); else // Time open-looped algorithms autoFocusRel(); } void Focus::clearDataPoints() { maxHFR = 1; hfr_position.clear(); hfr_value.clear(); polynomialGraph->data()->clear(); focusPoint->data()->clear(); polynomialGraphIsShown = false; HFRPlot->clearItems(); polynomialFit.reset(); drawHFRPlot(); } void Focus::drawHFRIndeces() { // Put the sample number inside the plot point's circle. for (int i = 0; i < hfr_position.size(); ++i) { QCPItemText *textLabel = new QCPItemText(HFRPlot); textLabel->setPositionAlignment(Qt::AlignCenter | Qt::AlignHCenter); textLabel->position->setType(QCPItemPosition::ptPlotCoords); textLabel->position->setCoords(hfr_position[i], hfr_value[i]); textLabel->setText(QString::number(i + 1)); textLabel->setFont(QFont(font().family(), 12)); textLabel->setPen(Qt::NoPen); textLabel->setColor(Qt::red); } } void Focus::drawHFRPlot() { // DrawHFRPlot is the base on which other things are built upon. // Clear any previous annotations. HFRPlot->clearItems(); v_graph->setData(hfr_position, hfr_value); drawHFRIndeces(); double minHFRVal = currentHFR / 2.5; if (hfr_value.size() > 0) minHFRVal = std::max(0, static_cast(0.9 * *std::min_element(hfr_value.begin(), hfr_value.end()))); // True for the position-based algorithms and those that simulate position. if (inFocusLoop == false && (canAbsMove || canRelMove || (focusAlgorithm == FOCUS_LINEAR))) { const double minPosition = hfr_position.empty() ? 0 : *std::min_element(hfr_position.constBegin(), hfr_position.constEnd()); const double maxPosition = hfr_position.empty() ? 1e6 : *std::max_element(hfr_position.constBegin(), hfr_position.constEnd()); HFRPlot->xAxis->setRange(minPosition - pulseDuration, maxPosition + pulseDuration); HFRPlot->yAxis->setRange(minHFRVal, maxHFR); } else { //HFRPlot->xAxis->setLabel(i18n("Iteration")); HFRPlot->xAxis->setRange(1, hfr_value.count() + 1); HFRPlot->yAxis->setRange(currentHFR / 2.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); } bool Focus::autoFocusChecks() { if (++absIterations > MAXIMUM_ABS_ITERATIONS) { appendLogText(i18n("Autofocus failed to reach proper focus. Try increasing tolerance value.")); abort(); setAutoFocusResult(false); return false; } // 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 false; } 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 false; } } else noStarCount = 0; return true; } void Focus::autoFocusLinear() { if (!autoFocusChecks()) return; if (!canAbsMove && !canRelMove && canTimerMove) { const bool kFixPosition = true; if (kFixPosition && (linearRequestedPosition != static_cast(currentPosition))) { qCDebug(KSTARS_EKOS_FOCUS) << "Linear: warning, changing position " << currentPosition << " to " << linearRequestedPosition; currentPosition = linearRequestedPosition; } } hfr_position.append(currentPosition); hfr_value.append(currentHFR); drawHFRPlot(); if (hfr_position.size() > 3) { polynomialFit.reset(new PolynomialFit(2, hfr_position, hfr_value)); double min_position, min_value; const FocusAlgorithmInterface::FocusParams ¶ms = linearFocuser->getParams(); double searchMin = std::max(params.minPositionAllowed, params.startPosition - params.maxTravel); double searchMax = std::min(params.maxPositionAllowed, params.startPosition + params.maxTravel); if (polynomialFit->findMinimum(linearFocuser->getParams().startPosition, searchMin, searchMax, &min_position, &min_value)) { QPen pen; pen.setWidth(1); pen.setColor(QColor(180, 180, 180)); polynomialGraph->setPen(pen); polynomialFit->drawPolynomial(HFRPlot, polynomialGraph); polynomialFit->drawMinimum(HFRPlot, focusPoint, min_position, min_value, font()); } else { // During development of this algorithm, we show the polynomial graph in red if // no minimum was found. That happens when the order-2 polynomial is an inverted U // instead of a U shape (i.e. it has a maximum, but no minimum). QPen pen; pen.setWidth(1); pen.setColor(QColor(254, 0, 0)); polynomialGraph->setPen(pen); polynomialFit->drawPolynomial(HFRPlot, polynomialGraph); polynomialGraph->data()->clear(); focusPoint->data()->clear(); } } linearRequestedPosition = linearFocuser->newMeasurement(currentPosition, currentHFR); const int nextPosition = adjustLinearPosition(static_cast(currentPosition), linearRequestedPosition); if (linearRequestedPosition == -1) { if (linearFocuser->isDone() && linearFocuser->solution() != -1) { appendLogText(i18np("Autofocus complete after %1 iteration.", "Autofocus complete after %1 iterations.", hfr_position.count())); stop(); setAutoFocusResult(true); } else { qCDebug(KSTARS_EKOS_FOCUS) << linearFocuser->doneReason(); appendLogText("Linear autofocus algorithm aborted."); abort(); setAutoFocusResult(false); } return; } else { const int delta = nextPosition - currentPosition; if (!changeFocus(delta)) { abort(); setAutoFocusResult(false); } return; } } 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 (!autoFocusChecks()) return; 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 (!changeFocus(pulseDuration)) { 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); if (focusAlgorithm == FOCUS_POLYNOMIAL) graphPolynomialFunction(); } 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) { polynomialFit.reset(new PolynomialFit(3, hfr_position, hfr_value)); double a = *std::min_element(hfr_position.constBegin(), hfr_position.constEnd()); double b = *std::max_element(hfr_position.constBegin(), hfr_position.constEnd()); double min_position = 0, min_hfr = 0; polyMinimumFound = polynomialFit->findMinimum(minHFRPos, a, b, &min_position, &min_hfr); 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))); polynomialFit->drawPolynomial(HFRPlot, polynomialGraph); polynomialFit->drawMinimum(HFRPlot, focusPoint, min_position, min_hfr, font()); } } 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); if (focusAlgorithm == FOCUS_POLYNOMIAL) graphPolynomialFunction(); 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; // Limit to Maximum permitted delta (Max Single Step Size) double limitedDelta = qMax(-1.0 * maxSingleStepIN->value(), qMin(1.0 * maxSingleStepIN->value(), delta)); if (std::fabs(limitedDelta - delta) > 0) { qCDebug(KSTARS_EKOS_FOCUS) << "Limited delta to maximum permitted single step " << maxSingleStepIN->value(); delta = limitedDelta; } // Now cross your fingers and wait if (!changeFocus(delta)) { abort(); setAutoFocusResult(false); } break; } } void Focus::graphPolynomialFunction() { if (polynomialGraph && polynomialFit) { polynomialGraphIsShown = true; polynomialFit->drawPolynomial(HFRPlot, polynomialGraph); } } 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; changeFocus(-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); if (focusAlgorithm == FOCUS_POLYNOMIAL) graphPolynomialFunction(); break; } else if (currentHFR < lastHFR) { if (currentHFR < minHFR) minHFR = currentHFR; lastHFR = currentHFR; changeFocus(lastFocusDirection == FOCUS_IN ? -pulseDuration : pulseDuration); HFRInc = 0; } else { HFRInc++; lastHFR = currentHFR; HFRInc = 0; pulseDuration *= 0.75; if (!changeFocus(lastFocusDirection == FOCUS_IN ? pulseDuration : -pulseDuration)) { 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::autoFocusProcessPositionChange(IPState state) { if (state == IPS_OK && captureInProgress == false) { // Normally, if we are auto-focusing, after we move the focuser we capture an image. // However, the Linear algorithm, at the start of its passes, requires two // consecutive focuser moves--the first out further than we want, and a second // move back in, so that we eliminate backlash and are always moving in before a capture. if (focuserAdditionalMovement > 0) { int temp = focuserAdditionalMovement; focuserAdditionalMovement = 0; qCDebug(KSTARS_EKOS_FOCUS) << QString("LinearFocuser: un-doing extension. Moving back in by %1").arg(temp); if (!focusIn(temp)) { appendLogText(i18n("Focuser error, check INDI panel.")); abort(); setAutoFocusResult(false); } } else { QTimer::singleShot(FocusSettleTime->value() * 1000, this, &Ekos::Focus::capture); } } else if (state == IPS_ALERT) { appendLogText(i18n("Focuser error, check INDI panel.")); abort(); setAutoFocusResult(false); } } void Focus::processFocusNumber(INumberVectorProperty *nvp) { // Return if it is not our current focuser if (nvp->device != currentFocuser->getDeviceName()) return; // Only process focus properties if (QString(nvp->name).contains("focus", Qt::CaseInsensitive) == false) return; // qCDebug(KSTARS_EKOS_FOCUS) << QString("processFocusNumber %1 state: %2") // .arg(nvp->name).arg(nvp->s); if (!strcmp(nvp->name, "FOCUS_BACKLASH_STEPS")) { focusBacklashSpin->setValue(nvp->np[0].value); return; } if (!strcmp(nvp->name, "FOCUS_TEMPERATURE")) { currentTemperature = nvp->np[0].value; if (lastFocusTemperature != INVALID_VALUE && currentTemperature != INVALID_VALUE) { emit newFocusTemperatureDelta(abs(currentTemperature - lastFocusTemperature)); } else { emit newFocusTemperatureDelta(0); } return; } if (!strcmp(nvp->name, "ABS_FOCUS_POSITION")) { INumber *pos = IUFindNumber(nvp, "FOCUS_ABSOLUTE_POSITION"); if (pos) { currentPosition = pos->value; qCDebug(KSTARS_EKOS_FOCUS) << QString("Abs Focuser position changed to %1").arg(currentPosition); 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) { autoFocusProcessPositionChange(nvp->s); } else if (nvp->s == IPS_ALERT) appendLogText(i18n("Focuser error, check INDI panel.")); return; } if (canAbsMove) return; if (!strcmp(nvp->name, "manualfocusdrive")) { INumber *pos = IUFindNumber(nvp, "manualfocusdrive"); if (pos && nvp->s == IPS_OK) { 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 (canRelMove && inAutoFocus) { autoFocusProcessPositionChange(nvp->s); } else if (nvp->s == IPS_ALERT) appendLogText(i18n("Focuser error, check INDI panel.")); 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); qCDebug(KSTARS_EKOS_FOCUS) << QString("Rel Focuser position changed by %1 to %2") .arg(pos->value).arg(currentPosition); 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) { autoFocusProcessPositionChange(nvp->s); } 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) { // Used by the linear focus algorithm. Ignored if that's not in use for the timer-focuser. INumber *pos = IUFindNumber(nvp, "FOCUS_TIMER_VALUE"); if (pos) { currentPosition += pos->value * (lastFocusDirection == FOCUS_IN ? -1 : 1); qCDebug(KSTARS_EKOS_FOCUS) << QString("Timer Focuser position changed by %1 to %2") .arg(pos->value).arg(currentPosition); } autoFocusProcessPositionChange(nvp->s); } 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", 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; if (status) { // CR add auto focus position, temperature and filter to log in CSV format // this will help with setting up focus offsets and temperature compensation qCInfo(KSTARS_EKOS_FOCUS) << "Autofocus values: position, " << currentPosition << ", temperature, " << currentTemperature << ", filter, " << filter(); lastFocusTemperature = currentTemperature; emit newFocusTemperatureDelta(0); } // 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; changeFocus(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; } } void Focus::removeDevice(ISD::GDInterface *deviceRemoved) { // Check in Focusers for (ISD::GDInterface *focuser : Focusers) { if (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 (ccd->getDeviceName() == deviceRemoved->getDeviceName()) { CCDs.removeAll(dynamic_cast(ccd)); CCDCaptureCombo->removeItem(CCDCaptureCombo->findText(ccd->getDeviceName())); CCDCaptureCombo->removeItem(CCDCaptureCombo->findText(ccd->getDeviceName() + QString(" Guider"))); if (CCDs.empty()) { currentCCD = nullptr; CCDCaptureCombo->setCurrentIndex(-1); } else CCDCaptureCombo->setCurrentIndex(0); checkCCD(); resetButtons(); } } // Check in Filters for (ISD::GDInterface *filter : Filters) { if (filter->getDeviceName() == deviceRemoved->getDeviceName()) { Filters.removeAll(filter); FilterDevicesCombo->removeItem(FilterDevicesCombo->findText(filter->getDeviceName())); if (Filters.empty()) { currentFilter = nullptr; FilterDevicesCombo->setCurrentIndex(-1); } else FilterDevicesCombo->setCurrentIndex(0); 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()); else Options::setFocusExposure(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::processCaptureFailure() { captureFailureCounter++; if (captureFailureCounter >= 3) { captureFailureCounter = 0; appendLogText(i18n("Exposure failure. Aborting...")); abort(); if (inAutoFocus) setAutoFocusResult(false); else if (m_GuidingSuspended) { emit resumeGuiding(); m_GuidingSuspended = false; } return; } appendLogText(i18n("Exposure failure. Restarting exposure...")); ISD::CCDChip *targetChip = currentCCD->getChip(ISD::CCDChip::PRIMARY_CCD); targetChip->abortExposure(); targetChip->capture(exposureIN->value()); } 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 (dsb == gaussianSigmaSpin) Options::setFocusGaussianSigma(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 == maxSingleStepIN) Options::setFocusMaxSingleStep(sb->value()); else if (sb == focusFramesSpin) Options::setFocusFramesCount(sb->value()); else if (sb == gaussianKernelSizeSpin) Options::setFocusGaussianKernelSize(sb->value()); else if (sb == multiRowAverageSpin) Options::setFocusMultiRowAverage(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 /////////////////////////////////////////////////////////////////////////// // Default Exposure exposureIN->setValue(Options::focusExposure()); // Binning activeBin = Options::focusXBin(); binningCombo->setCurrentIndex(activeBin - 1); // Gain gainIN->setValue(Options::focusGain()); /////////////////////////////////////////////////////////////////////////// /// Settings Group /////////////////////////////////////////////////////////////////////////// // 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()); // Single Max Step maxSingleStepIN->setValue(Options::focusMaxSingleStep()); // 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); // Gaussian blur gaussianSigmaSpin->setValue(Options::focusGaussianSigma()); gaussianKernelSizeSpin->setValue(Options::focusGaussianKernelSize()); // Hough algorithm multi row average multiRowAverageSpin->setValue(Options::focusMultiRowAverage()); multiRowAverageSpin->setEnabled(focusDetection == ALGORITHM_BAHTINOV); // Increase focus box size in case of Bahtinov mask focus // Disable auto star in case of Bahtinov mask focus if (focusDetection == ALGORITHM_BAHTINOV) { Options::setFocusAutoStarEnabled(false); focusBoxSize->setMaximum(512); } else { // When not using Bathinov mask, limit box size to 256 and make sure value stays within range. if (Options::focusBoxSize() > 256) { Options::setFocusBoxSize(32); } focusBoxSize->setMaximum(256); } // Box Size focusBoxSize->setValue(Options::focusBoxSize()); // Auto Star? useAutoStar->setChecked(Options::focusAutoStarEnabled()); useAutoStar->setEnabled(focusDetection != ALGORITHM_BAHTINOV); } 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(maxSingleStepIN, &QDoubleSpinBox::editingFinished, this, &Focus::syncSettings); connect(toleranceIN, &QDoubleSpinBox::editingFinished, this, &Focus::syncSettings); connect(thresholdSpin, &QDoubleSpinBox::editingFinished, this, &Focus::syncSettings); connect(gaussianSigmaSpin, &QDoubleSpinBox::editingFinished, this, &Focus::syncSettings); connect(gaussianKernelSizeSpin, static_cast(&QSpinBox::valueChanged), this, &Focus::syncSettings); connect(multiRowAverageSpin, static_cast(&QSpinBox::valueChanged), 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); polynomialGraph = HFRPlot->addGraph(); polynomialGraph->setLineStyle(QCPGraph::lsLine); polynomialGraph->setPen(QPen(QColor(140, 140, 140), 2, Qt::DotLine)); polynomialGraph->setScatterStyle(QCPScatterStyle::ssNone); connect(HFRPlot->xAxis, static_cast(&QCPAxis::rangeChanged), this, [this]() { drawHFRIndeces(); if (polynomialGraphIsShown) { if (focusAlgorithm == FOCUS_POLYNOMIAL) graphPolynomialFunction(); } }); connect(HFRPlot, &QCustomPlot::mouseMove, this, [this](QMouseEvent * event) { double key = HFRPlot->xAxis->pixelToCoord(event->localPos().x()); if (HFRPlot->xAxis->range().contains(key)) { QCPGraph *graph = qobject_cast(HFRPlot->plottableAt(event->pos(), false)); if (graph) { if(graph == v_graph) { int positionKey = v_graph->findBegin(key); double focusPosition = v_graph->dataMainKey(positionKey); double halfFluxRadius = v_graph->dataMainValue(positionKey); QToolTip::showText( event->globalPos(), i18nc("HFR graphics tooltip; %1 is the Focus Position; %2 is the Half Flux Radius;", "" "" "" "
POS: %1
HFR: %2
", QString::number(focusPosition, 'f', 0), QString::number(halfFluxRadius, 'f', 2))); } } } }); focusPoint = HFRPlot->addGraph(); focusPoint->setLineStyle(QCPGraph::lsImpulse); focusPoint->setPen(QPen(QColor(140, 140, 140), 2, Qt::SolidLine)); focusPoint->setScatterStyle(QCPScatterStyle(QCPScatterStyle::ssCircle, Qt::white, Qt::yellow, 10)); v_graph = HFRPlot->addGraph(); v_graph->setLineStyle(QCPGraph::lsNone); v_graph->setScatterStyle(QCPScatterStyle(QCPScatterStyle::ssCircle, Qt::white, Qt::white, 14)); } 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); multiRowAverageSpin->setEnabled(focusDetection == ALGORITHM_BAHTINOV); if (focusDetection == ALGORITHM_BAHTINOV) { // In case of Bahtinov mask uncheck auto select star useAutoStar->setChecked(false); focusBoxSize->setMaximum(512); } else { // When not using Bathinov mask, limit box size to 256 and make sure value stays within range. if (Options::focusBoxSize() > 256) { Options::setFocusBoxSize(32); // Focus box size changed, update control focusBoxSize->setValue(Options::focusBoxSize()); } focusBoxSize->setMaximum(256); } useAutoStar->setEnabled(focusDetection != ALGORITHM_BAHTINOV); }); // 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/indi/indiccd.cpp b/kstars/indi/indiccd.cpp index 6154bba35..339ca0005 100644 --- a/kstars/indi/indiccd.cpp +++ b/kstars/indi/indiccd.cpp @@ -1,2576 +1,2576 @@ /* INDI CCD 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 "indiccd.h" #include "config-kstars.h" #include "indi_debug.h" #include "clientmanager.h" #include "driverinfo.h" #include "guimanager.h" #include "kspaths.h" #include "kstars.h" #include "kstarsdata.h" #include "Options.h" #include "streamwg.h" //#include "ekos/manager.h" #ifdef HAVE_CFITSIO #include "fitsviewer/fitsdata.h" #endif #include #include #include #include #include const QStringList RAWFormats = { "cr2", "cr3", "crw", "nef", "raf", "dng", "arw" }; namespace { void addFITSKeywords(const QString &filename, const QString &filter_used) { #ifdef HAVE_CFITSIO int status = 0; if (filter_used.isEmpty() == false) { QString filt(filter_used); QString key_comment("Filter name"); filt.replace(' ', '_'); fitsfile *fptr = nullptr; // 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); return; } if (fits_movabs_hdu(fptr, 1, IMAGE_HDU, &status)) { fits_report_error(stderr, status); return; } if (fits_update_key_str(fptr, "FILTER", filt.toLatin1().data(), key_comment.toLatin1().data(), &status)) { fits_report_error(stderr, status); return; } fits_flush_file(fptr, &status); fits_close_file(fptr, &status); } #endif } // Internal function to write an image blob to disk. bool WriteImageFileInternal(const QString &filename, char *buffer, const size_t size, bool add_fits_keywords, const QString &filter) { QFile file(filename); if (!file.open(QIODevice::WriteOnly)) { qCCritical(KSTARS_INDI) << "ISD:CCD Error: Unable to open write file: " << filename; return false; } size_t n = 0; QDataStream out(&file); for (size_t nr = 0; nr < size; nr += n) n = out.writeRawData(buffer + nr, size - nr); file.flush(); file.close(); file.setPermissions(QFileDevice::ReadUser | QFileDevice::WriteUser | QFileDevice::ReadGroup | QFileDevice::ReadOther); if (add_fits_keywords) addFITSKeywords(filename, filter); return true; } // Internal function to write a temporary file image blob to disk. bool writeTempImageFile(const QString &format, char * buffer, size_t size, QString *filename) { QTemporaryFile tmpFile(QDir::tempPath() + "/fitsXXXXXX" + format); tmpFile.setAutoRemove(false); if (!tmpFile.open()) { qCCritical(KSTARS_INDI) << "ISD:CCD Error: Unable to open tempfile: " << tmpFile.fileName(); return false; } QDataStream out(&tmpFile); size_t n = 0; for (size_t nr = 0; nr < size; nr += n) n = out.writeRawData(buffer + nr, size - nr); tmpFile.flush(); tmpFile.close(); tmpFile.setPermissions(QFileDevice::ReadUser | QFileDevice::WriteUser | QFileDevice::ReadGroup | QFileDevice::ReadOther); *filename = tmpFile.fileName(); return true; } } namespace ISD { CCDChip::CCDChip(ISD::CCD *ccd, ChipType cType) { baseDevice = ccd->getBaseDevice(); clientManager = ccd->getDriverInfo()->getClientManager(); parentCCD = ccd; type = cType; } FITSView *CCDChip::getImageView(FITSMode imageType) { switch (imageType) { case FITS_NORMAL: return normalImage; case FITS_FOCUS: return focusImage; case FITS_GUIDE: return guideImage; case FITS_CALIBRATE: return calibrationImage; case FITS_ALIGN: return alignImage; } return nullptr; } void CCDChip::setImageView(FITSView *image, FITSMode imageType) { switch (imageType) { case FITS_NORMAL: normalImage = image; break; case FITS_FOCUS: focusImage = image; break; case FITS_GUIDE: guideImage = image; break; case FITS_CALIBRATE: calibrationImage = image; break; case FITS_ALIGN: alignImage = image; break; } if (image) imageData = image->getImageData(); } bool CCDChip::getFrameMinMax(int *minX, int *maxX, int *minY, int *maxY, int *minW, int *maxW, int *minH, int *maxH) { INumberVectorProperty *frameProp = nullptr; switch (type) { case PRIMARY_CCD: frameProp = baseDevice->getNumber("CCD_FRAME"); break; case GUIDE_CCD: frameProp = baseDevice->getNumber("GUIDER_FRAME"); break; } if (frameProp == nullptr) return false; INumber *arg = IUFindNumber(frameProp, "X"); if (arg == nullptr) return false; if (minX) *minX = arg->min; if (maxX) *maxX = arg->max; arg = IUFindNumber(frameProp, "Y"); if (arg == nullptr) return false; if (minY) *minY = arg->min; if (maxY) *maxY = arg->max; arg = IUFindNumber(frameProp, "WIDTH"); if (arg == nullptr) return false; if (minW) *minW = arg->min; if (maxW) *maxW = arg->max; arg = IUFindNumber(frameProp, "HEIGHT"); if (arg == nullptr) return false; if (minH) *minH = arg->min; if (maxH) *maxH = arg->max; return true; } bool CCDChip::setImageInfo(uint16_t width, uint16_t height, double pixelX, double pixelY, uint8_t bitdepth) { INumberVectorProperty *ccdInfoProp = nullptr; switch (type) { case PRIMARY_CCD: ccdInfoProp = baseDevice->getNumber("CCD_INFO"); break; case GUIDE_CCD: ccdInfoProp = baseDevice->getNumber("GUIDER_INFO"); break; } if (ccdInfoProp == nullptr) return false; ccdInfoProp->np[0].value = width; ccdInfoProp->np[1].value = height; ccdInfoProp->np[2].value = std::hypotf(pixelX, pixelY); ccdInfoProp->np[3].value = pixelX; ccdInfoProp->np[4].value = pixelY; ccdInfoProp->np[5].value = bitdepth; clientManager->sendNewNumber(ccdInfoProp); return true; } bool CCDChip::getImageInfo(uint16_t &width, uint16_t &height, double &pixelX, double &pixelY, uint8_t &bitdepth) { INumberVectorProperty *ccdInfoProp = nullptr; switch (type) { case PRIMARY_CCD: ccdInfoProp = baseDevice->getNumber("CCD_INFO"); break; case GUIDE_CCD: ccdInfoProp = baseDevice->getNumber("GUIDER_INFO"); break; } if (ccdInfoProp == nullptr) return false; width = ccdInfoProp->np[0].value; height = ccdInfoProp->np[1].value; pixelX = ccdInfoProp->np[2].value; pixelY = ccdInfoProp->np[3].value; bitdepth = ccdInfoProp->np[5].value; return true; } bool CCDChip::getBayerInfo(uint16_t &offsetX, uint16_t &offsetY, QString &pattern) { ITextVectorProperty * bayerTP = baseDevice->getText("CCD_CFA"); if (!bayerTP) return false; offsetX = QString(bayerTP->tp[0].text).toInt(); offsetY = QString(bayerTP->tp[1].text).toInt(); pattern = QString(bayerTP->tp[2].text); return true; } bool CCDChip::getFrame(int *x, int *y, int *w, int *h) { INumberVectorProperty *frameProp = nullptr; switch (type) { case PRIMARY_CCD: frameProp = baseDevice->getNumber("CCD_FRAME"); break; case GUIDE_CCD: frameProp = baseDevice->getNumber("GUIDER_FRAME"); break; } if (frameProp == nullptr) return false; INumber *arg = IUFindNumber(frameProp, "X"); if (arg == nullptr) return false; *x = arg->value; arg = IUFindNumber(frameProp, "Y"); if (arg == nullptr) return false; *y = arg->value; arg = IUFindNumber(frameProp, "WIDTH"); if (arg == nullptr) return false; *w = arg->value; arg = IUFindNumber(frameProp, "HEIGHT"); if (arg == nullptr) return false; *h = arg->value; return true; } bool CCDChip::resetFrame() { INumberVectorProperty *frameProp = nullptr; switch (type) { case PRIMARY_CCD: frameProp = baseDevice->getNumber("CCD_FRAME"); break; case GUIDE_CCD: frameProp = baseDevice->getNumber("GUIDER_FRAME"); break; } if (frameProp == nullptr) return false; INumber *xarg = IUFindNumber(frameProp, "X"); INumber *yarg = IUFindNumber(frameProp, "Y"); INumber *warg = IUFindNumber(frameProp, "WIDTH"); INumber *harg = IUFindNumber(frameProp, "HEIGHT"); if (xarg && yarg && warg && harg) { if (!std::fabs(xarg->value - xarg->min) && !std::fabs(yarg->value - yarg->min) && !std::fabs(warg->value - warg->max) && !std::fabs(harg->value - harg->max)) return false; xarg->value = xarg->min; yarg->value = yarg->min; warg->value = warg->max; harg->value = harg->max; clientManager->sendNewNumber(frameProp); return true; } return false; } bool CCDChip::setFrame(int x, int y, int w, int h, bool force) { INumberVectorProperty *frameProp = nullptr; switch (type) { case PRIMARY_CCD: frameProp = baseDevice->getNumber("CCD_FRAME"); break; case GUIDE_CCD: frameProp = baseDevice->getNumber("GUIDER_FRAME"); break; } if (frameProp == nullptr) return false; INumber *xarg = IUFindNumber(frameProp, "X"); INumber *yarg = IUFindNumber(frameProp, "Y"); INumber *warg = IUFindNumber(frameProp, "WIDTH"); INumber *harg = IUFindNumber(frameProp, "HEIGHT"); if (xarg && yarg && warg && harg) { if (!force && !std::fabs(xarg->value - x) && !std::fabs(yarg->value - y) && !std::fabs(warg->value - w) && !std::fabs(harg->value - h)) return true; xarg->value = x; yarg->value = y; warg->value = w; harg->value = h; clientManager->sendNewNumber(frameProp); return true; } return false; } bool CCDChip::capture(double exposure) { //qCDebug(KSTARS_INDI) << "IndiCCD: capture()" << (type==PRIMARY_CCD?"CCD":"Guide"); INumberVectorProperty *expProp = nullptr; switch (type) { case PRIMARY_CCD: expProp = baseDevice->getNumber("CCD_EXPOSURE"); break; case GUIDE_CCD: expProp = baseDevice->getNumber("GUIDER_EXPOSURE"); break; } if (expProp == nullptr) return false; // If we have exposure presets, let's limit the exposure value // to the preset values if it falls within their range of max/min if (Options::forceDSLRPresets()) { QMap exposurePresets = parentCCD->getExposurePresets(); if (!exposurePresets.isEmpty()) { double min, max; QPair minmax = parentCCD->getExposurePresetsMinMax(); min = minmax.first; max = minmax.second; if (exposure > min && exposure < max) { double diff = 1e6; double closestMatch = exposure; for (auto oneValue : exposurePresets.values()) { double newDiff = std::fabs(exposure - oneValue); if (newDiff < diff) { closestMatch = oneValue; diff = newDiff; } } qCDebug(KSTARS_INDI) << "Requested exposure" << exposure << "closes match is" << closestMatch; exposure = closestMatch; } } } // clone the INumberVectorProperty, to avoid modifications to the same // property from two threads INumber n; strcpy(n.name, expProp->np[0].name); n.value = exposure; std::unique_ptr newExpProp(new INumberVectorProperty()); strncpy(newExpProp->device, expProp->device, MAXINDIDEVICE); strncpy(newExpProp->name, expProp->name, MAXINDINAME); strncpy(newExpProp->label, expProp->label, MAXINDILABEL); newExpProp->np = &n; newExpProp->nnp = 1; clientManager->sendNewNumber(newExpProp.get()); return true; } bool CCDChip::abortExposure() { ISwitchVectorProperty *abortProp = nullptr; switch (type) { case PRIMARY_CCD: abortProp = baseDevice->getSwitch("CCD_ABORT_EXPOSURE"); break; case GUIDE_CCD: abortProp = baseDevice->getSwitch("GUIDER_ABORT_EXPOSURE"); break; } if (abortProp == nullptr) return false; ISwitch *abort = IUFindSwitch(abortProp, "ABORT"); if (abort == nullptr) return false; abort->s = ISS_ON; clientManager->sendNewSwitch(abortProp); return true; } bool CCDChip::canBin() const { return CanBin; } void CCDChip::setCanBin(bool value) { CanBin = value; } bool CCDChip::canSubframe() const { return CanSubframe; } void CCDChip::setCanSubframe(bool value) { CanSubframe = value; } bool CCDChip::canAbort() const { return CanAbort; } void CCDChip::setCanAbort(bool value) { CanAbort = value; } FITSData *CCDChip::getImageData() const { return imageData; } int CCDChip::getISOIndex() const { ISwitchVectorProperty *isoProp = baseDevice->getSwitch("CCD_ISO"); if (isoProp == nullptr) return -1; return IUFindOnSwitchIndex(isoProp); } bool CCDChip::setISOIndex(int value) { ISwitchVectorProperty *isoProp = baseDevice->getSwitch("CCD_ISO"); if (isoProp == nullptr) return false; IUResetSwitch(isoProp); isoProp->sp[value].s = ISS_ON; clientManager->sendNewSwitch(isoProp); return true; } QStringList CCDChip::getISOList() const { QStringList isoList; ISwitchVectorProperty *isoProp = baseDevice->getSwitch("CCD_ISO"); if (isoProp == nullptr) return isoList; for (int i = 0; i < isoProp->nsp; i++) isoList << isoProp->sp[i].label; return isoList; } bool CCDChip::isCapturing() { INumberVectorProperty *expProp = nullptr; switch (type) { case PRIMARY_CCD: expProp = baseDevice->getNumber("CCD_EXPOSURE"); break; case GUIDE_CCD: expProp = baseDevice->getNumber("GUIDER_EXPOSURE"); break; } if (expProp == nullptr) return false; return (expProp->s == IPS_BUSY); } bool CCDChip::setFrameType(const QString &name) { CCDFrameType fType = FRAME_LIGHT; if (name == "FRAME_LIGHT" || name == "Light") fType = FRAME_LIGHT; else if (name == "FRAME_DARK" || name == "Dark") fType = FRAME_DARK; else if (name == "FRAME_BIAS" || name == "Bias") fType = FRAME_BIAS; else if (name == "FRAME_FLAT" || name == "Flat") fType = FRAME_FLAT; else { qCWarning(KSTARS_INDI) << name << " frame type is unknown." ; return false; } return setFrameType(fType); } bool CCDChip::setFrameType(CCDFrameType fType) { ISwitchVectorProperty *frameProp = nullptr; if (type == PRIMARY_CCD) frameProp = baseDevice->getSwitch("CCD_FRAME_TYPE"); else frameProp = baseDevice->getSwitch("GUIDER_FRAME_TYPE"); if (frameProp == nullptr) return false; ISwitch *ccdFrame = nullptr; if (fType == FRAME_LIGHT) ccdFrame = IUFindSwitch(frameProp, "FRAME_LIGHT"); else if (fType == FRAME_DARK) ccdFrame = IUFindSwitch(frameProp, "FRAME_DARK"); else if (fType == FRAME_BIAS) ccdFrame = IUFindSwitch(frameProp, "FRAME_BIAS"); else if (fType == FRAME_FLAT) ccdFrame = IUFindSwitch(frameProp, "FRAME_FLAT"); if (ccdFrame == nullptr) return false; if (ccdFrame->s == ISS_ON) return true; if (fType != FRAME_LIGHT) captureMode = FITS_CALIBRATE; IUResetSwitch(frameProp); ccdFrame->s = ISS_ON; clientManager->sendNewSwitch(frameProp); return true; } CCDFrameType CCDChip::getFrameType() { CCDFrameType fType = FRAME_LIGHT; ISwitchVectorProperty *frameProp = nullptr; if (type == PRIMARY_CCD) frameProp = baseDevice->getSwitch("CCD_FRAME_TYPE"); else frameProp = baseDevice->getSwitch("GUIDER_FRAME_TYPE"); if (frameProp == nullptr) return fType; ISwitch *ccdFrame = nullptr; ccdFrame = IUFindOnSwitch(frameProp); if (ccdFrame == nullptr) { qCWarning(KSTARS_INDI) << "ISD:CCD Cannot find active frame in CCD!"; return fType; } if (!strcmp(ccdFrame->name, "FRAME_LIGHT")) fType = FRAME_LIGHT; else if (!strcmp(ccdFrame->name, "FRAME_DARK")) fType = FRAME_DARK; else if (!strcmp(ccdFrame->name, "FRAME_FLAT")) fType = FRAME_FLAT; else if (!strcmp(ccdFrame->name, "FRAME_BIAS")) fType = FRAME_BIAS; return fType; } bool CCDChip::setBinning(CCDBinType binType) { switch (binType) { case SINGLE_BIN: return setBinning(1, 1); case DOUBLE_BIN: return setBinning(2, 2); case TRIPLE_BIN: return setBinning(3, 3); case QUADRAPLE_BIN: return setBinning(4, 4); } return false; } CCDBinType CCDChip::getBinning() { CCDBinType binType = SINGLE_BIN; INumberVectorProperty *binProp = nullptr; switch (type) { case PRIMARY_CCD: binProp = baseDevice->getNumber("CCD_BINNING"); break; case GUIDE_CCD: binProp = baseDevice->getNumber("GUIDER_BINNING"); break; } if (binProp == nullptr) return binType; INumber *horBin = nullptr, *verBin = nullptr; horBin = IUFindNumber(binProp, "HOR_BIN"); verBin = IUFindNumber(binProp, "VER_BIN"); if (!horBin || !verBin) return binType; switch (static_cast(horBin->value)) { case 2: binType = DOUBLE_BIN; break; case 3: binType = TRIPLE_BIN; break; case 4: binType = QUADRAPLE_BIN; break; default: break; } return binType; } bool CCDChip::getBinning(int *bin_x, int *bin_y) { INumberVectorProperty *binProp = nullptr; *bin_x = *bin_y = 1; switch (type) { case PRIMARY_CCD: binProp = baseDevice->getNumber("CCD_BINNING"); break; case GUIDE_CCD: binProp = baseDevice->getNumber("GUIDER_BINNING"); break; } if (binProp == nullptr) return false; INumber *horBin = nullptr, *verBin = nullptr; horBin = IUFindNumber(binProp, "HOR_BIN"); verBin = IUFindNumber(binProp, "VER_BIN"); if (!horBin || !verBin) return false; *bin_x = horBin->value; *bin_y = verBin->value; return true; } bool CCDChip::getMaxBin(int *max_xbin, int *max_ybin) { if (!max_xbin || !max_ybin) return false; INumberVectorProperty *binProp = nullptr; *max_xbin = *max_ybin = 1; switch (type) { case PRIMARY_CCD: binProp = baseDevice->getNumber("CCD_BINNING"); break; case GUIDE_CCD: binProp = baseDevice->getNumber("GUIDER_BINNING"); break; } if (binProp == nullptr) return false; INumber *horBin = nullptr, *verBin = nullptr; horBin = IUFindNumber(binProp, "HOR_BIN"); verBin = IUFindNumber(binProp, "VER_BIN"); if (!horBin || !verBin) return false; *max_xbin = horBin->max; *max_ybin = verBin->max; return true; } bool CCDChip::setBinning(int bin_x, int bin_y) { INumberVectorProperty *binProp = nullptr; switch (type) { case PRIMARY_CCD: binProp = baseDevice->getNumber("CCD_BINNING"); break; case GUIDE_CCD: binProp = baseDevice->getNumber("GUIDER_BINNING"); break; } if (binProp == nullptr) return false; INumber *horBin = IUFindNumber(binProp, "HOR_BIN"); INumber *verBin = IUFindNumber(binProp, "VER_BIN"); if (!horBin || !verBin) return false; if (!std::fabs(horBin->value - bin_x) && !std::fabs(verBin->value - bin_y)) return true; if (bin_x > horBin->max || bin_y > verBin->max) return false; horBin->value = bin_x; verBin->value = bin_y; clientManager->sendNewNumber(binProp); return true; } CCD::CCD(GDInterface *iPtr) : DeviceDecorator(iPtr) { primaryChip.reset(new CCDChip(this, CCDChip::PRIMARY_CCD)); readyTimer.reset(new QTimer()); readyTimer.get()->setInterval(250); readyTimer.get()->setSingleShot(true); connect(readyTimer.get(), &QTimer::timeout, this, &CCD::ready); m_Media.reset(new WSMedia(this)); connect(m_Media.get(), &WSMedia::newFile, this, &CCD::setWSBLOB); connect(clientManager, &ClientManager::newBLOBManager, this, &CCD::setBLOBManager, Qt::UniqueConnection); m_LastNotificationTS = QDateTime::currentDateTime(); } CCD::~CCD() { if (m_ImageViewerWindow) m_ImageViewerWindow->close(); if (fileWriteThread.isRunning()) fileWriteThread.waitForFinished(); if (fileWriteBuffer != nullptr) delete [] fileWriteBuffer; } void CCD::setBLOBManager(const char *device, INDI::Property *prop) { if (!prop->getRegistered()) return; if (device == getDeviceName()) emit newBLOBManager(prop); } void CCD::registerProperty(INDI::Property *prop) { if (isConnected()) readyTimer.get()->start(); if (!strcmp(prop->getName(), "GUIDER_EXPOSURE")) { HasGuideHead = true; guideChip.reset(new CCDChip(this, CCDChip::GUIDE_CCD)); } else if (!strcmp(prop->getName(), "CCD_FRAME_TYPE")) { ISwitchVectorProperty *ccdFrame = prop->getSwitch(); primaryChip->clearFrameTypes(); for (int i = 0; i < ccdFrame->nsp; i++) primaryChip->addFrameLabel(ccdFrame->sp[i].label); } else if (!strcmp(prop->getName(), "CCD_FRAME")) { INumberVectorProperty *np = prop->getNumber(); if (np && np->p != IP_RO) primaryChip->setCanSubframe(true); } else if (!strcmp(prop->getName(), "GUIDER_FRAME")) { INumberVectorProperty *np = prop->getNumber(); if (np && np->p != IP_RO) guideChip->setCanSubframe(true); } else if (!strcmp(prop->getName(), "CCD_BINNING")) { INumberVectorProperty *np = prop->getNumber(); if (np && np->p != IP_RO) primaryChip->setCanBin(true); } else if (!strcmp(prop->getName(), "GUIDER_BINNING")) { INumberVectorProperty *np = prop->getNumber(); if (np && np->p != IP_RO) guideChip->setCanBin(true); } else if (!strcmp(prop->getName(), "CCD_ABORT_EXPOSURE")) { ISwitchVectorProperty *sp = prop->getSwitch(); if (sp && sp->p != IP_RO) primaryChip->setCanAbort(true); } else if (!strcmp(prop->getName(), "GUIDER_ABORT_EXPOSURE")) { ISwitchVectorProperty *sp = prop->getSwitch(); if (sp && sp->p != IP_RO) guideChip->setCanAbort(true); } else if (!strcmp(prop->getName(), "CCD_TEMPERATURE")) { INumberVectorProperty *np = prop->getNumber(); HasCooler = true; CanCool = (np->p != IP_RO); if (np) emit newTemperatureValue(np->np[0].value); } else if (!strcmp(prop->getName(), "CCD_COOLER")) { // Can turn cooling on/off HasCoolerControl = true; } else if (!strcmp(prop->getName(), "CCD_VIDEO_STREAM")) { // Has Video Stream HasVideoStream = true; } else if (!strcmp(prop->getName(), "CCD_TRANSFER_FORMAT")) { ISwitchVectorProperty *sp = prop->getSwitch(); if (sp) { ISwitch *format = IUFindSwitch(sp, "FORMAT_NATIVE"); if (format && format->s == ISS_ON) transferFormat = FORMAT_NATIVE; else transferFormat = FORMAT_FITS; } } else if (!strcmp(prop->getName(), "CCD_EXPOSURE_PRESETS")) { ISwitchVectorProperty *svp = prop->getSwitch(); if (svp) { bool ok = false; for (int i = 0; i < svp->nsp; i++) { QString key = QString(svp->sp[i].label); double value = key.toDouble(&ok); if (!ok) { QStringList parts = key.split("/"); if (parts.count() == 2) { bool numOk = false, denOk = false; double numerator = parts[0].toDouble(&numOk); double denominator = parts[1].toDouble(&denOk); if (numOk && denOk && denominator > 0) { ok = true; value = numerator / denominator; } } } if (ok) m_ExposurePresets.insert(key, value); double min = 1e6, max = 1e-6; for (auto oneValue : m_ExposurePresets.values()) { if (oneValue < min) min = oneValue; if (oneValue > max) max = oneValue; } m_ExposurePresetsMinMax = qMakePair(min, max); } } } else if (!strcmp(prop->getName(), "CCD_EXPOSURE_LOOP")) { ISwitchVectorProperty *sp = prop->getSwitch(); if (sp) { ISwitch *looping = IUFindSwitch(sp, "LOOP_ON"); if (looping && looping->s == ISS_ON) IsLooping = true; else IsLooping = false; } } else if (!strcmp(prop->getName(), "TELESCOPE_TYPE")) { ISwitchVectorProperty *sp = prop->getSwitch(); if (sp) { ISwitch *format = IUFindSwitch(sp, "TELESCOPE_PRIMARY"); if (format && format->s == ISS_ON) telescopeType = TELESCOPE_PRIMARY; else telescopeType = TELESCOPE_GUIDE; } } else if (!strcmp(prop->getName(), "CCD_WEBSOCKET_SETTINGS")) { INumberVectorProperty *np = prop->getNumber(); m_Media->setURL(QUrl(QString("ws://%1:%2").arg(clientManager->getHost()).arg(np->np[0].value))); m_Media->connectServer(); } else if (!strcmp(prop->getName(), "CCD1")) { IBLOBVectorProperty *bp = prop->getBLOB(); primaryCCDBLOB = bp->bp; primaryCCDBLOB->bvp = bp; } // try to find gain property, if any else if (gainN == nullptr && prop->getType() == INDI_NUMBER) { // Since gain is spread among multiple property depending on the camera providing it // we need to search in all possible number properties INumberVectorProperty *gainNP = prop->getNumber(); if (gainNP) { for (int i = 0; i < gainNP->nnp; i++) { QString name = QString(gainNP->np[i].name).toLower(); QString label = QString(gainNP->np[i].label).toLower(); if (name == "gain" || label == "gain") { gainN = gainNP->np + i; gainPerm = gainNP->p; break; } } } } DeviceDecorator::registerProperty(prop); } void CCD::removeProperty(const QString &name) { if (name == "CCD_WEBSOCKET_SETTINGS") { m_Media->disconnectServer(); } DeviceDecorator::removeProperty(name); } void CCD::processLight(ILightVectorProperty *lvp) { DeviceDecorator::processLight(lvp); } void CCD::processNumber(INumberVectorProperty *nvp) { if (!strcmp(nvp->name, "CCD_EXPOSURE")) { INumber *np = IUFindNumber(nvp, "CCD_EXPOSURE_VALUE"); if (np) emit newExposureValue(primaryChip.get(), np->value, nvp->s); if (nvp->s == IPS_ALERT) emit captureFailed(); } else if (!strcmp(nvp->name, "CCD_TEMPERATURE")) { HasCooler = true; INumber *np = IUFindNumber(nvp, "CCD_TEMPERATURE_VALUE"); if (np) emit newTemperatureValue(np->value); } else if (!strcmp(nvp->name, "GUIDER_EXPOSURE")) { INumber *np = IUFindNumber(nvp, "GUIDER_EXPOSURE_VALUE"); if (np) emit newExposureValue(guideChip.get(), np->value, nvp->s); } else if (!strcmp(nvp->name, "FPS")) { emit newFPS(nvp->np[0].value, nvp->np[1].value); } else if (!strcmp(nvp->name, "CCD_RAPID_GUIDE_DATA")) { double dx = -1, dy = -1, fit = -1; INumber *np = nullptr; if (nvp->s == IPS_ALERT) { emit newGuideStarData(primaryChip.get(), -1, -1, -1); } else { np = IUFindNumber(nvp, "GUIDESTAR_X"); if (np) dx = np->value; np = IUFindNumber(nvp, "GUIDESTAR_Y"); if (np) dy = np->value; np = IUFindNumber(nvp, "GUIDESTAR_FIT"); if (np) fit = np->value; if (dx >= 0 && dy >= 0 && fit >= 0) emit newGuideStarData(primaryChip.get(), dx, dy, fit); } } else if (!strcmp(nvp->name, "GUIDER_RAPID_GUIDE_DATA")) { double dx = -1, dy = -1, fit = -1; INumber *np = nullptr; if (nvp->s == IPS_ALERT) { emit newGuideStarData(guideChip.get(), -1, -1, -1); } else { np = IUFindNumber(nvp, "GUIDESTAR_X"); if (np) dx = np->value; np = IUFindNumber(nvp, "GUIDESTAR_Y"); if (np) dy = np->value; np = IUFindNumber(nvp, "GUIDESTAR_FIT"); if (np) fit = np->value; if (dx >= 0 && dy >= 0 && fit >= 0) emit newGuideStarData(guideChip.get(), dx, dy, fit); } } DeviceDecorator::processNumber(nvp); } void CCD::processSwitch(ISwitchVectorProperty *svp) { if (!strcmp(svp->name, "CCD_COOLER")) { // Can turn cooling on/off HasCoolerControl = true; emit coolerToggled(svp->sp[0].s == ISS_ON); } else if (QString(svp->name).endsWith("VIDEO_STREAM")) { // If BLOB is not enabled for this camera, then ignore all VIDEO_STREAM calls. if (isBLOBEnabled() == false) return; HasVideoStream = true; - if (streamWindow.get() == nullptr && svp->sp[0].s == ISS_ON) + if (!streamWindow && svp->sp[0].s == ISS_ON) { streamWindow.reset(new StreamWG(this)); INumberVectorProperty *streamFrame = baseDevice->getNumber("CCD_STREAM_FRAME"); INumber *w = nullptr, *h = nullptr; if (streamFrame) { w = IUFindNumber(streamFrame, "WIDTH"); h = IUFindNumber(streamFrame, "HEIGHT"); } if (w && h) { streamW = w->value; streamH = h->value; } else { // Only use CCD dimensions if we are receiving raw stream and not stream of images (i.e. mjpeg..etc) IBLOBVectorProperty *rawBP = baseDevice->getBLOB("CCD1"); if (rawBP) { int x = 0, y = 0, w = 0, h = 0; int binx = 0, biny = 0; primaryChip->getFrame(&x, &y, &w, &h); primaryChip->getBinning(&binx, &biny); streamW = w / binx; streamH = h / biny; } } streamWindow->setSize(streamW, streamH); } - if (streamWindow.get() != nullptr) + if (streamWindow) { connect(streamWindow.get(), &StreamWG::hidden, this, &CCD::StreamWindowHidden, Qt::UniqueConnection); connect(streamWindow.get(), &StreamWG::imageChanged, this, &CCD::newVideoFrame, Qt::UniqueConnection); streamWindow->enableStream(svp->sp[0].s == ISS_ON); emit videoStreamToggled(svp->sp[0].s == ISS_ON); } } else if (!strcmp(svp->name, "CCD_TRANSFER_FORMAT")) { ISwitch *format = IUFindSwitch(svp, "FORMAT_NATIVE"); if (format && format->s == ISS_ON) transferFormat = FORMAT_NATIVE; else transferFormat = FORMAT_FITS; } else if (!strcmp(svp->name, "RECORD_STREAM")) { ISwitch *recordOFF = IUFindSwitch(svp, "RECORD_OFF"); if (recordOFF && recordOFF->s == ISS_ON) { emit videoRecordToggled(false); KNotification::event(QLatin1String("RecordingStopped"), i18n("Video Recording Stopped")); } else { emit videoRecordToggled(true); KNotification::event(QLatin1String("RecordingStarted"), i18n("Video Recording Started")); } } else if (!strcmp(svp->name, "TELESCOPE_TYPE")) { ISwitch *format = IUFindSwitch(svp, "TELESCOPE_PRIMARY"); if (format && format->s == ISS_ON) telescopeType = TELESCOPE_PRIMARY; else telescopeType = TELESCOPE_GUIDE; } else if (!strcmp(svp->name, "CCD_EXPOSURE_LOOP")) { ISwitch *looping = IUFindSwitch(svp, "LOOP_ON"); if (looping && looping->s == ISS_ON) IsLooping = true; else IsLooping = false; } - else if (!strcmp(svp->name, "CONNECTION")) + else if (streamWindow && !strcmp(svp->name, "CONNECTION")) { ISwitch *dSwitch = IUFindSwitch(svp, "DISCONNECT"); - if (dSwitch && dSwitch->s == ISS_ON && streamWindow.get() != nullptr) + if (dSwitch && dSwitch->s == ISS_ON) { streamWindow->enableStream(false); emit videoStreamToggled(false); streamWindow->close(); streamWindow.reset(); } //emit switchUpdated(svp); //return; } DeviceDecorator::processSwitch(svp); } void CCD::processText(ITextVectorProperty *tvp) { if (!strcmp(tvp->name, "CCD_FILE_PATH")) { IText *filepath = IUFindText(tvp, "FILE_PATH"); if (filepath) emit newRemoteFile(QString(filepath->text)); } DeviceDecorator::processText(tvp); } void CCD::setWSBLOB(const QByteArray &message, const QString &extension) { if (!primaryCCDBLOB) return; primaryCCDBLOB->blob = const_cast(message.data()); primaryCCDBLOB->size = message.size(); strncpy(primaryCCDBLOB->format, extension.toLatin1().constData(), MAXINDIFORMAT); processBLOB(primaryCCDBLOB); // Disassociate primaryCCDBLOB->blob = nullptr; } void CCD::processStream(IBLOB *bp) { - if (streamWindow->isStreamEnabled() == false) + if (!streamWindow || streamWindow->isStreamEnabled() == false) return; INumberVectorProperty *streamFrame = baseDevice->getNumber("CCD_STREAM_FRAME"); INumber *w = nullptr, *h = nullptr; if (streamFrame) { w = IUFindNumber(streamFrame, "WIDTH"); h = IUFindNumber(streamFrame, "HEIGHT"); } if (w && h) { streamW = w->value; streamH = h->value; } else { int x, y, w, h; int binx, biny; primaryChip->getFrame(&x, &y, &w, &h); primaryChip->getBinning(&binx, &biny); streamW = w / binx; streamH = h / biny; } streamWindow->setSize(streamW, streamH); streamWindow->show(); streamWindow->newFrame(bp); } bool CCD::generateFilename(const QString &format, bool batch_mode, QString *filename) { QString currentDir; if (batch_mode) currentDir = fitsDir.isEmpty() ? Options::fitsDir() : fitsDir; else currentDir = KSPaths::writableLocation(QStandardPaths::TempLocation); if (QDir(currentDir).exists() == false) QDir().mkpath(currentDir); if (currentDir.endsWith('/') == false) currentDir.append('/'); // 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 ts = QDateTime::currentDateTime().toString("yyyy-MM-ddThh-mm-ss"); if (seqPrefix.contains("_ISO8601")) { QString finalPrefix = seqPrefix; finalPrefix.replace("ISO8601", ts); *filename = currentDir + finalPrefix + QString("_%1%2").arg(QString().asprintf("%03d", nextSequenceID), format); } else *filename = currentDir + seqPrefix + (seqPrefix.isEmpty() ? "" : "_") + QString("%1%2").arg(QString().asprintf("%03d", nextSequenceID), format); QFile test_file(*filename); if (!test_file.open(QIODevice::WriteOnly)) { qCCritical(KSTARS_INDI) << "ISD:CCD Error: Unable to open " << test_file.fileName(); return false; } test_file.flush(); test_file.close(); return true; } bool CCD::writeImageFile(const QString &filename, IBLOB *bp, bool is_fits) { // TODO: Not yet threading the writes for non-fits files. // Would need to deal with the raw conversion, etc. if (is_fits) { // Check if the last write is still ongoing, and if so wait. // It is using the fileWriteBuffer. if (fileWriteThread.isRunning()) { fileWriteThread.waitForFinished(); } // Wait until the file is written before overwritting the filename. fileWriteFilename = filename; // Will write blob data in a separate thread, and can't depend on the blob // memory, so copy it first. // Check buffer size. if (fileWriteBufferSize != bp->size) { if (fileWriteBuffer != nullptr) delete [] fileWriteBuffer; fileWriteBufferSize = bp->size; fileWriteBuffer = new char[fileWriteBufferSize]; } // Copy memory, and write file on a separate thread. // Probably too late to return an error if the file couldn't write. memcpy(fileWriteBuffer, bp->blob, bp->size); fileWriteThread = QtConcurrent::run(WriteImageFileInternal, fileWriteFilename, fileWriteBuffer, bp->size, is_fits, filter); filter = ""; } else { if (!WriteImageFileInternal(filename, static_cast(bp->blob), bp->size, false, filter)) return false; } return true; } void CCD::setupFITSViewerWindows() { normalTabID = calibrationTabID = focusTabID = guideTabID = alignTabID = -1; if (Options::singleWindowCapturedFITS()) m_FITSViewerWindows = KStars::Instance()->genericFITSViewer(); else { m_FITSViewerWindows = new FITSViewer(Options::independentWindowFITS() ? nullptr : KStars::Instance()); KStars::Instance()->addFITSViewer(m_FITSViewerWindows); } connect(m_FITSViewerWindows, &FITSViewer::closed, [&](int tabIndex) { if (tabIndex == normalTabID) normalTabID = -1; else if (tabIndex == calibrationTabID) calibrationTabID = -1; else if (tabIndex == focusTabID) focusTabID = -1; else if (tabIndex == guideTabID) guideTabID = -1; else if (tabIndex == alignTabID) alignTabID = -1; }); } void CCD::processBLOB(IBLOB *bp) { // Ignore write-only BLOBs since we only receive it for state-change if (bp->bvp->p == IP_WO || bp->size == 0) return; BType = BLOB_OTHER; QString format = QString(bp->format).toLower(); // If stream, process it first - if (format.contains("stream") && streamWindow.get() != nullptr) + if (format.contains("stream") && streamWindow) { processStream(bp); return; } // Format without leading . (.jpg --> jpg) QString shortFormat = format.mid(1); // If it's not FITS or an image, don't process it. if ((QImageReader::supportedImageFormats().contains(shortFormat.toLatin1()))) BType = BLOB_IMAGE; else if (format.contains("fits")) BType = BLOB_FITS; else if (RAWFormats.contains(shortFormat)) BType = BLOB_RAW; if (BType == BLOB_OTHER) { DeviceDecorator::processBLOB(bp); return; } CCDChip *targetChip = nullptr; if (!strcmp(bp->name, "CCD2")) targetChip = guideChip.get(); else { targetChip = primaryChip.get(); qCDebug(KSTARS_INDI) << "processBLOB() mode " << targetChip->getCaptureMode(); } // Create temporary name if ANY of the following conditions are met: // 1. file is preview or batch mode is not enabled // 2. file type is not FITS_NORMAL (focus, guide..etc) QString filename; if (targetChip->isBatchMode() == false || targetChip->getCaptureMode() != FITS_NORMAL) { if (!writeTempImageFile(format, static_cast(bp->blob), bp->size, &filename)) { emit BLOBUpdated(nullptr); return; } if (BType == BLOB_FITS) addFITSKeywords(filename, filter); } // Create file name for others else { if (!generateFilename(format, targetChip->isBatchMode(), &filename) || !writeImageFile(filename, bp, BType == BLOB_FITS)) { emit BLOBUpdated(nullptr); return; } } // store file name strncpy(BLOBFilename, filename.toLatin1(), MAXINDIFILENAME); bp->aux0 = targetChip; bp->aux1 = &BType; bp->aux2 = BLOBFilename; if (targetChip->getCaptureMode() == FITS_NORMAL && targetChip->isBatchMode() == true) { KStars::Instance()->statusBar()->showMessage(i18n("%1 file saved to %2", shortFormat.toUpper(), filename), 0); qCInfo(KSTARS_INDI) << shortFormat.toUpper() << "file saved to" << filename; } // Don't spam, just one notification per 3 seconds if (QDateTime::currentDateTime().secsTo(m_LastNotificationTS) <= -3) { KNotification::event(QLatin1String("FITSReceived"), i18n("Image file is received")); m_LastNotificationTS = QDateTime::currentDateTime(); } // Check if we need to process RAW or regular image. Anything but FITS. if (BType == BLOB_IMAGE || BType == BLOB_RAW) { bool useFITSViewer = Options::autoImageToFITS() && (Options::useFITSViewer() || (Options::useDSLRImageViewer() == false && targetChip->isBatchMode() == false)); bool useDSLRViewer = (Options::useDSLRImageViewer() || targetChip->isBatchMode() == false); // For raw image, we only process them to JPG if we need to open them in the image viewer if (BType == BLOB_RAW && (useFITSViewer || useDSLRViewer)) { QString rawFileName = filename; rawFileName = rawFileName.remove(0, rawFileName.lastIndexOf(QLatin1String("/"))); QString templateName = QString("%1/%2.XXXXXX").arg(QDir::tempPath(), rawFileName); QTemporaryFile imgPreview(templateName); imgPreview.setAutoRemove(false); imgPreview.open(); imgPreview.close(); QString preview_filename = imgPreview.fileName(); QString errorMessage; if (KSUtils::RAWToJPEG(filename, preview_filename, errorMessage) == false) { KStars::Instance()->statusBar()->showMessage(errorMessage); emit BLOBUpdated(bp); return; } // Remove tempeorary CR2 files if (targetChip->isBatchMode() == false) QFile::remove(filename); filename = preview_filename; format = ".jpg"; shortFormat = "jpg"; } // Convert to FITS if checked. QString output; if (useFITSViewer && (FITSData::ImageToFITS(filename, shortFormat, output))) { if (BType == BLOB_RAW || targetChip->isBatchMode() == false) QFile::remove(filename); filename = output; BType = BLOB_FITS; emit previewFITSGenerated(output); FITSData *blob_fits_data = new FITSData(targetChip->getCaptureMode()); QFuture fitsloader = blob_fits_data->loadFITS(filename, false); fitsloader.waitForFinished(); if (!fitsloader.result()) { // If reading the blob fails, we treat it the same as exposure failure // and recapture again if possible delete (blob_fits_data); qCCritical(KSTARS_INDI) << "failed reading FITS memory buffer"; emit newExposureValue(targetChip, 0, IPS_ALERT); return; } displayFits(targetChip, filename, bp, blob_fits_data); return; } else if (useDSLRViewer) { if (m_ImageViewerWindow.isNull()) m_ImageViewerWindow = new ImageViewer(getDeviceName(), KStars::Instance()); m_ImageViewerWindow->loadImage(filename); emit previewJPEGGenerated(filename, m_ImageViewerWindow->metadata()); } } // Load FITS if either: // #1 FITS Viewer is set to enabled. // #2 This is a preview, so we MUST open FITS Viewer even if disabled. if (BType == BLOB_FITS) { // Don't display if (NORMAL or CALIBRATE) and ((not using fitsviewer) and (no in batch mode)) if ((targetChip->getCaptureMode() == FITS_NORMAL || targetChip->getCaptureMode() == FITS_CALIBRATE) && - (!Options::useFITSViewer() && targetChip->isBatchMode())) + (!Options::useFITSViewer() && targetChip->isBatchMode())) { emit BLOBUpdated(bp); return; } FITSData *blob_fits_data = new FITSData(targetChip->getCaptureMode()); if (!blob_fits_data->loadFITSFromMemory(filename, bp->blob, bp->size, false)) { // If reading the blob fails, we treat it the same as exposure failure // and recapture again if possible delete (blob_fits_data); qCCritical(KSTARS_INDI) << "failed reading FITS memory buffer"; emit newExposureValue(targetChip, 0, IPS_ALERT); return; } - displayFits(targetChip, filename, bp, blob_fits_data); + displayFits(targetChip, filename, bp, blob_fits_data); } else emit BLOBUpdated(bp); } void CCD::displayFits(CCDChip *targetChip, const QString &filename, IBLOB *bp, FITSData *blob_fits_data) { FITSMode captureMode = targetChip->getCaptureMode(); // Get or Create FITSViewer if we are using FITSViewer // or if capture mode is calibrate since for now we are forced to open the file in the viewer // this should be fixed in the future and should only use FITSData if (Options::useFITSViewer() || targetChip->isBatchMode() == false) { if (m_FITSViewerWindows.isNull() && (captureMode == FITS_NORMAL || captureMode == FITS_CALIBRATE)) setupFITSViewerWindows(); } switch (captureMode) { case FITS_NORMAL: case FITS_CALIBRATE: { bool success; int tabIndex; int *tabID = (captureMode == FITS_NORMAL) ? &normalTabID : &calibrationTabID; QUrl fileURL = QUrl::fromLocalFile(filename); FITSScale captureFilter = targetChip->getCaptureFilter(); if (*tabID == -1 || Options::singlePreviewFITS() == false) { // If image is preview and we should display all captured images in a // single tab called "Preview", then set the title to "Preview", // Otherwise, the title will be the captured image name QString previewTitle; if (targetChip->isBatchMode() == false && Options::singlePreviewFITS()) { // If we are displaying all images from all cameras in a single FITS // Viewer window, then we prefix the camera name to the "Preview" string if (Options::singleWindowCapturedFITS()) previewTitle = i18n("%1 Preview", getDeviceName()); else // Otherwise, just use "Preview" previewTitle = i18n("Preview"); } success = m_FITSViewerWindows->addFITSFromData( - blob_fits_data, fileURL, &tabIndex, captureMode, captureFilter, - previewTitle); + blob_fits_data, fileURL, &tabIndex, captureMode, captureFilter, + previewTitle); } else success = m_FITSViewerWindows->updateFITSFromData( - blob_fits_data, fileURL, *tabID, &tabIndex, captureFilter); + blob_fits_data, fileURL, *tabID, &tabIndex, captureFilter); if (!success) { // If opening file fails, we treat it the same as exposure failure // and recapture again if possible qCCritical(KSTARS_INDI) << "error adding/updating FITS"; emit newExposureValue(targetChip, 0, IPS_ALERT); return; } *tabID = tabIndex; targetChip->setImageView(m_FITSViewerWindows->getView(tabIndex), captureMode); if (Options::focusFITSOnNewImage()) m_FITSViewerWindows->raise(); emit BLOBUpdated(bp); } break; case FITS_FOCUS: case FITS_GUIDE: case FITS_ALIGN: loadImageInView(bp, targetChip, blob_fits_data); break; } } void CCD::loadImageInView(IBLOB *bp, ISD::CCDChip *targetChip, FITSData *data) { FITSMode mode = targetChip->getCaptureMode(); FITSView *view = targetChip->getImageView(mode); QString filename = QString(static_cast(bp->aux2)); if (view) { view->setFilter(targetChip->getCaptureFilter()); if (!view->loadFITSFromData(data, filename)) { emit newExposureValue(targetChip, 0, IPS_ALERT); return; } // FITSViewer is shown if: // Image in preview mode, or useFITSViewer is true; AND // Image type is either NORMAL or CALIBRATION since the rest have their dedicated windows. // NORMAL is used for raw INDI drivers without Ekos. if ( (Options::useFITSViewer() || targetChip->isBatchMode() == false) && (mode == FITS_NORMAL || mode == FITS_CALIBRATE)) m_FITSViewerWindows->show(); emit BLOBUpdated(bp); } } CCD::TransferFormat CCD::getTargetTransferFormat() const { return targetTransferFormat; } void CCD::setTargetTransferFormat(const TransferFormat &value) { targetTransferFormat = value; } void CCD::FITSViewerDestroyed() { m_FITSViewerWindows = nullptr; normalTabID = calibrationTabID = focusTabID = guideTabID = alignTabID = -1; } void CCD::StreamWindowHidden() { if (baseDevice->isConnected()) { // We can have more than one *_VIDEO_STREAM property active so disable them all ISwitchVectorProperty *streamSP = baseDevice->getSwitch("CCD_VIDEO_STREAM"); if (streamSP) { IUResetSwitch(streamSP); streamSP->sp[0].s = ISS_OFF; streamSP->sp[1].s = ISS_ON; streamSP->s = IPS_IDLE; clientManager->sendNewSwitch(streamSP); } streamSP = baseDevice->getSwitch("VIDEO_STREAM"); if (streamSP) { IUResetSwitch(streamSP); streamSP->sp[0].s = ISS_OFF; streamSP->sp[1].s = ISS_ON; streamSP->s = IPS_IDLE; clientManager->sendNewSwitch(streamSP); } streamSP = baseDevice->getSwitch("AUX_VIDEO_STREAM"); if (streamSP) { IUResetSwitch(streamSP); streamSP->sp[0].s = ISS_OFF; streamSP->sp[1].s = ISS_ON; streamSP->s = IPS_IDLE; clientManager->sendNewSwitch(streamSP); } } - if (streamWindow.get() != nullptr) + if (streamWindow) streamWindow->disconnect(); } bool CCD::hasGuideHead() { return HasGuideHead; } bool CCD::hasCooler() { return HasCooler; } bool CCD::hasCoolerControl() { return HasCoolerControl; } bool CCD::setCoolerControl(bool enable) { if (HasCoolerControl == false) return false; ISwitchVectorProperty *coolerSP = baseDevice->getSwitch("CCD_COOLER"); if (coolerSP == nullptr) return false; // Cooler ON/OFF ISwitch *coolerON = IUFindSwitch(coolerSP, "COOLER_ON"); ISwitch *coolerOFF = IUFindSwitch(coolerSP, "COOLER_OFF"); if (coolerON == nullptr || coolerOFF == nullptr) return false; coolerON->s = enable ? ISS_ON : ISS_OFF; coolerOFF->s = enable ? ISS_OFF : ISS_ON; clientManager->sendNewSwitch(coolerSP); return true; } CCDChip *CCD::getChip(CCDChip::ChipType cType) { switch (cType) { case CCDChip::PRIMARY_CCD: return primaryChip.get(); case CCDChip::GUIDE_CCD: return guideChip.get(); } return nullptr; } bool CCD::setRapidGuide(CCDChip *targetChip, bool enable) { ISwitchVectorProperty *rapidSP = nullptr; ISwitch *enableS = nullptr; if (targetChip == primaryChip.get()) rapidSP = baseDevice->getSwitch("CCD_RAPID_GUIDE"); else rapidSP = baseDevice->getSwitch("GUIDER_RAPID_GUIDE"); if (rapidSP == nullptr) return false; enableS = IUFindSwitch(rapidSP, "ENABLE"); if (enableS == nullptr) return false; // Already updated, return OK if ((enable && enableS->s == ISS_ON) || (!enable && enableS->s == ISS_OFF)) return true; IUResetSwitch(rapidSP); rapidSP->sp[0].s = enable ? ISS_ON : ISS_OFF; rapidSP->sp[1].s = enable ? ISS_OFF : ISS_ON; clientManager->sendNewSwitch(rapidSP); return true; } bool CCD::configureRapidGuide(CCDChip *targetChip, bool autoLoop, bool sendImage, bool showMarker) { ISwitchVectorProperty *rapidSP = nullptr; ISwitch *autoLoopS = nullptr, *sendImageS = nullptr, *showMarkerS = nullptr; if (targetChip == primaryChip.get()) rapidSP = baseDevice->getSwitch("CCD_RAPID_GUIDE_SETUP"); else rapidSP = baseDevice->getSwitch("GUIDER_RAPID_GUIDE_SETUP"); if (rapidSP == nullptr) return false; autoLoopS = IUFindSwitch(rapidSP, "AUTO_LOOP"); sendImageS = IUFindSwitch(rapidSP, "SEND_IMAGE"); showMarkerS = IUFindSwitch(rapidSP, "SHOW_MARKER"); if (!autoLoopS || !sendImageS || !showMarkerS) return false; // If everything is already set, let's return. if (((autoLoop && autoLoopS->s == ISS_ON) || (!autoLoop && autoLoopS->s == ISS_OFF)) && ((sendImage && sendImageS->s == ISS_ON) || (!sendImage && sendImageS->s == ISS_OFF)) && ((showMarker && showMarkerS->s == ISS_ON) || (!showMarker && showMarkerS->s == ISS_OFF))) return true; autoLoopS->s = autoLoop ? ISS_ON : ISS_OFF; sendImageS->s = sendImage ? ISS_ON : ISS_OFF; showMarkerS->s = showMarker ? ISS_ON : ISS_OFF; clientManager->sendNewSwitch(rapidSP); return true; } void CCD::updateUploadSettings(const QString &remoteDir) { QString filename = seqPrefix + (seqPrefix.isEmpty() ? "" : "_") + QString("XXX"); ITextVectorProperty *uploadSettingsTP = nullptr; IText *uploadT = nullptr; uploadSettingsTP = baseDevice->getText("UPLOAD_SETTINGS"); if (uploadSettingsTP) { uploadT = IUFindText(uploadSettingsTP, "UPLOAD_DIR"); if (uploadT && remoteDir.isEmpty() == false) IUSaveText(uploadT, remoteDir.toLatin1().constData()); uploadT = IUFindText(uploadSettingsTP, "UPLOAD_PREFIX"); if (uploadT) IUSaveText(uploadT, filename.toLatin1().constData()); clientManager->sendNewText(uploadSettingsTP); } } CCD::UploadMode CCD::getUploadMode() { ISwitchVectorProperty *uploadModeSP = nullptr; uploadModeSP = baseDevice->getSwitch("UPLOAD_MODE"); if (uploadModeSP == nullptr) { qWarning() << "No UPLOAD_MODE in CCD driver. Please update driver to INDI compliant CCD driver."; return UPLOAD_CLIENT; } if (uploadModeSP) { ISwitch *modeS = nullptr; modeS = IUFindSwitch(uploadModeSP, "UPLOAD_CLIENT"); if (modeS && modeS->s == ISS_ON) return UPLOAD_CLIENT; modeS = IUFindSwitch(uploadModeSP, "UPLOAD_LOCAL"); if (modeS && modeS->s == ISS_ON) return UPLOAD_LOCAL; modeS = IUFindSwitch(uploadModeSP, "UPLOAD_BOTH"); if (modeS && modeS->s == ISS_ON) return UPLOAD_BOTH; } // Default return UPLOAD_CLIENT; } bool CCD::setUploadMode(UploadMode mode) { ISwitchVectorProperty *uploadModeSP = nullptr; ISwitch *modeS = nullptr; uploadModeSP = baseDevice->getSwitch("UPLOAD_MODE"); if (uploadModeSP == nullptr) { qWarning() << "No UPLOAD_MODE in CCD driver. Please update driver to INDI compliant CCD driver."; return false; } switch (mode) { case UPLOAD_CLIENT: modeS = IUFindSwitch(uploadModeSP, "UPLOAD_CLIENT"); if (modeS == nullptr) return false; if (modeS->s == ISS_ON) return true; break; case UPLOAD_BOTH: modeS = IUFindSwitch(uploadModeSP, "UPLOAD_BOTH"); if (modeS == nullptr) return false; if (modeS->s == ISS_ON) return true; break; case UPLOAD_LOCAL: modeS = IUFindSwitch(uploadModeSP, "UPLOAD_LOCAL"); if (modeS == nullptr) return false; if (modeS->s == ISS_ON) return true; break; } IUResetSwitch(uploadModeSP); modeS->s = ISS_ON; clientManager->sendNewSwitch(uploadModeSP); return true; } bool CCD::getTemperature(double *value) { if (HasCooler == false) return false; INumberVectorProperty *temperatureNP = baseDevice->getNumber("CCD_TEMPERATURE"); if (temperatureNP == nullptr) return false; *value = temperatureNP->np[0].value; return true; } bool CCD::setTemperature(double value) { INumberVectorProperty *nvp = baseDevice->getNumber("CCD_TEMPERATURE"); if (nvp == nullptr) return false; INumber *np = IUFindNumber(nvp, "CCD_TEMPERATURE_VALUE"); if (np == nullptr) return false; np->value = value; clientManager->sendNewNumber(nvp); return true; } bool CCD::setTransformFormat(CCD::TransferFormat format) { if (format == transferFormat) return true; ISwitchVectorProperty *svp = baseDevice->getSwitch("CCD_TRANSFER_FORMAT"); if (svp == nullptr) return false; ISwitch *formatFITS = IUFindSwitch(svp, "FORMAT_FITS"); ISwitch *formatNative = IUFindSwitch(svp, "FORMAT_NATIVE"); if (formatFITS == nullptr || formatNative == nullptr) return false; transferFormat = format; formatFITS->s = (transferFormat == FORMAT_FITS) ? ISS_ON : ISS_OFF; formatNative->s = (transferFormat == FORMAT_FITS) ? ISS_OFF : ISS_ON; clientManager->sendNewSwitch(svp); return true; } bool CCD::setTelescopeType(TelescopeType type) { if (type == telescopeType) return true; ISwitchVectorProperty *svp = baseDevice->getSwitch("TELESCOPE_TYPE"); if (svp == nullptr) return false; ISwitch *typePrimary = IUFindSwitch(svp, "TELESCOPE_PRIMARY"); ISwitch *typeGuide = IUFindSwitch(svp, "TELESCOPE_GUIDE"); if (typePrimary == nullptr || typeGuide == nullptr) return false; telescopeType = type; typePrimary->s = (telescopeType == TELESCOPE_PRIMARY) ? ISS_ON : ISS_OFF; typeGuide->s = (telescopeType == TELESCOPE_PRIMARY) ? ISS_OFF : ISS_ON; clientManager->sendNewSwitch(svp); setConfig(SAVE_CONFIG); return true; } bool CCD::setVideoStreamEnabled(bool enable) { if (HasVideoStream == false) return false; ISwitchVectorProperty *svp = baseDevice->getSwitch("CCD_VIDEO_STREAM"); if (svp == nullptr) return false; // If already on and enable is set or vice versa no need to change anything we return true if ((enable && svp->sp[0].s == ISS_ON) || (!enable && svp->sp[1].s == ISS_ON)) return true; svp->sp[0].s = enable ? ISS_ON : ISS_OFF; svp->sp[1].s = enable ? ISS_OFF : ISS_ON; clientManager->sendNewSwitch(svp); return true; } bool CCD::resetStreamingFrame() { INumberVectorProperty *frameProp = baseDevice->getNumber("CCD_STREAM_FRAME"); if (frameProp == nullptr) return false; INumber *xarg = IUFindNumber(frameProp, "X"); INumber *yarg = IUFindNumber(frameProp, "Y"); INumber *warg = IUFindNumber(frameProp, "WIDTH"); INumber *harg = IUFindNumber(frameProp, "HEIGHT"); if (xarg && yarg && warg && harg) { if (!std::fabs(xarg->value - xarg->min) && !std::fabs(yarg->value - yarg->min) && !std::fabs(warg->value - warg->max) && !std::fabs(harg->value - harg->max)) return false; xarg->value = xarg->min; yarg->value = yarg->min; warg->value = warg->max; harg->value = harg->max; clientManager->sendNewNumber(frameProp); return true; } return false; } bool CCD::setStreamingFrame(int x, int y, int w, int h) { INumberVectorProperty *frameProp = baseDevice->getNumber("CCD_STREAM_FRAME"); if (frameProp == nullptr) return false; INumber *xarg = IUFindNumber(frameProp, "X"); INumber *yarg = IUFindNumber(frameProp, "Y"); INumber *warg = IUFindNumber(frameProp, "WIDTH"); INumber *harg = IUFindNumber(frameProp, "HEIGHT"); if (xarg && yarg && warg && harg) { if (!std::fabs(xarg->value - x) && !std::fabs(yarg->value - y) && !std::fabs(warg->value - w) && !std::fabs(harg->value - h)) return true; // N.B. We add offset since the X, Y are relative to whatever streaming frame is currently active xarg->value = qBound(xarg->min, static_cast(x) + xarg->value, xarg->max); yarg->value = qBound(yarg->min, static_cast(y) + yarg->value, yarg->max); warg->value = qBound(warg->min, static_cast(w), warg->max); harg->value = qBound(harg->min, static_cast(h), harg->max); clientManager->sendNewNumber(frameProp); return true; } return false; } bool CCD::isStreamingEnabled() { - if (HasVideoStream == false || streamWindow.get() == nullptr) + if (HasVideoStream == false || !streamWindow) return false; return streamWindow->isStreamEnabled(); } bool CCD::setSERNameDirectory(const QString &filename, const QString &directory) { ITextVectorProperty *tvp = baseDevice->getText("RECORD_FILE"); if (tvp == nullptr) return false; IText *filenameT = IUFindText(tvp, "RECORD_FILE_NAME"); IText *dirT = IUFindText(tvp, "RECORD_FILE_DIR"); if (filenameT == nullptr || dirT == nullptr) return false; IUSaveText(filenameT, filename.toLatin1().data()); IUSaveText(dirT, directory.toLatin1().data()); clientManager->sendNewText(tvp); return true; } bool CCD::getSERNameDirectory(QString &filename, QString &directory) { ITextVectorProperty *tvp = baseDevice->getText("RECORD_FILE"); if (tvp == nullptr) return false; IText *filenameT = IUFindText(tvp, "RECORD_FILE_NAME"); IText *dirT = IUFindText(tvp, "RECORD_FILE_DIR"); if (filenameT == nullptr || dirT == nullptr) return false; filename = QString(filenameT->text); directory = QString(dirT->text); return true; } bool CCD::startRecording() { ISwitchVectorProperty *svp = baseDevice->getSwitch("RECORD_STREAM"); if (svp == nullptr) return false; ISwitch *recordON = IUFindSwitch(svp, "RECORD_ON"); if (recordON == nullptr) return false; if (recordON->s == ISS_ON) return true; IUResetSwitch(svp); recordON->s = ISS_ON; clientManager->sendNewSwitch(svp); return true; } bool CCD::startDurationRecording(double duration) { INumberVectorProperty *nvp = baseDevice->getNumber("RECORD_OPTIONS"); if (nvp == nullptr) return false; INumber *durationN = IUFindNumber(nvp, "RECORD_DURATION"); if (durationN == nullptr) return false; ISwitchVectorProperty *svp = baseDevice->getSwitch("RECORD_STREAM"); if (svp == nullptr) return false; ISwitch *recordON = IUFindSwitch(svp, "RECORD_DURATION_ON"); if (recordON == nullptr) return false; if (recordON->s == ISS_ON) return true; durationN->value = duration; clientManager->sendNewNumber(nvp); IUResetSwitch(svp); recordON->s = ISS_ON; clientManager->sendNewSwitch(svp); return true; } bool CCD::startFramesRecording(uint32_t frames) { INumberVectorProperty *nvp = baseDevice->getNumber("RECORD_OPTIONS"); if (nvp == nullptr) return false; INumber *frameN = IUFindNumber(nvp, "RECORD_FRAME_TOTAL"); ISwitchVectorProperty *svp = baseDevice->getSwitch("RECORD_STREAM"); if (frameN == nullptr || svp == nullptr) return false; ISwitch *recordON = IUFindSwitch(svp, "RECORD_FRAME_ON"); if (recordON == nullptr) return false; if (recordON->s == ISS_ON) return true; frameN->value = frames; clientManager->sendNewNumber(nvp); IUResetSwitch(svp); recordON->s = ISS_ON; clientManager->sendNewSwitch(svp); return true; } bool CCD::stopRecording() { ISwitchVectorProperty *svp = baseDevice->getSwitch("RECORD_STREAM"); if (svp == nullptr) return false; ISwitch *recordOFF = IUFindSwitch(svp, "RECORD_OFF"); if (recordOFF == nullptr) return false; // If already set if (recordOFF->s == ISS_ON) return true; IUResetSwitch(svp); recordOFF->s = ISS_ON; clientManager->sendNewSwitch(svp); return true; } bool CCD::setFITSHeader(const QMap &values) { ITextVectorProperty *tvp = baseDevice->getText("FITS_HEADER"); if (tvp == nullptr) return false; QMapIterator i(values); while (i.hasNext()) { i.next(); IText *headerT = IUFindText(tvp, i.key().toLatin1().data()); if (headerT == nullptr) continue; IUSaveText(headerT, i.value().toLatin1().data()); } clientManager->sendNewText(tvp); return true; } bool CCD::setGain(double value) { if (gainN == nullptr) return false; gainN->value = value; clientManager->sendNewNumber(gainN->nvp); return true; } bool CCD::getGain(double *value) { if (gainN == nullptr) return false; *value = gainN->value; return true; } bool CCD::getGainMinMaxStep(double *min, double *max, double *step) { if (gainN == nullptr) return false; *min = gainN->min; *max = gainN->max; *step = gainN->step; return true; } bool CCD::isBLOBEnabled() { return (clientManager->isBLOBEnabled(getDeviceName(), "CCD1")); } bool CCD::setBLOBEnabled(bool enable, const QString &prop) { clientManager->setBLOBEnabled(enable, getDeviceName(), prop); return true; } bool CCD::setExposureLoopingEnabled(bool enable) { // Set value immediately IsLooping = enable; ISwitchVectorProperty *svp = baseDevice->getSwitch("CCD_EXPOSURE_LOOP"); if (svp == nullptr) return false; svp->sp[0].s = enable ? ISS_ON : ISS_OFF; svp->sp[1].s = enable ? ISS_OFF : ISS_ON; clientManager->sendNewSwitch(svp); return true; } bool CCD::setExposureLoopCount(uint32_t count) { INumberVectorProperty *nvp = baseDevice->getNumber("CCD_EXPOSURE_LOOP_COUNT"); if (nvp == nullptr) return false; nvp->np[0].value = count; clientManager->sendNewNumber(nvp); return true; } bool CCD::setStreamExposure(double duration) { INumberVectorProperty *nvp = baseDevice->getNumber("STREAMING_EXPOSURE"); if (nvp == nullptr) return false; nvp->np[0].value = duration; clientManager->sendNewNumber(nvp); return true; } bool CCD::getStreamExposure(double *duration) { INumberVectorProperty *nvp = baseDevice->getNumber("STREAMING_EXPOSURE"); if (nvp == nullptr) return false; *duration = nvp->np[0].value; return true; } bool CCD::isCoolerOn() { ISwitchVectorProperty *svp = baseDevice->getSwitch("CCD_COOLER"); if (svp == nullptr) return false; return (svp->sp[0].s == ISS_ON); } } diff --git a/kstars/indi/indilistener.cpp b/kstars/indi/indilistener.cpp index bf9965d92..f50f147a7 100644 --- a/kstars/indi/indilistener.cpp +++ b/kstars/indi/indilistener.cpp @@ -1,472 +1,472 @@ /* INDI Listener Copyright (C) 2012 Jasem Mutlaq (mutlaqja@ikarustech.com) This application is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version. Handle INDI Standard properties. */ #include "indilistener.h" #include "clientmanager.h" #include "deviceinfo.h" #include "indicap.h" #include "indiccd.h" #include "indidome.h" #include "indifilter.h" #include "indifocuser.h" #include "indilightbox.h" #include "inditelescope.h" #include "indiweather.h" #include "kstars.h" #include "Options.h" #include "auxiliary/ksnotification.h" #include #include #include #define NINDI_STD 35 /* INDI standard property used across all clients to enable interoperability. */ static const char *indi_std[NINDI_STD] = { "CONNECTION", "DEVICE_PORT", "TIME_UTC", "TIME_LST", "GEOGRAPHIC_COORD", "EQUATORIAL_COORD", "EQUATORIAL_EOD_COORD", "EQUATORIAL_EOD_COORD_REQUEST", "HORIZONTAL_COORD", "TELESCOPE_ABORT_MOTION", "ON_COORD_SET", "SOLAR_SYSTEM", "TELESCOPE_MOTION_NS", "TELESCOPE_MOTION_WE", "TELESCOPE_PARK", "DOME_PARK", "GPS_REFRESH", "WEATHER_STATUS", "CCD_EXPOSURE", "CCD_TEMPERATURE", "CCD_FRAME", "CCD_FRAME_TYPE", "CCD_BINNING", "CCD_INFO", "CCD_VIDEO_STREAM", "RAW_STREAM", "IMAGE_STREAM", "FOCUS_SPEED", "FOCUS_MOTION", "FOCUS_TIMER", "FILTER_SLOT", "WATCHDOG_HEARTBEAT", "CAP_PARK", "FLAT_LIGHT_CONTROL", "FLAT_LIGHT_INTENSITY" }; INDIListener *INDIListener::_INDIListener = nullptr; INDIListener *INDIListener::Instance() { if (_INDIListener == nullptr) { _INDIListener = new INDIListener(KStars::Instance()); connect(_INDIListener, &INDIListener::newTelescope, [&]() { KStars::Instance()->slotSetTelescopeEnabled(true); }); connect(_INDIListener, &INDIListener::newDome, [&]() { KStars::Instance()->slotSetDomeEnabled(true); }); } return _INDIListener; } INDIListener::INDIListener(QObject *parent) : QObject(parent) { } INDIListener::~INDIListener() { qDeleteAll(devices); qDeleteAll(st4Devices); } bool INDIListener::isStandardProperty(const QString &name) { for (auto &item : indi_std) { if (!strcmp(name.toLatin1().constData(), item)) return true; } return false; } ISD::GDInterface *INDIListener::getDevice(const QString &name) { for (auto &oneDevice : devices) { if (oneDevice->getDeviceName() == name) return oneDevice; } return nullptr; } void INDIListener::addClient(ClientManager *cm) { qCDebug(KSTARS_INDI) << "INDIListener: Adding a new client manager to INDI listener.."; clients.append(cm); connect(cm, &ClientManager::newINDIDevice, this, &INDIListener::processDevice, Qt::BlockingQueuedConnection); connect(cm, &ClientManager::newINDIProperty, this, &INDIListener::registerProperty); connect(cm, &ClientManager::removeINDIDevice, this, &INDIListener::removeDevice); connect(cm, &ClientManager::removeINDIProperty, this, &INDIListener::removeProperty); connect(cm, &ClientManager::newINDISwitch, this, &INDIListener::processSwitch); connect(cm, &ClientManager::newINDIText, this, &INDIListener::processText); connect(cm, &ClientManager::newINDINumber, this, &INDIListener::processNumber); connect(cm, &ClientManager::newINDILight, this, &INDIListener::processLight); connect(cm, &ClientManager::newINDIBLOB, this, &INDIListener::processBLOB); #if INDI_VERSION_MAJOR >= 1 && INDI_VERSION_MINOR >= 5 connect(cm, &ClientManager::newINDIUniversalMessage, this, &INDIListener::processUniversalMessage); #endif } void INDIListener::removeClient(ClientManager *cm) { qCDebug(KSTARS_INDI) << "INDIListener: Removing client manager for server" << cm->getHost() << "@" << cm->getPort(); QList::iterator it = devices.begin(); clients.removeOne(cm); while (it != devices.end()) { DriverInfo *dv = (*it)->getDriverInfo(); bool hostSource = (dv->getDriverSource() == HOST_SOURCE) || (dv->getDriverSource() == GENERATED_SOURCE); if (cm->isDriverManaged(dv)) { // If we have multiple devices per driver, we need to remove them all if (dv->getAuxInfo().value("mdpd", false).toBool() == true) { while (it != devices.end()) { if (dv->getDevice((*it)->getDeviceName()) != nullptr) { it = devices.erase(it); } else break; } } else it = devices.erase(it); cm->removeManagedDriver(dv); cm->disconnect(this); if (hostSource) return; } else ++it; } } void INDIListener::processDevice(DeviceInfo *dv) { qCDebug(KSTARS_INDI) << "INDIListener: New device" << dv->getBaseDevice()->getDeviceName(); ISD::GDInterface *gd = new ISD::GenericDevice(*dv); devices.append(gd); emit newDevice(gd); } //void INDIListener::removeDevice(DeviceInfo *dv) //{ // qCDebug(KSTARS_INDI) << "INDIListener: Removing device" << dv->getBaseDevice()->getDeviceName() << "with unique label " // << dv->getDriverInfo()->getUniqueLabel(); // foreach (ISD::GDInterface *gd, devices) // { // if (gd->getDeviceInfo() == dv) // { // emit deviceRemoved(gd); // devices.removeOne(gd); // delete (gd); // } // } //} void INDIListener::removeDevice(const QString &deviceName) { qCDebug(KSTARS_INDI) << "INDIListener: Removing device" << deviceName; for (ISD::GDInterface *oneDevice : devices) { if (oneDevice->getDeviceName() == deviceName) { emit deviceRemoved(oneDevice); devices.removeOne(oneDevice); delete (oneDevice); break; } } } void INDIListener::registerProperty(INDI::Property *prop) { if (!prop->getRegistered()) return; qCDebug(KSTARS_INDI) << "<" << prop->getDeviceName() << ">: <" << prop->getName() << ">"; for (auto oneDevice : devices) { if (oneDevice->getDeviceName() == prop->getDeviceName()) { if (!strcmp(prop->getName(), "ON_COORD_SET") || !strcmp(prop->getName(), "EQUATORIAL_EOD_COORD") || !strcmp(prop->getName(), "EQUATORIAL_COORD") || !strcmp(prop->getName(), "HORIZONTAL_COORD")) { if (oneDevice->getType() == KSTARS_UNKNOWN) { devices.removeOne(oneDevice); oneDevice = new ISD::Telescope(oneDevice); devices.append(oneDevice); } emit newTelescope(oneDevice); } else if (!strcmp(prop->getName(), "CCD_EXPOSURE")) { - //if (gd->getType() != KSTARS_CCD) - if (oneDevice->getDriverInterface() & INDI::BaseDevice::CCD_INTERFACE) + //if (oneDevice->getDriverInterface() & INDI::BaseDevice::CCD_INTERFACE) + if (oneDevice->getType() == KSTARS_UNKNOWN) { devices.removeOne(oneDevice); oneDevice = new ISD::CCD(oneDevice); devices.append(oneDevice); } emit newCCD(oneDevice); } else if (!strcmp(prop->getName(), "FILTER_NAME")) { if (oneDevice->getType() == KSTARS_UNKNOWN) { devices.removeOne(oneDevice); oneDevice = new ISD::Filter(oneDevice); devices.append(oneDevice); } emit newFilter(oneDevice); } else if (!strcmp(prop->getName(), "FOCUS_MOTION")) { if (oneDevice->getType() == KSTARS_UNKNOWN) { devices.removeOne(oneDevice); oneDevice = new ISD::Focuser(oneDevice); devices.append(oneDevice); } emit newFocuser(oneDevice); } else if (!strcmp(prop->getName(), "DOME_SHUTTER") || !strcmp(prop->getName(), "DOME_MOTION")) { if (oneDevice->getType() == KSTARS_UNKNOWN) { devices.removeOne(oneDevice); oneDevice = new ISD::Dome(oneDevice); devices.append(oneDevice); } emit newDome(oneDevice); } else if (!strcmp(prop->getName(), "WEATHER_STATUS")) { if (oneDevice->getType() == KSTARS_UNKNOWN) { devices.removeOne(oneDevice); oneDevice = new ISD::Weather(oneDevice); devices.append(oneDevice); } emit newWeather(oneDevice); } else if (!strcmp(prop->getName(), "CAP_PARK")) { if (oneDevice->getType() == KSTARS_UNKNOWN) { devices.removeOne(oneDevice); oneDevice = new ISD::DustCap(oneDevice); devices.append(oneDevice); } emit newDustCap(oneDevice); } else if (!strcmp(prop->getName(), "FLAT_LIGHT_CONTROL")) { // If light box part of dust cap if (oneDevice->getType() == KSTARS_UNKNOWN) { if (oneDevice->getBaseDevice()->getDriverInterface() & INDI::BaseDevice::DUSTCAP_INTERFACE) { devices.removeOne(oneDevice); oneDevice = new ISD::DustCap(oneDevice); devices.append(oneDevice); emit newDustCap(oneDevice); } // If stand-alone light box else { devices.removeOne(oneDevice); oneDevice = new ISD::LightBox(oneDevice); devices.append(oneDevice); emit newLightBox(oneDevice); } } } if (!strcmp(prop->getName(), "TELESCOPE_TIMED_GUIDE_WE")) { ISD::ST4 *st4Driver = new ISD::ST4(oneDevice->getBaseDevice(), oneDevice->getDriverInfo()->getClientManager()); st4Devices.append(st4Driver); emit newST4(st4Driver); } oneDevice->registerProperty(prop); break; } } } void INDIListener::removeProperty(const QString &device, const QString &name) { for (auto &oneDevice : devices) { if (oneDevice->getDeviceName() == device) { oneDevice->removeProperty(name); return; } } } void INDIListener::processSwitch(ISwitchVectorProperty *svp) { for (auto &oneDevice : devices) { if (oneDevice->getDeviceName() == svp->device) { oneDevice->processSwitch(svp); break; } } } void INDIListener::processNumber(INumberVectorProperty *nvp) { for (auto &oneDevice : devices) { if (oneDevice->getDeviceName() == nvp->device) { oneDevice->processNumber(nvp); break; } } } void INDIListener::processText(ITextVectorProperty *tvp) { for (auto &oneDevice : devices) { if (oneDevice->getDeviceName() == tvp->device) { oneDevice->processText(tvp); break; } } } void INDIListener::processLight(ILightVectorProperty *lvp) { for (auto &oneDevice : devices) { if (oneDevice->getDeviceName() == lvp->device) { oneDevice->processLight(lvp); break; } } } void INDIListener::processBLOB(IBLOB *bp) { for (auto &oneDevice : devices) { if (oneDevice->getDeviceName() == bp->bvp->device) { oneDevice->processBLOB(bp); break; } } } void INDIListener::processMessage(INDI::BaseDevice *dp, int messageID) { for (auto &oneDevice : devices) { if (oneDevice->getDeviceName() == dp->getDeviceName()) { oneDevice->processMessage(messageID); break; } } } void INDIListener::processUniversalMessage(const QString &message) { QString displayMessage = message; // Remove timestamp info as it is not suitable for message box int colonIndex = displayMessage.indexOf(": "); if (colonIndex > 0) displayMessage = displayMessage.mid(colonIndex + 2); // Special case for Alignment since it is not tied to a device if (displayMessage.startsWith("[ALIGNMENT]")) { qCDebug(KSTARS_INDI) << "AlignmentSubSystem:" << displayMessage; return; } if (Options::messageNotificationINDI()) { KNotification::event(QLatin1String("IndiServerMessage"), displayMessage + " (INDI)"); } else { KSNotification::transient(displayMessage, i18n("INDI Server Message")); } }