diff --git a/kstars/ekos/capture/capture.cpp b/kstars/ekos/capture/capture.cpp index fb4e8bd14..86f94c8c9 100644 --- a/kstars/ekos/capture/capture.cpp +++ b/kstars/ekos/capture/capture.cpp @@ -1,5838 +1,5860 @@ /* Ekos Copyright (C) 2012 Jasem Mutlaq This application is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version. */ #include "capture.h" #include "captureadaptor.h" #include "dslrinfodialog.h" #include "kstars.h" #include "kstarsdata.h" #include "Options.h" #include "rotatorsettings.h" #include "sequencejob.h" #include "skymap.h" #include "ui_calibrationoptions.h" #include "auxiliary/QProgressIndicator.h" #include "ekos/manager.h" #include "ekos/auxiliary/darklibrary.h" #include "fitsviewer/fitsdata.h" #include "fitsviewer/fitsview.h" #include "indi/driverinfo.h" #include "indi/indifilter.h" #include "indi/clientmanager.h" #include "oal/observeradd.h" #include #include #define MF_TIMER_TIMEOUT 90000 #define GD_TIMER_TIMEOUT 60000 #define MF_RA_DIFF_LIMIT 4 // Wait 3-minutes as maximum beyond exposure // value. #define CAPTURE_TIMEOUT_THRESHOLD 180000 // Current Sequence File Format: #define SQ_FORMAT_VERSION 2.0 // We accept file formats with version back to: #define SQ_COMPAT_VERSION 2.0 namespace Ekos { Capture::Capture() { setupUi(this); qRegisterMetaType("Ekos::CaptureState"); qDBusRegisterMetaType(); new CaptureAdaptor(this); QDBusConnection::sessionBus().registerObject("/KStars/Ekos/Capture", this); + QPointer ekosInterface = new QDBusInterface("org.kde.kstars", "/KStars/Ekos", "org.kde.kstars.Ekos", + QDBusConnection::sessionBus(), this); + + // Connecting DBus signals + connect(ekosInterface, SIGNAL(newModule(QString)), this, SLOT(registerNewModule(QString))); + + // ensure that the mount interface is present + registerNewModule("Mount"); + KStarsData::Instance()->userdb()->GetAllDSLRInfos(DSLRInfos); dirPath = QUrl::fromLocalFile(QDir::homePath()); //isAutoGuiding = false; rotatorSettings.reset(new RotatorSettings(this)); pi = new QProgressIndicator(this); progressLayout->addWidget(pi, 0, 4, 1, 1); seqFileCount = 0; //seqWatcher = new KDirWatch(); seqTimer = new QTimer(this); connect(seqTimer, &QTimer::timeout, this, &Ekos::Capture::captureImage); connect(startB, &QPushButton::clicked, this, &Ekos::Capture::toggleSequence); connect(pauseB, &QPushButton::clicked, this, &Ekos::Capture::pause); startB->setIcon(QIcon::fromTheme("media-playback-start")); startB->setAttribute(Qt::WA_LayoutUsesWidgetRect); pauseB->setIcon(QIcon::fromTheme("media-playback-pause")); pauseB->setAttribute(Qt::WA_LayoutUsesWidgetRect); filterManagerB->setIcon(QIcon::fromTheme("view-filter")); filterManagerB->setAttribute(Qt::WA_LayoutUsesWidgetRect); FilterDevicesCombo->addItem("--"); connect(binXIN, static_cast(&QSpinBox::valueChanged), binYIN, &QSpinBox::setValue); connect(CCDCaptureCombo, static_cast(&QComboBox::activated), this, &Ekos::Capture::setDefaultCCD); connect(CCDCaptureCombo, static_cast(&QComboBox::activated), this, &Ekos::Capture::checkCCD); connect(liveVideoB, &QPushButton::clicked, this, &Ekos::Capture::toggleVideo); guideDeviationTimer.setInterval(GD_TIMER_TIMEOUT); connect(&guideDeviationTimer, &QTimer::timeout, this, &Ekos::Capture::checkGuideDeviationTimeout); connect(clearConfigurationB, &QPushButton::clicked, this, &Ekos::Capture::clearCameraConfiguration); connect(FilterDevicesCombo, static_cast(&QComboBox::activated), this, &Ekos::Capture::checkFilter); connect(temperatureCheck, &QCheckBox::toggled, [this](bool toggled) { if (currentCCD) { QVariantMap auxInfo = currentCCD->getDriverInfo()->getAuxInfo(); auxInfo[QString("%1_TC").arg(currentCCD->getDeviceName())] = toggled; currentCCD->getDriverInfo()->setAuxInfo(auxInfo); } }); connect(FilterPosCombo, static_cast(&QComboBox::currentIndexChanged), [=]() { updateHFRThreshold(); }); connect(previewB, &QPushButton::clicked, this, &Ekos::Capture::captureOne); //connect( seqWatcher, SIGNAL(dirty(QString)), this, &Ekos::Capture::checkSeqFile(QString))); connect(addToQueueB, &QPushButton::clicked, this, &Ekos::Capture::addJob); connect(removeFromQueueB, &QPushButton::clicked, this, &Ekos::Capture::removeJob); connect(queueUpB, &QPushButton::clicked, this, &Ekos::Capture::moveJobUp); connect(queueDownB, &QPushButton::clicked, this, &Ekos::Capture::moveJobDown); connect(selectFITSDirB, &QPushButton::clicked, this, &Ekos::Capture::saveFITSDirectory); connect(queueSaveB, &QPushButton::clicked, this, static_cast(&Ekos::Capture::saveSequenceQueue)); connect(queueSaveAsB, &QPushButton::clicked, this, &Ekos::Capture::saveSequenceQueueAs); connect(queueLoadB, &QPushButton::clicked, this, static_cast(&Ekos::Capture::loadSequenceQueue)); connect(resetB, &QPushButton::clicked, this, &Ekos::Capture::resetJobs); connect(queueTable, &QAbstractItemView::doubleClicked, this, &Ekos::Capture::editJob); connect(queueTable, &QTableWidget::itemSelectionChanged, this, &Ekos::Capture::resetJobEdit); connect(setTemperatureB, &QPushButton::clicked, [&]() { if (currentCCD) currentCCD->setTemperature(temperatureIN->value()); }); connect(coolerOnB, &QPushButton::clicked, [&]() { if (currentCCD) currentCCD->setCoolerControl(true); }); connect(coolerOffB, &QPushButton::clicked, [&]() { if (currentCCD) currentCCD->setCoolerControl(false); }); connect(temperatureIN, &QDoubleSpinBox::editingFinished, setTemperatureB, static_cast(&QPushButton::setFocus)); connect(frameTypeCombo, static_cast(&QComboBox::activated), this, &Ekos::Capture::checkFrameType); connect(resetFrameB, &QPushButton::clicked, this, &Ekos::Capture::resetFrame); connect(calibrationB, &QPushButton::clicked, this, &Ekos::Capture::openCalibrationDialog); connect(rotatorB, &QPushButton::clicked, rotatorSettings.get(), &Ekos::Capture::show); addToQueueB->setIcon(QIcon::fromTheme("list-add")); addToQueueB->setAttribute(Qt::WA_LayoutUsesWidgetRect); removeFromQueueB->setIcon(QIcon::fromTheme("list-remove")); removeFromQueueB->setAttribute(Qt::WA_LayoutUsesWidgetRect); queueUpB->setIcon(QIcon::fromTheme("go-up")); queueUpB->setAttribute(Qt::WA_LayoutUsesWidgetRect); queueDownB->setIcon(QIcon::fromTheme("go-down")); queueDownB->setAttribute(Qt::WA_LayoutUsesWidgetRect); selectFITSDirB->setIcon( QIcon::fromTheme("document-open-folder")); selectFITSDirB->setAttribute(Qt::WA_LayoutUsesWidgetRect); queueLoadB->setIcon(QIcon::fromTheme("document-open")); queueLoadB->setAttribute(Qt::WA_LayoutUsesWidgetRect); queueSaveB->setIcon(QIcon::fromTheme("document-save")); queueSaveB->setAttribute(Qt::WA_LayoutUsesWidgetRect); queueSaveAsB->setIcon(QIcon::fromTheme("document-save-as")); queueSaveAsB->setAttribute(Qt::WA_LayoutUsesWidgetRect); resetB->setIcon(QIcon::fromTheme("system-reboot")); resetB->setAttribute(Qt::WA_LayoutUsesWidgetRect); resetFrameB->setIcon(QIcon::fromTheme("view-refresh")); resetFrameB->setAttribute(Qt::WA_LayoutUsesWidgetRect); calibrationB->setIcon(QIcon::fromTheme("run-build")); calibrationB->setAttribute(Qt::WA_LayoutUsesWidgetRect); rotatorB->setIcon(QIcon::fromTheme("kstars_solarsystem")); rotatorB->setAttribute(Qt::WA_LayoutUsesWidgetRect); addToQueueB->setToolTip(i18n("Add job to sequence queue")); removeFromQueueB->setToolTip(i18n("Remove job from sequence queue")); fitsDir->setText(Options::fitsDir()); for (auto &filter : FITSViewer::filterTypes) filterCombo->addItem(filter); guideDeviationCheck->setChecked(Options::enforceGuideDeviation()); guideDeviation->setValue(Options::guideDeviation()); autofocusCheck->setChecked(Options::enforceAutofocus()); refocusEveryNCheck->setChecked(Options::enforceRefocusEveryN()); meridianCheck->setChecked(Options::autoMeridianFlip()); meridianHours->setValue(Options::autoMeridianHours()); QCheckBox * const checkBoxes[] = { guideDeviationCheck, refocusEveryNCheck, guideDeviationCheck, meridianCheck }; for (const QCheckBox* control : checkBoxes) connect(control, &QCheckBox::toggled, this, &Ekos::Capture::setDirty); QDoubleSpinBox *const dspinBoxes[] { HFRPixels, guideDeviation, meridianHours }; for (const QDoubleSpinBox *control : dspinBoxes) connect(control, static_cast(&QDoubleSpinBox::valueChanged), this, &Ekos::Capture::setDirty); connect(uploadModeCombo, static_cast(&QComboBox::activated), this, &Ekos::Capture::setDirty); connect(remoteDirIN, &QLineEdit::editingFinished, this, &Ekos::Capture::setDirty); m_ObserverName = Options::defaultObserver(); observerB->setIcon(QIcon::fromTheme("im-user")); observerB->setAttribute(Qt::WA_LayoutUsesWidgetRect); connect(observerB, &QPushButton::clicked, this, &Ekos::Capture::showObserverDialog); // Exposure Timeout captureTimeout.setSingleShot(true); connect(&captureTimeout, &QTimer::timeout, this, &Ekos::Capture::processCaptureTimeout); // Post capture script connect(&postCaptureScript, static_cast(&QProcess::finished), this, &Ekos::Capture::postScriptFinished); // Remote directory connect(uploadModeCombo, static_cast(&QComboBox::activated), this, [&](int index) { remoteDirIN->setEnabled(index != 0); }); customPropertiesDialog.reset(new CustomProperties()); connect(customValuesB, &QPushButton::clicked, [&]() { customPropertiesDialog.get()->show(); customPropertiesDialog.get()->raise(); }); flatFieldSource = static_cast(Options::calibrationFlatSourceIndex()); flatFieldDuration = static_cast(Options::calibrationFlatDurationIndex()); wallCoord.setAz(Options::calibrationWallAz()); wallCoord.setAlt(Options::calibrationWallAlt()); targetADU = Options::calibrationADUValue(); targetADUTolerance = Options::calibrationADUValueTolerance(); fitsDir->setText(Options::captureDirectory()); connect(fitsDir, &QLineEdit::textChanged, [&]() { Options::setCaptureDirectory(fitsDir->text());}); if (Options::remoteCaptureDirectory().isEmpty() == false) { remoteDirIN->setText(Options::remoteCaptureDirectory()); } connect(remoteDirIN, &QLineEdit::editingFinished, [&]() { Options::setRemoteCaptureDirectory(remoteDirIN->text());}); // Keep track of TARGET transfer format when changing CCDs (FITS or NATIVE). Actual format is not changed until capture connect( transferFormatCombo, static_cast(&QComboBox::activated), this, [&](int index) { if (currentCCD) currentCCD->setTargetTransferFormat(static_cast(index)); Options::setCaptureFormatIndex(index); }); // Load FIlter Offets //loadFilterOffsets(); //Note: This is to prevent a button from being called the default button //and then executing when the user hits the enter key such as when on a Text Box QList qButtons = findChildren(); for (auto &button : qButtons) button->setAutoDefault(false); } Capture::~Capture() { qDeleteAll(jobs); } void Capture::setDefaultCCD(QString ccd) { Options::setDefaultCaptureCCD(ccd); } void Capture::addCCD(ISD::GDInterface *newCCD) { ISD::CCD *ccd = static_cast(newCCD); if (CCDs.contains(ccd)) return; CCDs.append(ccd); CCDCaptureCombo->addItem(ccd->getDeviceName()); if (Filters.count() > 0) syncFilterInfo(); checkCCD(); } void Capture::addGuideHead(ISD::GDInterface *newCCD) { QString guiderName = newCCD->getDeviceName() + QString(" Guider"); if (CCDCaptureCombo->findText(guiderName) == -1) { CCDCaptureCombo->addItem(guiderName); CCDs.append(static_cast(newCCD)); } } void Capture::addFilter(ISD::GDInterface *newFilter) { foreach (ISD::GDInterface *filter, Filters) { if (!strcmp(filter->getDeviceName(), newFilter->getDeviceName())) return; } FilterDevicesCombo->addItem(newFilter->getDeviceName()); Filters.append(static_cast(newFilter)); filterManagerB->setEnabled(true); checkFilter(1); FilterDevicesCombo->setCurrentIndex(1); } void Capture::pause() { pauseFunction = nullptr; m_State = CAPTURE_PAUSED; emit newStatus(Ekos::CAPTURE_PAUSED); appendLogText(i18n("Sequence shall be paused after current exposure is complete.")); pauseB->setEnabled(false); startB->setIcon(QIcon::fromTheme("media-playback-start")); startB->setToolTip(i18n("Resume Sequence")); } void Capture::toggleSequence() { if (m_State == CAPTURE_PAUSED) { startB->setIcon( QIcon::fromTheme("media-playback-stop")); startB->setToolTip(i18n("Stop Sequence")); pauseB->setEnabled(true); m_State = CAPTURE_CAPTURING; emit newStatus(Ekos::CAPTURE_CAPTURING); appendLogText(i18n("Sequence resumed.")); // Call from where ever we have left of when we paused if (pauseFunction) (this->*pauseFunction)(); } else if (m_State == CAPTURE_IDLE || m_State == CAPTURE_ABORTED || m_State == CAPTURE_COMPLETE) { start(); } else { abort(); } } + +void Capture::registerNewModule(const QString &name) +{ + if (name == "Mount") + { + if (mountInterface != nullptr) + delete mountInterface; + mountInterface = new QDBusInterface("org.kde.kstars", "/KStars/Ekos/Mount", "org.kde.kstars.Ekos.Mount", + QDBusConnection::sessionBus(), this); + } + +} + + + void Capture::start() { if (darkSubCheck->isChecked()) { KMessageBox::error(this, i18n("Auto dark subtract is not supported in batch mode.")); return; } Options::setGuideDeviation(guideDeviation->value()); Options::setEnforceGuideDeviation(guideDeviationCheck->isChecked()); Options::setEnforceAutofocus(autofocusCheck->isChecked()); Options::setEnforceRefocusEveryN(refocusEveryNCheck->isChecked()); Options::setAutoMeridianFlip(meridianCheck->isChecked()); Options::setAutoMeridianHours(meridianHours->value()); // Reset progress option if there is no captured frame map set at the time of start - fixes the end-user setting the option just before starting ignoreJobProgress = !capturedFramesMap.count() && Options::alwaysResetSequenceWhenStarting(); if (queueTable->rowCount() == 0) { if (addJob() == false) return; } SequenceJob *first_job = nullptr; foreach (SequenceJob *job, jobs) { if (job->getStatus() == SequenceJob::JOB_IDLE || job->getStatus() == SequenceJob::JOB_ABORTED) { first_job = job; break; } } // If there are no idle nor aborted jobs, question is whether to reset and restart // Scheduler will start a non-empty new job each time and doesn't use this execution path if (first_job == nullptr) { // If we have at least one job that are in error, bail out, even if ignoring job progress foreach (SequenceJob *job, jobs) { if (job->getStatus() != SequenceJob::JOB_DONE) { appendLogText(i18n("No pending jobs found. Please add a job to the sequence queue.")); return; } } // If we only have completed jobs and we don't ignore job progress, ask the end-user what to do if (!ignoreJobProgress) if(KMessageBox::warningContinueCancel( nullptr, i18n("All jobs are complete. Do you want to reset the status of all jobs and restart capturing?"), i18n("Reset job status"), KStandardGuiItem::cont(), KStandardGuiItem::cancel(), "reset_job_complete_status_warning") != KMessageBox::Continue) return; // If the end-user accepted to reset, reset all jobs and restart foreach (SequenceJob *job, jobs) job->resetStatus(); first_job = jobs.first(); } // If we need to ignore job progress, systematically reset all jobs and restart // Scheduler will never ignore job progress and doesn't use this path else if (ignoreJobProgress) { appendLogText(i18n("Warning: option \"Always Reset Sequence When Starting\" is enabled and resets the sequence counts.")); foreach (SequenceJob *job, jobs) job->resetStatus(); } - // Record initialHA and initialMount position when we are starting fresh - // If recovering from deviation error, these values should not be recorded. // Refocus timer should not be reset on deviation error if (m_DeviationDetected == false && m_State != CAPTURE_SUSPENDED) { - initialHA = getCurrentHA(); - qCDebug(KSTARS_EKOS_CAPTURE) << "Initial hour angle:" << initialHA; meridianFlipStage = MF_NONE; - // Record initial mount coordinates that we may use later to perform a meridian flip - if (currentTelescope) - { - double initialRA, initialDE; - currentTelescope->getEqCoords(&initialRA, &initialDE); - if (currentTelescope->isJ2000()) - { - initialMountCoords.setRA0(initialRA); - initialMountCoords.setDec0(initialDE); - initialMountCoords.apparentCoord(static_cast(J2000), KStars::Instance()->data()->ut().djd()); - } - else - { - initialMountCoords.setRA(initialRA); - initialMountCoords.setDec(initialDE); - } - - qCDebug(KSTARS_EKOS_CAPTURE) << "Initial mount coordinates RA:" << initialMountCoords.ra().toHMSString() - << "DE:" << initialMountCoords.dec().toDMSString(); - } - // start timer to measure time until next forced refocus startRefocusEveryNTimer(); } // Only reset these counters if we are NOT restarting from deviation errors // So when starting a new job or fresh then we reset them. if (m_DeviationDetected == false) { ditherCounter = Options::ditherFrames(); inSequenceFocusCounter = Options::inSequenceCheckFrames(); } m_DeviationDetected = false; m_SpikeDetected = false; m_State = CAPTURE_PROGRESS; emit newStatus(Ekos::CAPTURE_PROGRESS); startB->setIcon(QIcon::fromTheme("media-playback-stop")); startB->setToolTip(i18n("Stop Sequence")); pauseB->setEnabled(true); setBusy(true); if (guideDeviationCheck->isChecked() && autoGuideReady == false) appendLogText(i18n("Warning: Guide deviation is selected but autoguide process was not started.")); if (autofocusCheck->isChecked() && autoFocusReady == false) appendLogText(i18n("Warning: in-sequence focusing is selected but autofocus process was not started.")); prepareJob(first_job); } void Capture::stop(CaptureState targetState) { retries = 0; //seqTotalCount = 0; //seqCurrentCount = 0; captureTimeout.stop(); ADURaw.clear(); ExpRaw.clear(); if (activeJob) { if (activeJob->getStatus() == SequenceJob::JOB_BUSY) { QString stopText; switch (targetState) { case CAPTURE_IDLE: stopText = i18n("CCD capture stopped"); break; case CAPTURE_SUSPENDED: stopText = i18n("CCD capture suspended"); break; default: stopText = i18n("CCD capture aborted"); break; } KSNotification::event(QLatin1String("CaptureFailed"), stopText); appendLogText(stopText); activeJob->abort(); if (activeJob->isPreview() == false) { int index = jobs.indexOf(activeJob); QJsonObject oneSequence = m_SequenceArray[index].toObject(); oneSequence["Status"] = "Aborted"; m_SequenceArray.replace(index, oneSequence); emit sequenceChanged(m_SequenceArray); } emit newStatus(targetState); } // In case of batch job if (activeJob->isPreview() == false) { activeJob->disconnect(this); activeJob->reset(); } // or preview job in calibration stage else if (calibrationStage == CAL_CALIBRATION) { activeJob->disconnect(this); activeJob->reset(); activeJob->setPreview(false); currentCCD->setUploadMode(rememberUploadMode); } // or regular preview job else { currentCCD->setUploadMode(rememberUploadMode); jobs.removeOne(activeJob); // Delete preview job delete (activeJob); activeJob = nullptr; } } calibrationStage = CAL_NONE; m_State = targetState; // Turn off any calibration light, IF they were turned on by Capture module if (dustCap && dustCapLightEnabled) { dustCapLightEnabled = false; dustCap->SetLightEnabled(false); } if (lightBox && lightBoxLightEnabled) { lightBoxLightEnabled = false; lightBox->SetLightEnabled(false); } secondsLabel->clear(); disconnect(currentCCD, &ISD::CCD::BLOBUpdated, this, &Ekos::Capture::newFITS); disconnect(currentCCD, &ISD::CCD::newExposureValue, this, &Ekos::Capture::setExposureProgress); disconnect(currentCCD, &ISD::CCD::ready, this, &Ekos::Capture::ready); currentCCD->setFITSDir(""); // In case of exposure looping, let's abort if (currentCCD->isLooping()) targetChip->abortExposure(); imgProgress->reset(); imgProgress->setEnabled(false); fullImgCountOUT->setText(QString()); currentImgCountOUT->setText(QString()); exposeOUT->setText(QString()); setBusy(false); if (m_State == CAPTURE_ABORTED || m_State == CAPTURE_SUSPENDED) { startB->setIcon( QIcon::fromTheme("media-playback-start")); startB->setToolTip(i18n("Start Sequence")); pauseB->setEnabled(false); } //foreach (QAbstractButton *button, queueEditButtonGroup->buttons()) //button->setEnabled(true); seqTimer->stop(); activeJob = nullptr; } void Capture::sendNewImage(const QString &filename, ISD::CCDChip *myChip) { if (activeJob && (myChip == nullptr || myChip == targetChip)) { emit newImage(activeJob); // We only emit this for client/both images since remote images already send this automatically if (currentCCD->getUploadMode() != ISD::CCD::UPLOAD_LOCAL && activeJob->isPreview() == false) emit newSequenceImage(filename); } } bool Capture::setCamera(const QString &device) { for (int i = 0; i < CCDCaptureCombo->count(); i++) if (device == CCDCaptureCombo->itemText(i)) { CCDCaptureCombo->setCurrentIndex(i); checkCCD(i); return true; } return false; } QString Capture::camera() { if (currentCCD) return currentCCD->getDeviceName(); return QString(); } void Capture::checkCCD(int ccdNum) { if (ccdNum == -1) { ccdNum = CCDCaptureCombo->currentIndex(); if (ccdNum == -1) return; } if (ccdNum <= CCDs.count()) { // Check whether main camera or guide head only currentCCD = CCDs.at(ccdNum); if (CCDCaptureCombo->itemText(ccdNum).right(6) == QString("Guider")) { useGuideHead = true; targetChip = currentCCD->getChip(ISD::CCDChip::GUIDE_CCD); } else { currentCCD = CCDs.at(ccdNum); targetChip = currentCCD->getChip(ISD::CCDChip::PRIMARY_CCD); useGuideHead = false; } // Do not change any settings if we are capturing. if (targetChip && targetChip->isCapturing()) return; foreach (ISD::CCD *ccd, CCDs) { disconnect(ccd, &ISD::CCD::numberUpdated, this, &Ekos::Capture::processCCDNumber); disconnect(ccd, &ISD::CCD::newTemperatureValue, this, &Ekos::Capture::updateCCDTemperature); disconnect(ccd, &ISD::CCD::coolerToggled, this, &Ekos::Capture::setCoolerToggled); disconnect(ccd, &ISD::CCD::newRemoteFile, this, &Ekos::Capture::setNewRemoteFile); disconnect(ccd, &ISD::CCD::videoStreamToggled, this, &Ekos::Capture::setVideoStreamEnabled); disconnect(ccd, &ISD::CCD::ready, this, &Ekos::Capture::ready); } if (currentCCD->hasCoolerControl()) { coolerOnB->setEnabled(true); coolerOffB->setEnabled(true); coolerOnB->setChecked(currentCCD->isCoolerOn()); coolerOffB->setChecked(!currentCCD->isCoolerOn()); } else { coolerOnB->setEnabled(false); coolerOnB->setChecked(false); coolerOffB->setEnabled(false); coolerOffB->setChecked(false); } if (currentCCD->hasCooler()) { temperatureCheck->setEnabled(true); temperatureIN->setEnabled(true); if (currentCCD->getBaseDevice()->getPropertyPermission("CCD_TEMPERATURE") != IP_RO) { double min, max, step; setTemperatureB->setEnabled(true); temperatureIN->setReadOnly(false); temperatureCheck->setEnabled(true); currentCCD->getMinMaxStep("CCD_TEMPERATURE", "CCD_TEMPERATURE_VALUE", &min, &max, &step); temperatureIN->setMinimum(min); temperatureIN->setMaximum(max); temperatureIN->setSingleStep(1); bool isChecked = currentCCD->getDriverInfo()->getAuxInfo().value(QString("%1_TC").arg(currentCCD->getDeviceName()), false).toBool(); temperatureCheck->setChecked(isChecked); } else { setTemperatureB->setEnabled(false); temperatureIN->setReadOnly(true); temperatureCheck->setEnabled(false); temperatureCheck->setChecked(false); } double temperature = 0; if (currentCCD->getTemperature(&temperature)) { temperatureOUT->setText(QString("%L1").arg(temperature, 0, 'f', 2)); if (temperatureIN->cleanText().isEmpty()) temperatureIN->setValue(temperature); } } else { temperatureCheck->setEnabled(false); temperatureIN->setEnabled(false); temperatureIN->clear(); temperatureOUT->clear(); setTemperatureB->setEnabled(false); } updateFrameProperties(); QStringList frameTypes = targetChip->getFrameTypes(); frameTypeCombo->clear(); if (frameTypes.isEmpty()) frameTypeCombo->setEnabled(false); else { frameTypeCombo->setEnabled(true); frameTypeCombo->addItems(frameTypes); frameTypeCombo->setCurrentIndex(targetChip->getFrameType()); } QStringList isoList = targetChip->getISOList(); ISOCombo->clear(); transferFormatCombo->blockSignals(true); transferFormatCombo->clear(); if (isoList.isEmpty()) { ISOCombo->setEnabled(false); ISOLabel->setEnabled(false); // Only one transfer format transferFormatCombo->addItem(i18n("FITS")); } else { ISOCombo->setEnabled(true); ISOLabel->setEnabled(true); ISOCombo->addItems(isoList); ISOCombo->setCurrentIndex(targetChip->getISOIndex()); // DSLRs have two transfer formats transferFormatCombo->addItem(i18n("FITS")); transferFormatCombo->addItem(i18n("Native")); //transferFormatCombo->setCurrentIndex(currentCCD->getTargetTransferFormat()); // 2018-05-07 JM: Set value to the value in options transferFormatCombo->setCurrentIndex(Options::captureFormatIndex()); double pixelX = 0, pixelY = 0; bool rc = targetChip->getPixelSize(pixelX, pixelY); bool isModelInDB = isModelinDSLRInfo(QString(currentCCD->getDeviceName())); // If rc == true, then the property has been defined by the driver already // Only then we check if the pixels are zero if (rc == true && (pixelX == 0 || pixelY == 0 || isModelInDB == false)) { // If model is already in database, no need to show dialog // The zeros above are the initial packets so we can safely ignore them if (isModelInDB == false) { DSLRInfo infoDialog(this, currentCCD); if (infoDialog.exec() == QDialog::Accepted) { addDSLRInfo(QString(currentCCD->getDeviceName()), infoDialog.sensorMaxWidth, infoDialog.sensorMaxHeight, infoDialog.sensorPixelW, infoDialog.sensorPixelH); } } } } transferFormatCombo->blockSignals(false); customPropertiesDialog->setCCD(currentCCD); liveVideoB->setEnabled(currentCCD->hasVideoStream()); if (currentCCD->hasVideoStream()) setVideoStreamEnabled(currentCCD->isStreamingEnabled()); else liveVideoB->setIcon(QIcon::fromTheme("camera-off")); connect(currentCCD, &ISD::CCD::numberUpdated, this, &Ekos::Capture::processCCDNumber, Qt::UniqueConnection); connect(currentCCD, &ISD::CCD::newTemperatureValue, this, &Ekos::Capture::updateCCDTemperature, Qt::UniqueConnection); connect(currentCCD, &ISD::CCD::coolerToggled, this, &Ekos::Capture::setCoolerToggled, Qt::UniqueConnection); connect(currentCCD, &ISD::CCD::newRemoteFile, this, &Ekos::Capture::setNewRemoteFile); connect(currentCCD, &ISD::CCD::videoStreamToggled, this, &Ekos::Capture::setVideoStreamEnabled); connect(currentCCD, &ISD::CCD::ready, this, &Ekos::Capture::ready); } } void Capture::setGuideChip(ISD::CCDChip *chip) { guideChip = chip; // We should suspend guide in two scenarios: // 1. If guide chip is within the primary CCD, then we cannot download any data from guide chip while primary CCD is downloading. // 2. If we have two CCDs running from ONE driver (Multiple-Devices-Per-Driver mpdp is true). Same issue as above, only one download // at a time. // After primary CCD download is complete, we resume guiding. suspendGuideOnDownload = (currentCCD->getChip(ISD::CCDChip::GUIDE_CCD) == guideChip) || (guideChip->getCCD() == currentCCD && currentCCD->getDriverInfo()->getAuxInfo().value("mdpd", false).toBool()); } void Capture::resetFrameToZero() { frameXIN->setMinimum(0); frameXIN->setMaximum(0); frameXIN->setValue(0); frameYIN->setMinimum(0); frameYIN->setMaximum(0); frameYIN->setValue(0); frameWIN->setMinimum(0); frameWIN->setMaximum(0); frameWIN->setValue(0); frameHIN->setMinimum(0); frameHIN->setMaximum(0); frameHIN->setValue(0); } void Capture::updateFrameProperties(int reset) { int binx = 1, biny = 1; double min, max, step; int xstep = 0, ystep = 0; QString frameProp = useGuideHead ? QString("GUIDER_FRAME") : QString("CCD_FRAME"); QString exposureProp = useGuideHead ? QString("GUIDER_EXPOSURE") : QString("CCD_EXPOSURE"); QString exposureElem = useGuideHead ? QString("GUIDER_EXPOSURE_VALUE") : QString("CCD_EXPOSURE_VALUE"); targetChip = useGuideHead ? currentCCD->getChip(ISD::CCDChip::GUIDE_CCD) : currentCCD->getChip(ISD::CCDChip::PRIMARY_CCD); frameWIN->setEnabled(targetChip->canSubframe()); frameHIN->setEnabled(targetChip->canSubframe()); frameXIN->setEnabled(targetChip->canSubframe()); frameYIN->setEnabled(targetChip->canSubframe()); binXIN->setEnabled(targetChip->canBin()); binYIN->setEnabled(targetChip->canBin()); QList exposureValues; exposureValues << 0.01 << 0.02 << 0.05 << 0.1 << 0.2 << 0.25 << 0.5 << 1 << 1.5 << 2 << 2.5 << 3 << 5 << 6 << 7 << 8 << 9 << 10 << 20 << 30 << 40 << 50 << 60 << 120 << 180 << 300 << 600 << 900 << 1200 << 1800; if (currentCCD->getMinMaxStep(exposureProp, exposureElem, &min, &max, &step)) { if (min < 0.001) exposureIN->setDecimals(6); else exposureIN->setDecimals(3); for(int i = 0; i < exposureValues.count(); i++) { double value = exposureValues.at(i); if(value < min || value > max) { exposureValues.removeAt(i); i--; //So we don't skip one } } exposureValues.prepend(min); exposureValues.append(max); } exposureIN->setRecommendedValues(exposureValues); if (currentCCD->getMinMaxStep(frameProp, "WIDTH", &min, &max, &step)) { if (min >= max) { resetFrameToZero(); return; } if (step == 0) xstep = static_cast(max * 0.05); else xstep = step; if (min >= 0 && max > 0) { frameWIN->setMinimum(min); frameWIN->setMaximum(max); frameWIN->setSingleStep(xstep); } } else return; if (currentCCD->getMinMaxStep(frameProp, "HEIGHT", &min, &max, &step)) { if (min >= max) { resetFrameToZero(); return; } if (step == 0) ystep = static_cast(max * 0.05); else ystep = step; if (min >= 0 && max > 0) { frameHIN->setMinimum(min); frameHIN->setMaximum(max); frameHIN->setSingleStep(ystep); } } else return; if (currentCCD->getMinMaxStep(frameProp, "X", &min, &max, &step)) { if (min >= max) { resetFrameToZero(); return; } if (step == 0) step = xstep; if (min >= 0 && max > 0) { frameXIN->setMinimum(min); frameXIN->setMaximum(max); frameXIN->setSingleStep(step); } } else return; if (currentCCD->getMinMaxStep(frameProp, "Y", &min, &max, &step)) { if (min >= max) { resetFrameToZero(); return; } if (step == 0) step = ystep; if (min >= 0 && max > 0) { frameYIN->setMinimum(min); frameYIN->setMaximum(max); frameYIN->setSingleStep(step); } } else return; // cull to camera limits, if there are any if (useGuideHead == false) cullToDSLRLimits(); if (reset == 1 || frameSettings.contains(targetChip) == false) { QVariantMap settings; settings["x"] = 0; settings["y"] = 0; settings["w"] = frameWIN->maximum(); settings["h"] = frameHIN->maximum(); settings["binx"] = 1; settings["biny"] = 1; frameSettings[targetChip] = settings; } else if (reset == 2 && frameSettings.contains(targetChip)) { QVariantMap settings = frameSettings[targetChip]; int x, y, w, h; x = settings["x"].toInt(); y = settings["y"].toInt(); w = settings["w"].toInt(); h = settings["h"].toInt(); // Bound them x = qBound(frameXIN->minimum(), x, frameXIN->maximum() - 1); y = qBound(frameYIN->minimum(), y, frameYIN->maximum() - 1); w = qBound(frameWIN->minimum(), w, frameWIN->maximum()); h = qBound(frameHIN->minimum(), h, frameHIN->maximum()); settings["x"] = x; settings["y"] = y; settings["w"] = w; settings["h"] = h; frameSettings[targetChip] = settings; } if (frameSettings.contains(targetChip)) { QVariantMap settings = frameSettings[targetChip]; int x = settings["x"].toInt(); int y = settings["y"].toInt(); int w = settings["w"].toInt(); int h = settings["h"].toInt(); if (targetChip->canBin()) { targetChip->getMaxBin(&binx, &biny); binXIN->setMaximum(binx); binYIN->setMaximum(biny); binXIN->setValue(settings["binx"].toInt()); binYIN->setValue(settings["biny"].toInt()); } else { binXIN->setValue(1); binYIN->setValue(1); } if (x >= 0) frameXIN->setValue(x); if (y >= 0) frameYIN->setValue(y); if (w > 0) frameWIN->setValue(w); if (h > 0) frameHIN->setValue(h); } } void Capture::processCCDNumber(INumberVectorProperty *nvp) { if (currentCCD == nullptr) return; if ((!strcmp(nvp->name, "CCD_FRAME") && useGuideHead == false) || (!strcmp(nvp->name, "GUIDER_FRAME") && useGuideHead)) updateFrameProperties(); else if ((!strcmp(nvp->name, "CCD_INFO") && useGuideHead == false) || (!strcmp(nvp->name, "GUIDER_INFO") && useGuideHead)) updateFrameProperties(2); } void Capture::resetFrame() { targetChip = useGuideHead ? currentCCD->getChip(ISD::CCDChip::GUIDE_CCD) : currentCCD->getChip(ISD::CCDChip::PRIMARY_CCD); targetChip->resetFrame(); updateFrameProperties(1); } void Capture::syncFrameType(ISD::GDInterface *ccd) { if (strcmp(ccd->getDeviceName(), CCDCaptureCombo->currentText().toLatin1())) return; ISD::CCDChip *tChip = (static_cast(ccd))->getChip(ISD::CCDChip::PRIMARY_CCD); QStringList frameTypes = tChip->getFrameTypes(); frameTypeCombo->clear(); if (frameTypes.isEmpty()) frameTypeCombo->setEnabled(false); else { frameTypeCombo->setEnabled(true); frameTypeCombo->addItems(frameTypes); frameTypeCombo->setCurrentIndex(tChip->getFrameType()); } } bool Capture::setFilterWheel(const QString &device) { bool deviceFound = false; for (int i = 1; i < FilterDevicesCombo->count(); i++) if (device == FilterDevicesCombo->itemText(i)) { checkFilter(i); deviceFound = true; break; } if (deviceFound == false) return false; return true; } QString Capture::filterWheel() { if (FilterDevicesCombo->currentIndex() >= 1) return FilterDevicesCombo->currentText(); return QString(); } bool Capture::setFilter(const QString &filter) { if (FilterDevicesCombo->currentIndex() >= 1) { FilterPosCombo->setCurrentText(filter); return true; } return false; } QString Capture::filter() { return FilterPosCombo->currentText(); } void Capture::checkFilter(int filterNum) { if (filterNum == -1) { filterNum = FilterDevicesCombo->currentIndex(); if (filterNum == -1) return; } // "--" is no filter if (filterNum == 0) { currentFilter = nullptr; m_CurrentFilterPosition=-1; FilterPosCombo->clear(); syncFilterInfo(); return; } if (filterNum <= Filters.count()) currentFilter = Filters.at(filterNum-1); filterManager->setCurrentFilterWheel(currentFilter); syncFilterInfo(); FilterPosCombo->clear(); FilterPosCombo->addItems(filterManager->getFilterLabels()); m_CurrentFilterPosition = filterManager->getFilterPosition(); FilterPosCombo->setCurrentIndex(m_CurrentFilterPosition-1); /*if (activeJob && (activeJob->getStatus() == SequenceJob::JOB_ABORTED || activeJob->getStatus() == SequenceJob::JOB_IDLE)) activeJob->setCurrentFilter(currentFilterPosition);*/ } void Capture::syncFilterInfo() { if (currentCCD) { ITextVectorProperty *activeDevices = currentCCD->getBaseDevice()->getText("ACTIVE_DEVICES"); if (activeDevices) { IText *activeFilter = IUFindText(activeDevices, "ACTIVE_FILTER"); if (activeFilter) { if (currentFilter != nullptr && strcmp(activeFilter->text, currentFilter->getDeviceName())) { IUSaveText(activeFilter, currentFilter->getDeviceName()); currentCCD->getDriverInfo()->getClientManager()->sendNewText(activeDevices); } // Reset filter name in CCD driver else if (currentFilter == nullptr && strlen(activeFilter->text) > 0) { IUSaveText(activeFilter, ""); currentCCD->getDriverInfo()->getClientManager()->sendNewText(activeDevices); } } } } } bool Capture::startNextExposure() { if (m_State == CAPTURE_PAUSED) { pauseFunction = &Capture::startNextExposure; appendLogText(i18n("Sequence paused.")); secondsLabel->setText(i18n("Paused...")); return false; } if (seqDelay > 0) { secondsLabel->setText(i18n("Waiting...")); m_State = CAPTURE_WAITING; emit newStatus(Ekos::CAPTURE_WAITING); } seqTimer->start(seqDelay); return true; } void Capture::newFITS(IBLOB *bp) { ISD::CCDChip *tChip = nullptr; // If there is no active job, ignore if (activeJob == nullptr || meridianFlipStage >= MF_ALIGNING) return; if (currentCCD->getUploadMode() != ISD::CCD::UPLOAD_LOCAL) { if (bp == nullptr) { appendLogText(i18n("Failed to save file to %1", activeJob->getSignature())); abort(); return; } if (!strcmp(bp->name, "CCD2")) tChip = currentCCD->getChip(ISD::CCDChip::GUIDE_CCD); else tChip = currentCCD->getChip(ISD::CCDChip::PRIMARY_CCD); if (tChip != targetChip) return; if (targetChip->getCaptureMode() == FITS_FOCUS || targetChip->getCaptureMode() == FITS_GUIDE) return; // If this is a preview job, make sure to enable preview button after // we receive the FITS if (activeJob->isPreview() && previewB->isEnabled() == false) previewB->setEnabled(true); // If the FITS is not for our device, simply ignore //if (QString(bp->bvp->device) != currentCCD->getDeviceName() || (startB->isEnabled() && previewB->isEnabled())) if (QString(bp->bvp->device) != currentCCD->getDeviceName() || m_State == CAPTURE_IDLE || m_State == CAPTURE_ABORTED) return; if (currentCCD->isLooping() == false) { disconnect(currentCCD, &ISD::CCD::BLOBUpdated, this, &Ekos::Capture::newFITS); if (useGuideHead == false && darkSubCheck->isChecked() && activeJob->isPreview()) { FITSView *currentImage = targetChip->getImageView(FITS_NORMAL); FITSData *darkData = DarkLibrary::Instance()->getDarkFrame(targetChip, activeJob->getExposure()); uint16_t offsetX = activeJob->getSubX() / activeJob->getXBin(); uint16_t offsetY = activeJob->getSubY() / activeJob->getYBin(); connect(DarkLibrary::Instance(), &DarkLibrary::darkFrameCompleted, this, &Ekos::Capture::setCaptureComplete); connect(DarkLibrary::Instance(), &DarkLibrary::newLog, this, &Ekos::Capture::appendLogText); if (darkData) DarkLibrary::Instance()->subtract(darkData, currentImage, activeJob->getCaptureFilter(), offsetX, offsetY); else DarkLibrary::Instance()->captureAndSubtract(targetChip, currentImage, activeJob->getExposure(), offsetX, offsetY); return; } } } blobChip = bp ? static_cast(bp->aux0) : nullptr; blobFilename= bp ? static_cast(bp->aux2) : QString(); setCaptureComplete(); } bool Capture::setCaptureComplete() { captureTimeout.stop(); captureTimeoutCounter = 0; if (currentCCD->isLooping() == false) { disconnect(currentCCD, &ISD::CCD::newExposureValue, this, &Ekos::Capture::setExposureProgress); DarkLibrary::Instance()->disconnect(this); } secondsLabel->setText(i18n("Complete.")); // Do not display notifications for very short captures if (activeJob->getExposure() >= 1) KSNotification::event(QLatin1String("EkosCaptureImageReceived"), i18n("Captured image received"), KSNotification::EVENT_INFO); // If it was initially set as pure preview job and NOT as preview for calibration if (activeJob->isPreview() && calibrationStage != CAL_CALIBRATION) { sendNewImage(blobFilename, blobChip); jobs.removeOne(activeJob); // Reset upload mode if it was changed by preview currentCCD->setUploadMode(rememberUploadMode); delete (activeJob); // Reset active job pointer activeJob = nullptr; abort(); if (guideState == GUIDE_SUSPENDED && suspendGuideOnDownload) emit resumeGuiding(); m_State = CAPTURE_IDLE; emit newStatus(Ekos::CAPTURE_IDLE); return true; } if (m_State == CAPTURE_PAUSED) { pauseFunction = &Capture::setCaptureComplete; appendLogText(i18n("Sequence paused.")); secondsLabel->setText(i18n("Paused...")); return false; } /* Increase the sequence's current capture count */ if (! activeJob->isPreview()) activeJob->setCompleted(activeJob->getCompleted() + 1); sendNewImage(blobFilename, blobChip); /* If we were assigned a captured frame map, also increase the relevant counter for prepareJob */ SchedulerJob::CapturedFramesMap::iterator frame_item = capturedFramesMap.find(activeJob->getSignature()); if (capturedFramesMap.end() != frame_item) frame_item.value()++; if (activeJob->getFrameType() != FRAME_LIGHT) { if (processPostCaptureCalibrationStage() == false) return true; if (calibrationStage == CAL_CALIBRATION_COMPLETE) calibrationStage = CAL_CAPTURING; } /* The image progress has now one more capture */ imgProgress->setValue(activeJob->getCompleted()); appendLogText(i18n("Received image %1 out of %2.", activeJob->getCompleted(), activeJob->getCount())); m_State = CAPTURE_IMAGE_RECEIVED; emit newStatus(Ekos::CAPTURE_IMAGE_RECEIVED); currentImgCountOUT->setText(QString("%L1").arg(activeJob->getCompleted())); // Check if we need to execute post capture script first if (activeJob->getPostCaptureScript().isEmpty() == false) { postCaptureScript.start(activeJob->getPostCaptureScript()); appendLogText(i18n("Executing post capture script %1", activeJob->getPostCaptureScript())); return true; } // if we're done if (activeJob->getCount() <= activeJob->getCompleted()) { processJobCompletion(); return true; } // Check if meridian condition is met if (checkMeridianFlip()) return true; return resumeSequence(); } void Capture::processJobCompletion() { activeJob->done(); if (activeJob->isPreview() == false) { int index = jobs.indexOf(activeJob); QJsonObject oneSequence = m_SequenceArray[index].toObject(); oneSequence["Status"] = "Complete"; m_SequenceArray.replace(index, oneSequence); emit sequenceChanged(m_SequenceArray); } stop(); // Check if meridian condition is met IF there are more pending jobs in the queue // Otherwise, no need to check meridian flip is all jobs are over. if (getPendingJobCount() > 0 && checkMeridianFlip()) return; // Check if there are more pending jobs and execute them if (resumeSequence()) return; // Otherwise, we're done. We park if required and resume guiding if no parking is done and autoguiding was engaged before. else { //KNotification::event(QLatin1String("CaptureSuccessful"), i18n("CCD capture sequence completed")); KSNotification::event(QLatin1String("CaptureSuccessful"), i18n("CCD capture sequence completed"), KSNotification::EVENT_INFO); abort(); m_State = CAPTURE_COMPLETE; emit newStatus(Ekos::CAPTURE_COMPLETE); //Resume guiding if it was suspended before //if (isAutoGuiding && currentCCD->getChip(ISD::CCDChip::GUIDE_CCD) == guideChip) if (guideState == GUIDE_SUSPENDED && suspendGuideOnDownload) emit resumeGuiding(); } } bool Capture::resumeSequence() { if (m_State == CAPTURE_PAUSED) { pauseFunction = &Capture::resumeSequence; appendLogText(i18n("Sequence paused.")); secondsLabel->setText(i18n("Paused...")); return false; } // If no job is active, we have to find if there are more pending jobs in the queue if (!activeJob) { SequenceJob *next_job = nullptr; foreach (SequenceJob *job, jobs) { if (job->getStatus() == SequenceJob::JOB_IDLE || job->getStatus() == SequenceJob::JOB_ABORTED) { next_job = job; break; } } if (next_job) { prepareJob(next_job); //Resume guiding if it was suspended before //if (isAutoGuiding && currentCCD->getChip(ISD::CCDChip::GUIDE_CCD) == guideChip) if (guideState == GUIDE_SUSPENDED && suspendGuideOnDownload) { qCDebug(KSTARS_EKOS_CAPTURE) << "Resuming guiding..."; emit resumeGuiding(); } return true; } else { qCDebug(KSTARS_EKOS_CAPTURE) << "All capture jobs complete."; return false; } } // Otherwise, let's prepare for next exposure after making sure in-sequence focus and dithering are complete if applicable. else { isInSequenceFocus = (autoFocusReady && autofocusCheck->isChecked()/* && HFRPixels->value() > 0*/); // if (isInSequenceFocus) // requiredAutoFocusStarted = false; // Reset HFR pixels to file value after meridian flip if (isInSequenceFocus && meridianFlipStage != MF_NONE) { qCDebug(KSTARS_EKOS_CAPTURE) << "Resetting HFR value to file value of" << fileHFR << "pixels after meridian flip."; //firstAutoFocus = true; HFRPixels->setValue(fileHFR); } // If we suspended guiding due to primary chip download, resume guide chip guiding now if (guideState == GUIDE_SUSPENDED && suspendGuideOnDownload) { qCInfo(KSTARS_EKOS_CAPTURE) << "Resuming guiding..."; emit resumeGuiding(); } // Dither either when guiding or IF Non-Guide either option is enabled if ( Options::ditherEnabled() // 2017-09-20 Jasem: No need to dither after post meridian flip guiding && meridianFlipStage != MF_GUIDING // If CCD is looping, we cannot dither UNLESS a different camera and NOT a guide chip is doing the guiding for us. && (currentCCD->isLooping() == false || guideChip == nullptr) // We must be either in guide mode or if non-guide dither (via pulsing) is enabled && (guideState == GUIDE_GUIDING || Options::ditherNoGuiding()) // Must be only done for light frames && activeJob->getFrameType() == FRAME_LIGHT // Check dither counter && --ditherCounter == 0) { ditherCounter = Options::ditherFrames(); secondsLabel->setText(i18n("Dithering...")); qCInfo(KSTARS_EKOS_CAPTURE) << "Dithering..."; if (currentCCD->isLooping()) targetChip->abortExposure(); m_State = CAPTURE_DITHERING; emit newStatus(Ekos::CAPTURE_DITHERING); } #if 0 else if (isRefocus && activeJob->getFrameType() == FRAME_LIGHT) { appendLogText(i18n("Scheduled refocus starting after %1 seconds...",getRefocusEveryNTimerElapsedSec())); secondsLabel->setText(i18n("Focusing...")); if (currentCCD->isLooping()) targetChip->abortExposure(); // If we are over 30 mins since last autofocus, we'll reset frame. if (refocusEveryN->value() >= 30) emit resetFocus(); // force refocus emit checkFocus(0.1); m_State = CAPTURE_FOCUSING; emit newStatus(Ekos::CAPTURE_FOCUSING); } else if (isInSequenceFocus && activeJob->getFrameType() == FRAME_LIGHT && --inSequenceFocusCounter == 0) { inSequenceFocusCounter = Options::inSequenceCheckFrames(); // Post meridian flip we need to reset filter _before_ running in-sequence focusing // as it could have changed for whatever reason (e.g. alignment used a different filter). // Then when focus process begins with the _target_ filter in place, it should take all the necessary actions to make it // work for the next set of captures. This is direct reset to the filter device, not via Filter Manager. if (meridianFlipStage != MF_NONE && currentFilter) { int targetFilterPosition = activeJob->getTargetFilter(); int currentFilterPosition= filterManager->getFilterPosition(); if (targetFilterPosition > 0 && targetFilterPosition != currentFilterPosition) currentFilter->runCommand(INDI_SET_FILTER, &targetFilterPosition); } secondsLabel->setText(i18n("Focusing...")); if (currentCCD->isLooping()) targetChip->abortExposure(); if (HFRPixels->value() == 0) emit checkFocus(0.1); else emit checkFocus(HFRPixels->value()); qCDebug(KSTARS_EKOS_CAPTURE) << "In-sequence focusing started..."; m_State = CAPTURE_FOCUSING; emit newStatus(Ekos::CAPTURE_FOCUSING); } #endif // Check if we need to do autofocus, if not let's check if we need looping or start next exposure else if (startFocusIfRequired() == false) { // If looping, we just increment the file system image count if (currentCCD->isLooping()) { if (currentCCD->getUploadMode() != ISD::CCD::UPLOAD_LOCAL) { checkSeqBoundary(activeJob->getSignature()); currentCCD->setNextSequenceID(nextSequenceID); } } else startNextExposure(); } } return true; } bool Capture::startFocusIfRequired() { if (activeJob->getFrameType() != FRAME_LIGHT) return false; // if (autoFocusReady == false) // return false; // check if time for forced refocus if (refocusEveryNCheck->isChecked()) { qCDebug(KSTARS_EKOS_CAPTURE) << "NFocus Elapsed Time (secs): " << getRefocusEveryNTimerElapsedSec() << " Requested Interval (secs): " << refocusEveryN->value()*60; isRefocus = getRefocusEveryNTimerElapsedSec() >= refocusEveryN->value()*60; } else isRefocus = false; if (isRefocus) { appendLogText(i18n("Scheduled refocus starting after %1 seconds...",getRefocusEveryNTimerElapsedSec())); secondsLabel->setText(i18n("Focusing...")); if (currentCCD->isLooping()) targetChip->abortExposure(); // If we are over 30 mins since last autofocus, we'll reset frame. if (refocusEveryN->value() >= 30) emit resetFocus(); // force refocus emit checkFocus(0.1); m_State = CAPTURE_FOCUSING; emit newStatus(Ekos::CAPTURE_FOCUSING); return true; } else if (isInSequenceFocus && --inSequenceFocusCounter == 0) { inSequenceFocusCounter = Options::inSequenceCheckFrames(); // Post meridian flip we need to reset filter _before_ running in-sequence focusing // as it could have changed for whatever reason (e.g. alignment used a different filter). // Then when focus process begins with the _target_ filter in place, it should take all the necessary actions to make it // work for the next set of captures. This is direct reset to the filter device, not via Filter Manager. if (meridianFlipStage != MF_NONE && currentFilter) { int targetFilterPosition = activeJob->getTargetFilter(); int currentFilterPosition= filterManager->getFilterPosition(); if (targetFilterPosition > 0 && targetFilterPosition != currentFilterPosition) currentFilter->runCommand(INDI_SET_FILTER, &targetFilterPosition); } secondsLabel->setText(i18n("Focusing...")); if (currentCCD->isLooping()) targetChip->abortExposure(); if (HFRPixels->value() == 0) emit checkFocus(0.1); else emit checkFocus(HFRPixels->value()); qCDebug(KSTARS_EKOS_CAPTURE) << "In-sequence focusing started..."; m_State = CAPTURE_FOCUSING; emit newStatus(Ekos::CAPTURE_FOCUSING); return true; } return false; } void Capture::captureOne() { //if (currentCCD->getUploadMode() == ISD::CCD::UPLOAD_LOCAL) /*if (uploadModeCombo->currentIndex() != ISD::CCD::UPLOAD_CLIENT) { appendLogText(i18n("Cannot take preview image while CCD upload mode is set to local or both. Please change " "upload mode to client and try again.")); return; }*/ if (transferFormatCombo->currentIndex() == ISD::CCD::FORMAT_NATIVE && darkSubCheck->isChecked()) { appendLogText(i18n("Cannot perform auto dark subtraction of native DSLR formats.")); return; } if (addJob(true)) prepareJob(jobs.last()); } void Capture::captureImage() { if (activeJob == nullptr) return; captureTimeout.stop(); seqTimer->stop(); SequenceJob::CAPTUREResult rc = SequenceJob::CAPTURE_OK; if (currentCCD->isConnected() == false) { appendLogText(i18n("Error: Lost connection to CCD.")); abort(); return; } if (focusState >= FOCUS_PROGRESS) { appendLogText(i18n("Cannot capture while focus module is busy.")); abort(); return; } /* if (filterSlot != nullptr) { currentFilterPosition = (int)filterSlot->np[0].value; activeJob->setCurrentFilter(currentFilterPosition); }*/ if (currentFilter != nullptr) { m_CurrentFilterPosition = filterManager->getFilterPosition(); activeJob->setCurrentFilter(m_CurrentFilterPosition); } if (currentCCD->hasCooler()) { double temperature = 0; currentCCD->getTemperature(&temperature); activeJob->setCurrentTemperature(temperature); } if (currentCCD->isLooping()) { int remaining = activeJob->getCount() - activeJob->getCompleted(); if (remaining > 1) currentCCD->setExposureLoopCount(remaining); } connect(currentCCD, &ISD::CCD::BLOBUpdated, this, &Ekos::Capture::newFITS, Qt::UniqueConnection); if (activeJob->getFrameType() == FRAME_FLAT) { // If we have to calibrate ADU levels, first capture must be preview and not in batch mode if (activeJob->isPreview() == false && activeJob->getFlatFieldDuration() == DURATION_ADU && calibrationStage == CAL_PRECAPTURE_COMPLETE) { if (currentCCD->getTransferFormat() == ISD::CCD::FORMAT_NATIVE) { appendLogText(i18n("Cannot calculate ADU levels in non-FITS images.")); abort(); return; } calibrationStage = CAL_CALIBRATION; // We need to be in preview mode and in client mode for this to work activeJob->setPreview(true); } } // Temporary change upload mode to client when requesting previews if (activeJob->isPreview()) { rememberUploadMode = activeJob->getUploadMode(); currentCCD->setUploadMode(ISD::CCD::UPLOAD_CLIENT); } if (currentCCD->getUploadMode() != ISD::CCD::UPLOAD_LOCAL) { checkSeqBoundary(activeJob->getSignature()); currentCCD->setNextSequenceID(nextSequenceID); } m_State = CAPTURE_CAPTURING; //if (activeJob->isPreview() == false) // NOTE: Why we didn't emit this before for preview? emit newStatus(Ekos::CAPTURE_CAPTURING); if (frameSettings.contains(activeJob->getActiveChip())) { QVariantMap settings; settings["x"] = activeJob->getSubX(); settings["y"] = activeJob->getSubY(); settings["w"] = activeJob->getSubW(); settings["h"] = activeJob->getSubH(); settings["binx"] = activeJob->getXBin(); settings["biny"] = activeJob->getYBin(); frameSettings[activeJob->getActiveChip()] = settings; } // If using DSLR, make sure it is set to correct transfer format currentCCD->setTransformFormat(activeJob->getTransforFormat()); connect(currentCCD, &ISD::CCD::newExposureValue, this, &Ekos::Capture::setExposureProgress, Qt::UniqueConnection); rc = activeJob->capture(darkSubCheck->isChecked() ? true : false); if (rc != SequenceJob::CAPTURE_OK) { disconnect(currentCCD, &ISD::CCD::newExposureValue, this, &Ekos::Capture::setExposureProgress); } switch (rc) { case SequenceJob::CAPTURE_OK: { appendLogText(i18n("Capturing %1-second %2 image...", QString("%L1").arg(activeJob->getExposure(), 0, 'f', 3), activeJob->getFilterName())); captureTimeout.start(activeJob->getExposure() * 1000 + CAPTURE_TIMEOUT_THRESHOLD); if (activeJob->isPreview() == false) { int index = jobs.indexOf(activeJob); QJsonObject oneSequence = m_SequenceArray[index].toObject(); oneSequence["Status"] = "In Progress"; m_SequenceArray.replace(index, oneSequence); emit sequenceChanged(m_SequenceArray); } } break; case SequenceJob::CAPTURE_FRAME_ERROR: appendLogText(i18n("Failed to set sub frame.")); abort(); break; case SequenceJob::CAPTURE_BIN_ERROR: appendLogText(i18n("Failed to set binning.")); abort(); break; case SequenceJob::CAPTURE_FILTER_BUSY: // Try again in 1 second if filter is busy QTimer::singleShot(1000, this, &Ekos::Capture::captureImage); break; case SequenceJob::CAPTURE_FOCUS_ERROR: appendLogText(i18n("Cannot capture while focus module is busy.")); abort(); break; } } bool Capture::resumeCapture() { if (m_State == CAPTURE_PAUSED) { pauseFunction = &Capture::resumeCapture; appendLogText(i18n("Sequence paused.")); secondsLabel->setText(i18n("Paused...")); return false; } #if 0 /* Refresh isRefocus when resuming */ if (autoFocusReady && refocusEveryNCheck->isChecked()) { qCDebug(KSTARS_EKOS_CAPTURE) << "NFocus Elapsed Time (secs): " << getRefocusEveryNTimerElapsedSec() << " Requested Interval (secs): " << refocusEveryN->value()*60; isRefocus = getRefocusEveryNTimerElapsedSec() >= refocusEveryN->value()*60; } // FIXME ought to be able to combine these - only different is value passed // to checkFocus() // 2018-08-23 Jasem: For now in-sequence-focusing takes precedense. if (isInSequenceFocus && requiredAutoFocusStarted == false) { requiredAutoFocusStarted = true; secondsLabel->setText(i18n("Focusing...")); qCDebug(KSTARS_EKOS_CAPTURE) << "Requesting focusing if HFR >" << HFRPixels->value(); emit checkFocus(HFRPixels->value()); m_State = CAPTURE_FOCUSING; emit newStatus(Ekos::CAPTURE_FOCUSING); return true; } else if (isRefocus) { appendLogText(i18n("Scheduled refocus started...")); secondsLabel->setText(i18n("Focusing...")); emit checkFocus(0.1); m_State = CAPTURE_FOCUSING; emit newStatus(Ekos::CAPTURE_FOCUSING); return true; } #endif if (m_State == CAPTURE_DITHERING && autoFocusReady && startFocusIfRequired()) return true; startNextExposure(); return true; } /*******************************************************************************/ /* Update the prefix for the sequence of images to be captured */ /*******************************************************************************/ void Capture::updateSequencePrefix(const QString &newPrefix, const QString &dir) { seqPrefix = newPrefix; // If it doesn't exist, create it QDir().mkpath(dir); nextSequenceID = 1; } /*******************************************************************************/ /* Determine the next file number sequence. That is, if we have file1.png */ /* and file2.png, then the next sequence should be file3.png */ /*******************************************************************************/ void Capture::checkSeqBoundary(const QString &path) { int newFileIndex = -1; QFileInfo const path_info(path); QString const sig_dir(path_info.dir().path()); QString const sig_file(path_info.baseName()); QString tempName; // seqFileCount = 0; // No updates during meridian flip if (meridianFlipStage >= MF_ALIGNING) return; QDirIterator it(sig_dir, QDir::Files); while (it.hasNext()) { tempName = it.next(); QFileInfo info(tempName); //tempName = info.baseName(); tempName = info.completeBaseName(); QString finalSeqPrefix = seqPrefix; finalSeqPrefix.remove(SequenceJob::ISOMarker); // find the prefix first if (tempName.startsWith(finalSeqPrefix, Qt::CaseInsensitive) == false) continue; /* Do not change the number of captures. * - If the sequence is required by the end-user, unconditionally run what each sequence item is requiring. * - If the sequence is required by the scheduler, use capturedFramesMap to determine when to stop capturing. */ //seqFileCount++; int lastUnderScoreIndex = tempName.lastIndexOf("_"); if (lastUnderScoreIndex > 0) { bool indexOK = false; newFileIndex = tempName.midRef(lastUnderScoreIndex + 1).toInt(&indexOK); if (indexOK && newFileIndex >= nextSequenceID) nextSequenceID = newFileIndex + 1; } } } void Capture::appendLogText(const QString &text) { m_LogText.insert(0, i18nc("log entry; %1 is the date, %2 is the text", "%1 %2", QDateTime::currentDateTime().toString("yyyy-MM-ddThh:mm:ss"), text)); qCInfo(KSTARS_EKOS_CAPTURE) << text; emit newLog(text); } void Capture::clearLog() { m_LogText.clear(); emit newLog(QString()); } void Capture::setExposureProgress(ISD::CCDChip *tChip, double value, IPState state) { if (targetChip != tChip || targetChip->getCaptureMode() != FITS_NORMAL || meridianFlipStage >= MF_ALIGNING) return; exposeOUT->setText(QString("%L1").arg(value, 0, 'd', 2)); if (activeJob) { activeJob->setExposeLeft(value); emit newExposureProgress(activeJob); } if (activeJob && state == IPS_ALERT) { int retries = activeJob->getCaptureRetires() + 1; activeJob->setCaptureRetires(retries); appendLogText(i18n("Capture failed. Check INDI Control Panel for details.")); if (retries == 3) { abort(); return; } appendLogText(i18n("Restarting capture attempt #%1", retries)); nextSequenceID = 1; captureImage(); return; } //qDebug() << "Exposure with value " << value << "state" << pstateStr(state); if (activeJob != nullptr && state == IPS_OK) { activeJob->setCaptureRetires(0); activeJob->setExposeLeft(0); if (currentCCD && currentCCD->getUploadMode() == ISD::CCD::UPLOAD_LOCAL) { if (activeJob && activeJob->getStatus() == SequenceJob::JOB_BUSY) { newFITS(nullptr); return; } } //if (isAutoGuiding && Options::useEkosGuider() && currentCCD->getChip(ISD::CCDChip::GUIDE_CCD) == guideChip) if (guideState == GUIDE_GUIDING && Options::guiderType() == 0 && suspendGuideOnDownload) { qCDebug(KSTARS_EKOS_CAPTURE) << "Autoguiding suspended until primary CCD chip completes downloading..."; emit suspendGuiding(); } secondsLabel->setText(i18n("Downloading...")); //disconnect(currentCCD, &ISD::CCD::newExposureValue(ISD::CCDChip*,double,IPState)), this, &Ekos::Capture::updateCaptureProgress(ISD::CCDChip*,double,IPState))); } // JM: Don't change to i18np, value is DOUBLE, not Integer. else if (value <= 1) secondsLabel->setText(i18n("second left")); else secondsLabel->setText(i18n("seconds left")); } void Capture::updateCCDTemperature(double value) { if (temperatureCheck->isEnabled() == false) { if (currentCCD->getBaseDevice()->getPropertyPermission("CCD_TEMPERATURE") != IP_RO) checkCCD(); } temperatureOUT->setText(QString("%L1").arg(value, 0, 'f', 2)); if (temperatureIN->cleanText().isEmpty()) temperatureIN->setValue(value); //if (activeJob && (activeJob->getStatus() == SequenceJob::JOB_ABORTED || activeJob->getStatus() == SequenceJob::JOB_IDLE)) if (activeJob) activeJob->setCurrentTemperature(value); } void Capture::updateRotatorNumber(INumberVectorProperty *nvp) { if (!strcmp(nvp->name, "ABS_ROTATOR_ANGLE")) { // Update widget rotator position rotatorSettings->setCurrentAngle(nvp->np[0].value); //if (activeJob && (activeJob->getStatus() == SequenceJob::JOB_ABORTED || activeJob->getStatus() == SequenceJob::JOB_IDLE)) if (activeJob) activeJob->setCurrentRotation(rotatorSettings->getCurrentRotationPA()); } } bool Capture::addJob(bool preview) { if (m_State != CAPTURE_IDLE && m_State != CAPTURE_ABORTED && m_State != CAPTURE_COMPLETE) return false; SequenceJob *job = nullptr; QString imagePrefix; if (preview == false && darkSubCheck->isChecked()) { KMessageBox::error(this, i18n("Auto dark subtract is not supported in batch mode.")); return false; } if (uploadModeCombo->currentIndex() != ISD::CCD::UPLOAD_CLIENT && remoteDirIN->text().isEmpty()) { KMessageBox::error(this, i18n("You must set remote directory for Local & Both modes.")); return false; } if (uploadModeCombo->currentIndex() != ISD::CCD::UPLOAD_LOCAL && fitsDir->text().isEmpty()) { KMessageBox::error(this, i18n("You must set local directory for Client & Both modes.")); return false; } if (m_JobUnderEdit) job = jobs.at(queueTable->currentRow()); else { job = new SequenceJob(); job->setFilterManager(filterManager); } if (job == nullptr) { qWarning() << "Job is nullptr!" << endl; return false; } if (ISOCombo->isEnabled()) job->setISOIndex(ISOCombo->currentIndex()); job->setTransforFormat(static_cast(transferFormatCombo->currentIndex())); job->setPreview(preview); if (temperatureIN->isEnabled()) { double currentTemperature; currentCCD->getTemperature(¤tTemperature); job->setEnforceTemperature(temperatureCheck->isChecked()); job->setTargetTemperature(temperatureIN->value()); job->setCurrentTemperature(currentTemperature); } job->setCaptureFilter(static_cast(filterCombo->currentIndex())); job->setUploadMode(static_cast(uploadModeCombo->currentIndex())); job->setPostCaptureScript(postCaptureScriptIN->text()); job->setFlatFieldDuration(flatFieldDuration); job->setFlatFieldSource(flatFieldSource); job->setPreMountPark(preMountPark); job->setPreDomePark(preDomePark); job->setWallCoord(wallCoord); job->setTargetADU(targetADU); job->setTargetADUTolerance(targetADUTolerance); imagePrefix = prefixIN->text(); constructPrefix(imagePrefix); job->setPrefixSettings(prefixIN->text(), filterCheck->isChecked(), expDurationCheck->isChecked(), ISOCheck->isChecked()); job->setFrameType(static_cast(frameTypeCombo->currentIndex())); job->setFullPrefix(imagePrefix); //if (filterSlot != nullptr && currentFilter != nullptr) if (FilterPosCombo->currentIndex() != -1 && currentFilter != nullptr) job->setTargetFilter(FilterPosCombo->currentIndex() + 1, FilterPosCombo->currentText()); job->setExposure(exposureIN->value()); job->setCount(countIN->value()); job->setBin(binXIN->value(), binYIN->value()); job->setDelay(delayIN->value() * 1000); /* in ms */ job->setActiveChip(targetChip); job->setActiveCCD(currentCCD); job->setActiveFilter(currentFilter); // Custom Properties job->setCustomProperties(customPropertiesDialog->getCustomProperties()); if (currentRotator && rotatorSettings->isRotationEnforced()) { job->setActiveRotator(currentRotator); job->setTargetRotation(rotatorSettings->getTargetRotationPA()); job->setCurrentRotation(rotatorSettings->getCurrentRotationPA()); } job->setFrame(frameXIN->value(), frameYIN->value(), frameWIN->value(), frameHIN->value()); job->setRemoteDir(remoteDirIN->text()); job->setLocalDir(fitsDir->text()); if (m_JobUnderEdit == false) { // JM 2018-09-24: If this is the first job added // We always ignore job progress by default. if (jobs.isEmpty() && preview == false) ignoreJobProgress = true; jobs.append(job); // Nothing more to do if preview if (preview) return true; } QJsonObject jsonJob = {{"Status", "Idle"}}; QString directoryPostfix; /* FIXME: Refactor directoryPostfix assignment, whose code is duplicated in scheduler.cpp */ if (m_TargetName.isEmpty()) directoryPostfix = QLatin1Literal("/") + frameTypeCombo->currentText(); else directoryPostfix = QLatin1Literal("/") + m_TargetName + QLatin1Literal("/") + frameTypeCombo->currentText(); if ((job->getFrameType() == FRAME_LIGHT || job->getFrameType() == FRAME_FLAT) && job->getFilterName().isEmpty() == false) directoryPostfix += QLatin1Literal("/") + job->getFilterName(); job->setDirectoryPostfix(directoryPostfix); int currentRow = 0; if (m_JobUnderEdit == false) { currentRow = queueTable->rowCount(); queueTable->insertRow(currentRow); } else currentRow = queueTable->currentRow(); QTableWidgetItem *status = m_JobUnderEdit ? queueTable->item(currentRow, 0) : new QTableWidgetItem(); job->setStatusCell(status); QTableWidgetItem *filter = m_JobUnderEdit ? queueTable->item(currentRow, 1) : new QTableWidgetItem(); filter->setText("--"); jsonJob.insert("Filter", "--"); /*if (frameTypeCombo->currentText().compare("Bias", Qt::CaseInsensitive) && frameTypeCombo->currentText().compare("Dark", Qt::CaseInsensitive) && FilterPosCombo->count() > 0)*/ if (FilterPosCombo->count() > 0 && (frameTypeCombo->currentIndex() == FRAME_LIGHT || frameTypeCombo->currentIndex() == FRAME_FLAT)) { filter->setText(FilterPosCombo->currentText()); jsonJob.insert("Filter", FilterPosCombo->currentText()); } filter->setTextAlignment(Qt::AlignHCenter); filter->setFlags(Qt::ItemIsSelectable | Qt::ItemIsEnabled); QTableWidgetItem *type = m_JobUnderEdit ? queueTable->item(currentRow, 2) : new QTableWidgetItem(); type->setText(frameTypeCombo->currentText()); type->setTextAlignment(Qt::AlignHCenter); type->setFlags(Qt::ItemIsSelectable | Qt::ItemIsEnabled); jsonJob.insert("Type", type->text()); QTableWidgetItem *bin = m_JobUnderEdit ? queueTable->item(currentRow, 3) : new QTableWidgetItem(); bin->setText(QString("%1x%2").arg(binXIN->value()).arg(binYIN->value())); bin->setTextAlignment(Qt::AlignHCenter); bin->setFlags(Qt::ItemIsSelectable | Qt::ItemIsEnabled); jsonJob.insert("Bin", bin->text()); QTableWidgetItem *exp = m_JobUnderEdit ? queueTable->item(currentRow, 4) : new QTableWidgetItem(); exp->setText(QString("%L1").arg(exposureIN->value(), 0, 'f', exposureIN->decimals())); exp->setTextAlignment(Qt::AlignHCenter); exp->setFlags(Qt::ItemIsSelectable | Qt::ItemIsEnabled); jsonJob.insert("Exp", exp->text()); QTableWidgetItem *iso = m_JobUnderEdit ? queueTable->item(currentRow, 5) : new QTableWidgetItem(); if (ISOCombo->currentIndex() != -1) { iso->setText(ISOCombo->currentText()); jsonJob.insert("ISO", iso->text()); } else { iso->setText("--"); jsonJob.insert("ISO", "--"); } iso->setTextAlignment(Qt::AlignHCenter); iso->setFlags(Qt::ItemIsSelectable | Qt::ItemIsEnabled); QTableWidgetItem *count = m_JobUnderEdit ? queueTable->item(currentRow, 6) : new QTableWidgetItem(); job->setCountCell(count); jsonJob.insert("Count", count->text()); if (m_JobUnderEdit == false) { queueTable->setItem(currentRow, 0, status); queueTable->setItem(currentRow, 1, filter); queueTable->setItem(currentRow, 2, type); queueTable->setItem(currentRow, 3, bin); queueTable->setItem(currentRow, 4, exp); queueTable->setItem(currentRow, 5, iso); queueTable->setItem(currentRow, 6, count); m_SequenceArray.append(jsonJob); emit sequenceChanged(m_SequenceArray); } removeFromQueueB->setEnabled(true); if (queueTable->rowCount() > 0) { queueSaveAsB->setEnabled(true); queueSaveB->setEnabled(true); resetB->setEnabled(true); m_Dirty = true; } if (queueTable->rowCount() > 1) { queueUpB->setEnabled(true); queueDownB->setEnabled(true); } if (m_JobUnderEdit) { m_JobUnderEdit = false; resetJobEdit(); appendLogText(i18n("Job #%1 changes applied.", currentRow + 1)); m_SequenceArray.replace(currentRow, jsonJob); emit sequenceChanged(m_SequenceArray); } return true; } void Capture::removeJob(int index) { if (m_State != CAPTURE_IDLE && m_State != CAPTURE_ABORTED && m_State != CAPTURE_COMPLETE) return; if (m_JobUnderEdit) { resetJobEdit(); return; } int currentRow = queueTable->currentRow(); if (index >= 0) currentRow = index; if (currentRow < 0) { currentRow = queueTable->rowCount() - 1; if (currentRow < 0) return; } queueTable->removeRow(currentRow); m_SequenceArray.removeAt(currentRow); emit sequenceChanged(m_SequenceArray); SequenceJob *job = jobs.at(currentRow); jobs.removeOne(job); if (job == activeJob) activeJob = nullptr; delete job; if (queueTable->rowCount() == 0) removeFromQueueB->setEnabled(false); if (queueTable->rowCount() == 1) { queueUpB->setEnabled(false); queueDownB->setEnabled(false); } for (int i = 0; i < jobs.count(); i++) jobs.at(i)->setStatusCell(queueTable->item(i, 0)); queueTable->selectRow(queueTable->currentRow()); if (queueTable->rowCount() == 0) { queueSaveAsB->setEnabled(false); queueSaveB->setEnabled(false); resetB->setEnabled(false); } m_Dirty = true; } void Capture::moveJobUp() { int currentRow = queueTable->currentRow(); int columnCount = queueTable->columnCount(); if (currentRow <= 0 || queueTable->rowCount() == 1) return; int destinationRow = currentRow - 1; for (int i = 0; i < columnCount; i++) { QTableWidgetItem *downItem = queueTable->takeItem(currentRow, i); QTableWidgetItem *upItem = queueTable->takeItem(destinationRow, i); queueTable->setItem(destinationRow, i, downItem); queueTable->setItem(currentRow, i, upItem); } SequenceJob *job = jobs.takeAt(currentRow); jobs.removeOne(job); jobs.insert(destinationRow, job); QJsonObject currentJob = m_SequenceArray[currentRow].toObject(); m_SequenceArray.replace(currentRow, m_SequenceArray[destinationRow]); m_SequenceArray.replace(destinationRow, currentJob); emit sequenceChanged(m_SequenceArray); queueTable->selectRow(destinationRow); for (int i = 0; i < jobs.count(); i++) jobs.at(i)->setStatusCell(queueTable->item(i, 0)); m_Dirty = true; } void Capture::moveJobDown() { int currentRow = queueTable->currentRow(); int columnCount = queueTable->columnCount(); if (currentRow < 0 || queueTable->rowCount() == 1 || (currentRow + 1) == queueTable->rowCount()) return; int destinationRow = currentRow + 1; for (int i = 0; i < columnCount; i++) { QTableWidgetItem *downItem = queueTable->takeItem(currentRow, i); QTableWidgetItem *upItem = queueTable->takeItem(destinationRow, i); queueTable->setItem(destinationRow, i, downItem); queueTable->setItem(currentRow, i, upItem); } SequenceJob *job = jobs.takeAt(currentRow); jobs.removeOne(job); jobs.insert(destinationRow, job); QJsonObject currentJob = m_SequenceArray[currentRow].toObject(); m_SequenceArray.replace(currentRow, m_SequenceArray[destinationRow]); m_SequenceArray.replace(destinationRow, currentJob); emit sequenceChanged(m_SequenceArray); queueTable->selectRow(destinationRow); for (int i = 0; i < jobs.count(); i++) jobs.at(i)->setStatusCell(queueTable->item(i, 0)); m_Dirty = true; } void Capture::setBusy(bool enable) { isBusy = enable; enable ? pi->startAnimation() : pi->stopAnimation(); previewB->setEnabled(!enable); foreach (QAbstractButton *button, queueEditButtonGroup->buttons()) button->setEnabled(!enable); } void Capture::prepareJob(SequenceJob *job) { activeJob = job; qCDebug(KSTARS_EKOS_CAPTURE) << "Preparing capture job" << job->getSignature() << "for execution."; if (activeJob->getActiveCCD() != currentCCD) { setCamera(activeJob->getActiveCCD()->getDeviceName()); } /*if (activeJob->isPreview()) seqTotalCount = -1; else seqTotalCount = activeJob->getCount();*/ seqDelay = activeJob->getDelay(); // seqCurrentCount = activeJob->getCompleted(); if (activeJob->isPreview() == false) { fullImgCountOUT->setText(QString("%L1").arg(activeJob->getCount())); currentImgCountOUT->setText(QString("%L1").arg(activeJob->getCompleted())); // set the progress info imgProgress->setEnabled(true); imgProgress->setMaximum(activeJob->getCount()); imgProgress->setValue(activeJob->getCompleted()); if (currentCCD->getUploadMode() != ISD::CCD::UPLOAD_LOCAL) updateSequencePrefix(activeJob->getFullPrefix(), QFileInfo(activeJob->getSignature()).path()); } // We check if the job is already fully or partially complete by checking how many files of its type exist on the file system if (activeJob->isPreview() == false) { // The signature is the unique identification path in the system for a particular job. Format is "///". // If the Scheduler is requesting the Capture tab to process a sequence job, a target name will be inserted after the sequence file storage field (e.g. /path/to/storage/target/Light/...) // If the end-user is requesting the Capture tab to process a sequence job, the sequence file storage will be used as is (e.g. /path/to/storage/Light/...) QString signature = activeJob->getSignature(); // Now check on the file system ALL the files that exist with the above signature // If 29 files exist for example, then nextSequenceID would be the NEXT file number (30) // Therefore, we know how to number the next file. // However, we do not deduce the number of captures to process from this function. checkSeqBoundary(signature); // Captured Frames Map contains a list of signatures:count of _already_ captured files in the file system. // This map is set by the Scheduler in order to complete efficiently the required captures. // When the end-user requests a sequence to be processed, that map is empty. // // Example with a 5xL-5xR-5xG-5xB sequence // // When the end-user loads and runs this sequence, each filter gets to capture 5 frames, then the procedure stops. // When the Scheduler executes a job with this sequence, the procedure depends on what is in the storage. // // Let's consider the Scheduler has 3 instances of this job to run. // // When the first job completes the sequence, there are 20 images in the file system (5 for each filter). // When the second job starts, Scheduler finds those 20 images but requires 20 more images, thus sets the frames map counters to 0 for all LRGB frames. // When the third job starts, Scheduler now has 40 images, but still requires 20 more, thus again sets the frames map counters to 0 for all LRGB frames. // // Now let's consider something went wrong, and the third job was aborted before getting to 60 images, say we have full LRG, but only 1xB. // When Scheduler attempts to run the aborted job again, it will count captures in storage, subtract previous job requirements, and set the frames map counters to 0 for LRG, and 4 for B. // When the sequence runs, the procedure will bypass LRG and proceed to capture 4xB. if (capturedFramesMap.contains(signature)) { // Get the current capture count from the map int count = capturedFramesMap[signature]; // Count how many captures this job has to process, given that previous jobs may have done some work already foreach (SequenceJob *a_job, jobs) if (a_job == activeJob) break; else if (a_job->getSignature() == activeJob->getSignature()) count -= a_job->getCompleted(); // This is the current completion count of the current job activeJob->setCompleted(count); } // JM 2018-09-24: Only set completed jobs to 0 IF the scheduler set captured frames map to begin with // If the map is empty, then no scheduler is used and it should proceed as normal. else if (capturedFramesMap.count() > 0) { // No preliminary information, we reset the job count and run the job unconditionally to clarify the behavior activeJob->setCompleted(0); } // JM 2018-09-24: In case ignoreJobProgress is enabled // We check if this particular job progress ignore flag is set. If not, // then we set it and reset completed to zero. Next time it is evaluated here again // It will maintain its count regardless else if (ignoreJobProgress && activeJob->getJobProgressIgnored() == false) { activeJob->setJobProgressIgnored(true); activeJob->setCompleted(0); } // We cannot rely on sequenceID to give us a count - if we don't ignore job progress, we leave the count as it was originally #if 0 // If we cannot ignore job progress, then we set completed job number according to what // was found on the file system. else if (ignoreJobProgress == false) { int count = nextSequenceID - 1; if (count < activeJob->getCount()) activeJob->setCompleted(count); else activeJob->setCompleted(activeJob->getCount()); } #endif // Check whether active job is complete by comparing required captures to what is already available if (activeJob->getCount() <= activeJob->getCompleted()) { activeJob->setCompleted(activeJob->getCount()); appendLogText(i18n("Job requires %1-second %2 images, has already %3/%4 captures and does not need to run.", QString("%L1").arg(job->getExposure(), 0, 'f', 3), job->getFilterName(), activeJob->getCompleted(), activeJob->getCount())); processJobCompletion(); /* FIXME: find a clearer way to exit here */ return; } else { // There are captures to process currentImgCountOUT->setText(QString("%L1").arg(activeJob->getCompleted())); appendLogText(i18n("Job requires %1-second %2 images, has %3/%4 frames captured and will be processed.", QString("%L1").arg(job->getExposure(), 0, 'f', 3), job->getFilterName(), activeJob->getCompleted(), activeJob->getCount())); // Emit progress update - done a few lines below // emit newImage(nullptr, activeJob); currentCCD->setNextSequenceID(nextSequenceID); } } if (currentCCD->isBLOBEnabled() == false) { // FIXME: Move this warning pop-up elsewhere, it will interfere with automation. if (Options::guiderType() != Ekos::Guide::GUIDE_INTERNAL || KMessageBox::questionYesNo(nullptr, i18n("Image transfer is disabled for this camera. Would you like to enable it?")) == KMessageBox::Yes) { currentCCD->setBLOBEnabled(true); } else { setBusy(false); return; } } // Just notification of active job stating up emit newImage(activeJob); //connect(job, SIGNAL(checkFocus()), this, &Ekos::Capture::startPostFilterAutoFocus())); // Reset calibration stage if (calibrationStage == CAL_CAPTURING) { if (job->getFrameType() != FRAME_LIGHT) calibrationStage = CAL_PRECAPTURE_COMPLETE; else calibrationStage = CAL_NONE; } /* Disable this restriction, let the sequence run even if focus did not run prior to the capture. * Besides, this locks up the Scheduler when the Capture module starts a sequence without any prior focus procedure done. * This is quite an old code block. The message "Manual scheduled" seems to even refer to some manual intervention? * With the new HFR threshold, it might be interesting to prevent the execution because we actually need an HFR value to * begin capturing, but even there, on one hand it makes sense for the end-user to know what HFR to put in the edit box, * and on the other hand the focus procedure will deduce the next HFR automatically. * But in the end, it's not entirely clear what the intent was. Note there is still a warning that a preliminary autofocus * procedure is important to avoid any surprise that could make the whole schedule ineffective. */ #if 0 // If we haven't performed a single autofocus yet, we stop //if (!job->isPreview() && Options::enforceRefocusEveryN() && autoFocusReady && isInSequenceFocus == false && firstAutoFocus == true) if (!job->isPreview() && Options::enforceRefocusEveryN() && autoFocusReady == false && isInSequenceFocus == false) { appendLogText(i18n("Manual scheduled focusing is not supported. Run Autofocus process before trying again.")); abort(); return; } #endif #if 0 if (currentFilterPosition > 0) { // If we haven't performed a single autofocus yet, we stop if (!job->isPreview() && Options::autoFocusOnFilterChange() && (isInSequenceFocus == false && firstAutoFocus == true)) { appendLogText(i18n( "Manual focusing post filter change is not supported. Run Autofocus process before trying again.")); abort(); return; } /* if (currentFilterPosition != activeJob->getTargetFilter() && filterFocusOffsets.empty() == false) { int16_t targetFilterOffset = 0; foreach (FocusOffset *offset, filterFocusOffsets) { if (offset->filter == activeJob->getFilterName()) { targetFilterOffset = offset->offset - lastFilterOffset; lastFilterOffset = offset->offset; break; } } if (targetFilterOffset != 0 && (activeJob->getFrameType() == FRAME_LIGHT || activeJob->getFrameType() == FRAME_FLAT)) { appendLogText(i18n("Adjust focus offset by %1 steps", targetFilterOffset)); secondsLabel->setText(i18n("Adjusting filter offset")); if (activeJob->isPreview() == false) { state = CAPTURE_FILTER_FOCUS; emit newStatus(Ekos::CAPTURE_FILTER_FOCUS); } setBusy(true); emit newFocusOffset(targetFilterOffset); return; } } */ } #endif preparePreCaptureActions(); } void Capture::preparePreCaptureActions() { // Update position if (m_CurrentFilterPosition > 0) activeJob->setCurrentFilter(m_CurrentFilterPosition); // update temperature if (currentCCD->hasCooler() && activeJob->getEnforceTemperature()) { double temperature = 0; currentCCD->getTemperature(&temperature); activeJob->setCurrentTemperature(temperature); } // update rotator angle if (currentRotator != nullptr && activeJob->getTargetRotation() != Ekos::INVALID_VALUE) activeJob->setCurrentRotation(rotatorSettings->getCurrentRotationPA()); setBusy(true); if (activeJob->isPreview()) { startB->setIcon( QIcon::fromTheme("media-playback-stop")); startB->setToolTip(i18n("Stop")); } connect(activeJob, &SequenceJob::prepareState, this, &Ekos::Capture::updatePrepareState); connect(activeJob, &SequenceJob::prepareComplete, this, &Ekos::Capture::executeJob); activeJob->prepareCapture(); } void Capture::updatePrepareState(Ekos::CaptureState prepareState) { m_State = prepareState; emit newStatus(prepareState); switch (prepareState) { case CAPTURE_SETTING_TEMPERATURE: appendLogText(i18n("Setting temperature to %1 C...", activeJob->getTargetTemperature())); secondsLabel->setText(i18n("Set %1 C...", activeJob->getTargetTemperature())); break; case CAPTURE_SETTING_ROTATOR: appendLogText(i18n("Setting rotation to %1 degrees E of N...", activeJob->getTargetRotation())); secondsLabel->setText(i18n("Set Rotator %1...", activeJob->getTargetRotation())); break; default: break; } } void Capture::executeJob() { activeJob->disconnect(this); QMap FITSHeader; if (m_ObserverName.isEmpty() == false) FITSHeader["FITS_OBSERVER"] = m_ObserverName; if (m_TargetName.isEmpty() == false) FITSHeader["FITS_OBJECT"] = m_TargetName; else if (activeJob->getRawPrefix().isEmpty() == false) { FITSHeader["FITS_OBJECT"] = activeJob->getRawPrefix(); } if (FITSHeader.count() > 0) currentCCD->setFITSHeader(FITSHeader); // Update button status setBusy(true); useGuideHead = (activeJob->getActiveChip()->getType() == ISD::CCDChip::PRIMARY_CCD) ? false : true; syncGUIToJob(activeJob); updatePreCaptureCalibrationStatus(); // Check calibration frame requirements #if 0 if (activeJob->getFrameType() != FRAME_LIGHT && activeJob->isPreview() == false) { updatePreCaptureCalibrationStatus(); return; } captureImage(); #endif } void Capture::updatePreCaptureCalibrationStatus() { // If process was aborted or stopped by the user if (isBusy == false) { appendLogText(i18n("Warning: Calibration process was prematurely terminated.")); return; } IPState rc = processPreCaptureCalibrationStage(); if (rc == IPS_ALERT) return; else if (rc == IPS_BUSY) { secondsLabel->clear(); QTimer::singleShot(1000, this, &Ekos::Capture::updatePreCaptureCalibrationStatus); return; } captureImage(); } void Capture::setGuideDeviation(double delta_ra, double delta_dec) { // if (activeJob == nullptr) // { // if (deviationDetected == false) // return; // // Try to find first job that was aborted due to deviation // for(SequenceJob *job : jobs) // { // if (job->getStatus() == SequenceJob::JOB_ABORTED) // { // activeJob = job; // break; // } // } // if (activeJob == nullptr) // return; // } // If guiding is started after a meridian flip we will start getting guide deviations again // if the guide deviations are within our limits, we resume the sequence if (meridianFlipStage == MF_GUIDING) { double deviation_rms = sqrt(delta_ra * delta_ra + delta_dec * delta_dec); // If the user didn't select any guiding deviation, we fall through // otherwise we can for deviation RMS if (guideDeviationCheck->isChecked() == false || deviation_rms < guideDeviation->value()) { - initialHA = getCurrentHA(); appendLogText(i18n("Post meridian flip calibration completed successfully.")); resumeSequence(); // N.B. Set meridian flip stage AFTER resumeSequence() always meridianFlipStage = MF_NONE; return; } } // We don't enforce limit on previews if (guideDeviationCheck->isChecked() == false || (activeJob && (activeJob->isPreview() || activeJob->getExposeLeft() == 0))) return; double deviation_rms = sqrt(delta_ra * delta_ra + delta_dec * delta_dec); QString deviationText = QString("%1").arg(deviation_rms, 0, 'f', 3); // If we have an active busy job, let's abort it if guiding deviation is exceeded. // And we accounted for the spike if (activeJob && activeJob->getStatus() == SequenceJob::JOB_BUSY && activeJob->getFrameType() == FRAME_LIGHT) { if (deviation_rms > guideDeviation->value()) { // Ignore spikes ONCE if (m_SpikeDetected == false) { m_SpikeDetected = true; return; } appendLogText(i18n("Guiding deviation %1 exceeded limit value of %2 arcsecs, " "suspending exposure and waiting for guider up to %3 seconds.", deviationText, guideDeviation->value(), QString("%L1").arg(guideDeviationTimer.interval()/1000.0,0,'f',3))); suspend(); m_SpikeDetected = false; // Check if we need to start meridian flip if (checkMeridianFlip()) return; m_DeviationDetected = true; guideDeviationTimer.start(); } return; } // Find the first aborted job SequenceJob * abortedJob = nullptr; for(SequenceJob *job : jobs) { if (job->getStatus() == SequenceJob::JOB_ABORTED) { abortedJob = job; break; } } if (abortedJob && m_DeviationDetected) { if (deviation_rms <= guideDeviation->value()) { guideDeviationTimer.stop(); if (seqDelay == 0) appendLogText(i18n("Guiding deviation %1 is now lower than limit value of %2 arcsecs, " "resuming exposure.", deviationText, guideDeviation->value())); else appendLogText(i18n("Guiding deviation %1 is now lower than limit value of %2 arcsecs, " "resuming exposure in %3 seconds.", deviationText, guideDeviation->value(), seqDelay / 1000.0)); QTimer::singleShot(seqDelay, this, &Ekos::Capture::start); return; } else appendLogText(i18n("Guiding deviation %1 is still higher than limit value of %2 arcsecs.", deviationText, guideDeviation->value())); } } void Capture::setFocusStatus(FocusState state) { // Do NOT update state if in the process of meridian flip if (meridianFlipStage != MF_NONE) return; focusState = state; if (focusState > FOCUS_ABORTED) return; if (focusState == FOCUS_COMPLETE) { // enable option to have a refocus event occur if HFR goes over threshold autoFocusReady = true; //if (HFRPixels->value() == 0.0 && fileHFR == 0.0) if (fileHFR == 0.0) { QList filterHFRList; if (m_CurrentFilterPosition > 0) { // If we are using filters, then we retrieve which filter is currently active. // We check if filter lock is used, and store that instead of the current filter. // e.g. If current filter HA, but lock filter is L, then the HFR value is stored for L filter. // If no lock filter exists, then we store as is (HA) QString currentFilterText = FilterPosCombo->itemText(m_CurrentFilterPosition-1); //QString filterLock = filterManager.data()->getFilterLock(currentFilterText); //QString finalFilter = (filterLock == "--" ? currentFilterText : filterLock); //filterHFRList = HFRMap[finalFilter]; filterHFRList = HFRMap[currentFilterText]; filterHFRList.append(focusHFR); //HFRMap[finalFilter] = filterHFRList; HFRMap[currentFilterText] = filterHFRList; } // No filters else { filterHFRList = HFRMap["--"]; filterHFRList.append(focusHFR); HFRMap["--"] = filterHFRList; } double median = focusHFR; int count = filterHFRList.size(); if (Options::useMedianFocus() && count > 1) median = (count % 2) ? filterHFRList[count/2] : (filterHFRList[count/2-1] + filterHFRList[count/2])/2.0; // Add 2.5% (default) to the automatic initial HFR value to allow for minute changes in HFR without need to refocus // in case in-sequence-focusing is used. HFRPixels->setValue(median + (median * (Options::hFRThresholdPercentage() / 100.0))); } #if 0 if (focusHFR > 0 && firstAutoFocus && HFRPixels->value() == 0 && fileHFR == 0) { firstAutoFocus = false; // Add 2.5% (default) to the automatic initial HFR value to allow for minute changes in HFR without need to refocus // in case in-sequence-focusing is used. HFRPixels->setValue(focusHFR + (focusHFR * (Options::hFRThresholdPercentage() / 100.0))); } #endif // successful focus so reset elapsed time restartRefocusEveryNTimer(); } #if 0 if (activeJob && (activeJob->getStatus() == SequenceJob::JOB_ABORTED || activeJob->getStatus() == SequenceJob::JOB_IDLE)) { if (focusState == FOCUS_COMPLETE) { //HFRPixels->setValue(focusHFR + (focusHFR * 0.025)); appendLogText(i18n("Focus complete.")); } else if (focusState == FOCUS_FAILED) { appendLogText(i18n("Autofocus failed. Aborting exposure...")); secondsLabel->setText(""); abort(); } return; } #endif if ((isRefocus || isInSequenceFocus) && activeJob && activeJob->getStatus() == SequenceJob::JOB_BUSY) { secondsLabel->setText(QString()); if (focusState == FOCUS_COMPLETE) { appendLogText(i18n("Focus complete.")); startNextExposure(); } else if (focusState == FOCUS_FAILED) { appendLogText(i18n("Autofocus failed. Aborting exposure...")); abort(); } } } void Capture::updateHFRThreshold() { if (fileHFR != 0.0) return; QList filterHFRList; if (FilterPosCombo->currentIndex() != -1) { // If we are using filters, then we retrieve which filter is currently active. // We check if filter lock is used, and store that instead of the current filter. // e.g. If current filter HA, but lock filter is L, then the HFR value is stored for L filter. // If no lock filter exists, then we store as is (HA) QString currentFilterText = FilterPosCombo->currentText(); QString filterLock = filterManager.data()->getFilterLock(currentFilterText); QString finalFilter = (filterLock == "--" ? currentFilterText : filterLock); filterHFRList = HFRMap[finalFilter]; } // No filters else { filterHFRList = HFRMap["--"]; } double median = 0; int count = filterHFRList.size(); if (count > 1) median = (count % 2) ? filterHFRList[count/2] : (filterHFRList[count/2-1] + filterHFRList[count/2])/2.0; else if (count == 1) median = filterHFRList[0]; // Add 2.5% (default) to the automatic initial HFR value to allow for minute changes in HFR without need to refocus // in case in-sequence-focusing is used. HFRPixels->setValue(median + (median * (Options::hFRThresholdPercentage() / 100.0))); } +SkyPoint Capture::getInitialMountCoords() const +{ + QVariant const result = mountInterface->property("currentTarget"); + SkyPoint point = result.value(); + return point; +} + +bool Capture::executeMeridianFlip() { + + QDBusReply const reply = mountInterface->call("executeMeridianFlip"); + + if (reply.error().type() == QDBusError::NoError) + return reply.value(); + + // error occured + qCCritical(KSTARS_EKOS_CAPTURE) << QString("Warning: execute meridian flip request received DBUS error: %1").arg(QDBusError::errorString(reply.error().type())); + + return false; +} + int Capture::getTotalFramesCount(QString signature) { int result = 0; bool found = false; foreach (SequenceJob *job, jobs) { // FIXME: this should be part of SequenceJob QString sig = job->getSignature(); if (sig == signature) { result += job->getCount(); found = true; } } if (found) return result; else return -1; } void Capture::setRotator(ISD::GDInterface *newRotator) { currentRotator = newRotator; connect(currentRotator, &ISD::GDInterface::numberUpdated, this, &Ekos::Capture::updateRotatorNumber, Qt::UniqueConnection); rotatorB->setEnabled(true); rotatorSettings->setRotator(newRotator); INumberVectorProperty *nvp = newRotator->getBaseDevice()->getNumber("ABS_ROTATOR_ANGLE"); rotatorSettings->setCurrentAngle(nvp->np[0].value); } void Capture::setTelescope(ISD::GDInterface *newTelescope) { currentTelescope = static_cast(newTelescope); currentTelescope->disconnect(this); connect(currentTelescope, &ISD::GDInterface::numberUpdated, this, &Ekos::Capture::processTelescopeNumber); connect(currentTelescope, &ISD::Telescope::newTarget, [&](const QString &target) { if (m_State == CAPTURE_IDLE) prefixIN->setText(target); }); meridianHours->setEnabled(true); syncTelescopeInfo(); } void Capture::syncTelescopeInfo() { if (currentTelescope && currentTelescope->isConnected()) { // Sync ALL CCDs to current telescope for (ISD::CCD *oneCCD : CCDs) { ITextVectorProperty *activeDevices = oneCCD->getBaseDevice()->getText("ACTIVE_DEVICES"); if (activeDevices) { IText *activeTelescope = IUFindText(activeDevices, "ACTIVE_TELESCOPE"); if (activeTelescope) { IUSaveText(activeTelescope, currentTelescope->getDeviceName()); oneCCD->getDriverInfo()->getClientManager()->sendNewText(activeDevices); } } } } } void Capture::saveFITSDirectory() { QString dir = QFileDialog::getExistingDirectory(KStars::Instance(), i18n("FITS Save Directory"), dirPath.toLocalFile()); if (dir.isEmpty()) return; fitsDir->setText(dir); } void Capture::loadSequenceQueue() { QUrl fileURL = QFileDialog::getOpenFileUrl(KStars::Instance(), i18n("Open Ekos Sequence Queue"), dirPath, "Ekos Sequence Queue (*.esq)"); if (fileURL.isEmpty()) return; if (fileURL.isValid() == false) { QString message = i18n("Invalid URL: %1", fileURL.toLocalFile()); KMessageBox::sorry(nullptr, message, i18n("Invalid URL")); return; } dirPath = QUrl(fileURL.url(QUrl::RemoveFilename)); loadSequenceQueue(fileURL.toLocalFile()); } bool Capture::loadSequenceQueue(const QString &fileURL) { QFile sFile(fileURL); if (!sFile.open(QIODevice::ReadOnly)) { QString message = i18n("Unable to open file %1", fileURL); KMessageBox::sorry(nullptr, message, i18n("Could Not Open File")); return false; } capturedFramesMap.clear(); clearSequenceQueue(); LilXML *xmlParser = newLilXML(); char errmsg[MAXRBUF]; XMLEle *root = nullptr; XMLEle *ep = nullptr; char c; // We expect all data read from the XML to be in the C locale - QLocale::c(). QLocale cLocale = QLocale::c(); while (sFile.getChar(&c)) { root = readXMLEle(xmlParser, c, errmsg); if (root) { double sqVersion = cLocale.toFloat(findXMLAttValu(root, "version")); if (sqVersion < SQ_COMPAT_VERSION) { appendLogText(i18n("Deprecated sequence file format version %1. Please construct a new sequence file.", sqVersion)); return false; } for (ep = nextXMLEle(root, 1); ep != nullptr; ep = nextXMLEle(root, 0)) { if (!strcmp(tagXMLEle(ep), "Observer")) { m_ObserverName = QString(pcdataXMLEle(ep)); } else if (!strcmp(tagXMLEle(ep), "GuideDeviation")) { guideDeviationCheck->setChecked(!strcmp(findXMLAttValu(ep, "enabled"), "true")); guideDeviation->setValue(cLocale.toDouble(pcdataXMLEle(ep))); } else if (!strcmp(tagXMLEle(ep), "Autofocus")) { autofocusCheck->setChecked(!strcmp(findXMLAttValu(ep, "enabled"), "true")); double const HFRValue = cLocale.toDouble(pcdataXMLEle(ep)); // Set the HFR value from XML, or reset it to zero, don't let another unrelated older HFR be used // Note that HFR value will only be serialized to XML when option "Save Sequence HFR to File" is enabled fileHFR = HFRValue > 0.0 ? HFRValue : 0.0; HFRPixels->setValue(fileHFR); } else if (!strcmp(tagXMLEle(ep), "RefocusEveryN")) { refocusEveryNCheck->setChecked(!strcmp(findXMLAttValu(ep, "enabled"), "true")); int const minutesValue = cLocale.toInt(pcdataXMLEle(ep)); // Set the refocus period from XML, or reset it to zero, don't let another unrelated older refocus period be used. refocusEveryNMinutesValue = minutesValue > 0 ? minutesValue : 0; refocusEveryN->setValue(refocusEveryNMinutesValue); } else if (!strcmp(tagXMLEle(ep), "MeridianFlip")) { meridianCheck->setChecked(!strcmp(findXMLAttValu(ep, "enabled"), "true")); meridianHours->setValue(cLocale.toDouble(pcdataXMLEle(ep))); } else if (!strcmp(tagXMLEle(ep), "CCD")) { CCDCaptureCombo->setCurrentText(pcdataXMLEle(ep)); // Signal "activated" of QComboBox does not fire when changing the text programmatically setCamera(pcdataXMLEle(ep)); } else if (!strcmp(tagXMLEle(ep), "FilterWheel")) { FilterDevicesCombo->setCurrentText(pcdataXMLEle(ep)); checkFilter(); } else { processJobInfo(ep); } } delXMLEle(root); } else if (errmsg[0]) { appendLogText(QString(errmsg)); delLilXML(xmlParser); return false; } } m_SequenceURL = QUrl::fromLocalFile(fileURL); m_Dirty = false; delLilXML(xmlParser); return true; } bool Capture::processJobInfo(XMLEle *root) { XMLEle *ep; XMLEle *subEP; rotatorSettings->setRotationEnforced(false); QLocale cLocale = QLocale::c(); for (ep = nextXMLEle(root, 1); ep != nullptr; ep = nextXMLEle(root, 0)) { if (!strcmp(tagXMLEle(ep), "Exposure")) exposureIN->setValue(cLocale.toDouble(pcdataXMLEle(ep))); else if (!strcmp(tagXMLEle(ep), "Binning")) { subEP = findXMLEle(ep, "X"); if (subEP) binXIN->setValue(cLocale.toInt(pcdataXMLEle(subEP))); subEP = findXMLEle(ep, "Y"); if (subEP) binYIN->setValue(cLocale.toInt(pcdataXMLEle(subEP))); } else if (!strcmp(tagXMLEle(ep), "Frame")) { subEP = findXMLEle(ep, "X"); if (subEP) frameXIN->setValue(cLocale.toInt(pcdataXMLEle(subEP))); subEP = findXMLEle(ep, "Y"); if (subEP) frameYIN->setValue(cLocale.toInt(pcdataXMLEle(subEP))); subEP = findXMLEle(ep, "W"); if (subEP) frameWIN->setValue(cLocale.toInt(pcdataXMLEle(subEP))); subEP = findXMLEle(ep, "H"); if (subEP) frameHIN->setValue(cLocale.toInt(pcdataXMLEle(subEP))); } else if (!strcmp(tagXMLEle(ep), "Temperature")) { if (temperatureIN->isEnabled()) temperatureIN->setValue(cLocale.toDouble(pcdataXMLEle(ep))); // If force attribute exist, we change temperatureCheck, otherwise do nothing. if (!strcmp(findXMLAttValu(ep, "force"), "true")) temperatureCheck->setChecked(true); else if (!strcmp(findXMLAttValu(ep, "force"), "false")) temperatureCheck->setChecked(false); } else if (!strcmp(tagXMLEle(ep), "Filter")) { //FilterPosCombo->setCurrentIndex(atoi(pcdataXMLEle(ep))-1); FilterPosCombo->setCurrentText(pcdataXMLEle(ep)); } else if (!strcmp(tagXMLEle(ep), "Type")) { frameTypeCombo->setCurrentText(pcdataXMLEle(ep)); } else if (!strcmp(tagXMLEle(ep), "Prefix")) { subEP = findXMLEle(ep, "RawPrefix"); if (subEP) prefixIN->setText(pcdataXMLEle(subEP)); subEP = findXMLEle(ep, "FilterEnabled"); if (subEP) filterCheck->setChecked(!strcmp("1", pcdataXMLEle(subEP))); subEP = findXMLEle(ep, "ExpEnabled"); if (subEP) expDurationCheck->setChecked(!strcmp("1", pcdataXMLEle(subEP))); subEP = findXMLEle(ep, "TimeStampEnabled"); if (subEP) ISOCheck->setChecked(!strcmp("1", pcdataXMLEle(subEP))); } else if (!strcmp(tagXMLEle(ep), "Count")) { countIN->setValue(cLocale.toInt(pcdataXMLEle(ep))); } else if (!strcmp(tagXMLEle(ep), "Delay")) { delayIN->setValue(cLocale.toInt(pcdataXMLEle(ep))); } else if (!strcmp(tagXMLEle(ep), "PostCaptureScript")) { postCaptureScriptIN->setText(pcdataXMLEle(ep)); } else if (!strcmp(tagXMLEle(ep), "FITSDirectory")) { fitsDir->setText(pcdataXMLEle(ep)); } else if (!strcmp(tagXMLEle(ep), "RemoteDirectory")) { remoteDirIN->setText(pcdataXMLEle(ep)); } else if (!strcmp(tagXMLEle(ep), "UploadMode")) { uploadModeCombo->setCurrentIndex(cLocale.toInt(pcdataXMLEle(ep))); } else if (!strcmp(tagXMLEle(ep), "ISOIndex")) { if (ISOCombo->isEnabled()) ISOCombo->setCurrentIndex(cLocale.toInt(pcdataXMLEle(ep))); } else if (!strcmp(tagXMLEle(ep), "FormatIndex")) { transferFormatCombo->setCurrentIndex(cLocale.toInt(pcdataXMLEle(ep))); } else if (!strcmp(tagXMLEle(ep), "Rotation")) { rotatorSettings->setRotationEnforced(true); rotatorSettings->setTargetRotationPA(cLocale.toDouble(pcdataXMLEle(ep))); } else if (!strcmp(tagXMLEle(ep), "Properties")) { QMap> propertyMap; for (subEP = nextXMLEle(ep, 1); subEP != nullptr; subEP = nextXMLEle(ep, 0)) { QMap numbers; XMLEle *oneNumber = nullptr; for (oneNumber = nextXMLEle(subEP, 1); oneNumber != nullptr; oneNumber = nextXMLEle(subEP, 0)) { const char *name = findXMLAttValu(oneNumber, "name"); numbers[name] = cLocale.toDouble(pcdataXMLEle(oneNumber)); } const char *name = findXMLAttValu(subEP, "name"); propertyMap[name] = numbers; } customPropertiesDialog->setCustomProperties(propertyMap); } else if (!strcmp(tagXMLEle(ep), "Calibration")) { subEP = findXMLEle(ep, "FlatSource"); if (subEP) { XMLEle *typeEP = findXMLEle(subEP, "Type"); if (typeEP) { if (!strcmp(pcdataXMLEle(typeEP), "Manual")) flatFieldSource = SOURCE_MANUAL; else if (!strcmp(pcdataXMLEle(typeEP), "FlatCap")) flatFieldSource = SOURCE_FLATCAP; else if (!strcmp(pcdataXMLEle(typeEP), "DarkCap")) flatFieldSource = SOURCE_DARKCAP; else if (!strcmp(pcdataXMLEle(typeEP), "Wall")) { XMLEle *azEP = findXMLEle(subEP, "Az"); XMLEle *altEP = findXMLEle(subEP, "Alt"); if (azEP && altEP) { flatFieldSource = SOURCE_WALL; wallCoord.setAz(cLocale.toDouble(pcdataXMLEle(azEP))); wallCoord.setAlt(cLocale.toDouble(pcdataXMLEle(altEP))); } } else flatFieldSource = SOURCE_DAWN_DUSK; } } subEP = findXMLEle(ep, "FlatDuration"); if (subEP) { XMLEle *typeEP = findXMLEle(subEP, "Type"); if (typeEP) { if (!strcmp(pcdataXMLEle(typeEP), "Manual")) flatFieldDuration = DURATION_MANUAL; } XMLEle *aduEP = findXMLEle(subEP, "Value"); if (aduEP) { flatFieldDuration = DURATION_ADU; targetADU = cLocale.toDouble(pcdataXMLEle(aduEP)); } aduEP = findXMLEle(subEP, "Tolerance"); if (aduEP) { targetADUTolerance = cLocale.toDouble(pcdataXMLEle(aduEP)); } } subEP = findXMLEle(ep, "PreMountPark"); if (subEP) { if (!strcmp(pcdataXMLEle(subEP), "True")) preMountPark = true; else preMountPark = false; } subEP = findXMLEle(ep, "PreDomePark"); if (subEP) { if (!strcmp(pcdataXMLEle(subEP), "True")) preDomePark = true; else preDomePark = false; } } } addJob(false); return true; } void Capture::saveSequenceQueue() { QUrl backupCurrent = m_SequenceURL; if (m_SequenceURL.toLocalFile().startsWith(QLatin1String("/tmp/")) || m_SequenceURL.toLocalFile().contains("/Temp")) m_SequenceURL.clear(); // If no changes made, return. if (m_Dirty == false && !m_SequenceURL.isEmpty()) return; if (m_SequenceURL.isEmpty()) { m_SequenceURL = QFileDialog::getSaveFileUrl(KStars::Instance(), i18n("Save Ekos Sequence Queue"), dirPath, "Ekos Sequence Queue (*.esq)"); // if user presses cancel if (m_SequenceURL.isEmpty()) { m_SequenceURL = backupCurrent; return; } dirPath = QUrl(m_SequenceURL.url(QUrl::RemoveFilename)); if (m_SequenceURL.toLocalFile().endsWith(QLatin1String(".esq")) == false) m_SequenceURL.setPath(m_SequenceURL.toLocalFile() + ".esq"); /*if (QFile::exists(sequenceURL.toLocalFile())) { int r = KMessageBox::warningContinueCancel(0, i18n("A file named \"%1\" already exists. " "Overwrite it?", sequenceURL.fileName()), i18n("Overwrite File?"), KStandardGuiItem::overwrite()); if (r == KMessageBox::Cancel) return; }*/ } if (m_SequenceURL.isValid()) { if ((saveSequenceQueue(m_SequenceURL.toLocalFile())) == false) { KMessageBox::error(KStars::Instance(), i18n("Failed to save sequence queue"), i18n("Save")); return; } m_Dirty = false; } else { QString message = i18n("Invalid URL: %1", m_SequenceURL.url()); KMessageBox::sorry(KStars::Instance(), message, i18n("Invalid URL")); } } void Capture::saveSequenceQueueAs() { m_SequenceURL.clear(); saveSequenceQueue(); } bool Capture::saveSequenceQueue(const QString &path) { QFile file; QString rawPrefix; bool filterEnabled, expEnabled, tsEnabled; const QMap frameTypes = { { "Light", FRAME_LIGHT }, { "Dark", FRAME_DARK }, { "Bias", FRAME_BIAS }, { "Flat", FRAME_FLAT } }; file.setFileName(path); if (!file.open(QIODevice::WriteOnly)) { QString message = i18n("Unable to write to file %1", path); KMessageBox::sorry(nullptr, message, i18n("Could not open file")); return false; } QTextStream outstream(&file); // We serialize sequence data to XML using the C locale QLocale cLocale = QLocale::c(); outstream << "" << endl; outstream << "" << endl; if (m_ObserverName.isEmpty() == false) outstream << "" << m_ObserverName << "" << endl; outstream << "" << CCDCaptureCombo->currentText() << "" << endl; outstream << "" << FilterDevicesCombo->currentText() << "" << endl; outstream << "" << cLocale.toString(guideDeviation->value()) << "" << endl; // Issue a warning when autofocus is enabled but Ekos options prevent HFR value from being written if (autofocusCheck->isChecked() && !Options::saveHFRToFile()) appendLogText(i18n( "Warning: HFR-based autofocus is set but option \"Save Sequence HFR Value to File\" is not enabled. " "Current HFR value will not be written to sequence file.")); outstream << "" << cLocale.toString(Options::saveHFRToFile() ? HFRPixels->value() : 0) << "" << endl; outstream << "" << cLocale.toString(refocusEveryN->value()) << "" << endl; outstream << "" << cLocale.toString(meridianHours->value()) << "" << endl; foreach (SequenceJob *job, jobs) { job->getPrefixSettings(rawPrefix, filterEnabled, expEnabled, tsEnabled); outstream << "" << endl; outstream << "" << cLocale.toString(job->getExposure()) << "" << endl; outstream << "" << endl; outstream << "" << cLocale.toString(job->getXBin()) << "" << endl; outstream << "" << cLocale.toString(job->getXBin()) << "" << endl; outstream << "" << endl; outstream << "" << endl; outstream << "" << cLocale.toString(job->getSubX()) << "" << endl; outstream << "" << cLocale.toString(job->getSubY()) << "" << endl; outstream << "" << cLocale.toString(job->getSubW()) << "" << endl; outstream << "" << cLocale.toString(job->getSubH()) << "" << endl; outstream << "" << endl; if (job->getTargetTemperature() != Ekos::INVALID_VALUE) outstream << "" << cLocale.toString(job->getTargetTemperature()) << "" << endl; if (job->getTargetFilter() >= 0) //outstream << "" << job->getTargetFilter() << "" << endl; outstream << "" << job->getFilterName() << "" << endl; outstream << "" << frameTypes.key(job->getFrameType()) << "" << endl; outstream << "" << endl; //outstream << "" << job->getPrefix() << "" << endl; outstream << "" << rawPrefix << "" << endl; outstream << "" << (filterEnabled ? 1 : 0) << "" << endl; outstream << "" << (expEnabled ? 1 : 0) << "" << endl; outstream << "" << (tsEnabled ? 1 : 0) << "" << endl; outstream << "" << endl; outstream << "" << cLocale.toString(job->getCount()) << "" << endl; // ms to seconds outstream << "" << cLocale.toString(job->getDelay() / 1000.0) << "" << endl; if (job->getPostCaptureScript().isEmpty() == false) outstream << "" << job->getPostCaptureScript() << "" << endl; outstream << "" << job->getLocalDir() << "" << endl; outstream << "" << job->getUploadMode() << "" << endl; if (job->getRemoteDir().isEmpty() == false) outstream << "" << job->getRemoteDir() << "" << endl; if (job->getISOIndex() != -1) outstream << "" << (job->getISOIndex()) << "" << endl; outstream << "" << (job->getTransforFormat()) << "" << endl; if (job->getTargetRotation() != Ekos::INVALID_VALUE) outstream << "" << (job->getTargetRotation()) << "" << endl; QMapIterator> customIter(job->getCustomProperties()); outstream << "" << endl; while (customIter.hasNext()) { customIter.next(); outstream << "" << endl; QMap numbers = customIter.value(); QMapIterator numberIter(numbers); while (numberIter.hasNext()) { numberIter.next(); outstream << "" << cLocale.toString(numberIter.value()) << "" << endl; } outstream << "" << endl; } outstream << "" << endl; outstream << "" << endl; outstream << "" << endl; if (job->getFlatFieldSource() == SOURCE_MANUAL) outstream << "Manual" << endl; else if (job->getFlatFieldSource() == SOURCE_FLATCAP) outstream << "FlatCap" << endl; else if (job->getFlatFieldSource() == SOURCE_DARKCAP) outstream << "DarkCap" << endl; else if (job->getFlatFieldSource() == SOURCE_WALL) { outstream << "Wall" << endl; outstream << "" << cLocale.toString(job->getWallCoord().az().Degrees()) << "" << endl; outstream << "" << cLocale.toString(job->getWallCoord().alt().Degrees()) << "" << endl; } else outstream << "DawnDust" << endl; outstream << "" << endl; outstream << "" << endl; if (job->getFlatFieldDuration() == DURATION_MANUAL) outstream << "Manual" << endl; else { outstream << "ADU" << endl; outstream << "" << cLocale.toString(job->getTargetADU()) << "" << endl; outstream << "" << cLocale.toString(job->getTargetADUTolerance()) << "" << endl; } outstream << "" << endl; outstream << "" << (job->isPreMountPark() ? "True" : "False") << "" << endl; outstream << "" << (job->isPreDomePark() ? "True" : "False") << "" << endl; outstream << "" << endl; outstream << "" << endl; } outstream << "" << endl; appendLogText(i18n("Sequence queue saved to %1", path)); file.close(); return true; } void Capture::resetJobs() { // Stop any running capture stop(); // If a job is selected for edit, reset only that job if (m_JobUnderEdit == true) { SequenceJob * job = jobs.at(queueTable->currentRow()); if (nullptr != job) job->resetStatus(); } else { if (KMessageBox::warningContinueCancel( nullptr, i18n("Are you sure you want to reset status of all jobs?"), i18n("Reset job status"), KStandardGuiItem::cont(), KStandardGuiItem::cancel(), "reset_job_status_warning") != KMessageBox::Continue) { return; } foreach (SequenceJob *job, jobs) job->resetStatus(); } // Also reset the storage count for all jobs capturedFramesMap.clear(); // We're not controlled by the Scheduler, restore progress option ignoreJobProgress = Options::alwaysResetSequenceWhenStarting(); } void Capture::ignoreSequenceHistory() { // This function is called independently from the Scheduler or the UI, so honor the change ignoreJobProgress = true; } void Capture::syncGUIToJob(SequenceJob *job) { QString rawPrefix; bool filterEnabled, expEnabled, tsEnabled; job->getPrefixSettings(rawPrefix, filterEnabled, expEnabled, tsEnabled); exposureIN->setValue(job->getExposure()); binXIN->setValue(job->getXBin()); binYIN->setValue(job->getYBin()); frameXIN->setValue(job->getSubX()); frameYIN->setValue(job->getSubY()); frameWIN->setValue(job->getSubW()); frameHIN->setValue(job->getSubH()); FilterPosCombo->setCurrentIndex(job->getTargetFilter() - 1); frameTypeCombo->setCurrentIndex(job->getFrameType()); prefixIN->setText(rawPrefix); filterCheck->setChecked(filterEnabled); expDurationCheck->setChecked(expEnabled); ISOCheck->setChecked(tsEnabled); countIN->setValue(job->getCount()); delayIN->setValue(job->getDelay() / 1000); postCaptureScriptIN->setText(job->getPostCaptureScript()); uploadModeCombo->setCurrentIndex(job->getUploadMode()); remoteDirIN->setEnabled(uploadModeCombo->currentIndex() != 0); remoteDirIN->setText(job->getRemoteDir()); fitsDir->setText(job->getLocalDir()); // Temperature Options temperatureCheck->setChecked(job->getEnforceTemperature()); if (job->getEnforceTemperature()) temperatureIN->setValue(job->getTargetTemperature()); // Flat field options calibrationB->setEnabled(job->getFrameType() != FRAME_LIGHT); flatFieldDuration = job->getFlatFieldDuration(); flatFieldSource = job->getFlatFieldSource(); targetADU = job->getTargetADU(); targetADUTolerance = job->getTargetADUTolerance(); wallCoord = job->getWallCoord(); preMountPark = job->isPreMountPark(); preDomePark = job->isPreDomePark(); // Custom Properties customPropertiesDialog->setCustomProperties(job->getCustomProperties()); if (ISOCombo->isEnabled()) ISOCombo->setCurrentIndex(job->getISOIndex()); transferFormatCombo->setCurrentIndex(job->getTransforFormat()); if (job->getTargetRotation() != Ekos::INVALID_VALUE) { rotatorSettings->setRotationEnforced(true); rotatorSettings->setTargetRotationPA(job->getTargetRotation()); } else rotatorSettings->setRotationEnforced(false); 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", binXIN->value()); settings.insert("iso", ISOCombo->currentIndex()); settings.insert("frameType", frameTypeCombo->currentIndex()); settings.insert("targetTemperature", temperatureIN->value()); emit settingsUpdated(settings); } void Capture::editJob(QModelIndex i) { SequenceJob *job = jobs.at(i.row()); if (job == nullptr) return; syncGUIToJob(job); appendLogText(i18n("Editing job #%1...", i.row() + 1)); addToQueueB->setIcon(QIcon::fromTheme("dialog-ok-apply")); addToQueueB->setToolTip(i18n("Apply job changes.")); removeFromQueueB->setToolTip(i18n("Cancel job changes.")); m_JobUnderEdit = true; } void Capture::resetJobEdit() { if (m_JobUnderEdit) appendLogText(i18n("Editing job canceled.")); m_JobUnderEdit = false; addToQueueB->setIcon(QIcon::fromTheme("list-add")); addToQueueB->setToolTip(i18n("Add job to sequence queue")); removeFromQueueB->setToolTip(i18n("Remove job from sequence queue")); } void Capture::constructPrefix(QString &imagePrefix) { if (imagePrefix.isEmpty() == false) imagePrefix += '_'; imagePrefix += frameTypeCombo->currentText(); /*if (filterCheck->isChecked() && FilterPosCombo->currentText().isEmpty() == false && frameTypeCombo->currentText().compare("Bias", Qt::CaseInsensitive) && frameTypeCombo->currentText().compare("Dark", Qt::CaseInsensitive))*/ if (filterCheck->isChecked() && FilterPosCombo->currentText().isEmpty() == false && (frameTypeCombo->currentIndex() == FRAME_LIGHT || frameTypeCombo->currentIndex() == FRAME_FLAT)) { imagePrefix += '_'; imagePrefix += FilterPosCombo->currentText(); } if (expDurationCheck->isChecked()) { //if (imagePrefix.isEmpty() == false || frameTypeCheck->isChecked()) imagePrefix += '_'; double exposureValue = exposureIN->value(); // Don't use the locale for exposure value in the capture file name, so that we get a "." as decimal separator if (exposureValue == static_cast(exposureValue)) // Whole number imagePrefix += QString::number(exposureIN->value(), 'd', 0) + QString("_secs"); else // Decimal imagePrefix += QString::number(exposureIN->value(), 'f', 3) + QString("_secs"); } if (ISOCheck->isChecked()) { imagePrefix += SequenceJob::ISOMarker; } } double Capture::getProgressPercentage() { int totalImageCount = 0; int totalImageCompleted = 0; foreach (SequenceJob *job, jobs) { totalImageCount += job->getCount(); totalImageCompleted += job->getCompleted(); } if (totalImageCount != 0) return ((static_cast(totalImageCompleted) / totalImageCount) * 100.0); else return -1; } int Capture::getActiveJobID() { if (activeJob == nullptr) return -1; for (int i = 0; i < jobs.count(); i++) { if (activeJob == jobs[i]) return i; } return -1; } int Capture::getPendingJobCount() { int completedJobs = 0; foreach (SequenceJob *job, jobs) { if (job->getStatus() == SequenceJob::JOB_DONE) completedJobs++; } return (jobs.count() - completedJobs); } QString Capture::getJobState(int id) { if (id < jobs.count()) { SequenceJob *job = jobs.at(id); return job->getStatusString(); } return QString(); } int Capture::getJobImageProgress(int id) { if (id < jobs.count()) { SequenceJob *job = jobs.at(id); return job->getCompleted(); } return -1; } int Capture::getJobImageCount(int id) { if (id < jobs.count()) { SequenceJob *job = jobs.at(id); return job->getCount(); } return -1; } double Capture::getJobExposureProgress(int id) { if (id < jobs.count()) { SequenceJob *job = jobs.at(id); return job->getExposeLeft(); } return -1; } double Capture::getJobExposureDuration(int id) { if (id < jobs.count()) { SequenceJob *job = jobs.at(id); return job->getExposure(); } return -1; } int Capture::getJobRemainingTime(SequenceJob *job) { int remaining = 0; if (job->getStatus() == SequenceJob::JOB_BUSY) remaining += (job->getExposure() + job->getDelay() / 1000) * (job->getCount() - job->getCompleted()) + job->getExposeLeft(); else remaining += (job->getExposure() + job->getDelay() / 1000) * (job->getCount() - job->getCompleted()); return remaining; } int Capture::getOverallRemainingTime() { double remaining = 0; foreach (SequenceJob *job, jobs) remaining += getJobRemainingTime(job); return remaining; } int Capture::getActiveJobRemainingTime() { if (activeJob == nullptr) return -1; return getJobRemainingTime(activeJob); } void Capture::setMaximumGuidingDeviation(bool enable, double value) { guideDeviationCheck->setChecked(enable); if (enable) guideDeviation->setValue(value); } void Capture::setInSequenceFocus(bool enable, double HFR) { autofocusCheck->setChecked(enable); if (enable) HFRPixels->setValue(HFR); } void Capture::setTargetTemperature(double temperature) { temperatureIN->setValue(temperature); } void Capture::clearSequenceQueue() { activeJob = nullptr; //m_TargetName.clear(); //stop(); qDeleteAll(jobs); jobs.clear(); while (queueTable->rowCount() > 0) queueTable->removeRow(0); } QString Capture::getSequenceQueueStatus() { if (jobs.count() == 0) return "Invalid"; if (isBusy) return "Running"; int idle = 0, error = 0, complete = 0, aborted = 0, running = 0; foreach (SequenceJob *job, jobs) { switch (job->getStatus()) { case SequenceJob::JOB_ABORTED: aborted++; break; case SequenceJob::JOB_BUSY: running++; break; case SequenceJob::JOB_DONE: complete++; break; case SequenceJob::JOB_ERROR: error++; break; case SequenceJob::JOB_IDLE: idle++; break; } } if (error > 0) return "Error"; if (aborted > 0) { //if (guideState >= GUIDE_GUIDING && deviationDetected) if (m_State == CAPTURE_SUSPENDED) return "Suspended"; else return "Aborted"; } if (running > 0) return "Running"; if (idle == jobs.count()) return "Idle"; if (complete == jobs.count()) return "Complete"; return "Invalid"; } void Capture::processTelescopeNumber(INumberVectorProperty *nvp) { // If it is not ours, return. if (strcmp(nvp->device, currentTelescope->getDeviceName()) || strstr(nvp->name, "EQUATORIAL_") == nullptr) return; switch (meridianFlipStage) { case MF_NONE: break; case MF_INITIATED: { if (nvp->s == IPS_BUSY) meridianFlipStage = MF_FLIPPING; } break; case MF_FLIPPING: { double ra, dec; currentTelescope->getEqCoords(&ra, &dec); - double diffRA = initialMountCoords.ra().Hours() - ra; + double diffRA = getInitialMountCoords().ra().Hours() - ra; // If the mount is actually flipping then we should see a difference in RA // which if it exceeded MF_RA_DIFF_LIMIT (4 hours) then we consider it to be // undertaking the flip. Otherwise, it's not flipping and let timeout takes care of // of that // Are there any mounts that do NOT change RA while flipping? i.e. do it silently? // Need to investigate that bit if (fabs(diffRA) > MF_RA_DIFF_LIMIT /* || nvp->s == IPS_OK*/) meridianFlipStage = MF_SLEWING; } break; case MF_SLEWING: if (nvp->s != IPS_OK) break; // If dome is syncing, wait until it stops if (dome && dome->isMoving()) break; - // We are at a new initialHA - initialHA = getCurrentHA(); - appendLogText(i18n("Telescope completed the meridian flip.")); //KNotification::event(QLatin1String("MeridianFlipCompleted"), i18n("Meridian flip is successfully completed")); KSNotification::event(QLatin1String("MeridianFlipCompleted"), i18n("Meridian flip is successfully completed"), KSNotification::EVENT_INFO); if (resumeAlignmentAfterFlip == true) { appendLogText(i18n("Performing post flip re-alignment...")); secondsLabel->setText(i18n("Aligning...")); retries = 0; m_State = CAPTURE_ALIGNING; emit newStatus(Ekos::CAPTURE_ALIGNING); meridianFlipStage = MF_ALIGNING; //QTimer::singleShot(Options::settlingTime(), [this]() {emit meridialFlipTracked();}); //emit meridialFlipTracked(); return; } retries = 0; checkGuidingAfterFlip(); break; default: break; } } void Capture::checkGuidingAfterFlip() { // If we're not autoguiding then we're done if (resumeGuidingAfterFlip == false) { resumeSequence(); // N.B. Set meridian flip stage AFTER resumeSequence() always meridianFlipStage = MF_NONE; } else { appendLogText(i18n("Performing post flip re-calibration and guiding...")); secondsLabel->setText(i18n("Calibrating...")); m_State = CAPTURE_CALIBRATING; emit newStatus(Ekos::CAPTURE_CALIBRATING); meridianFlipStage = MF_GUIDING; emit meridianFlipCompleted(); } } double Capture::getCurrentHA() { - double currentRA, currentDEC; + QVariant HA = mountInterface->property("hourAngle"); + return HA.toDouble(); +} - if (currentTelescope == nullptr) - return Ekos::INVALID_VALUE; +double Capture::getInitialHA() +{ + QVariant HA = mountInterface->property("initialHA"); + return HA.toDouble(); +} - if (currentTelescope->getEqCoords(¤tRA, ¤tDEC) == false) - { - appendLogText(i18n("Failed to retrieve telescope coordinates. Unable to calculate telescope's hour angle.")); - return Ekos::INVALID_VALUE; - } +bool Capture::slew(const SkyPoint target) +{ + QList telescopeSlew; + telescopeSlew.append(target.ra().Hours()); + telescopeSlew.append(target.dec().Degrees()); - /* Edge case: undefined HA at poles */ - if (90.0f == currentDEC || -90.0f == currentDEC) - return Ekos::INVALID_VALUE; + QDBusReply const slewModeReply = mountInterface->callWithArgumentList(QDBus::AutoDetect, "slew", telescopeSlew); - dms lst = KStarsData::Instance()->geo()->GSTtoLST(KStarsData::Instance()->clock()->utc().gst()); + if (slewModeReply.error().type() == QDBusError::NoError) + return true; - dms ha = (lst - dms(currentRA * 15.0)); - double HA = ha.Hours(); - if (HA > 12) - HA -= 24; - return HA; + // error occured + qCCritical(KSTARS_EKOS_CAPTURE) << QString("Warning: slew request received DBUS error: %1").arg(QDBusError::errorString(slewModeReply.error().type())); + return false; } + bool Capture::checkMeridianFlip() { - if (currentTelescope == nullptr || meridianCheck->isChecked() == false || initialHA > 0) + if (currentTelescope == nullptr || meridianCheck->isChecked() == false || getInitialHA() > 0) return false; // If active job is taking flat field image at a wall source // then do not flip. if (activeJob && activeJob->getFrameType() == FRAME_FLAT && activeJob->getFlatFieldSource() == SOURCE_WALL) return false; double currentHA = getCurrentHA(); //appendLogText(i18n("Current hour angle %1", currentHA)); if (currentHA == Ekos::INVALID_VALUE) return false; if (currentHA > meridianHours->value()) { //NOTE: DO NOT make the follow sentence PLURAL as the value is in double appendLogText( i18n("Current hour angle %1 hours exceeds meridian flip limit of %2 hours. Auto meridian flip is initiated.", QString("%L1").arg(currentHA, 0, 'f', 2), QString("%L1").arg(meridianHours->value(), 0, 'f', 2))); meridianFlipStage = MF_INITIATED; //KNotification::event(QLatin1String("MeridianFlipStarted"), i18n("Meridian flip started")); KSNotification::event(QLatin1String("MeridianFlipStarted"), i18n("Meridian flip started"), KSNotification::EVENT_INFO); // Suspend guiding first before commanding a meridian flip //if (isAutoGuiding && currentCCD->getChip(ISD::CCDChip::GUIDE_CCD) == guideChip) // emit suspendGuiding(false); // If we are autoguiding, we should resume autoguiding after flip resumeGuidingAfterFlip = (guideState == GUIDE_GUIDING); emit meridianFlipStarted(); // Reset frame if we need to do focusing later on if (isInSequenceFocus || (refocusEveryNCheck->isChecked() && getRefocusEveryNTimerElapsedSec() > 0)) emit resetFocus(); - //double dec; - //currentTelescope->getEqCoords(&initialRA, &dec); - currentTelescope->Slew(&initialMountCoords); - secondsLabel->setText(i18n("Meridian Flip...")); + if (executeMeridianFlip()) + { + secondsLabel->setText(i18n("Meridian Flip...")); - retries = 0; + retries = 0; - m_State = CAPTURE_MERIDIAN_FLIP; - emit newStatus(Ekos::CAPTURE_MERIDIAN_FLIP); + m_State = CAPTURE_MERIDIAN_FLIP; + emit newStatus(Ekos::CAPTURE_MERIDIAN_FLIP); - QTimer::singleShot(MF_TIMER_TIMEOUT, this, &Ekos::Capture::checkMeridianFlipTimeout); - return true; + QTimer::singleShot(MF_TIMER_TIMEOUT, this, &Ekos::Capture::checkMeridianFlipTimeout); + return true; + } } return false; } void Capture::checkMeridianFlipTimeout() { if (meridianFlipStage == MF_NONE) return; if (meridianFlipStage < MF_ALIGNING) { appendLogText(i18n("Telescope meridian flip timed out. Please make sure your mount supports meridian flip.")); if (++retries == 3) { //KNotification::event(QLatin1String("MeridianFlipFailed"), i18n("Meridian flip failed")); KSNotification::event(QLatin1String("MeridianFlipFailed"), i18n("Meridian flip failed"), KSNotification::EVENT_ALERT); abort(); } else { - //double dec; - //currentTelescope->getEqCoords(&initialRA, &dec); - //currentTelescope->Slew(initialRA, dec); - currentTelescope->Slew(&initialMountCoords); - appendLogText(i18n("Retrying meridian flip again...")); + if (executeMeridianFlip()) + appendLogText(i18n("Retrying meridian flip again...")); } } } void Capture::checkGuideDeviationTimeout() { if (activeJob && activeJob->getStatus() == SequenceJob::JOB_ABORTED && m_DeviationDetected) { appendLogText(i18n("Guide module timed out.")); m_DeviationDetected = false; // If capture was suspended, it should be aborted (failed) now. if (m_State == CAPTURE_SUSPENDED) { m_State = CAPTURE_ABORTED; emit newStatus(m_State); } } } void Capture::setAlignStatus(AlignState state) { alignState = state; resumeAlignmentAfterFlip = true; switch (state) { case ALIGN_COMPLETE: if (meridianFlipStage == MF_ALIGNING) { appendLogText(i18n("Post flip re-alignment completed successfully.")); retries = 0; checkGuidingAfterFlip(); } break; case ALIGN_FAILED: // TODO run it 3 times before giving up if (meridianFlipStage == MF_ALIGNING) { if (++retries == 3) { appendLogText(i18n("Post-flip alignment failed.")); abort(); } else { appendLogText(i18n("Post-flip alignment failed. Retrying...")); secondsLabel->setText(i18n("Aligning...")); this->m_State = CAPTURE_ALIGNING; emit newStatus(Ekos::CAPTURE_ALIGNING); meridianFlipStage = MF_ALIGNING; } } break; default: break; } } void Capture::setGuideStatus(GuideState state) { switch (state) { case GUIDE_IDLE: case GUIDE_ABORTED: // If Autoguiding was started before and now stopped, let's abort (unless we're doing a meridian flip) if (guideState == GUIDE_GUIDING && meridianFlipStage == MF_NONE && activeJob && activeJob->getStatus() == SequenceJob::JOB_BUSY) { appendLogText(i18n("Autoguiding stopped. Aborting...")); abort(); autoGuideAbortedCapture = true; } break; case GUIDE_GUIDING: case GUIDE_CALIBRATION_SUCESS: // If capture was aborted due to guide abort // then let's resume capture once we are guiding again. if (autoGuideAbortedCapture && (guideState == GUIDE_ABORTED || guideState == GUIDE_IDLE) && (this->m_State == CAPTURE_ABORTED || this->m_State == CAPTURE_SUSPENDED)) { start(); autoGuideAbortedCapture = false; } autoGuideReady = true; break; case GUIDE_CALIBRATION_ERROR: // TODO try restarting calibration a couple of times before giving up if (meridianFlipStage == MF_GUIDING) { if (++retries == 3) { appendLogText(i18n("Post meridian flip calibration error. Aborting...")); abort(); } else { appendLogText(i18n("Post meridian flip calibration error. Restarting...")); checkGuidingAfterFlip(); } } autoGuideReady = false; break; case GUIDE_DITHERING_SUCCESS: if (Options::guidingSettle() > 0) { // N.B. Do NOT convert to i18np since guidingRate is DOUBLE value (e.g. 1.36) so we always use plural with that. appendLogText(i18n("Dither complete. Resuming capture in %1 seconds...", Options::guidingSettle())); QTimer::singleShot(Options::guidingSettle()*1000, this, &Ekos::Capture::resumeCapture); } else { appendLogText(i18n("Dither complete.")); resumeCapture(); } break; case GUIDE_DITHERING_ERROR: if (Options::guidingSettle() > 0) { // N.B. Do NOT convert to i18np since guidingRate is DOUBLE value (e.g. 1.36) so we always use plural with that. appendLogText(i18n("Warning: Dithering failed. Resuming capture in %1 seconds...", Options::guidingSettle())); QTimer::singleShot(Options::guidingSettle()*1000, this, &Ekos::Capture::resumeCapture); } else { appendLogText(i18n("Warning: Dithering failed.")); resumeCapture(); } break; default: break; } guideState = state; } void Capture::checkFrameType(int index) { if (index == FRAME_LIGHT) calibrationB->setEnabled(false); else calibrationB->setEnabled(true); } double Capture::setCurrentADU(double value) { double nextExposure = 0; double targetADU = activeJob->getTargetADU(); std::vector coeff; // Check if saturated, then take shorter capture and discard value ExpRaw.append(activeJob->getExposure()); ADURaw.append(value); qCDebug(KSTARS_EKOS_CAPTURE) << "Capture: Current ADU = " << value << " targetADU = " << targetADU << " Exposure Count: " << ExpRaw.count(); // Most CCDs are quite linear so 1st degree polynomial is quite sufficient // But DSLRs can exhibit non-linear response curve and so a 2nd degree polynomial is more appropriate if (ExpRaw.count() >= 2) { if (ExpRaw.count() >= 5) { double chisq = 0; coeff = gsl_polynomial_fit(ADURaw.data(), ExpRaw.data(), ExpRaw.count(), 2, chisq); qCDebug(KSTARS_EKOS_CAPTURE) << "Running polynomial fitting. Found " << coeff.size() << " coefficients."; for (size_t i = 0; i < coeff.size(); i++) qCDebug(KSTARS_EKOS_CAPTURE) << "Coeff #" << i << "=" << coeff[i]; } bool looping = false; if (ExpRaw.count() >= 10) { int size = ExpRaw.count(); looping = (ExpRaw[size - 1] == ExpRaw[size - 2]) && (ExpRaw[size - 2] == ExpRaw[size - 3]); if (looping) qWarning(KSTARS_EKOS_CAPTURE) << "Detected looping in polynomial results. Falling back to llsqr."; } // If we get invalid data, let's fall back to llsq // Since polyfit can be unreliable at low counts, let's only use it at the 5th exposure // if we don't have results already. if (looping || ExpRaw.count() < 5 || std::isnan(coeff[0]) || std::isinf(coeff[0])) { double a = 0, b = 0; llsq(ExpRaw, ADURaw, a, b); qWarning(KSTARS_EKOS_CAPTURE) << "Polynomial fitting invalid, falling back to llsq. a=" << a << " b=" << b; // If we have valid results, let's calculate next exposure if (a != 0) { nextExposure = (targetADU - b) / a; // If we get invalid value, let's just proceed iteratively if (nextExposure < 0) nextExposure = 0; } } else if (coeff.size() == 3) { nextExposure = coeff[0] + (coeff[1] * targetADU) + (coeff[2] * pow(targetADU, 2)); // If we get invalid exposure time, let's try to capture again and discard last point. if (nextExposure < 0) { qCDebug(KSTARS_EKOS_CAPTURE) << "Invalid polynomial exposure" << nextExposure << "Will discard last result."; ExpRaw.removeLast(); ADURaw.removeLast(); nextExposure = activeJob->getExposure(); } } } if (nextExposure == 0) { if (value < targetADU) nextExposure = activeJob->getExposure() * 1.25; else nextExposure = activeJob->getExposure() * .75; } qCDebug(KSTARS_EKOS_CAPTURE) << "next flat exposure is" << nextExposure; return nextExposure; } // Based on John Burkardt LLSQ (LGPL) void Capture::llsq(QVector x, QVector y, double &a, double &b) { double bot; int i; double top; double xbar; double ybar; int n = x.count(); // // Special case. // if (n == 1) { a = 0.0; b = y[0]; return; } // // Average X and Y. // xbar = 0.0; ybar = 0.0; for (i = 0; i < n; i++) { xbar = xbar + x[i]; ybar = ybar + y[i]; } xbar = xbar / static_cast(n); ybar = ybar / static_cast(n); // // Compute Beta. // top = 0.0; bot = 0.0; for (i = 0; i < n; i++) { top = top + (x[i] - xbar) * (y[i] - ybar); bot = bot + (x[i] - xbar) * (x[i] - xbar); } a = top / bot; b = ybar - a * xbar; } void Capture::setDirty() { m_Dirty = true; } void Capture::setMeridianFlip(bool enable) { meridianCheck->setChecked(enable); } void Capture::setMeridianFlipHour(double hours) { meridianHours->setValue(hours); } bool Capture::hasCoolerControl() { if (currentCCD && currentCCD->hasCoolerControl()) return true; return false; } bool Capture::setCoolerControl(bool enable) { if (currentCCD && currentCCD->hasCoolerControl()) return currentCCD->setCoolerControl(enable); return false; } void Capture::clearAutoFocusHFR() { // If HFR limit was set from file, we cannot override it. if (fileHFR > 0) return; HFRPixels->setValue(0); //firstAutoFocus = true; } void Capture::openCalibrationDialog() { QDialog calibrationDialog; Ui_calibrationOptions calibrationOptions; calibrationOptions.setupUi(&calibrationDialog); if (currentTelescope) { calibrationOptions.parkMountC->setEnabled(currentTelescope->canPark()); calibrationOptions.parkMountC->setChecked(preMountPark); } else calibrationOptions.parkMountC->setEnabled(false); if (dome) { calibrationOptions.parkDomeC->setEnabled(dome->canPark()); calibrationOptions.parkDomeC->setChecked(preDomePark); } else calibrationOptions.parkDomeC->setEnabled(false); //connect(calibrationOptions.wallSourceC, SIGNAL(toggled(bool)), calibrationOptions.parkC, &Ekos::Capture::setDisabled(bool))); switch (flatFieldSource) { case SOURCE_MANUAL: calibrationOptions.manualSourceC->setChecked(true); break; case SOURCE_FLATCAP: calibrationOptions.flatDeviceSourceC->setChecked(true); break; case SOURCE_DARKCAP: calibrationOptions.darkDeviceSourceC->setChecked(true); break; case SOURCE_WALL: calibrationOptions.wallSourceC->setChecked(true); calibrationOptions.azBox->setText(wallCoord.az().toDMSString()); calibrationOptions.altBox->setText(wallCoord.alt().toDMSString()); break; case SOURCE_DAWN_DUSK: calibrationOptions.dawnDuskFlatsC->setChecked(true); break; } switch (flatFieldDuration) { case DURATION_MANUAL: calibrationOptions.manualDurationC->setChecked(true); break; case DURATION_ADU: calibrationOptions.ADUC->setChecked(true); calibrationOptions.ADUValue->setValue(targetADU); calibrationOptions.ADUTolerance->setValue(targetADUTolerance); break; } if (calibrationDialog.exec() == QDialog::Accepted) { if (calibrationOptions.manualSourceC->isChecked()) flatFieldSource = SOURCE_MANUAL; else if (calibrationOptions.flatDeviceSourceC->isChecked()) flatFieldSource = SOURCE_FLATCAP; else if (calibrationOptions.darkDeviceSourceC->isChecked()) flatFieldSource = SOURCE_DARKCAP; else if (calibrationOptions.wallSourceC->isChecked()) { dms wallAz, wallAlt; bool azOk = false, altOk = false; wallAz = calibrationOptions.azBox->createDms(true, &azOk); wallAlt = calibrationOptions.altBox->createDms(true, &altOk); if (azOk && altOk) { flatFieldSource = SOURCE_WALL; wallCoord.setAz(wallAz); wallCoord.setAlt(wallAlt); } else { calibrationOptions.manualSourceC->setChecked(true); KMessageBox::error(this, i18n("Wall coordinates are invalid.")); } } else flatFieldSource = SOURCE_DAWN_DUSK; if (calibrationOptions.manualDurationC->isChecked()) flatFieldDuration = DURATION_MANUAL; else { flatFieldDuration = DURATION_ADU; targetADU = calibrationOptions.ADUValue->value(); targetADUTolerance = calibrationOptions.ADUTolerance->value(); } preMountPark = calibrationOptions.parkMountC->isChecked(); preDomePark = calibrationOptions.parkDomeC->isChecked(); setDirty(); Options::setCalibrationFlatSourceIndex(flatFieldSource); Options::setCalibrationFlatDurationIndex(flatFieldDuration); Options::setCalibrationWallAz(wallCoord.az().Degrees()); Options::setCalibrationWallAlt(wallCoord.alt().Degrees()); Options::setCalibrationADUValue(targetADU); Options::setCalibrationADUValueTolerance(targetADUTolerance); } } IPState Capture::processPreCaptureCalibrationStage() { // Unpark dust cap if we have to take light images. - if (activeJob->getFrameType() == FRAME_LIGHT && dustCap) + if (activeJob->getFrameType() == FRAME_LIGHT) { - if (dustCap->isLightOn() == true) - { - dustCapLightEnabled = false; - dustCap->SetLightEnabled(false); - } - - // If cap is not unparked, unpark it - if (calibrationStage < CAL_DUSTCAP_UNPARKING && dustCap->isParked()) - { - if (dustCap->UnPark()) + // step 1: unpark dust cap + if (dustCap != nullptr) { + if (dustCap->isLightOn() == true) { - calibrationStage = CAL_DUSTCAP_UNPARKING; - appendLogText(i18n("Unparking dust cap...")); - return IPS_BUSY; + dustCapLightEnabled = false; + dustCap->SetLightEnabled(false); } - else + + // If cap is not unparked, unpark it + if (calibrationStage < CAL_DUSTCAP_UNPARKING && dustCap->isParked()) { - appendLogText(i18n("Unparking dust cap failed, aborting...")); - abort(); - return IPS_ALERT; + if (dustCap->UnPark()) + { + calibrationStage = CAL_DUSTCAP_UNPARKING; + appendLogText(i18n("Unparking dust cap...")); + return IPS_BUSY; + } + else + { + appendLogText(i18n("Unparking dust cap failed, aborting...")); + abort(); + return IPS_ALERT; + } } - } - // Wait until cap is unparked - if (calibrationStage == CAL_DUSTCAP_UNPARKING) - { - if (dustCap->isUnParked() == false) - return IPS_BUSY; - else + // Wait until cap is unparked + if (calibrationStage == CAL_DUSTCAP_UNPARKING) { - calibrationStage = CAL_DUSTCAP_UNPARKED; - appendLogText(i18n("Dust cap unparked.")); + if (dustCap->isUnParked() == false) + return IPS_BUSY; + else + { + calibrationStage = CAL_DUSTCAP_UNPARKED; + appendLogText(i18n("Dust cap unparked.")); + } } } + // step 2: check if meridian flip is required + if (meridianFlipStage != MF_NONE || checkMeridianFlip()) + return IPS_BUSY; + calibrationStage = CAL_PRECAPTURE_COMPLETE; if (guideState == GUIDE_SUSPENDED) { appendLogText(i18n("Autoguiding resumed.")); emit resumeGuiding(); } return IPS_OK; } if (activeJob->getFrameType() != FRAME_LIGHT && guideState == GUIDE_GUIDING) { appendLogText(i18n("Autoguiding suspended.")); emit suspendGuiding(); } // Let's check what actions to be taken, if any, for the flat field source switch (activeJob->getFlatFieldSource()) { case SOURCE_MANUAL: case SOURCE_DAWN_DUSK: // Not yet implemented break; // For Dark, Flat, Bias: Park cap, if not parked. Turn on Light For Flat. Turn off Light for Bias + Darks // For Lights: Unpark cap and turn off light case SOURCE_FLATCAP: if (dustCap) { // If cap is not park, park it if (calibrationStage < CAL_DUSTCAP_PARKING && dustCap->isParked() == false) { if (dustCap->Park()) { calibrationStage = CAL_DUSTCAP_PARKING; appendLogText(i18n("Parking dust cap...")); return IPS_BUSY; } else { appendLogText(i18n("Parking dust cap failed, aborting...")); abort(); return IPS_ALERT; } } // Wait until cap is parked if (calibrationStage == CAL_DUSTCAP_PARKING) { if (dustCap->isParked() == false) return IPS_BUSY; else { calibrationStage = CAL_DUSTCAP_PARKED; appendLogText(i18n("Dust cap parked.")); } } // If light is not on, turn it on. For flat frames only if (activeJob->getFrameType() == FRAME_FLAT && dustCap->isLightOn() == false) { dustCapLightEnabled = true; dustCap->SetLightEnabled(true); } else if (activeJob->getFrameType() != FRAME_FLAT && dustCap->isLightOn() == true) { dustCapLightEnabled = false; dustCap->SetLightEnabled(false); } } break; // Park cap, if not parked and not flat frame // Unpark cap, if flat frame // Turn on Light case SOURCE_DARKCAP: if (dustCap) { // If cap is not park, park it if not flat frame. (external lightsource) if (calibrationStage < CAL_DUSTCAP_PARKING && dustCap->isParked() == false && activeJob->getFrameType() != FRAME_FLAT) { if (dustCap->Park()) { calibrationStage = CAL_DUSTCAP_PARKING; appendLogText(i18n("Parking dust cap...")); return IPS_BUSY; } else { appendLogText(i18n("Parking dust cap failed, aborting...")); abort(); return IPS_ALERT; } } // Wait until cap is parked if (calibrationStage == CAL_DUSTCAP_PARKING) { if (dustCap->isParked() == false) return IPS_BUSY; else { calibrationStage = CAL_DUSTCAP_PARKED; appendLogText(i18n("Dust cap parked.")); } } // If cap is parked, unpark it if flat frame. (external lightsource) if (calibrationStage < CAL_DUSTCAP_UNPARKING && dustCap->isParked() == true && activeJob->getFrameType() == FRAME_FLAT) { if (dustCap->UnPark()) { calibrationStage = CAL_DUSTCAP_UNPARKING; appendLogText(i18n("UnParking dust cap...")); return IPS_BUSY; } else { appendLogText(i18n("UnParking dust cap failed, aborting...")); abort(); return IPS_ALERT; } } // Wait until cap is parked if (calibrationStage == CAL_DUSTCAP_UNPARKING) { if (dustCap->isParked() == true) return IPS_BUSY; else { calibrationStage = CAL_DUSTCAP_UNPARKED; appendLogText(i18n("Dust cap unparked.")); } } // If light is not on, turn it on. For flat frames only if (activeJob->getFrameType() == FRAME_FLAT && dustCap->isLightOn() == false) { dustCapLightEnabled = true; dustCap->SetLightEnabled(true); } else if (activeJob->getFrameType() != FRAME_FLAT && dustCap->isLightOn() == true) { dustCapLightEnabled = false; dustCap->SetLightEnabled(false); } } break; // Go to wall coordinates case SOURCE_WALL: if (currentTelescope && activeJob->getFrameType() == FRAME_FLAT) { if (calibrationStage < CAL_SLEWING) { wallCoord = activeJob->getWallCoord(); wallCoord.HorizontalToEquatorial(KStarsData::Instance()->lst(), KStarsData::Instance()->geo()->lat()); currentTelescope->Slew(&wallCoord); appendLogText(i18n("Mount slewing to wall position...")); calibrationStage = CAL_SLEWING; return IPS_BUSY; } // Check if slewing is complete if (calibrationStage == CAL_SLEWING) { if (currentTelescope->isSlewing() == false) { // Disable mount tracking if supported by the driver. currentTelescope->setTrackEnabled(false); calibrationStage = CAL_SLEWING_COMPLETE; appendLogText(i18n("Slew to wall position complete.")); } else return IPS_BUSY; } if (lightBox) { // Check if we have a light box to turn on if (activeJob->getFrameType() == FRAME_FLAT && lightBox->isLightOn() == false) { lightBoxLightEnabled = true; lightBox->SetLightEnabled(true); } else if (activeJob->getFrameType() != FRAME_FLAT && lightBox->isLightOn() == true) { lightBoxLightEnabled = false; lightBox->SetLightEnabled(false); } } } break; } // Check if we need to perform mount prepark if (preMountPark && currentTelescope && activeJob->getFlatFieldSource() != SOURCE_WALL) { if (calibrationStage < CAL_MOUNT_PARKING && currentTelescope->isParked() == false) { if (currentTelescope->Park()) { calibrationStage = CAL_MOUNT_PARKING; //emit mountParking(); appendLogText(i18n("Parking mount prior to calibration frames capture...")); return IPS_BUSY; } else { appendLogText(i18n("Parking mount failed, aborting...")); abort(); return IPS_ALERT; } } if (calibrationStage == CAL_MOUNT_PARKING) { // If not parked yet, check again in 1 second // Otherwise proceed to the rest of the algorithm if (currentTelescope->isParked() == false) return IPS_BUSY; else { calibrationStage = CAL_MOUNT_PARKED; appendLogText(i18n("Mount parked.")); } } } // Check if we need to perform dome prepark if (preDomePark && dome) { if (calibrationStage < CAL_DOME_PARKING && dome->isParked() == false) { if (dome->Park()) { calibrationStage = CAL_DOME_PARKING; //emit mountParking(); appendLogText(i18n("Parking dome...")); return IPS_BUSY; } else { appendLogText(i18n("Parking dome failed, aborting...")); abort(); return IPS_ALERT; } } if (calibrationStage == CAL_DOME_PARKING) { // If not parked yet, check again in 1 second // Otherwise proceed to the rest of the algorithm if (dome->isParked() == false) return IPS_BUSY; else { calibrationStage = CAL_DOME_PARKED; appendLogText(i18n("Dome parked.")); } } } // If we used AUTOFOCUS before for a specific frame (e.g. Lum) // then the absolute focus position for Lum is recorded in the filter manager // when we take flats again, we always go back to the same focus position as the light frames to ensure // near identical focus for both frames. if (activeJob->getFrameType() == FRAME_FLAT && Options::flatSyncFocus()) { if (currentFilter != nullptr) { if (filterManager->syncAbsoluteFocusPosition(activeJob->getTargetFilter()-1) == false) return IPS_BUSY; } } calibrationStage = CAL_PRECAPTURE_COMPLETE; return IPS_OK; } bool Capture::processPostCaptureCalibrationStage() { // If there are no more images to capture, do not bother calculating next exposure if (calibrationStage == CAL_CALIBRATION_COMPLETE) if (activeJob && activeJob->getCount() <= activeJob->getCompleted()) return true; // Check if we need to do flat field slope calculation if the user specified a desired ADU value if (activeJob->getFrameType() == FRAME_FLAT && activeJob->getFlatFieldDuration() == DURATION_ADU && activeJob->getTargetADU() > 0) { if (Options::useFITSViewer() == false) { Options::setUseFITSViewer(true); qCInfo(KSTARS_EKOS_CAPTURE) << "Enabling FITS Viewer..."; } FITSData *image_data = nullptr; FITSView *currentImage = targetChip->getImageView(FITS_NORMAL); if (currentImage) { image_data = currentImage->getImageData(); double currentADU = image_data->getADU(); bool outOfRange=false, saturated=false; switch (image_data->bpp()) { case 8: if (activeJob->getTargetADU() > UINT8_MAX) outOfRange = true; else if (currentADU/UINT8_MAX > 0.95) saturated = true; break; case 16: if (activeJob->getTargetADU() > UINT16_MAX) outOfRange = true; else if (currentADU/UINT16_MAX > 0.95) saturated = true; break; case 32: if (activeJob->getTargetADU() > UINT32_MAX) outOfRange = true; else if (currentADU/UINT32_MAX > 0.95) saturated = true; break; default: break; } if (outOfRange) { appendLogText(i18n("Flat calibration failed. Captured image is only %1-bit while requested ADU is %2.", QString::number(image_data->bpp()) , QString::number(activeJob->getTargetADU(), 'f', 2))); abort(); return false; } else if (saturated) { double nextExposure = activeJob->getExposure() * 0.1; nextExposure = qBound(exposureIN->minimum(), nextExposure, exposureIN->maximum()); appendLogText(i18n("Current image is saturated (%1). Next exposure is %2 seconds.", QString::number(currentADU, 'f', 0), QString("%L1").arg(nextExposure, 0, 'f', 6))); calibrationStage = CAL_CALIBRATION; activeJob->setExposure(nextExposure); activeJob->setPreview(true); rememberUploadMode = activeJob->getUploadMode(); currentCCD->setUploadMode(ISD::CCD::UPLOAD_CLIENT); startNextExposure(); return false; } double ADUDiff = fabs(currentADU - activeJob->getTargetADU()); // If it is within tolerance range of target ADU if (ADUDiff <= targetADUTolerance) { if (calibrationStage == CAL_CALIBRATION) { appendLogText( i18n("Current ADU %1 within target ADU tolerance range.", QString::number(currentADU, 'f', 0))); activeJob->setPreview(false); currentCCD->setUploadMode(rememberUploadMode); // Get raw prefix exposureIN->setValue(activeJob->getExposure()); QString imagePrefix = activeJob->getRawPrefix(); constructPrefix(imagePrefix); activeJob->setFullPrefix(imagePrefix); seqPrefix = imagePrefix; currentCCD->setSeqPrefix(imagePrefix); currentCCD->updateUploadSettings(activeJob->getRemoteDir() + activeJob->getDirectoryPostfix()); calibrationStage = CAL_CALIBRATION_COMPLETE; startNextExposure(); return false; } return true; } double nextExposure = -1; // If value is saturated, try to reduce it to valid range first if (std::fabs(image_data->getMax(0) - image_data->getMin(0)) < 10) nextExposure = activeJob->getExposure() * 0.5; else nextExposure = setCurrentADU(currentADU); if (nextExposure <= 0 || std::isnan(nextExposure)) { appendLogText( i18n("Unable to calculate optimal exposure settings, please capture the flats manually.")); //activeJob->setTargetADU(0); //targetADU = 0; abort(); return false; } // Limit to minimum and maximum values nextExposure = qBound(exposureIN->minimum(), nextExposure, exposureIN->maximum()); appendLogText(i18n("Current ADU is %1 Next exposure is %2 seconds.", QString::number(currentADU, 'f', 0), QString("%L1").arg(nextExposure, 0, 'f', 6))); calibrationStage = CAL_CALIBRATION; activeJob->setExposure(nextExposure); activeJob->setPreview(true); rememberUploadMode = activeJob->getUploadMode(); currentCCD->setUploadMode(ISD::CCD::UPLOAD_CLIENT); startNextExposure(); return false; // Start next exposure in case ADU Slope is not calculated yet /*if (currentSlope == 0) { startNextExposure(); return; }*/ } else { appendLogText(i18n("An empty image is received, aborting...")); abort(); return false; } } calibrationStage = CAL_CALIBRATION_COMPLETE; return true; } void Capture::setNewRemoteFile(QString file) { appendLogText(i18n("Remote image saved to %1", file)); emit newSequenceImage(file); } /* void Capture::startPostFilterAutoFocus() { if (focusState >= FOCUS_PROGRESS || state == CAPTURE_FOCUSING) return; secondsLabel->setText(i18n("Focusing...")); state = CAPTURE_FOCUSING; emit newStatus(Ekos::CAPTURE_FOCUSING); appendLogText(i18n("Post filter change Autofocus...")); // Force it to always run autofocus routine emit checkFocus(0.1); } */ void Capture::postScriptFinished(int exitCode) { appendLogText(i18n("Post capture script finished with code %1.", exitCode)); // If we're done, proceed to completion. if (activeJob->getCount() <= activeJob->getCompleted()) { processJobCompletion(); } // Else check if meridian condition is met. else if (checkMeridianFlip()) { appendLogText(i18n("Processing meridian flip...")); } // Then if nothing else, just resume sequence. else { appendLogText(i18n("Resuming sequence...")); resumeSequence(); } } // FIXME Migrate to Filter Manager #if 0 void Capture::loadFilterOffsets() { // Get all OAL equipment filter list KStarsData::Instance()->userdb()->GetAllFilters(m_filterList); filterFocusOffsets.clear(); for (int i = 0; i < FilterPosCombo->count(); i++) { FocusOffset *oneOffset = new FocusOffset; oneOffset->filter = FilterPosCombo->itemText(i); oneOffset->offset = 0; // Find matching filter if any and loads its offset foreach (OAL::Filter *o, m_filterList) { if (o->vendor() == FilterCaptureCombo->currentText() && o->color() == oneOffset->filter) { oneOffset->offset = o->offset().toInt(); break; } } filterFocusOffsets.append(oneOffset); } } void Capture::showFilterOffsetDialog() { loadFilterOffsets(); QDialog filterOffsetDialog; filterOffsetDialog.setWindowTitle(i18n("Filter Focus Offsets")); QDialogButtonBox *buttonBox = new QDialogButtonBox(QDialogButtonBox::Ok | QDialogButtonBox::Cancel, Qt::Horizontal, &filterOffsetDialog); connect(buttonBox, SIGNAL(accepted()), &filterOffsetDialog, &Ekos::Capture::accept())); connect(buttonBox, SIGNAL(rejected()), &filterOffsetDialog, &Ekos::Capture::reject())); QVBoxLayout *mainLayout = new QVBoxLayout(&filterOffsetDialog); QGridLayout *grid = new QGridLayout(&filterOffsetDialog); QHBoxLayout *tipLayout = new QHBoxLayout(&filterOffsetDialog); QLabel *tipIcon = new QLabel(&filterOffsetDialog); QLabel *tipText = new QLabel(&filterOffsetDialog); tipIcon->setPixmap( QIcon::fromTheme("kstars_flag").pixmap(QSize(32, 32))); tipIcon->setFixedSize(32, 32); tipText->setText(i18n("Set relative filter focus offset in steps.")); tipLayout->addWidget(tipIcon); tipLayout->addWidget(tipText); mainLayout->addLayout(grid); mainLayout->addLayout(tipLayout); mainLayout->addWidget(buttonBox); //filterOffsetDialog.setLayout(mainLayout); for (int i = 0; i < filterFocusOffsets.count(); i++) { FocusOffset *oneOffset = filterFocusOffsets.at(i); QLabel *label = new QLabel(oneOffset->filter, &filterOffsetDialog); QSpinBox *spin = new QSpinBox(&filterOffsetDialog); spin->setMinimum(-10000); spin->setMaximum(10000); spin->setSingleStep(100); spin->setValue(oneOffset->offset); grid->addWidget(label, i, 0); grid->addWidget(spin, i, 1); } if (filterOffsetDialog.exec() == QDialog::Accepted) { for (int i = 0; i < filterFocusOffsets.count(); i++) { FocusOffset *oneOffset = filterFocusOffsets.at(i); oneOffset->offset = static_cast(grid->itemAtPosition(i, 1)->widget())->value(); // Find matching filter if any and save its offset OAL::Filter *matchedFilter = nullptr; foreach (OAL::Filter *o, m_filterList) { if (o->vendor() == FilterCaptureCombo->currentText() && o->color() == oneOffset->filter) { o->setOffset(QString::number(oneOffset->offset)); matchedFilter = o; break; } } #if 0 // If no filter exists, let's create one if (matchedFilter == nullptr) { KStarsData::Instance()->userdb()->AddFilter(FilterCaptureCombo->currentText(), "", "", QString::number(oneOffset->offset), oneOffset->filter, "1"); } // Or update Existing one else { KStarsData::Instance()->userdb()->AddFilter(FilterCaptureCombo->currentText(), "", "", QString::number(oneOffset->offset), oneOffset->filter, matchedFilter->exposure(), matchedFilter->id()); } #endif } } } #endif void Capture::toggleVideo(bool enabled) { if (currentCCD == nullptr) return; if (currentCCD->isBLOBEnabled() == false) { if (Options::guiderType() != Ekos::Guide::GUIDE_INTERNAL || KMessageBox::questionYesNo(nullptr, i18n("Image transfer is disabled for this camera. Would you like to enable it?")) == KMessageBox::Yes) currentCCD->setBLOBEnabled(true); else return; } currentCCD->setVideoStreamEnabled(enabled); } void Capture::setVideoStreamEnabled(bool enabled) { if (enabled) { liveVideoB->setChecked(true); liveVideoB->setIcon(QIcon::fromTheme("camera-on")); //liveVideoB->setStyleSheet("color:red;"); } else { liveVideoB->setChecked(false); liveVideoB->setIcon(QIcon::fromTheme("camera-ready")); //liveVideoB->setStyleSheet(QString()); } } void Capture::setMountStatus(ISD::Telescope::Status newState) { switch (newState) { case ISD::Telescope::MOUNT_PARKING: case ISD::Telescope::MOUNT_SLEWING: case ISD::Telescope::MOUNT_MOVING: previewB->setEnabled(false); liveVideoB->setEnabled(false); // Only disable when button is "Start", and not "Stopped" // If mount is in motion, Stopped button should always be enabled to terminate // the sequence if (pi->isAnimated() == false) startB->setEnabled(false); break; default: if (pi->isAnimated() == false) { previewB->setEnabled(true); if (currentCCD) liveVideoB->setEnabled(currentCCD->hasVideoStream()); startB->setEnabled(true); } break; } } void Capture::showObserverDialog() { QList m_observerList; KStars::Instance()->data()->userdb()->GetAllObservers(m_observerList); QStringList observers; for (auto &o : m_observerList) observers << QString("%1 %2").arg(o->name(), o->surname()); QDialog observersDialog(this); observersDialog.setWindowTitle(i18n("Select Current Observer")); QLabel label(i18n("Current Observer:")); QComboBox observerCombo(&observersDialog); observerCombo.addItems(observers); observerCombo.setCurrentText(m_ObserverName); observerCombo.setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Fixed); QPushButton manageObserver(&observersDialog); manageObserver.setFixedSize(QSize(32, 32)); manageObserver.setIcon(QIcon::fromTheme("document-edit")); manageObserver.setAttribute(Qt::WA_LayoutUsesWidgetRect); manageObserver.setToolTip(i18n("Manage Observers")); connect(&manageObserver, &QPushButton::clicked, this, [&]() { ObserverAdd add; add.exec(); QList m_observerList; KStars::Instance()->data()->userdb()->GetAllObservers(m_observerList); QStringList observers; for (auto &o : m_observerList) observers << QString("%1 %2").arg(o->name(), o->surname()); observerCombo.clear(); observerCombo.addItems(observers); observerCombo.setCurrentText(m_ObserverName); }); QHBoxLayout *layout = new QHBoxLayout; layout->addWidget(&label); layout->addWidget(&observerCombo); layout->addWidget(&manageObserver); observersDialog.setLayout(layout); observersDialog.exec(); m_ObserverName = observerCombo.currentText(); Options::setDefaultObserver(m_ObserverName); } void Capture::startRefocusTimer(bool forced) { /* If refocus is requested, only restart timer if not already running in order to keep current elapsed time since last refocus */ if (refocusEveryNCheck->isChecked()) { // How much time passed since we last started the time uint32_t elapsedSecs = refocusEveryNTimer.elapsed()/1000; // How many seconds do we wait for between focusing (60 mins ==> 3600 secs) uint32_t totalSecs = refocusEveryN->value()*60; if (!refocusEveryNTimer.isValid() || forced) { appendLogText(i18n("Ekos will refocus in %1 seconds.", totalSecs)); refocusEveryNTimer.restart(); } else if (elapsedSecs < totalSecs) { //appendLogText(i18n("Ekos will refocus in %1 seconds, last procedure was %2 seconds ago.", refocusEveryNTimer.elapsed()/1000-refocusEveryNTimer.elapsed()*60, refocusEveryNTimer.elapsed()/1000)); appendLogText(i18n("Ekos will refocus in %1 seconds, last procedure was %2 seconds ago.", totalSecs - elapsedSecs, elapsedSecs)); } else { appendLogText(i18n("Ekos will refocus as soon as possible, last procedure was %1 seconds ago.", elapsedSecs)); } } } int Capture::getRefocusEveryNTimerElapsedSec() { /* If timer isn't valid, consider there is no focus to be done, that is, that focus was just done */ return refocusEveryNTimer.isValid() ? refocusEveryNTimer.elapsed()/1000 : 0; } void Capture::setAlignResults(double orientation, double ra, double de, double pixscale) { Q_UNUSED(orientation); Q_UNUSED(ra); Q_UNUSED(de); Q_UNUSED(pixscale); if (currentRotator == nullptr) return; rotatorSettings->refresh(); } void Capture::setFilterManager(const QSharedPointer &manager) { filterManager = manager; connect(filterManagerB, &QPushButton::clicked, [this]() { filterManager->show(); filterManager->raise(); }); connect(filterManager.data(), &FilterManager::ready, [this]() { m_CurrentFilterPosition = filterManager->getFilterPosition(); // Due to race condition, focusState = FOCUS_IDLE; if (activeJob) activeJob->setCurrentFilter(m_CurrentFilterPosition); } ); connect(filterManager.data(), &FilterManager::failed, [this]() { if (activeJob) { appendLogText(i18n("Filter operation failed.")); abort(); } } ); connect(filterManager.data(), &FilterManager::newStatus, [this](Ekos::FilterState filterState) { if (m_State == CAPTURE_CHANGING_FILTER) { secondsLabel->setText(Ekos::getFilterStatusString(filterState)); switch (filterState) { case FILTER_OFFSET: appendLogText(i18n("Changing focus offset by %1 steps...", filterManager->getTargetFilterOffset())); break; case FILTER_CHANGE: appendLogText(i18n("Changing filter to %1...", FilterPosCombo->itemText(filterManager->getTargetFilterPosition()-1))); break; case FILTER_AUTOFOCUS: appendLogText(i18n("Auto focus on filter change...")); clearAutoFocusHFR(); break; default: break; } } }); connect(filterManager.data(), &FilterManager::labelsChanged, this, [this]() { FilterPosCombo->clear(); FilterPosCombo->addItems(filterManager->getFilterLabels()); m_CurrentFilterPosition = filterManager->getFilterPosition(); FilterPosCombo->setCurrentIndex(m_CurrentFilterPosition-1); }); connect(filterManager.data(), &FilterManager::positionChanged, this, [this]() { m_CurrentFilterPosition = filterManager->getFilterPosition(); FilterPosCombo->setCurrentIndex(m_CurrentFilterPosition-1); }); } void Capture::addDSLRInfo(const QString &model, uint32_t maxW, uint32_t maxH, double pixelW, double pixelH) { // Check if model already exists auto pos = std::find_if(DSLRInfos.begin(), DSLRInfos.end(), [model](QMap &oneDSLRInfo) { return (oneDSLRInfo["Model"] == model);}); if (pos != DSLRInfos.end()) { KStarsData::Instance()->userdb()->DeleteDSLRInfo(model); DSLRInfos.removeOne(*pos); } QMap oneDSLRInfo; oneDSLRInfo["Model"] = model; oneDSLRInfo["Width"] = maxW; oneDSLRInfo["Height"] = maxH; oneDSLRInfo["PixelW"] = pixelW; oneDSLRInfo["PixelH"] = pixelH; KStarsData::Instance()->userdb()->AddDSLRInfo(oneDSLRInfo); KStarsData::Instance()->userdb()->GetAllDSLRInfos(DSLRInfos); } bool Capture::isModelinDSLRInfo(const QString &model) { auto pos = std::find_if(DSLRInfos.begin(), DSLRInfos.end(), [model](QMap &oneDSLRInfo) { return (oneDSLRInfo["Model"] == model);}); return (pos != DSLRInfos.end()); } #if 0 void Capture::syncDriverToDSLRLimits() { if (targetChip == nullptr) return; QString model(currentCCD->getDeviceName()); // Check if model already exists auto pos = std::find_if(DSLRInfos.begin(), DSLRInfos.end(), [model](QMap &oneDSLRInfo) { return (oneDSLRInfo["Model"] == model);}); if (pos != DSLRInfos.end()) targetChip->setImageInfo((*pos)["Width"].toInt(), (*pos)["Height"].toInt(), (*pos)["PixelW"].toDouble(), (*pos)["PixelH"].toDouble(), 8); } #endif void Capture::cullToDSLRLimits() { QString model(currentCCD->getDeviceName()); // Check if model already exists auto pos = std::find_if(DSLRInfos.begin(), DSLRInfos.end(), [model](QMap &oneDSLRInfo) { return (oneDSLRInfo["Model"] == model);}); if (pos != DSLRInfos.end()) { if (frameWIN->maximum() == 0 || frameWIN->maximum() > (*pos)["Width"].toInt()) { frameWIN->setValue((*pos)["Width"].toInt()); frameWIN->setMaximum((*pos)["Width"].toInt()); } if (frameHIN->maximum() == 0 || frameHIN->maximum() > (*pos)["Height"].toInt()) { frameHIN->setValue((*pos)["Height"].toInt()); frameHIN->setMaximum((*pos)["Height"].toInt()); } } } void Capture::setCapturedFramesMap(const QString &signature, int count) { capturedFramesMap[signature] = count; qCDebug(KSTARS_EKOS_CAPTURE) << QString("Client module indicates that storage for '%1' has already %2 captures processed.").arg(signature).arg(count); // Scheduler's captured frame map overrides the progress option of the Capture module ignoreJobProgress = false; } void Capture::setSettings(const QJsonObject &settings) { // FIXME: QComboBox signal "activated" does not trigger when setting text programmatically. CCDCaptureCombo->setCurrentText(settings["camera"].toString()); FilterDevicesCombo->setCurrentText(settings["fw"].toString()); FilterPosCombo->setCurrentText(settings["filter"].toString()); exposureIN->setValue(settings["exp"].toDouble(1)); int bin = settings["bin"].toInt(1); setBinning(bin,bin); double temperature = settings["temperature"].toDouble(Ekos::INVALID_VALUE); if (temperature != Ekos::INVALID_VALUE) { setForceTemperature(true); setTargetTemperature(temperature); } double gain = settings["gain"].toDouble(Ekos::INVALID_VALUE); if (gain != Ekos::INVALID_VALUE && currentCCD) { QMap > customProps = customPropertiesDialog->getCustomProperties(); // Gain is manifested in two forms // Property CCD_GAIN and // Part of CCD_CONTROLS properties. // Therefore, we have to find what the currently camera supports first. if (currentCCD->getProperty("CCD_GAIN")) { QMap ccdGain; ccdGain["GAIN"] = gain; customProps["CCD_GAIN"] = ccdGain; } else if (currentCCD->getProperty("CCD_CONTROLS")) { QMap ccdGain; ccdGain["Gain"] = gain; customProps["CCD_CONTROLS"] = ccdGain; } customPropertiesDialog->setCustomProperties(customProps); } frameTypeCombo->setCurrentIndex(settings["frameType"].toInt(0)); // ISO int isoIndex = settings["iso"].toInt(-1); if (isoIndex >= 0) setISO(isoIndex); } void Capture::clearCameraConfiguration() { if (KMessageBox::questionYesNo(nullptr, i18n("Reset %1 configuration to default?", currentCCD->getDeviceName()), i18n("Confirmation")) == KMessageBox::No) return; currentCCD->setConfig(LOAD_DEFAULT_CONFIG); KStarsData::Instance()->userdb()->DeleteDSLRInfo(currentCCD->getDeviceName()); QStringList shutterfulCCDs = Options::shutterfulCCDs(); QStringList shutterlessCCDs = Options::shutterlessCCDs(); // Remove camera from shutterful and shutterless CCDs if (shutterfulCCDs.contains(currentCCD->getDeviceName())) { shutterfulCCDs.removeOne(currentCCD->getDeviceName()); Options::setShutterfulCCDs(shutterfulCCDs); } if (shutterlessCCDs.contains(currentCCD->getDeviceName())) { shutterlessCCDs.removeOne(currentCCD->getDeviceName()); Options::setShutterlessCCDs(shutterlessCCDs); } // For DSLRs, immediately ask them to enter the values again. if (ISOCombo->count() > 0) { DSLRInfo infoDialog(this, currentCCD); if (infoDialog.exec() == QDialog::Accepted) { addDSLRInfo(QString(currentCCD->getDeviceName()), infoDialog.sensorMaxWidth, infoDialog.sensorMaxHeight, infoDialog.sensorPixelW, infoDialog.sensorPixelH); updateFrameProperties(); resetFrame(); } } } void Capture::setCoolerToggled(bool enabled) { coolerOnB->blockSignals(true); coolerOnB->setChecked(enabled); coolerOnB->blockSignals(false); coolerOffB->blockSignals(true); coolerOffB->setChecked(!enabled); coolerOffB->blockSignals(false); appendLogText(enabled ? i18n("Cooler is on") : i18n("Cooler is off")); } void Capture::processCaptureTimeout() { captureTimeoutCounter++; if (captureTimeoutCounter >= 3) { captureTimeoutCounter = 0; appendLogText(i18n("Exposure timeout. Aborting...")); abort(); return; } appendLogText(i18n("Exposure timeout. Restarting exposure...")); ISD::CCDChip *targetChip = currentCCD->getChip(ISD::CCDChip::PRIMARY_CCD); targetChip->abortExposure(); targetChip->capture(exposureIN->value()); captureTimeout.start(exposureIN->value() * 1000 + CAPTURE_TIMEOUT_THRESHOLD); } } diff --git a/kstars/ekos/capture/capture.h b/kstars/ekos/capture/capture.h index c40a41283..8ee0db56c 100644 --- a/kstars/ekos/capture/capture.h +++ b/kstars/ekos/capture/capture.h @@ -1,787 +1,803 @@ /* Ekos Capture tool Copyright (C) 2012 Jasem Mutlaq This application is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version. */ #pragma once #include "ui_capture.h" #include "customproperties.h" #include "oal/filter.h" #include "ekos/ekos.h" #include "indi/indiccd.h" #include "indi/indicap.h" #include "indi/indidome.h" #include "indi/indilightbox.h" #include "indi/inditelescope.h" #include "ekos/auxiliary/filtermanager.h" #include "ekos/scheduler/schedulerjob.h" #include #include #include #include class QProgressIndicator; class QTableWidgetItem; class KDirWatch; class RotatorSettings; /** * @namespace Ekos * @short Ekos is an advanced Astrophotography tool for Linux. * It is based on a modular extensible framework to perform common astrophotography tasks. This includes highly accurate GOTOs using astrometry solver, ability to measure and correct polar alignment errors , * auto-focus & auto-guide capabilities, and capture of single or stack of images with filter wheel support.\n * Features: * - Control your telescope, CCD (& DSLRs), filter wheel, focuser, guider, adaptive optics unit, and any INDI-compatible auxiliary device from Ekos. * - Extremely accurate GOTOs using astrometry.net solver (both Online and Offline solvers supported). * - Load & Slew: Load a FITS image, slew to solved coordinates, and center the mount on the exact image coordinates in order to get the same desired frame. * - Measure & Correct Polar Alignment errors using astromety.net solver. * - Auto and manual focus modes using Half-Flux-Radius (HFR) method. * - Automated unattended meridian flip. Ekos performs post meridian flip alignment, calibration, and guiding to resume the capture session. * - Automatic focus between exposures when a user-configurable HFR limit is exceeded. * - Auto guiding with support for automatic dithering between exposures and support for Adaptive Optics devices in addition to traditional guiders. * - Powerful sequence queue for batch capture of images with optional prefixes, timestamps, filter wheel selection, and much more! * - Export and import sequence queue sets as Ekos Sequence Queue (.esq) files. * - Center the telescope anywhere in a captured FITS image or any FITS with World Coordinate System (WCS) header. * - Automatic flat field capture, just set the desired ADU and let Ekos does the rest! * - Automatic abort and resumption of exposure tasks if guiding errors exceed a user-configurable value. * - Support for dome slaving. * - Complete integration with KStars Observation Planner and SkyMap * - Integrate with all INDI native devices. * - Powerful scripting capabilities via \ref EkosDBusInterface "DBus." * * The primary class is Ekos::Manager. It handles startup and shutdown of local and remote INDI devices, manages and orchesterates the various Ekos modules, and provides advanced DBus * interface to enable unattended scripting. * * @author Jasem Mutlaq * @version 1.7 */ namespace Ekos { class SequenceJob; /** *@class Capture *@short Captures single or sequence of images from a CCD. * The capture class support capturing single or multiple images from a CCD, it provides a powerful sequence queue with filter wheel selection. Any sequence queue can be saved as Ekos Sequence Queue (.esq). * All image capture operations are saved as Sequence Jobs that encapsulate all the different options in a capture process. The user may select in sequence autofocusing by setting a maximum HFR limit. When the limit * is exceeded, it automatically trigger autofocus operation. The capture process can also be linked with guide module. If guiding deviations exceed a certain threshold, the capture operation aborts until * the guiding deviation resume to acceptable levels and the capture operation is resumed. *@author Jasem Mutlaq *@version 1.3 */ class Capture : public QWidget, public Ui::Capture { Q_OBJECT Q_CLASSINFO("D-Bus Interface", "org.kde.kstars.Ekos.Capture") Q_PROPERTY(Ekos::CaptureState status READ status NOTIFY newStatus) Q_PROPERTY(QString targetName MEMBER m_TargetName) Q_PROPERTY(QString observerName MEMBER m_ObserverName) Q_PROPERTY(QString camera READ camera WRITE setCamera) Q_PROPERTY(QString filterWheel READ filterWheel WRITE setFilterWheel) Q_PROPERTY(QString filter READ filter WRITE setFilter) Q_PROPERTY(bool coolerControl READ hasCoolerControl WRITE setCoolerControl) Q_PROPERTY(QStringList logText READ logText NOTIFY newLog) public: typedef enum { MF_NONE, MF_INITIATED, MF_FLIPPING, MF_SLEWING, MF_ALIGNING, MF_GUIDING } MFStage; typedef enum { CAL_NONE, CAL_DUSTCAP_PARKING, CAL_DUSTCAP_PARKED, CAL_LIGHTBOX_ON, CAL_SLEWING, CAL_SLEWING_COMPLETE, CAL_MOUNT_PARKING, CAL_MOUNT_PARKED, CAL_DOME_PARKING, CAL_DOME_PARKED, CAL_PRECAPTURE_COMPLETE, CAL_CALIBRATION, CAL_CALIBRATION_COMPLETE, CAL_CAPTURING, CAL_DUSTCAP_UNPARKING, CAL_DUSTCAP_UNPARKED } CalibrationStage; typedef bool (Capture::*PauseFunctionPointer)(); Capture(); ~Capture(); /** @defgroup CaptureDBusInterface Ekos DBus Interface - Capture Module * Ekos::Capture interface provides advanced scripting capabilities to capture image sequences. */ /*@{*/ /** DBUS interface function. * select the CCD device from the available CCD drivers. * @param device The CCD device name */ Q_SCRIPTABLE bool setCamera(const QString &device); Q_SCRIPTABLE QString camera(); /** DBUS interface function. * select the filter device from the available filter drivers. The filter device can be the same as the CCD driver if the filter functionality was embedded within the driver. * @param device The filter device name */ Q_SCRIPTABLE bool setFilterWheel(const QString &device); Q_SCRIPTABLE QString filterWheel(); /** DBUS interface function. * select the filter name from the available filters in case a filter device is active. * @param filter The filter name */ Q_SCRIPTABLE bool setFilter(const QString &filter); Q_SCRIPTABLE QString filter(); /** DBUS interface function. * Aborts any current jobs and remove all sequence queue jobs. */ Q_SCRIPTABLE Q_NOREPLY void clearSequenceQueue(); /** DBUS interface function. * Returns the overall sequence queue status. If there are no jobs pending, it returns "Invalid". If all jobs are idle, it returns "Idle". If all jobs are complete, it returns "Complete". If one or more jobs are aborted * it returns "Aborted" unless it was temporarily aborted due to guiding deviations, then it would return "Suspended". If one or more jobs have errors, it returns "Error". If any jobs is under progress, returns "Running". */ Q_SCRIPTABLE QString getSequenceQueueStatus(); /** DBUS interface function. * Loads the Ekos Sequence Queue file in the Sequence Queue. Jobs are appended to existing jobs. * @param fileURL full URL of the filename */ Q_SCRIPTABLE bool loadSequenceQueue(const QString &fileURL); /** DBUS interface function. * Enables or disables the maximum guiding deviation and sets its value. * @param enable If true, enable the guiding deviation check, otherwise, disable it. * @param value if enable is true, it sets the maximum guiding deviation in arcsecs. If the value is exceeded, the capture operation is aborted until the value falls below the value threshold. */ Q_SCRIPTABLE Q_NOREPLY void setMaximumGuidingDeviation(bool enable, double value); /** DBUS interface function. * Enables or disables the in sequence focus and sets Half-Flux-Radius (HFR) limit. * @param enable If true, enable the in sequence auto focus check, otherwise, disable it. * @param HFR if enable is true, it sets HFR in pixels. After each exposure, the HFR is re-measured and if it exceeds the specified value, an autofocus operation will be commanded. */ Q_SCRIPTABLE Q_NOREPLY void setInSequenceFocus(bool enable, double HFR); /** DBUS interface function. * Enable or Disable meridian flip, and sets its activation hour. * @param enable If true, meridian flip will be command after user-configurable number of hours. */ Q_SCRIPTABLE Q_NOREPLY void setMeridianFlip(bool enable); /** DBUS interface function. * Sets meridian flip trigger hour. * @param hours Number of hours after the meridian at which the mount is commanded to flip. */ Q_SCRIPTABLE Q_NOREPLY void setMeridianFlipHour(double hours); /** DBUS interface function. * Does the CCD has a cooler control (On/Off) ? */ Q_SCRIPTABLE bool hasCoolerControl(); /** DBUS interface function. * Set the CCD cooler ON/OFF * */ Q_SCRIPTABLE bool setCoolerControl(bool enable); /** DBUS interface function. * @return Returns the percentage of completed captures in all active jobs */ Q_SCRIPTABLE double getProgressPercentage(); /** DBUS interface function. * @return Returns the number of jobs in the sequence queue. */ Q_SCRIPTABLE int getJobCount() { return jobs.count(); } /** DBUS interface function. * @return Returns the number of pending uncompleted jobs in the sequence queue. */ Q_SCRIPTABLE int getPendingJobCount(); /** DBUS interface function. * @return Returns ID of current active job if any, or -1 if there are no active jobs. */ Q_SCRIPTABLE int getActiveJobID(); /** DBUS interface function. * @return Returns time left in seconds until active job is estimated to be complete. */ Q_SCRIPTABLE int getActiveJobRemainingTime(); /** DBUS interface function. * @return Returns overall time left in seconds until all jobs are estimated to be complete */ Q_SCRIPTABLE int getOverallRemainingTime(); /** DBUS interface function. * @param id job number. Job IDs start from 0 to N-1. * @return Returns the job state (Idle, In Progress, Error, Aborted, Complete) */ Q_SCRIPTABLE QString getJobState(int id); /** DBUS interface function. * @param id job number. Job IDs start from 0 to N-1. * @return Returns The number of images completed capture in the job. */ Q_SCRIPTABLE int getJobImageProgress(int id); /** DBUS interface function. * @param id job number. Job IDs start from 0 to N-1. * @return Returns the total number of images to capture in the job. */ Q_SCRIPTABLE int getJobImageCount(int id); /** DBUS interface function. * @param id job number. Job IDs start from 0 to N-1. * @return Returns the number of seconds left in an exposure operation. */ Q_SCRIPTABLE double getJobExposureProgress(int id); /** DBUS interface function. * @param id job number. Job IDs start from 0 to N-1. * @return Returns the total requested exposure duration in the job. */ Q_SCRIPTABLE double getJobExposureDuration(int id); /** DBUS interface function. * Clear in-sequence focus settings. It sets the autofocus HFR to zero so that next autofocus value is remembered for the in-sequence focusing. */ Q_SCRIPTABLE Q_NOREPLY void clearAutoFocusHFR(); /** DBUS interface function. * Jobs will NOT be checked for progress against the file system and will be always assumed as new jobs. */ Q_SCRIPTABLE Q_NOREPLY void ignoreSequenceHistory(); /** DBUS interface function. * Set count of already completed frames. This is required when we have identical external jobs * with identical paths, but we need to continue where we left off. For example, if we have 3 identical * jobs, each capturing 5 images. Let's suppose 9 images were captured before. If the count for this signature * is set to 1, then we continue to capture frame #2 even though the number of completed images is already * larger than required count (5). It is mostly used in conjunction with Ekos Scheduler. */ Q_SCRIPTABLE Q_NOREPLY void setCapturedFramesMap(const QString &signature, int count); Q_SCRIPTABLE QStringList logText() { return m_LogText; } Q_SCRIPTABLE Ekos::CaptureState status() { return m_State; } /** @}*/ void addCCD(ISD::GDInterface *newCCD); void addFilter(ISD::GDInterface *newFilter); void setDome(ISD::GDInterface *device) { dome = dynamic_cast(device); } void setDustCap(ISD::GDInterface *device) { dustCap = dynamic_cast(device); } void setLightBox(ISD::GDInterface *device) { lightBox = dynamic_cast(device); } void addGuideHead(ISD::GDInterface *newCCD); void syncFrameType(ISD::GDInterface *ccd); void setTelescope(ISD::GDInterface *newTelescope); void setRotator(ISD::GDInterface *newRotator); void setFilterManager(const QSharedPointer &manager); void syncTelescopeInfo(); void syncFilterInfo(); void clearLog(); QString getLogText() { return m_LogText.join("\n"); } /* Capture */ void updateSequencePrefix(const QString &newPrefix, const QString &dir); /** * @brief getSequence Return the JSON representation of the current sequeue queue * @return Reference to JSON array containing sequence queue jobs. */ const QJsonArray &getSequence() const { return m_SequenceArray;} /** * @brief setSettings Set capture settings * @param settings list of settings */ void setSettings(const QJsonObject &settings); - public slots: + SkyPoint getInitialMountCoords() const; + +public slots: /** \addtogroup CaptureDBusInterface * @{ */ /* Capture */ /** DBUS interface function. * Starts the sequence queue capture procedure sequentially by starting all jobs that are either Idle or Aborted in order. */ Q_SCRIPTABLE Q_NOREPLY void start(); /** DBUS interface function. * Stop all jobs and set current job status to aborted if abort is set to true, otherwise status is idle until * sequence is resumed or restarted. * @param targetState status of the job after abortion */ Q_SCRIPTABLE Q_NOREPLY void stop(CaptureState targetState = CAPTURE_IDLE); /** DBUS interface function. * Aborts all jobs and mark current state as ABORTED. It simply calls stop(CAPTURE_ABORTED) */ Q_SCRIPTABLE Q_NOREPLY void abort() { stop(CAPTURE_ABORTED); } /** DBUS interface function. * Aborts all jobs and mark current state as SUSPENDED. It simply calls stop(CAPTURE_SUSPENDED) * The only difference between SUSPENDED and ABORTED it that capture module can automatically resume a suspended * state on its own without external trigger once the right conditions are met. When whatever reason caused the module * to go into suspended state ceases to exist, the capture module automatically resumes. On the other hand, ABORTED state * must be started via an external programmatic or user trigger (e.g. click the start button again). */ Q_SCRIPTABLE Q_NOREPLY void suspend() { stop(CAPTURE_SUSPENDED); } /** DBUS interface function. * Toggle video streaming if supported by the device. * @param enabled Set to true to start video streaming, false to stop it if active. */ Q_SCRIPTABLE Q_NOREPLY void toggleVideo(bool enabled); /** @}*/ /** * @brief captureOne Capture one preview image */ void captureOne(); /** * @brief setExposure Set desired exposure value in seconds * @param value exposure values in seconds */ void setExposure(double value) { exposureIN->setValue(value);} /** * @brief seqCount Set required number of images to capture in one sequence job * @param count number of images to capture */ void setCount(uint16_t count) { countIN->setValue(count); } /** * @brief setDelay Set delay between capturing images within a sequence in seconds * @param delay numbers of seconds to wait before starting the next image. */ void setDelay(uint16_t delay) { delayIN->setValue(delay);} /** * @brief setPrefix Set target or prefix name used in constructing the generated file name * @param prefix Leading text of the generated image name. */ void setPrefix(const QString &prefix) { prefixIN->setText(prefix);} /** * @brief setBinning Set binning * @param horBin Horizontal binning * @param verBin Vertical binning */ void setBinning(int horBin, int verBin) { binXIN->setValue(horBin); binYIN->setValue(verBin); } /** * @brief setISO Set index of ISO list. * @param index index of ISO list. */ void setISO(int index) { ISOCombo->setCurrentIndex(index);} /** * @brief captureImage Initiates image capture in the active job. */ void captureImage(); /** * @brief newFITS process new FITS data received from camera. Update status of active job and overall sequence. * @param bp pointer to blob containing FITS data */ void newFITS(IBLOB *bp); /** * @brief checkCCD Refreshes the CCD information in the capture module. * @param CCDNum The CCD index in the CCD combo box to select as the active CCD. */ void checkCCD(int CCDNum = -1); /** * @brief checkFilter Refreshes the filter wheel information in the capture module. * @param filterNum The filter wheel index in the filter device combo box to set as the active filter. */ void checkFilter(int filterNum = -1); /** * @brief processCCDNumber Process number properties arriving from CCD. Currently, only CCD and Guider frames are processed. * @param nvp pointer to number property. */ void processCCDNumber(INumberVectorProperty *nvp); /** * @brief processTelescopeNumber Process number properties arriving from telescope for meridian flip purposes. * @param nvp pointer to number property. */ void processTelescopeNumber(INumberVectorProperty *nvp); /** * @brief addJob Add a new job to the sequence queue given the settings in the GUI. * @param preview True if the job is a preview job, otherwise, it is added as a batch job. * @return True if job is added successfully, false otherwise. */ bool addJob(bool preview = false); /** * @brief removeJob Remove a job sequence from the queue * @param index Row index for job to remove, if left as -1 (default), the currently selected row will be removed. * if no row is selected, the last job shall be removed. */ void removeJob(int index=-1); /** * @brief moveJobUp Move the job in the sequence queue one place up. */ void moveJobUp(); /** * @brief moveJobDown Move the job in the sequence queue one place down. */ void moveJobDown(); /** * @brief setGuideDeviation Set the guiding deviation as measured by the guiding module. Abort capture if deviation exceeds user value. Resume capture if capture was aborted and guiding deviations are below user value. * @param delta_ra Deviation in RA in arcsecs from the selected guide star. * @param delta_dec Deviation in DEC in arcsecs from the selected guide star. */ void setGuideDeviation(double delta_ra, double delta_dec); /** * @brief resumeCapture Resume capture after dither and/or focusing processes are complete. */ bool resumeCapture(); /** * @brief updateCCDTemperature Update CCD temperature in capture module. * @param value Temperature in celcius. */ void updateCCDTemperature(double value); /** * @brief setTemperature Set the target CCD temperature in the GUI settings. */ void setTargetTemperature(double temperature); void setForceTemperature(bool enabled) {temperatureCheck->setChecked(enabled);} /** * @brief preparePreCaptureActions Check if we need to update filter position or CCD temperature before starting capture process */ void preparePreCaptureActions(); void setFrameType(const QString& type) {frameTypeCombo->setCurrentText(type);} // Pause Sequence Queue void pause(); // Logs void appendLogText(const QString &); // Auto Focus void setFocusStatus(Ekos::FocusState state); void setHFR(double newHFR, int) { focusHFR = newHFR; } // Return TRUE if we need to run focus/autofocus. Otherwise false if not necessary bool startFocusIfRequired(); // Guide void setGuideStatus(Ekos::GuideState state); // Align void setAlignStatus(Ekos::AlignState state); void setAlignResults(double orientation, double ra, double de, double pixscale); // Update Mount module status void setMountStatus(ISD::Telescope::Status newState); void setGuideChip(ISD::CCDChip *chip); // Clear Camera Configuration void clearCameraConfiguration(); private slots: /** * @brief setDirty Set dirty bit to indicate sequence queue file was modified and needs saving. */ void setDirty(); void toggleSequence(); void checkFrameType(int index); void resetFrame(); void setExposureProgress(ISD::CCDChip *tChip, double value, IPState state); void checkSeqBoundary(const QString &path); void saveFITSDirectory(); void setDefaultCCD(QString ccd); void setNewRemoteFile(QString file); // Sequence Queue void loadSequenceQueue(); void saveSequenceQueue(); void saveSequenceQueueAs(); // Jobs void resetJobs(); void editJob(QModelIndex i); void resetJobEdit(); void executeJob(); // Meridian Flip void checkMeridianFlipTimeout(); //void checkAlignmentSlewComplete(); // AutoGuide void checkGuideDeviationTimeout(); // Auto Focus // Timed refocus void startRefocusEveryNTimer() { startRefocusTimer(false); } void restartRefocusEveryNTimer() { startRefocusTimer(true); } int getRefocusEveryNTimerElapsedSec(); // Flat field void openCalibrationDialog(); IPState processPreCaptureCalibrationStage(); bool processPostCaptureCalibrationStage(); void updatePreCaptureCalibrationStatus(); // Send image info void sendNewImage(const QString &filename, ISD::CCDChip *myChip); // Capture bool setCaptureComplete(); // post capture script void postScriptFinished(int exitCode); void setVideoStreamEnabled(bool enabled); // Observer void showObserverDialog(); // Active Job Prepare State void updatePrepareState(Ekos::CaptureState prepareState); // Rotator void updateRotatorNumber(INumberVectorProperty *nvp); // Cooler void setCoolerToggled(bool enabled); + /** + * @brief registerNewModule Register an Ekos module as it arrives via DBus + * and create the appropriate DBus interface to communicate with it. + * @param name of module + */ + void registerNewModule(const QString &name); + signals: Q_SCRIPTABLE void newLog(const QString &text); Q_SCRIPTABLE void meridianFlipStarted(); Q_SCRIPTABLE void meridianFlipCompleted(); Q_SCRIPTABLE void newStatus(Ekos::CaptureState status); Q_SCRIPTABLE void newSequenceImage(const QString &filename); void ready(); void checkFocus(double); void resetFocus(); void suspendGuiding(); void resumeGuiding(); void newImage(Ekos::SequenceJob *job); void newExposureProgress(Ekos::SequenceJob *job); void sequenceChanged(const QJsonArray &sequence); void settingsUpdated(const QJsonObject &settings); private: void setBusy(bool enable); bool resumeSequence(); bool startNextExposure(); // reset = 0 --> Do not reset // reset = 1 --> Full reset // reset = 2 --> Only update limits if needed void updateFrameProperties(int reset = 0); void prepareJob(SequenceJob *job); void syncGUIToJob(SequenceJob *job); bool processJobInfo(XMLEle *root); void processJobCompletion(); bool saveSequenceQueue(const QString &path); void constructPrefix(QString &imagePrefix); double setCurrentADU(double value); void llsq(QVector x, QVector y, double &a, double &b); // DSLR Info void addDSLRInfo(const QString &model, uint32_t maxW, uint32_t maxH, double pixelW, double pixelH); void cullToDSLRLimits(); //void syncDriverToDSLRLimits(); bool isModelinDSLRInfo(const QString &model); /* Meridian Flip */ bool checkMeridianFlip(); void checkGuidingAfterFlip(); double getCurrentHA(); + double getInitialHA(); // Remaining Time in seconds int getJobRemainingTime(SequenceJob *job); void resetFrameToZero(); + /* Slewing - true iff start slewing was successful */ + bool slew(const SkyPoint target); + /* Refocus */ void startRefocusTimer(bool forced = false); // If exposure timed out, let's handle it. void processCaptureTimeout(); /* Capture */ /** * @brief Determine the overall number of target frames with the same signature. * Assume capturing RGBLRGB, where in each sequence job there is only one frame. * For "L" the result will be 1, for "R" it will be 2 etc. * @param frame signature (typically the filter name) * @return */ int getTotalFramesCount(QString signature); double seqExpose { 0 }; int seqTotalCount; int seqCurrentCount { 0 }; int seqDelay { 0 }; int retries { 0 }; QTimer *seqTimer { nullptr }; QString seqPrefix; int nextSequenceID { 0 }; int seqFileCount { 0 }; bool isBusy { false }; // Capture timeout timer QTimer captureTimeout; uint8_t captureTimeoutCounter { 0 }; bool useGuideHead { false }; bool autoGuideReady { false}; bool autoGuideAbortedCapture { false }; QString m_TargetName; QString m_ObserverName; SequenceJob *activeJob { nullptr }; QList CCDs; ISD::CCDChip *targetChip { nullptr }; ISD::CCDChip *guideChip { nullptr }; ISD::CCDChip *blobChip { nullptr }; QString blobFilename; // They're generic GDInterface because they could be either ISD::CCD or ISD::Filter QList Filters; QList jobs; ISD::Telescope *currentTelescope { nullptr }; ISD::CCD *currentCCD { nullptr }; ISD::GDInterface *currentFilter { nullptr }; ISD::GDInterface *currentRotator { nullptr }; ISD::DustCap *dustCap { nullptr }; ISD::LightBox *lightBox { nullptr }; ISD::Dome *dome { nullptr }; + QPointer mountInterface { nullptr }; + QStringList m_LogText; QUrl m_SequenceURL; bool m_Dirty { false }; bool m_JobUnderEdit { false }; int m_CurrentFilterPosition { -1 }; QProgressIndicator *pi { nullptr }; // Guide Deviation bool m_DeviationDetected { false }; bool m_SpikeDetected { false }; QTimer guideDeviationTimer; // Autofocus /** * @brief updateHFRThreshold calculate new HFR threshold based on median value for current selected filter */ void updateHFRThreshold(); bool isInSequenceFocus { false }; bool autoFocusReady { false }; //bool requiredAutoFocusStarted { false }; //bool firstAutoFocus { true }; double focusHFR { 0 }; // HFR value as received from the Ekos focus module QMap> HFRMap; double fileHFR { 0 }; // HFR value as loaded from the sequence file // Refocus every N minutes bool isRefocus { false }; int refocusEveryNMinutesValue { 0 }; // number of minutes between forced refocus QElapsedTimer refocusEveryNTimer; // used to determine when next force refocus should occur - // Meridian flip - double initialHA { 0 }; - //double initialRA { 0 }; + // Meridan flip SkyPoint initialMountCoords; bool resumeAlignmentAfterFlip { false }; bool resumeGuidingAfterFlip { false }; MFStage meridianFlipStage { MF_NONE }; // Flat field automation QVector ExpRaw, ADURaw; double targetADU { 0 }; double targetADUTolerance { 1000 }; SkyPoint wallCoord; bool preMountPark { false }; bool preDomePark { false }; FlatFieldDuration flatFieldDuration { DURATION_MANUAL }; FlatFieldSource flatFieldSource { SOURCE_MANUAL }; CalibrationStage calibrationStage { CAL_NONE }; bool dustCapLightEnabled { false }; bool lightBoxLightEnabled { false }; ISD::CCD::UploadMode rememberUploadMode { ISD::CCD::UPLOAD_CLIENT }; QUrl dirPath; // Misc bool ignoreJobProgress { true }; bool suspendGuideOnDownload { false }; QJsonArray m_SequenceArray; // State CaptureState m_State { CAPTURE_IDLE }; FocusState focusState { FOCUS_IDLE }; GuideState guideState { GUIDE_IDLE }; AlignState alignState { ALIGN_IDLE }; PauseFunctionPointer pauseFunction; // CCD Chip frame settings QMap frameSettings; // Post capture script QProcess postCaptureScript; // Rotator Settings std::unique_ptr rotatorSettings; // How many images to capture before dithering operation is executed? uint8_t ditherCounter { 0 }; uint8_t inSequenceFocusCounter { 0 }; std::unique_ptr customPropertiesDialog; // Filter Manager QSharedPointer filterManager; // DSLR Infos QList> DSLRInfos; // Captured Frames Map SchedulerJob::CapturedFramesMap capturedFramesMap; + + // Execute the meridian flip + bool executeMeridianFlip(); }; } diff --git a/kstars/ekos/mount/mount.cpp b/kstars/ekos/mount/mount.cpp index e3acb4c74..9ff240492 100644 --- a/kstars/ekos/mount/mount.cpp +++ b/kstars/ekos/mount/mount.cpp @@ -1,1221 +1,1274 @@ /* Ekos Mount Module Copyright (C) 2015 Jasem Mutlaq This application is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version. */ #include "mount.h" #include #include #include #include #include #include "Options.h" #include "indi/driverinfo.h" #include "indi/indicommon.h" #include "indi/clientmanager.h" #include "indi/indifilter.h" #include "mountadaptor.h" #include "ekos/manager.h" #include "kstars.h" #include "skymapcomposite.h" #include "kspaths.h" #include "dialogs/finddialog.h" #include "kstarsdata.h" #include "ksutils.h" #include #include extern const char *libindi_strings_context; #define UPDATE_DELAY 1000 #define ABORT_DISPATCH_LIMIT 3 namespace Ekos { + Mount::Mount() { setupUi(this); new MountAdaptor(this); QDBusConnection::sessionBus().registerObject("/KStars/Ekos/Mount", this); + // Set up DBus interfaces + QPointer ekosInterface = new QDBusInterface("org.kde.kstars", "/KStars/Ekos", "org.kde.kstars.Ekos", + QDBusConnection::sessionBus(), this); + qDBusRegisterMetaType(); + + // Connecting DBus signals + connect(ekosInterface, SIGNAL(newModule(QString)), this, SLOT(registerNewModule(QString))); currentTelescope = nullptr; abortDispatch = -1; minAltLimit->setValue(Options::minimumAltLimit()); maxAltLimit->setValue(Options::maximumAltLimit()); connect(minAltLimit, SIGNAL(editingFinished()), this, SLOT(saveLimits())); connect(maxAltLimit, SIGNAL(editingFinished()), this, SLOT(saveLimits())); connect(mountToolBoxB, SIGNAL(clicked()), this, SLOT(toggleMountToolBox())); connect(saveB, SIGNAL(clicked()), this, SLOT(save())); connect(clearAlignmentModelB, &QPushButton::clicked, this, [this]() { resetModel(); }); connect(enableLimitsCheck, SIGNAL(toggled(bool)), this, SLOT(enableAltitudeLimits(bool))); enableLimitsCheck->setChecked(Options::enableAltitudeLimits()); altLimitEnabled = enableLimitsCheck->isChecked(); updateTimer.setInterval(UPDATE_DELAY); connect(&updateTimer, SIGNAL(timeout()), this, SLOT(updateTelescopeCoords())); QDateTime now = KStarsData::Instance()->lt(); // Set seconds to zero now = now.addSecs(now.time().second()*-1); startupTimeEdit->setDateTime(now); connect(&autoParkTimer, &QTimer::timeout, this, &Mount::startAutoPark); connect(startTimerB, &QPushButton::clicked, this, &Mount::startParkTimer); connect(stopTimerB, &QPushButton::clicked, this, &Mount::stopParkTimer); stopTimerB->setEnabled(false); // QML Stuff m_BaseView = new QQuickView(); #if 0 QString MountBox_Location; #if defined(Q_OS_OSX) MountBox_Location = QCoreApplication::applicationDirPath() + "/../Resources/data/ekos/mount/qml/mountbox.qml"; if (!QFileInfo(MountBox_Location).exists()) MountBox_Location = KSPaths::locate(QStandardPaths::AppDataLocation, "ekos/mount/qml/mountbox.qml"); #elif defined(Q_OS_WIN) MountBox_Location = KSPaths::locate(QStandardPaths::GenericDataLocation, "ekos/mount/qml/mountbox.qml"); #else MountBox_Location = KSPaths::locate(QStandardPaths::AppDataLocation, "ekos/mount/qml/mountbox.qml"); #endif m_BaseView->setSource(QUrl::fromLocalFile(MountBox_Location)); #endif m_BaseView->setSource(QUrl("qrc:/qml/mount/mountbox.qml")); m_BaseView->setTitle(i18n("Mount Control")); #ifdef Q_OS_OSX m_BaseView->setFlags(Qt::Tool | Qt::WindowStaysOnTopHint); #else m_BaseView->setFlags(Qt::WindowStaysOnTopHint | Qt::WindowCloseButtonHint); #endif // Theming? m_BaseView->setColor(Qt::black); m_BaseObj = m_BaseView->rootObject(); m_Ctxt = m_BaseView->rootContext(); ///Use instead of KDeclarative m_Ctxt->setContextObject(new KLocalizedContext(m_BaseView)); m_Ctxt->setContextProperty("mount", this); m_BaseView->setMaximumSize(QSize(200, 480)); m_BaseView->setMinimumSize(QSize(200, 480)); m_BaseView->setResizeMode(QQuickView::SizeRootObjectToView); m_SpeedSlider = m_BaseObj->findChild("speedSliderObject"); m_SpeedLabel = m_BaseObj->findChild("speedLabelObject"); m_raValue = m_BaseObj->findChild("raValueObject"); m_deValue = m_BaseObj->findChild("deValueObject"); m_azValue = m_BaseObj->findChild("azValueObject"); m_altValue = m_BaseObj->findChild("altValueObject"); m_haValue = m_BaseObj->findChild("haValueObject"); m_zaValue = m_BaseObj->findChild("zaValueObject"); m_targetText = m_BaseObj->findChild("targetTextObject"); m_targetRAText = m_BaseObj->findChild("targetRATextObject"); m_targetDEText = m_BaseObj->findChild("targetDETextObject"); m_J2000Check = m_BaseObj->findChild("j2000CheckObject"); m_JNowCheck = m_BaseObj->findChild("jnowCheckObject"); m_Park = m_BaseObj->findChild("parkButtonObject"); m_Unpark = m_BaseObj->findChild("unparkButtonObject"); m_statusText = m_BaseObj->findChild("statusTextObject"); //Note: This is to prevent a button from being called the default button //and then executing when the user hits the enter key such as when on a Text Box QList qButtons = findChildren(); for (auto &button : qButtons) button->setAutoDefault(false); } Mount::~Mount() { delete(m_Ctxt); delete(m_BaseObj); } void Mount::setTelescope(ISD::GDInterface *newTelescope) { if (newTelescope == currentTelescope) { updateTimer.start(); if (enableLimitsCheck->isChecked()) currentTelescope->setAltLimits(minAltLimit->value(), maxAltLimit->value()); syncTelescopeInfo(); return; } if (currentGPS != nullptr) syncGPS(); currentTelescope = static_cast(newTelescope); currentTelescope->disconnect(this); connect(currentTelescope, &ISD::GDInterface::numberUpdated, this, &Mount::updateNumber); connect(currentTelescope, &ISD::GDInterface::switchUpdated, this, &Mount::updateSwitch); connect(currentTelescope, &ISD::GDInterface::textUpdated, this, &Mount::updateText); connect(currentTelescope, &ISD::Telescope::newTarget, this, &Mount::newTarget); connect(currentTelescope, &ISD::Telescope::slewRateChanged, this, &Mount::slewRateChanged); connect(currentTelescope, &ISD::Telescope::pierSideChanged, this, &Mount::pierSideChanged); connect(currentTelescope, &ISD::Telescope::Disconnected, [this]() { updateTimer.stop(); m_BaseView->hide(); }); connect(currentTelescope, &ISD::Telescope::newParkStatus, [&](ISD::ParkStatus status) { m_ParkStatus = status; emit newParkStatus(status); }); connect(currentTelescope, &ISD::Telescope::ready, this, &Mount::ready); //Disable this for now since ALL INDI drivers now log their messages to verbose output //connect(currentTelescope, SIGNAL(messageUpdated(int)), this, SLOT(updateLog(int)), Qt::UniqueConnection); if (enableLimitsCheck->isChecked()) currentTelescope->setAltLimits(minAltLimit->value(), maxAltLimit->value()); updateTimer.start(); syncTelescopeInfo(); // Send initial status m_Status = currentTelescope->status(); emit newStatus(m_Status); m_ParkStatus = currentTelescope->parkStatus(); emit newParkStatus(m_ParkStatus); } void Mount::syncTelescopeInfo() { if (currentTelescope == nullptr || currentTelescope->isConnected() == false) return; INumberVectorProperty *nvp = currentTelescope->getBaseDevice()->getNumber("TELESCOPE_INFO"); if (nvp) { primaryScopeGroup->setTitle(currentTelescope->getDeviceName()); guideScopeGroup->setTitle(i18n("%1 guide scope", currentTelescope->getDeviceName())); INumber *np = IUFindNumber(nvp, "TELESCOPE_APERTURE"); if (np && np->value > 0) primaryScopeApertureIN->setValue(np->value); np = IUFindNumber(nvp, "TELESCOPE_FOCAL_LENGTH"); if (np && np->value > 0) primaryScopeFocalIN->setValue(np->value); np = IUFindNumber(nvp, "GUIDER_APERTURE"); if (np && np->value > 0) guideScopeApertureIN->setValue(np->value); np = IUFindNumber(nvp, "GUIDER_FOCAL_LENGTH"); if (np && np->value > 0) guideScopeFocalIN->setValue(np->value); } ISwitchVectorProperty *svp = currentTelescope->getBaseDevice()->getSwitch("TELESCOPE_SLEW_RATE"); if (svp) { int index = IUFindOnSwitchIndex(svp); // QtQuick m_SpeedSlider->setEnabled(true); m_SpeedSlider->setProperty("maximumValue", svp->nsp - 1); m_SpeedSlider->setProperty("value", index); m_SpeedLabel->setProperty("text", i18nc(libindi_strings_context, svp->sp[index].label)); m_SpeedLabel->setEnabled(true); } else { // QtQuick m_SpeedSlider->setEnabled(false); m_SpeedLabel->setEnabled(false); } if (currentTelescope->canPark()) { parkB->setEnabled(!currentTelescope->isParked()); unparkB->setEnabled(currentTelescope->isParked()); connect(parkB, SIGNAL(clicked()), currentTelescope, SLOT(Park()), Qt::UniqueConnection); connect(unparkB, SIGNAL(clicked()), currentTelescope, SLOT(UnPark()), Qt::UniqueConnection); // QtQuick m_Park->setEnabled(!currentTelescope->isParked()); m_Unpark->setEnabled(currentTelescope->isParked()); } else { parkB->setEnabled(false); unparkB->setEnabled(false); disconnect(parkB, SIGNAL(clicked()), currentTelescope, SLOT(Park())); disconnect(unparkB, SIGNAL(clicked()), currentTelescope, SLOT(UnPark())); // QtQuick m_Park->setEnabled(false); m_Unpark->setEnabled(false); } // Configs svp = currentTelescope->getBaseDevice()->getSwitch("APPLY_SCOPE_CONFIG"); if (svp) { scopeConfigCombo->disconnect(); scopeConfigCombo->clear(); for (int i=0; i < svp->nsp; i++) scopeConfigCombo->addItem(svp->sp[i].label); scopeConfigCombo->setCurrentIndex(IUFindOnSwitchIndex(svp)); connect(scopeConfigCombo, SIGNAL(activated(int)), this, SLOT(setScopeConfig(int))); } // Tracking State svp = currentTelescope->getBaseDevice()->getSwitch("TELESCOPE_TRACK_STATE"); if (svp) { trackingGroup->setEnabled(true); trackOnB->disconnect(); trackOffB->disconnect(); connect(trackOnB, &QPushButton::clicked, [&]() { currentTelescope->setTrackEnabled(true);}); connect(trackOffB, &QPushButton::clicked, [&]() { if (KMessageBox::questionYesNo(KStars::Instance(), i18n("Are you sure you want to turn off mount tracking?"), i18n("Mount Tracking"), KStandardGuiItem::yes(), KStandardGuiItem::no(), "turn_off_mount_tracking_dialog") == KMessageBox::Yes) currentTelescope->setTrackEnabled(false); }); } else { trackOnB->setChecked(false); trackOffB->setChecked(false); trackingGroup->setEnabled(false); } ITextVectorProperty *tvp = currentTelescope->getBaseDevice()->getText("SCOPE_CONFIG_NAME"); if (tvp) scopeConfigNameEdit->setText(tvp->tp[0].text); } +void Mount::registerNewModule(const QString &name) +{ + if (name == "Capture") + { + captureInterface = new QDBusInterface("org.kde.kstars", "/KStars/Ekos/Capture", "org.kde.kstars.Ekos.Capture", + QDBusConnection::sessionBus(), this); + } + +} + + void Mount::updateText(ITextVectorProperty *tvp) { if (!strcmp(tvp->name, "SCOPE_CONFIG_NAME")) { scopeConfigNameEdit->setText(tvp->tp[0].text); } } bool Mount::setScopeConfig(int index) { ISwitchVectorProperty *svp = currentTelescope->getBaseDevice()->getSwitch("APPLY_SCOPE_CONFIG"); if (svp == nullptr) return false; IUResetSwitch(svp); svp->sp[index].s = ISS_ON; // Clear scope config name so that it gets filled by INDI scopeConfigNameEdit->clear(); currentTelescope->getDriverInfo()->getClientManager()->sendNewSwitch(svp); return true; } void Mount::updateTelescopeCoords() { // No need to update coords if we are still parked. if (m_Status == ISD::Telescope::MOUNT_PARKED && m_Status == currentTelescope->status()) return; double ra=0, dec=0; if (currentTelescope && currentTelescope->getEqCoords(&ra, &dec)) { if (currentTelescope->isJ2000()) { telescopeCoord.setRA0(ra); telescopeCoord.setDec0(dec); // Get JNow as well telescopeCoord.apparentCoord(static_cast(J2000), KStars::Instance()->data()->ut().djd()); } else { telescopeCoord.setRA(ra); telescopeCoord.setDec(dec); } // Ekos Mount Tab coords are always in JNow raOUT->setText(telescopeCoord.ra().toHMSString()); decOUT->setText(telescopeCoord.dec().toDMSString()); // Mount Control Panel coords depend on the switch if (m_JNowCheck->property("checked").toBool()) { m_raValue->setProperty("text", telescopeCoord.ra().toHMSString()); m_deValue->setProperty("text", telescopeCoord.dec().toDMSString()); } else { // If epoch is already J2000, then we don't need to convert to JNow if (currentTelescope->isJ2000() == false) { SkyPoint J2000Coord(telescopeCoord.ra(), telescopeCoord.dec()); J2000Coord.apparentCoord(KStars::Instance()->data()->ut().djd(), static_cast(J2000)); //J2000Coord.precessFromAnyEpoch(KStars::Instance()->data()->ut().djd(), static_cast(J2000)); telescopeCoord.setRA0(J2000Coord.ra()); telescopeCoord.setDec0(J2000Coord.dec()); } m_raValue->setProperty("text", telescopeCoord.ra0().toHMSString()); m_deValue->setProperty("text", telescopeCoord.dec0().toDMSString()); } // Get horizontal coords telescopeCoord.EquatorialToHorizontal(KStarsData::Instance()->lst(), KStarsData::Instance()->geo()->lat()); azOUT->setText(telescopeCoord.az().toDMSString()); m_azValue->setProperty("text", telescopeCoord.az().toDMSString()); altOUT->setText(telescopeCoord.alt().toDMSString()); m_altValue->setProperty("text", telescopeCoord.alt().toDMSString()); dms lst = KStarsData::Instance()->geo()->GSTtoLST(KStarsData::Instance()->clock()->utc().gst()); dms ha(lst - telescopeCoord.ra()); QChar sgn('+'); if (ha.Hours() > 12.0) { ha.setH(24.0 - ha.Hours()); sgn = '-'; } haOUT->setText(QString("%1%2").arg(sgn).arg(ha.toHMSString())); m_haValue->setProperty("text", haOUT->text()); lstOUT->setText(lst.toHMSString()); double currentAlt = telescopeCoord.altRefracted().Degrees(); m_zaValue->setProperty("text", dms(90 - currentAlt).toDMSString()); if (minAltLimit->isEnabled() && (currentAlt < minAltLimit->value() || currentAlt > maxAltLimit->value())) { if (currentAlt < minAltLimit->value()) { // Only stop if current altitude is less than last altitude indicate worse situation if (currentAlt < lastAlt && (abortDispatch == -1 || (currentTelescope->isInMotion() /* && ++abortDispatch > ABORT_DISPATCH_LIMIT*/))) { appendLogText(i18n("Telescope altitude is below minimum altitude limit of %1. Aborting motion...", QString::number(minAltLimit->value(), 'g', 3))); currentTelescope->Abort(); //KNotification::event( QLatin1String( "OperationFailed" )); KNotification::beep(); abortDispatch++; } } else { // Only stop if current altitude is higher than last altitude indicate worse situation if (currentAlt > lastAlt && (abortDispatch == -1 || (currentTelescope->isInMotion() /* && ++abortDispatch > ABORT_DISPATCH_LIMIT*/))) { appendLogText(i18n("Telescope altitude is above maximum altitude limit of %1. Aborting motion...", QString::number(maxAltLimit->value(), 'g', 3))); currentTelescope->Abort(); //KNotification::event( QLatin1String( "OperationFailed" )); KNotification::beep(); abortDispatch++; } } } else abortDispatch = -1; lastAlt = currentAlt; emit newCoords(raOUT->text(), decOUT->text(), azOUT->text(), altOUT->text()); ISD::Telescope::Status currentStatus = currentTelescope->status(); if (m_Status != currentStatus) { m_Status = currentStatus; parkB->setEnabled(!currentTelescope->isParked()); unparkB->setEnabled(currentTelescope->isParked()); m_Park->setEnabled(!currentTelescope->isParked()); m_Unpark->setEnabled(currentTelescope->isParked()); m_statusText->setProperty("text", currentTelescope->getStatusString(currentStatus)); QAction *a = KStars::Instance()->actionCollection()->action("telescope_track"); if (a != nullptr) a->setChecked(currentStatus == ISD::Telescope::MOUNT_TRACKING); emit newStatus(m_Status); } if (trackingGroup->isEnabled()) { bool isTracking = (currentStatus == ISD::Telescope::MOUNT_TRACKING); trackOnB->setChecked(isTracking); trackOffB->setChecked(!isTracking); } if (currentTelescope->isConnected() == false) updateTimer.stop(); else if (updateTimer.isActive() == false) updateTimer.start(); // Auto Park Timer if (autoParkTimer.isActive()) { QTime remainingTime(0,0,0); remainingTime = remainingTime.addMSecs(autoParkTimer.remainingTime()); countdownLabel->setText(remainingTime.toString("hh:mm:ss")); } } else updateTimer.stop(); } void Mount::updateNumber(INumberVectorProperty *nvp) { if (!strcmp(nvp->name, "TELESCOPE_INFO")) { if (nvp->s == IPS_ALERT) { QString newMessage; if (primaryScopeApertureIN->value() == 1 || primaryScopeFocalIN->value() == 1) newMessage = i18n("Error syncing telescope info. Please fill telescope aperture and focal length."); else newMessage = i18n("Error syncing telescope info. Check INDI control panel for more details."); if (newMessage != lastNotificationMessage) { appendLogText(newMessage); lastNotificationMessage = newMessage; } } else { syncTelescopeInfo(); QString newMessage = i18n("Telescope info updated successfully."); if (newMessage != lastNotificationMessage) { appendLogText(newMessage); lastNotificationMessage = newMessage; } } } if (currentGPS != nullptr && !strcmp(nvp->device, currentGPS->getDeviceName()) && !strcmp(nvp->name, "GEOGRAPHIC_COORD") && nvp->s == IPS_OK) syncGPS(); } bool Mount::setSlewRate(int index) { if (currentTelescope) return currentTelescope->setSlewRate(index); return false; } void Mount::updateSwitch(ISwitchVectorProperty *svp) { if (!strcmp(svp->name, "TELESCOPE_SLEW_RATE")) { int index = IUFindOnSwitchIndex(svp); m_SpeedSlider->setProperty("value", index); m_SpeedLabel->setProperty("text", i18nc(libindi_strings_context, svp->sp[index].label)); } /*else if (!strcmp(svp->name, "TELESCOPE_PARK")) { ISwitch *sp = IUFindSwitch(svp, "PARK"); if (sp) { parkB->setEnabled((sp->s == ISS_OFF)); unparkB->setEnabled((sp->s == ISS_ON)); } }*/ } void Mount::appendLogText(const QString &text) { m_LogText.insert(0, i18nc("log entry; %1 is the date, %2 is the text", "%1 %2", QDateTime::currentDateTime().toString("yyyy-MM-ddThh:mm:ss"), text)); qCInfo(KSTARS_EKOS_MOUNT) << text; emit newLog(text); } void Mount::updateLog(int messageID) { INDI::BaseDevice *dv = currentTelescope->getBaseDevice(); QString message = QString::fromStdString(dv->messageQueue(messageID)); m_LogText.insert(0, i18nc("Message shown in Ekos Mount module", "%1", message)); emit newLog(message); } void Mount::clearLog() { m_LogText.clear(); emit newLog(QString()); } void Mount::motionCommand(int command, int NS, int WE) { if (NS != -1) { currentTelescope->MoveNS(static_cast(NS), static_cast(command)); } if (WE != -1) { currentTelescope->MoveWE(static_cast(WE), static_cast(command)); } } void Mount::save() { if (currentTelescope == nullptr) return; if (scopeConfigNameEdit->text().isEmpty() == false) { ITextVectorProperty *tvp = currentTelescope->getBaseDevice()->getText("SCOPE_CONFIG_NAME"); if (tvp) { IUSaveText(&(tvp->tp[0]), scopeConfigNameEdit->text().toLatin1().constData()); currentTelescope->getDriverInfo()->getClientManager()->sendNewText(tvp); } } INumberVectorProperty *nvp = currentTelescope->getBaseDevice()->getNumber("TELESCOPE_INFO"); if (nvp) { primaryScopeGroup->setTitle(currentTelescope->getDeviceName()); guideScopeGroup->setTitle(i18n("%1 guide scope", currentTelescope->getDeviceName())); INumber *np = IUFindNumber(nvp, "TELESCOPE_APERTURE"); if (np) np->value = primaryScopeApertureIN->value(); np = IUFindNumber(nvp, "TELESCOPE_FOCAL_LENGTH"); if (np) np->value = primaryScopeFocalIN->value(); np = IUFindNumber(nvp, "GUIDER_APERTURE"); if (np) np->value = guideScopeApertureIN->value() == 1 ? primaryScopeApertureIN->value() : guideScopeApertureIN->value(); np = IUFindNumber(nvp, "GUIDER_FOCAL_LENGTH"); if (np) np->value = guideScopeFocalIN->value() == 1 ? primaryScopeFocalIN->value() : guideScopeFocalIN->value(); ClientManager *clientManager = currentTelescope->getDriverInfo()->getClientManager(); clientManager->sendNewNumber(nvp); currentTelescope->setConfig(SAVE_CONFIG); //appendLogText(i18n("Saving telescope information...")); } else appendLogText(i18n("Failed to save telescope information.")); } void Mount::saveLimits() { Options::setMinimumAltLimit(minAltLimit->value()); Options::setMaximumAltLimit(maxAltLimit->value()); currentTelescope->setAltLimits(minAltLimit->value(), maxAltLimit->value()); } void Mount::enableAltitudeLimits(bool enable) { Options::setEnableAltitudeLimits(enable); if (enable) { minAltLabel->setEnabled(true); maxAltLabel->setEnabled(true); minAltLimit->setEnabled(true); maxAltLimit->setEnabled(true); if (currentTelescope) currentTelescope->setAltLimits(minAltLimit->value(), maxAltLimit->value()); } else { minAltLabel->setEnabled(false); maxAltLabel->setEnabled(false); minAltLimit->setEnabled(false); maxAltLimit->setEnabled(false); if (currentTelescope) currentTelescope->setAltLimits(-1, -1); } } void Mount::enableAltLimits() { //Only enable if it was already enabled before and the minAltLimit is currently disabled. if (altLimitEnabled && minAltLimit->isEnabled() == false) enableAltitudeLimits(true); } void Mount::disableAltLimits() { altLimitEnabled = enableLimitsCheck->isChecked(); enableAltitudeLimits(false); } QList Mount::altitudeLimits() { QList limits; limits.append(minAltLimit->value()); limits.append(maxAltLimit->value()); return limits; } void Mount::setAltitudeLimits(QList limits) { minAltLimit->setValue(limits[0]); maxAltLimit->setValue(limits[1]); } void Mount::setAltitudeLimitsEnabled(bool enable) { enableLimitsCheck->setChecked(enable); } bool Mount::altitudeLimitsEnabled() { return enableLimitsCheck->isChecked(); } void Mount::setJ2000Enabled(bool enabled) { m_J2000Check->setProperty("checked", enabled); } bool Mount::gotoTarget(const QString &target) { SkyObject *object = KStarsData::Instance()->skyComposite()->findByName(target); if (object != nullptr) return slew(object->ra().Hours(), object->dec().Degrees()); return false; } bool Mount::syncTarget(const QString &target) { SkyObject *object = KStarsData::Instance()->skyComposite()->findByName(target); if (object != nullptr) return sync(object->ra().Hours(), object->dec().Degrees()); return false; } bool Mount::slew(const QString &RA, const QString &DEC) { dms ra, de; ra = dms::fromString(RA, false); de = dms::fromString(DEC, true); // If J2000 was checked and the Mount is _not_ already using native J2000 coordinates // then we need to convert J2000 to JNow. Otherwise, we send J2000 as is. if (m_J2000Check->property("checked").toBool() && currentTelescope && currentTelescope->isJ2000() == false) { // J2000 ---> JNow SkyPoint J2000Coord(ra, de); J2000Coord.setRA0(ra); J2000Coord.setDec0(de); J2000Coord.apparentCoord(static_cast(J2000), KStars::Instance()->data()->ut().djd()); ra = J2000Coord.ra(); de = J2000Coord.dec(); } return slew(ra.Hours(), de.Degrees()); } bool Mount::slew(double RA, double DEC) { if (currentTelescope == nullptr || currentTelescope->isConnected() == false) return false; + dms lst = KStarsData::Instance()->geo()->GSTtoLST(KStarsData::Instance()->clock()->utc().gst()); + double HA = lst.Hours() - RA; + if (HA > 12.0) + HA -= 24.0; + + setInitialHA(HA); + + currentTargetPosition.setRA(RA); + currentTargetPosition.setDec(DEC); + return currentTelescope->Slew(RA, DEC); } +bool Mount::executeMeridianFlip() { + if (initialHA() > 0) + // no meridian flip necessary + return false; + + dms lst = KStarsData::Instance()->geo()->GSTtoLST(KStarsData::Instance()->clock()->utc().gst()); + double HA = lst.Hours() - currentTargetPosition.ra().Hours(); + if (HA > 12.0) + // no meridian flip necessary + return false; + + // execute meridian flip + slew(currentTargetPosition.ra().Hours(), currentTargetPosition.dec().Degrees()); + return true; + +} + +SkyPoint Mount::currentTarget() +{ + return currentTargetPosition; +} + + + bool Mount::sync(const QString &RA, const QString &DEC) { dms ra, de; ra = dms::fromString(RA, false); de = dms::fromString(DEC, true); if (m_J2000Check->property("checked").toBool()) { // J2000 ---> JNow SkyPoint J2000Coord(ra, de); J2000Coord.setRA0(ra); J2000Coord.setDec0(de); J2000Coord.updateCoordsNow(KStarsData::Instance()->updateNum()); ra = J2000Coord.ra(); de = J2000Coord.dec(); } return sync(ra.Hours(), de.Degrees()); } bool Mount::sync(double RA, double DEC) { if (currentTelescope == nullptr || currentTelescope->isConnected() == false) return false; return currentTelescope->Sync(RA, DEC); } bool Mount::abort() { return currentTelescope->Abort(); } IPState Mount::slewStatus() { if (currentTelescope == nullptr) return IPS_ALERT; return currentTelescope->getState("EQUATORIAL_EOD_COORD"); } QList Mount::equatorialCoords() { double ra, dec; QList coords; currentTelescope->getEqCoords(&ra, &dec); coords.append(ra); coords.append(dec); return coords; } QList Mount::horizontalCoords() { QList coords; coords.append(telescopeCoord.az().Degrees()); coords.append(telescopeCoord.alt().Degrees()); return coords; } double Mount::hourAngle() { dms lst = KStarsData::Instance()->geo()->GSTtoLST(KStarsData::Instance()->clock()->utc().gst()); dms ha(lst.Degrees() - telescopeCoord.ra().Degrees()); double HA = ha.Hours(); if (HA > 12.0) - return (24 - HA); + return (HA - 24.0); else return HA; } QList Mount::telescopeInfo() { QList info; info.append(primaryScopeFocalIN->value()); info.append(primaryScopeApertureIN->value()); info.append(guideScopeFocalIN->value()); info.append(guideScopeApertureIN->value()); return info; } void Mount::setTelescopeInfo(const QList &info) { if (info[0] > 0) primaryScopeFocalIN->setValue(info[0]); if (info[1] > 0) primaryScopeApertureIN->setValue(info[1]); if (info[2] > 0) guideScopeFocalIN->setValue(info[2]); if (info[3] > 0) guideScopeApertureIN->setValue(info[3]); if (scopeConfigNameEdit->text().isEmpty() == false) appendLogText(i18n("Warning: Overriding %1 configuration.", scopeConfigNameEdit->text())); save(); } bool Mount::canPark() { if (currentTelescope == nullptr) return false; return currentTelescope->canPark(); } bool Mount::park() { if (currentTelescope == nullptr || currentTelescope->canPark() == false) return false; return currentTelescope->Park(); } bool Mount::unpark() { if (currentTelescope == nullptr || currentTelescope->canPark() == false) return false; return currentTelescope->UnPark(); } #if 0 Mount::ParkingStatus Mount::getParkingStatus() { if (currentTelescope == nullptr) return PARKING_ERROR; // In the case mount can't park, return mount is unparked if (currentTelescope->canPark() == false) return UNPARKING_OK; ISwitchVectorProperty *parkSP = currentTelescope->getBaseDevice()->getSwitch("TELESCOPE_PARK"); if (parkSP == nullptr) return PARKING_ERROR; switch (parkSP->s) { case IPS_IDLE: // If mount is unparked on startup, state is OK and switch is UNPARK if (parkSP->sp[1].s == ISS_ON) return UNPARKING_OK; else return PARKING_IDLE; //break; case IPS_OK: if (parkSP->sp[0].s == ISS_ON) return PARKING_OK; else return UNPARKING_OK; //break; case IPS_BUSY: if (parkSP->sp[0].s == ISS_ON) return PARKING_BUSY; else return UNPARKING_BUSY; case IPS_ALERT: // If mount replied with an error to the last un/park request, // assume state did not change in order to return a clear state if (parkSP->sp[0].s == ISS_ON) return PARKING_OK; else return UNPARKING_OK; } return PARKING_ERROR; } #endif void Mount::toggleMountToolBox() { if (m_BaseView->isVisible()) { m_BaseView->hide(); QAction *a = KStars::Instance()->actionCollection()->action("show_mount_box"); if (a) a->setChecked(false); } else { m_BaseView->show(); QAction *a = KStars::Instance()->actionCollection()->action("show_mount_box"); if (a) a->setChecked(true); } } void Mount::findTarget() { KStarsData *data = KStarsData::Instance(); if (FindDialog::Instance()->exec() == QDialog::Accepted) { SkyObject *object = FindDialog::Instance()->targetObject(); if (object != nullptr) { SkyObject *o = object->clone(); o->updateCoords(data->updateNum(), true, data->geo()->lat(), data->lst(), false); m_targetText->setProperty("text", o->name()); if (m_JNowCheck->property("checked").toBool()) { m_targetRAText->setProperty("text", o->ra().toHMSString()); m_targetDEText->setProperty("text", o->dec().toDMSString()); } else { m_targetRAText->setProperty("text", o->ra0().toHMSString()); m_targetDEText->setProperty("text", o->dec0().toDMSString()); } } } } void Mount::centerMount() { if (currentTelescope) currentTelescope->runCommand(INDI_ENGAGE_TRACKING); } bool Mount::resetModel() { if (currentTelescope == nullptr) return false; if (currentTelescope->hasAlignmentModel() == false) return false; if (currentTelescope->clearAlignmentModel()) { appendLogText(i18n("Alignment Model cleared.")); return true; } appendLogText(i18n("Failed to clear Alignment Model.")); return false; } void Mount::setGPS(ISD::GDInterface *newGPS) { if (newGPS == currentGPS) return; //Options::setUseComputerSource(false); //Options::setUseDeviceSource(true); if (Options::useGPSSource() == false && KMessageBox::questionYesNo(KStars::Instance(), i18n("GPS is detected. Do you want to switch time and location source to GPS?"), i18n("GPS Settings"), KStandardGuiItem::yes(), KStandardGuiItem::no(), "use_gps_source_dialog") == KMessageBox::Yes) { Options::setUseKStarsSource(false); Options::setUseMountSource(false); Options::setUseGPSSource(true); } if (Options::useGPSSource() == false) return; currentGPS = newGPS; connect(newGPS, SIGNAL(numberUpdated(INumberVectorProperty*)), this, SLOT(updateNumber(INumberVectorProperty*)), Qt::UniqueConnection); appendLogText(i18n("GPS driver detected. KStars and mount time and location settings are now synced to the GPS driver.")); syncGPS(); } void Mount::syncGPS() { // We only update when location is OK INumberVectorProperty *location = currentGPS->getBaseDevice()->getNumber("GEOGRAPHIC_COORD"); if (location == nullptr || location->s != IPS_OK) return; // Sync name if (currentTelescope) { ITextVectorProperty *activeDevices = currentTelescope->getBaseDevice()->getText("ACTIVE_DEVICES"); if (activeDevices) { IText *activeGPS = IUFindText(activeDevices, "ACTIVE_GPS"); if (activeGPS) { if (strcmp(activeGPS->text, currentGPS->getDeviceName())) { IUSaveText(activeGPS, currentGPS->getDeviceName()); currentTelescope->getDriverInfo()->getClientManager()->sendNewText(activeDevices); } } } } // GPS Refresh should only be called once automatically. if (GPSInitialized == false) { ISwitchVectorProperty *refreshGPS = currentGPS->getBaseDevice()->getSwitch("GPS_REFRESH"); if (refreshGPS) { refreshGPS->sp[0].s = ISS_ON; currentGPS->getDriverInfo()->getClientManager()->sendNewSwitch(refreshGPS); GPSInitialized = true; } } } void Mount::setTrackEnabled(bool enabled) { if (enabled) trackOnB->click(); else trackOffB->click(); } int Mount::slewRate() { if (currentTelescope == nullptr) return -1; return currentTelescope->getSlewRate(); } QJsonArray Mount::getScopes() const { QJsonArray scopes; if (currentTelescope == nullptr) return scopes; QJsonObject primary = { {"name", "Primary"}, {"mount", currentTelescope->getDeviceName()}, {"aperture", primaryScopeApertureIN->value()}, {"focalLength", primaryScopeFocalIN->value()}, }; scopes.append(primary); QJsonObject guide = { {"name", "Guide"}, {"mount", currentTelescope->getDeviceName()}, {"aperture", primaryScopeApertureIN->value()}, {"focalLength", primaryScopeFocalIN->value()}, }; scopes.append(guide); return scopes; } void Mount::startParkTimer() { if (currentTelescope == nullptr) return; if (currentTelescope->isParked()) { appendLogText("Mount already parked."); return; } QDateTime parkTime = startupTimeEdit->dateTime(); qint64 parkSeconds = parkTime.msecsTo(KStarsData::Instance()->lt()); if (parkSeconds > 0) { appendLogText(i18n("Parking time cannot be in the past.")); return; } parkSeconds = std::abs(parkSeconds); if (parkSeconds > 24*60*60*1000) { appendLogText(i18n("Parking time must be within 24 hours of current time.")); return; } appendLogText(i18n("Caution: do not use Auto Park while scheduler is active.")); autoParkTimer.setInterval(parkSeconds); autoParkTimer.start(); startTimerB->setEnabled(false); stopTimerB->setEnabled(true); } void Mount::stopParkTimer() { autoParkTimer.stop(); countdownLabel->setText("00:00:00"); stopTimerB->setEnabled(false); startTimerB->setEnabled(true); } void Mount::startAutoPark() { appendLogText(i18n("Parking timer is up.")); autoParkTimer.stop(); startTimerB->setEnabled(true); stopTimerB->setEnabled(false); countdownLabel->setText("00:00:00"); if (currentTelescope) { if (currentTelescope->isParked() == false) { appendLogText(i18n("Starting auto park...")); park(); } } } } diff --git a/kstars/ekos/mount/mount.h b/kstars/ekos/mount/mount.h index a38353481..3b62bc109 100644 --- a/kstars/ekos/mount/mount.h +++ b/kstars/ekos/mount/mount.h @@ -1,355 +1,388 @@ /* Ekos Mount Module Copyright (C) 2015 Jasem Mutlaq This application is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version. */ #ifndef MOUNT_H #define MOUNT_H #include #include #include "ui_mount.h" #include "indi/indistd.h" #include "indi/indifocuser.h" #include "indi/inditelescope.h" class QQuickView; class QQuickItem; namespace Ekos { /** *@class Mount *@short Supports controlling INDI telescope devices including setting/retrieving mount properties, slewing, motion and speed controls, in addition to enforcing altitude limits and parking/unparking. *@author Jasem Mutlaq *@version 1.3 */ + class Mount : public QWidget, public Ui::Mount { Q_OBJECT Q_CLASSINFO("D-Bus Interface", "org.kde.kstars.Ekos.Mount") Q_PROPERTY(ISD::Telescope::Status status READ status NOTIFY newStatus) Q_PROPERTY(ISD::ParkStatus parkStatus READ parkStatus NOTIFY newParkStatus) Q_PROPERTY(QStringList logText READ logText NOTIFY newLog) Q_PROPERTY(QList altitudeLimits READ altitudeLimits WRITE setAltitudeLimits) Q_PROPERTY(bool altitudeLimitsEnabled READ altitudeLimitsEnabled WRITE setAltitudeLimitsEnabled) Q_PROPERTY(QList equatorialCoords READ equatorialCoords) Q_PROPERTY(QList horizontalCoords READ horizontalCoords) Q_PROPERTY(QList telescopeInfo READ telescopeInfo WRITE setTelescopeInfo) + Q_PROPERTY(SkyPoint currentTarget READ currentTarget) Q_PROPERTY(double hourAngle READ hourAngle) + Q_PROPERTY(double initialHA READ initialHA) Q_PROPERTY(int slewRate READ slewRate WRITE setSlewRate) Q_PROPERTY(int slewStatus READ slewStatus) Q_PROPERTY(bool canPark READ canPark) Q_PROPERTY(ISD::Telescope::PierSide pierSide READ pierSide NOTIFY pierSideChanged) public: Mount(); ~Mount(); //typedef enum { PARKING_IDLE, PARKING_OK, UNPARKING_OK, PARKING_BUSY, UNPARKING_BUSY, PARKING_ERROR } ParkingStatus; /** * @brief setTelescope Sets the mount module telescope interface * @param newTelescope pointer to telescope interface object */ void setTelescope(ISD::GDInterface *newTelescope); void setGPS(ISD::GDInterface *newGPS); // Log functions void appendLogText(const QString &); void clearLog(); QStringList logText() { return m_LogText; } QString getLogText() { return m_LogText.join("\n"); } ISD::Telescope::Status status() {return m_Status;} ISD::Telescope::PierSide pierSide() {return currentTelescope->pierSide();} ISD::ParkStatus parkStatus() {return m_ParkStatus;} /** @defgroup MountDBusInterface Ekos Mount DBus Interface * Mount interface provides advanced scripting capabilities to control INDI mounts. */ /*@{*/ /** DBUS interface function. * Returns the mount altitude limits. * @return Returns array of doubles. First item is minimum altititude in degrees. Second item is maximum altitude limit in degrees. */ Q_SCRIPTABLE QList altitudeLimits(); /** DBUS interface function. * Sets the mount altitude limits, and whether they are enabled or disabled. * @param limits is a list of double values. 2 values are expected: minAltitude & maxAltitude */ Q_SCRIPTABLE Q_NOREPLY void setAltitudeLimits(QList limits); /** DBUS interface function. * Enable or disable mount altitude limits. */ Q_SCRIPTABLE void setAltitudeLimitsEnabled(bool enable); /** DBUS interface function. * Returns whether the mount limits are enabled or disabled. * @return True if enabled, false otherwise. */ Q_SCRIPTABLE bool altitudeLimitsEnabled(); /** DBUS interface function. * Slew the mount to the RA/DEC (JNow). * @param RA Right ascention is hours. * @param DEC Declination in degrees. * @return true if the command is sent successfully, false otherwise. */ Q_INVOKABLE Q_SCRIPTABLE bool slew(double RA, double DEC); /** @brief Like above but RA and DEC are strings HH:MM:SS and DD:MM:SS */ Q_INVOKABLE bool slew(const QString &RA, const QString &DEC); /** DBUS interface function. * Slew the mount to the target. Target name must be valid in KStars. * @param target name * @return true if the command is sent successfully, false otherwise. */ Q_INVOKABLE Q_SCRIPTABLE bool gotoTarget(const QString &target); /** DBUS interface function. * Sync the mount to the RA/DEC (JNow). * @param RA Right ascention is hours. * @param DEC Declination in degrees. * @return true if the command is sent successfully, false otherwise. */ Q_INVOKABLE Q_SCRIPTABLE bool sync(double RA, double DEC); /** DBUS interface function. * Sync the mount to the target. Target name must be valid in KStars. * @param target name * @return true if the command is sent successfully, false otherwise. */ Q_INVOKABLE Q_SCRIPTABLE bool syncTarget(const QString &target); /** @brief Like above but RA and DEC are strings HH:MM:SS and DD:MM:SS */ Q_INVOKABLE bool sync(const QString &RA, const QString &DEC); /** DBUS interface function. * Get equatorial coords (JNow). An array of doubles is returned. First element is RA in hours. Second elements is DEC in degrees. */ Q_SCRIPTABLE QList equatorialCoords(); /** DBUS interface function. * Get Horizontal coords. An array of doubles is returned. First element is Azimuth in degrees. Second elements is Altitude in degrees. */ Q_SCRIPTABLE QList horizontalCoords(); + /** DBUS interface function. + * Get Horizontal coords. + */ + Q_SCRIPTABLE SkyPoint currentTarget(); + /** DBUS interface function. * Get mount hour angle in hours (-12 to +12). */ Q_SCRIPTABLE double hourAngle(); + double initialPositionHA; + /** DBUS interface function. + * Get the hour angle of that time the mount has slewed to the current position. + */ + Q_SCRIPTABLE double initialHA() {return initialPositionHA; } + + Q_SCRIPTABLE void setInitialHA(double ha) { initialPositionHA = ha; } + /** DBUS interface function. * Aborts the mount motion * @return true if the command is sent successfully, false otherwise. */ Q_INVOKABLE Q_SCRIPTABLE bool abort(); /** DBUS interface function. * Get the mount slew status ("Idle","Complete", "Busy", "Error") */ Q_INVOKABLE Q_SCRIPTABLE IPState slewStatus(); /** DBUS interface function. * Get the mount slew rate index 0 to N-1, or -1 if slew rates are not supported. */ Q_INVOKABLE Q_SCRIPTABLE int slewRate(); Q_INVOKABLE Q_SCRIPTABLE bool setSlewRate(int index); /** DBUS interface function. * Get telescope and guide scope info. An array of doubles is returned in order. * Primary Telescope Focal Length (mm), Primary Telescope Aperture (mm), Guide Telescope Focal Length (mm), Guide Telescope Aperture (mm) */ Q_INVOKABLE Q_SCRIPTABLE QList telescopeInfo(); /** DBUS interface function. * Set telescope and guide scope info and save them in INDI mount driver. All measurements is in millimeters. * @param info An ordered 4-item list as following: * primaryFocalLength Primary Telescope Focal Length. Set to 0 to skip setting this value. * primaryAperture Primary Telescope Aperture. Set to 0 to skip setting this value. * guideFocalLength Guide Telescope Focal Length. Set to 0 to skip setting this value. * guideAperture Guide Telescope Aperture. Set to 0 to skip setting this value. */ Q_SCRIPTABLE Q_NOREPLY void setTelescopeInfo(const QList &info); /** DBUS interface function. * Reset mount model if supported by the mount. * @return true if the command is executed successfully, false otherwise. */ Q_INVOKABLE Q_SCRIPTABLE bool resetModel(); /** DBUS interface function. * Can mount park? */ Q_INVOKABLE Q_SCRIPTABLE bool canPark(); /** DBUS interface function. * Park mount */ Q_INVOKABLE Q_SCRIPTABLE bool park(); /** DBUS interface function. * Unpark mount */ Q_INVOKABLE Q_SCRIPTABLE bool unpark(); /** DBUS interface function. * Return parking status of the mount. */ //Q_INVOKABLE Q_SCRIPTABLE ParkingStatus getParkingStatus(); Q_INVOKABLE void setTrackEnabled(bool enabled); Q_INVOKABLE void setJ2000Enabled(bool enabled); /** @}*/ Q_INVOKABLE void findTarget(); // Center mount in Sky Map Q_INVOKABLE void centerMount(); // Get list of scopes QJsonArray getScopes() const; - public slots: + /* + * @brief Execute a meridian flip if necessary. + * @return true if a meridian flip was necessary + */ + Q_INVOKABLE bool executeMeridianFlip(); + +public slots: /** * @brief syncTelescopeInfo Update telescope information to reflect any property changes */ void syncTelescopeInfo(); /** * @brief updateNumber Update number properties under watch in the mount module * @param nvp pointer to number property */ void updateNumber(INumberVectorProperty *nvp); /** * @brief updateSwitch Update switch properties under watch in the mount module * @param svp pointer to switch property */ void updateSwitch(ISwitchVectorProperty *svp); /** * @brief updateText Update text properties under watch in the mount module * @param tvp pointer to text property */ void updateText(ITextVectorProperty *tvp); /** * @brief updateLog Update mount module log to include any messages arriving for the telescope driver * @param messageID ID of the new message */ void updateLog(int messageID); /** * @brief updateTelescopeCoords runs every UPDATE_DELAY milliseconds to update the displayed coordinates of the mount and to ensure mount is * within altitude limits if the altitude limits are enabled. */ void updateTelescopeCoords(); /** * @brief move Issues motion command to the mount to move in a particular direction based the request NS and WE values * @param command Either ISD::Telescope::MOTION_START (0) or ISD::Telescope::MOTION_STOP (1) * @param NS is either -1 for no direction, or ISD::Telescope::MOTION_NORTH (0), or ISD::Telescope::MOTION_SOUTH (1) * @param WE is either -1 for no direction, or ISD::Telescope::MOTION_WEST (0), or ISD::Telescope::MOTION_EAST (1) */ void motionCommand(int command, int NS, int WE); /** * @brief save Save telescope focal length and aperture in the INDI telescope driver configuration. */ void save(); /** * @brief saveLimits Saves altitude limit to the user options and updates the INDI telescope driver limits */ void saveLimits(); /** * @brief enableAltitudeLimits Enable or disable altitude limits * @param enable True to enable, false to disable. */ void enableAltitudeLimits(bool enable); /** * @brief enableAltLimits calls enableAltitudeLimits(true). This function is mostly used to enable altitude limit after a meridian flip is complete. */ void enableAltLimits(); /** * @brief disableAltLimits calls enableAltitudeLimits(false). This function is mostly used to disable altitude limit once a meridial flip process is started. */ void disableAltLimits(); bool setScopeConfig(int index); void toggleMountToolBox(); private slots: + /** + * @brief registerNewModule Register an Ekos module as it arrives via DBus + * and create the appropriate DBus interface to communicate with it. + * @param name of module + */ + void registerNewModule(const QString &name); + void startParkTimer(); void stopParkTimer(); void startAutoPark(); signals: void newLog(const QString &text); void newCoords(const QString &ra, const QString &dec, const QString &az, const QString &alt); void newTarget(const QString &name); void newStatus(ISD::Telescope::Status status); void newParkStatus(ISD::ParkStatus status); void pierSideChanged(ISD::Telescope::PierSide side); void slewRateChanged(int index); void ready(); private: void syncGPS(); + QPointer captureInterface { nullptr }; + ISD::Telescope *currentTelescope = nullptr; ISD::GDInterface *currentGPS = nullptr; QStringList m_LogText; + SkyPoint currentTargetPosition; SkyPoint telescopeCoord; QString lastNotificationMessage; QTimer updateTimer; QTimer autoParkTimer; double lastAlt; int abortDispatch; bool altLimitEnabled; bool GPSInitialized = {false}; ISD::Telescope::Status m_Status = ISD::Telescope::MOUNT_IDLE; ISD::ParkStatus m_ParkStatus = ISD::PARK_UNKNOWN; QQuickView *m_BaseView = nullptr; QQuickItem *m_BaseObj = nullptr; QQmlContext *m_Ctxt = nullptr; QQuickItem *m_SpeedSlider = nullptr, *m_SpeedLabel = nullptr, *m_raValue = nullptr, *m_deValue = nullptr, *m_azValue = nullptr, *m_altValue = nullptr, *m_haValue = nullptr, *m_zaValue = nullptr, *m_targetText = nullptr, *m_targetRAText = nullptr, *m_targetDEText = nullptr, *m_Park = nullptr, *m_Unpark = nullptr, *m_statusText = nullptr, *m_J2000Check = nullptr, *m_JNowCheck=nullptr; }; } + #endif // Mount diff --git a/kstars/org.kde.kstars.Ekos.Mount.xml b/kstars/org.kde.kstars.Ekos.Mount.xml index d16937dd1..57b1cc13d 100644 --- a/kstars/org.kde.kstars.Ekos.Mount.xml +++ b/kstars/org.kde.kstars.Ekos.Mount.xml @@ -1,73 +1,80 @@ + + + + + + + diff --git a/kstars/skyobjects/skypoint.cpp b/kstars/skyobjects/skypoint.cpp index 637cc0824..400847c8a 100644 --- a/kstars/skyobjects/skypoint.cpp +++ b/kstars/skyobjects/skypoint.cpp @@ -1,938 +1,959 @@ /*************************************************************************** skypoint.cpp - K Desktop Planetarium ------------------- begin : Sun Feb 11 2001 copyright : (C) 2001-2005 by Jason Harris email : jharris@30doradus.org copyright : (C) 2004-2005 by Pablo de Vicente email : p.devicente@wanadoo.es ***************************************************************************/ /*************************************************************************** * * * This program is free software; you can redistribute it and/or modify * * it under the terms of the GNU General Public License as published by * * the Free Software Foundation; either version 2 of the License, or * * (at your option) any later version. * * * ***************************************************************************/ #include "skypoint.h" #include "dms.h" #include "ksnumbers.h" #include "kstarsdatetime.h" #include "kssun.h" #include "kstarsdata.h" #include "Options.h" #include "skyobject.h" #include "skycomponents/skymapcomposite.h" #include #include #include #ifdef PROFILE_COORDINATE_CONVERSION #include // For profiling, remove if not profiling. long unsigned SkyPoint::eqToHzCalls = 0; double SkyPoint::cpuTime_EqToHz = 0.; #endif KSSun *SkyPoint::m_Sun = nullptr; const double SkyPoint::altCrit = -1.0; SkyPoint::SkyPoint() { // Default constructor. Set nonsense values RA0.setD(-1); // RA >= 0 always :-) Dec0.setD(180); // Dec is between -90 and 90 Degrees :-) RA = RA0; Dec = Dec0; lastPrecessJD = J2000; // By convention, we use J2000 coordinates } void SkyPoint::set(const dms &r, const dms &d) { RA0 = RA = r; Dec0 = Dec = d; lastPrecessJD = J2000; // By convention, we use J2000 coordinates } void SkyPoint::EquatorialToHorizontal(const dms *LST, const dms *lat) { // qDebug() << "NOTE: This EquatorialToHorizontal overload (using dms pointers instead of CachingDms pointers) is deprecated and should be replaced with CachingDms prototype wherever speed is desirable!"; CachingDms _LST(*LST), _lat(*lat); EquatorialToHorizontal(&_LST, &_lat); } void SkyPoint::EquatorialToHorizontal(const CachingDms *LST, const CachingDms *lat) { #ifdef PROFILE_COORDINATE_CONVERSION std::clock_t start = std::clock(); #endif //Uncomment for spherical trig version double AltRad, AzRad; double sindec, cosdec, sinlat, coslat, sinHA, cosHA; double sinAlt, cosAlt; CachingDms HourAngle = (*LST) - ra(); // Using CachingDms subtraction operator to find cos/sin of HourAngle without calling sincos() lat->SinCos(sinlat, coslat); dec().SinCos(sindec, cosdec); HourAngle.SinCos(sinHA, cosHA); sinAlt = sindec * sinlat + cosdec * coslat * cosHA; AltRad = asin(sinAlt); cosAlt = sqrt( 1 - sinAlt * sinAlt); // Avoid trigonometric function. Return value of asin is always in [-pi/2, pi/2] and in this domain cosine is always non-negative, so we can use this. if (cosAlt == 0.) cosAlt = cos(AltRad); double arg = (sindec - sinlat * sinAlt) / (coslat * cosAlt); if (arg <= -1.0) AzRad = dms::PI; else if (arg >= 1.0) AzRad = 0.0; else AzRad = acos(arg); if (sinHA > 0.0) AzRad = 2.0 * dms::PI - AzRad; // resolve acos() ambiguity Alt.setRadians(AltRad); Az.setRadians(AzRad); #ifdef PROFILE_COORDINATE_CONVERSION std::clock_t stop = std::clock(); cpuTime_EqToHz += double(stop - start) / double(CLOCKS_PER_SEC); // Accumulate time in seconds ++eqToHzCalls; #endif // //Uncomment for XYZ version // double xr, yr, zr, xr1, zr1, sa, ca; // //Z-axis rotation by -LST // dms a = dms( -1.0*LST->Degrees() ); // a.SinCos( sa, ca ); // xr1 = m_X*ca + m_Y*sa; // yr = -1.0*m_X*sa + m_Y*ca; // zr1 = m_Z; // // //Y-axis rotation by lat - 90. // a = dms( lat->Degrees() - 90.0 ); // a.SinCos( sa, ca ); // xr = xr1*ca - zr1*sa; // zr = xr1*sa + zr1*ca; // // //FIXME: eventually, we will work with XYZ directly // Alt.setRadians( asin( zr ) ); // Az.setRadians( atan2( yr, xr ) ); } void SkyPoint::HorizontalToEquatorial(const dms *LST, const dms *lat) { double HARad, DecRad; double sinlat, coslat, sinAlt, cosAlt, sinAz, cosAz; double sinDec, cosDec; lat->SinCos(sinlat, coslat); alt().SinCos(sinAlt, cosAlt); Az.SinCos(sinAz, cosAz); sinDec = sinAlt * sinlat + cosAlt * coslat * cosAz; DecRad = asin(sinDec); cosDec = cos(DecRad); Dec.setRadians(DecRad); double x = (sinAlt - sinlat * sinDec) / (coslat * cosDec); //Under certain circumstances, x can be very slightly less than -1.0000, or slightly //greater than 1.0000, leading to a crash on acos(x). However, the value isn't //*really* out of range; it's a kind of roundoff error. if (x < -1.0 && x > -1.000001) HARad = dms::PI; else if (x > 1.0 && x < 1.000001) HARad = 0.0; else if (x < -1.0) { //qWarning() << "Coordinate out of range."; HARad = dms::PI; } else if (x > 1.0) { //qWarning() << "Coordinate out of range."; HARad = 0.0; } else HARad = acos(x); if (sinAz > 0.0) HARad = 2.0 * dms::PI - HARad; // resolve acos() ambiguity RA.setRadians(LST->radians() - HARad); RA.reduceToRange(dms::ZERO_TO_2PI); } void SkyPoint::findEcliptic(const CachingDms *Obliquity, dms &EcLong, dms &EcLat) { double sinRA, cosRA, sinOb, cosOb, sinDec, cosDec, tanDec; ra().SinCos(sinRA, cosRA); dec().SinCos(sinDec, cosDec); Obliquity->SinCos(sinOb, cosOb); tanDec = sinDec / cosDec; // FIXME: -jbb div by zero? double y = sinRA * cosOb + tanDec * sinOb; double ELongRad = atan2(y, cosRA); EcLong.setRadians(ELongRad); EcLong.reduceToRange(dms::ZERO_TO_2PI); EcLat.setRadians(asin(sinDec * cosOb - cosDec * sinOb * sinRA)); } void SkyPoint::setFromEcliptic(const CachingDms *Obliquity, const dms &EcLong, const dms &EcLat) { double sinLong, cosLong, sinLat, cosLat, sinObliq, cosObliq; EcLong.SinCos(sinLong, cosLong); EcLat.SinCos(sinLat, cosLat); Obliquity->SinCos(sinObliq, cosObliq); double sinDec = sinLat * cosObliq + cosLat * sinObliq * sinLong; double y = sinLong * cosObliq - (sinLat / cosLat) * sinObliq; // double RARad = atan2( y, cosLong ); RA.setUsing_atan2(y, cosLong); RA.reduceToRange(dms::ZERO_TO_2PI); Dec.setUsing_asin(sinDec); } void SkyPoint::precess(const KSNumbers *num) { double cosRA0, sinRA0, cosDec0, sinDec0; const Eigen::Matrix3d &precessionMatrix = num->p2(); Eigen::Vector3d v, s; RA0.SinCos(sinRA0, cosRA0); Dec0.SinCos(sinDec0, cosDec0); s[0] = cosRA0 * cosDec0; s[1] = sinRA0 * cosDec0; s[2] = sinDec0; // NOTE: Rotation matrices are the fastest way to do rotations on // a vector. Quaternions need more multiplications. The rotation // matrix compensates in some sense by having more 'precomputed' // multiplications. The matrix elements seem to cache nicely, so // there isn't much overhead in accessing them. //Multiply P2 and s to get v, the vector representing the new coords. // for ( unsigned int i=0; i<3; ++i ) { // v[i] = 0.0; // for (uint j=0; j< 3; ++j) { // v[i] += num->p2( j, i )*s[j]; // } // } v.noalias() = precessionMatrix * s; //Extract RA, Dec from the vector: RA.setUsing_atan2(v[1], v[0]); RA.reduceToRange(dms::ZERO_TO_2PI); Dec.setUsing_asin(v[2]); } SkyPoint SkyPoint::deprecess(const KSNumbers *num, long double epoch) { SkyPoint p1(RA, Dec); long double now = num->julianDay(); p1.precessFromAnyEpoch(now, epoch); if ((std::isnan(RA0.Degrees()) || std::isnan(Dec0.Degrees())) || (!std::isnan(Dec0.Degrees()) && fabs(Dec0.Degrees()) > 90.0)) { // We have invalid RA0 and Dec0, so set them if epoch = J2000. Otherwise, do not touch. if (epoch == J2000) { RA0 = p1.ra(); Dec0 = p1.dec(); } } return p1; } void SkyPoint::nutate(const KSNumbers *num) { double cosRA, sinRA, cosDec, sinDec, tanDec; double cosOb, sinOb; RA.SinCos(sinRA, cosRA); Dec.SinCos(sinDec, cosDec); num->obliquity()->SinCos(sinOb, cosOb); //Step 2: Nutation if (fabs(Dec.Degrees()) < 80.0) //approximate method { tanDec = sinDec / cosDec; double dRA = num->dEcLong() * (cosOb + sinOb * sinRA * tanDec) - num->dObliq() * cosRA * tanDec; double dDec = num->dEcLong() * (sinOb * cosRA) + num->dObliq() * sinRA; RA.setD(RA.Degrees() + dRA); Dec.setD(Dec.Degrees() + dDec); } else //exact method { dms EcLong, EcLat; findEcliptic(num->obliquity(), EcLong, EcLat); //Add dEcLong to the Ecliptic Longitude dms newLong(EcLong.Degrees() + num->dEcLong()); setFromEcliptic(num->obliquity(), newLong, EcLat); } } SkyPoint SkyPoint::moveAway(const SkyPoint &from, double dist) const { CachingDms lat1, dtheta; if (dist == 0.0) { qDebug() << "moveAway called with zero distance!"; return *this; } double dst = fabs(dist * dms::DegToRad / 3600.0); // In radian // Compute the bearing angle w.r.t. the RA axis ("latitude") CachingDms dRA(ra() - from.ra()); CachingDms dDec(dec() - from.dec()); double bearing = atan2(dRA.sin() / dRA.cos(), dDec.sin()); // Do not use dRA = PI / 2!! //double bearing = atan2( dDec.radians() , dRA.radians() ); // double dir0 = (dist >= 0 ) ? bearing : bearing + dms::PI; // in radian double dir0 = bearing + std::signbit(dist) * dms::PI; // might be faster? double sinDst = sin(dst), cosDst = cos(dst); lat1.setUsing_asin(dec().sin() * cosDst + dec().cos() * sinDst * cos(dir0)); dtheta.setUsing_atan2(sin(dir0) * sinDst * dec().cos(), cosDst - dec().sin() * lat1.sin()); return SkyPoint(ra() + dtheta, lat1); } bool SkyPoint::checkBendLight() { // First see if we are close enough to the sun to bother about the // gravitational lensing effect. We correct for the effect at // least till b = 10 solar radii, where the effect is only about // 0.06". Assuming min. sun-earth distance is 200 solar radii. static const dms maxAngle(1.75 * (30.0 / 200.0) / dms::DegToRad); if (!m_Sun) { SkyComposite *skycomopsite = KStarsData::Instance()->skyComposite(); if (skycomopsite == nullptr) return false; m_Sun = (KSSun *)skycomopsite->findByName("Sun"); if (m_Sun == nullptr) return false; } // TODO: This can be optimized further. We only need a ballpark estimate of the distance to the sun to start with. return (fabs(angularDistanceTo(static_cast(m_Sun)).Degrees()) <= maxAngle.Degrees()); // NOTE: dynamic_cast is slow and not important here. } bool SkyPoint::bendlight() { // NOTE: This should be applied before aberration // NOTE: One must call checkBendLight() before unnecessarily calling this. // We correct for GR effects // NOTE: This code is buggy. The sun needs to be initialized to // the current epoch -- but we are not certain that this is the // case. We have, as of now, no way of telling if the sun is // initialized or not. If we initialize the sun here, we will be // slowing down the program rather substantially and potentially // introducing bugs. Therefore, we just ignore this problem, and // hope that whenever the user is interested in seeing the effects // of GR, we have the sun initialized correctly. This is usually // the case. When the sun is not correctly initialized, rearth() // is not computed, so we just assume it is nominally equal to 1 // AU to get a reasonable estimate. Q_ASSERT(m_Sun); double corr_sec = 1.75 * m_Sun->physicalSize() / ((std::isfinite(m_Sun->rearth()) ? m_Sun->rearth() : 1) * AU_KM * angularDistanceTo(static_cast(m_Sun)).sin()); Q_ASSERT(corr_sec > 0); SkyPoint sp = moveAway(*m_Sun, corr_sec); setRA(sp.ra()); setDec(sp.dec()); return true; } void SkyPoint::aberrate(const KSNumbers *num) { double cosRA, sinRA, cosDec, sinDec; double cosOb, sinOb, cosL, sinL, cosP, sinP; double K = num->constAberr().Degrees(); //constant of aberration double e = num->earthEccentricity(); RA.SinCos(sinRA, cosRA); Dec.SinCos(sinDec, cosDec); num->obliquity()->SinCos(sinOb, cosOb); // double tanOb = sinOb/cosOb; num->sunTrueLongitude().SinCos(sinL, cosL); num->earthPerihelionLongitude().SinCos(sinP, cosP); //Step 3: Aberration // double dRA = -1.0 * K * ( cosRA * cosL * cosOb + sinRA * sinL )/cosDec // + e * K * ( cosRA * cosP * cosOb + sinRA * sinP )/cosDec; // double dDec = -1.0 * K * ( cosL * cosOb * ( tanOb * cosDec - sinRA * sinDec ) + cosRA * sinDec * sinL ) // + e * K * ( cosP * cosOb * ( tanOb * cosDec - sinRA * sinDec ) + cosRA * sinDec * sinP ); double dRA = K * (cosRA * cosOb / cosDec) * (e * cosP - cosL); double dDec = K * (sinRA * (sinOb * cosDec - cosOb * sinDec) * (e * cosP - cosL) + cosRA * sinDec * (e * sinP - sinL)); RA.setD(RA.Degrees() + dRA); Dec.setD(Dec.Degrees() + dDec); } // Note: This method is one of the major rate determining factors in how fast the map pans / zooms in or out void SkyPoint::updateCoords(const KSNumbers *num, bool /*includePlanets*/, const CachingDms *lat, const CachingDms *LST, bool forceRecompute) { //Correct the catalog coordinates for the time-dependent effects //of precession, nutation and aberration bool recompute, lens; // NOTE: The same short-circuiting checks are also implemented in // StarObject::JITUpdate(), even before calling // updateCoords(). While this is code-duplication, these bits of // code need to be really optimized, at least for stars. For // optimization purposes, the code is left duplicated in two // places. Please be wary of changing one without changing the // other. Q_ASSERT(std::isfinite(lastPrecessJD)); if (Options::useRelativistic() && checkBendLight()) { recompute = true; lens = true; } else { recompute = (Options::alwaysRecomputeCoordinates() || forceRecompute || std::abs(lastPrecessJD - num->getJD()) >= 0.00069444); // Update once per solar minute lens = false; } if (recompute) { precess(num); nutate(num); if (lens) bendlight(); // FIXME: Shouldn't we apply this on the horizontal coordinates? aberrate(num); lastPrecessJD = num->getJD(); Q_ASSERT(std::isfinite(RA.Degrees()) && std::isfinite(Dec.Degrees())); } if (lat || LST) qWarning() << i18n("lat and LST parameters should only be used in KSPlanetBase objects."); } void SkyPoint::precessFromAnyEpoch(long double jd0, long double jdf) { double cosRA, sinRA, cosDec, sinDec; double v[3], s[3]; RA = RA0; Dec = Dec0; // Is this necessary? if (jd0 == jdf) return; RA.SinCos(sinRA, cosRA); Dec.SinCos(sinDec, cosDec); if (jd0 == B1950) { B1950ToJ2000(); jd0 = J2000; RA.SinCos(sinRA, cosRA); Dec.SinCos(sinDec, cosDec); } if (jd0 != jdf) { // The original coordinate is referred to the FK5 system and // is NOT J2000. if (jd0 != J2000) { //v is a column vector representing input coordinates. v[0] = cosRA * cosDec; v[1] = sinRA * cosDec; v[2] = sinDec; //Need to first precess to J2000.0 coords //s is the product of P1 and v; s represents the //coordinates precessed to J2000 KSNumbers num(jd0); for (unsigned int i = 0; i < 3; ++i) { s[i] = num.p1(0, i) * v[0] + num.p1(1, i) * v[1] + num.p1(2, i) * v[2]; } //Input coords already in J2000, set s accordingly. } else { s[0] = cosRA * cosDec; s[1] = sinRA * cosDec; s[2] = sinDec; } if (jdf == B1950) { RA.setRadians(atan2(s[1], s[0])); Dec.setRadians(asin(s[2])); J2000ToB1950(); return; } KSNumbers num(jdf); for (unsigned int i = 0; i < 3; ++i) { v[i] = num.p2(0, i) * s[0] + num.p2(1, i) * s[1] + num.p2(2, i) * s[2]; } RA.setUsing_atan2(v[1], v[0]); Dec.setUsing_asin(v[2]); RA.reduceToRange(dms::ZERO_TO_2PI); return; } } void SkyPoint::apparentCoord(long double jd0, long double jdf) { precessFromAnyEpoch(jd0, jdf); KSNumbers num(jdf); nutate(&num); if (Options::useRelativistic() && checkBendLight()) bendlight(); aberrate(&num); } void SkyPoint::Equatorial1950ToGalactic(dms &galLong, dms &galLat) { double a = 192.25; double sinb, cosb, sina_RA, cosa_RA, sinDEC, cosDEC, tanDEC; dms c(303.0); dms b(27.4); tanDEC = tan(Dec.radians()); b.SinCos(sinb, cosb); dms(a - RA.Degrees()).SinCos(sina_RA, cosa_RA); Dec.SinCos(sinDEC, cosDEC); galLong.setRadians(c.radians() - atan2(sina_RA, cosa_RA * sinb - tanDEC * cosb)); galLong.reduceToRange(dms::ZERO_TO_2PI); galLat.setRadians(asin(sinDEC * sinb + cosDEC * cosb * cosa_RA)); } void SkyPoint::GalacticToEquatorial1950(const dms *galLong, const dms *galLat) { double a = 123.0; double sinb, cosb, singLat, cosgLat, tangLat, singLong_a, cosgLong_a; dms c(12.25); dms b(27.4); tangLat = tan(galLat->radians()); galLat->SinCos(singLat, cosgLat); dms(galLong->Degrees() - a).SinCos(singLong_a, cosgLong_a); b.SinCos(sinb, cosb); RA.setRadians(c.radians() + atan2(singLong_a, cosgLong_a * sinb - tangLat * cosb)); RA.reduceToRange(dms::ZERO_TO_2PI); Dec.setRadians(asin(singLat * sinb + cosgLat * cosb * cosgLong_a)); } void SkyPoint::B1950ToJ2000(void) { double cosRA, sinRA, cosDec, sinDec; // double cosRA0, sinRA0, cosDec0, sinDec0; double v[3], s[3]; // 1984 January 1 0h KSNumbers num(2445700.5); // Eterms due to aberration addEterms(); RA.SinCos(sinRA, cosRA); Dec.SinCos(sinDec, cosDec); // Precession from B1950 to J1984 s[0] = cosRA * cosDec; s[1] = sinRA * cosDec; s[2] = sinDec; for (unsigned int i = 0; i < 3; ++i) { v[i] = num.p2b(0, i) * s[0] + num.p2b(1, i) * s[1] + num.p2b(2, i) * s[2]; } // RA zero-point correction at 1984 day 1, 0h. RA.setRadians(atan2(v[1], v[0])); Dec.setRadians(asin(v[2])); RA.setH(RA.Hours() + 0.06390 / 3600.); RA.SinCos(sinRA, cosRA); Dec.SinCos(sinDec, cosDec); s[0] = cosRA * cosDec; s[1] = sinRA * cosDec; s[2] = sinDec; // Precession from 1984 to J2000. for (unsigned int i = 0; i < 3; ++i) { v[i] = num.p1(0, i) * s[0] + num.p1(1, i) * s[1] + num.p1(2, i) * s[2]; } RA.setRadians(atan2(v[1], v[0])); Dec.setRadians(asin(v[2])); } void SkyPoint::J2000ToB1950(void) { double cosRA, sinRA, cosDec, sinDec; // double cosRA0, sinRA0, cosDec0, sinDec0; double v[3], s[3]; // 1984 January 1 0h KSNumbers num(2445700.5); RA.SinCos(sinRA, cosRA); Dec.SinCos(sinDec, cosDec); s[0] = cosRA * cosDec; s[1] = sinRA * cosDec; s[2] = sinDec; // Precession from J2000 to 1984 day, 0h. for (unsigned int i = 0; i < 3; ++i) { v[i] = num.p2(0, i) * s[0] + num.p2(1, i) * s[1] + num.p2(2, i) * s[2]; } RA.setRadians(atan2(v[1], v[0])); Dec.setRadians(asin(v[2])); // RA zero-point correction at 1984 day 1, 0h. RA.setH(RA.Hours() - 0.06390 / 3600.); RA.SinCos(sinRA, cosRA); Dec.SinCos(sinDec, cosDec); // Precession from B1950 to J1984 s[0] = cosRA * cosDec; s[1] = sinRA * cosDec; s[2] = sinDec; for (unsigned int i = 0; i < 3; ++i) { v[i] = num.p1b(0, i) * s[0] + num.p1b(1, i) * s[1] + num.p1b(2, i) * s[2]; } RA.setRadians(atan2(v[1], v[0])); Dec.setRadians(asin(v[2])); // Eterms due to aberration subtractEterms(); } SkyPoint SkyPoint::Eterms(void) { double sd, cd, sinEterm, cosEterm; dms raTemp, raDelta, decDelta; Dec.SinCos(sd, cd); raTemp.setH(RA.Hours() + 11.25); raTemp.SinCos(sinEterm, cosEterm); raDelta.setH(0.0227 * sinEterm / (3600. * cd)); decDelta.setD(0.341 * cosEterm * sd / 3600. + 0.029 * cd / 3600.); return SkyPoint(raDelta, decDelta); } void SkyPoint::addEterms(void) { SkyPoint spd = Eterms(); RA = RA + spd.ra(); Dec = Dec + spd.dec(); } void SkyPoint::subtractEterms(void) { SkyPoint spd = Eterms(); RA = RA - spd.ra(); Dec = Dec - spd.dec(); } dms SkyPoint::angularDistanceTo(const SkyPoint *sp, double *const positionAngle) const { // double dalpha = sp->ra().radians() - ra().radians() ; // double ddelta = sp->dec().radians() - dec().radians(); CachingDms dalpha = sp->ra() - ra(); CachingDms ddelta = sp->dec() - dec(); // double sa = sin(dalpha/2.); // double sd = sin(ddelta/2.); // double hava = sa*sa; // double havd = sd*sd; // Compute the haversin directly: double hava = (1 - dalpha.cos()) / 2.; double havd = (1 - ddelta.cos()) / 2.; // Haversine law double aux = havd + (sp->dec().cos()) * dec().cos() * hava; dms angDist; angDist.setRadians(2. * fabs(asin(sqrt(aux)))); if (positionAngle) { // Also compute the position angle of the line from this SkyPoint to sp //*positionAngle = acos( tan(-ddelta)/tan( angDist.radians() ) ); // FIXME: Might fail for large ddelta / zero angDist //if( -dalpha < 0 ) // *positionAngle = 2*M_PI - *positionAngle; *positionAngle = atan2f(dalpha.sin(), (dec().cos()) * tan(sp->dec().radians()) - (dec().sin()) * dalpha.cos()) * 180 / M_PI; } return angDist; } double SkyPoint::vRSun(long double jd0) { double ca, sa, cd, sd, vsun; double cosRA, sinRA, cosDec, sinDec; /* Sun apex (where the sun goes) coordinates */ dms asun(270.9592); // Right ascention: 18h 3m 50.2s [J2000] dms dsun(30.00467); // Declination: 30^o 0' 16.8'' [J2000] vsun = 20.; // [km/s] asun.SinCos(sa, ca); dsun.SinCos(sd, cd); /* We need an auxiliary SkyPoint since we need the * source referred to the J2000 equinox and we do not want to overwrite * the current values */ SkyPoint aux; aux.set(RA0, Dec0); aux.precessFromAnyEpoch(jd0, J2000); aux.ra().SinCos(sinRA, cosRA); aux.dec().SinCos(sinDec, cosDec); /* Computation is done performing the scalar product of a unitary vector in the direction of the source with the vector velocity of Sun, both being in the LSR reference system: Vlsr = Vhel + Vsun.u_radial => Vlsr = Vhel + vsun(cos D cos A,cos D sen A,sen D).(cos d cos a,cos d sen a,sen d) Vhel = Vlsr - Vsun.u_radial */ return vsun * (cd * cosDec * (cosRA * ca + sa * sinRA) + sd * sinDec); } double SkyPoint::vHeliocentric(double vlsr, long double jd0) { return vlsr - vRSun(jd0); } double SkyPoint::vHelioToVlsr(double vhelio, long double jd0) { return vhelio + vRSun(jd0); } double SkyPoint::vREarth(long double jd0) { double sinRA, sinDec, cosRA, cosDec; /* u_radial = unitary vector in the direction of the source Vlsr = Vhel + Vsun.u_radial = Vgeo + VEarth.u_radial + Vsun.u_radial => Vgeo = (Vlsr -Vsun.u_radial) - VEarth.u_radial = Vhel - VEarth.u_radial = Vhel - (vx, vy, vz).(cos d cos a,cos d sen a,sen d) */ /* We need an auxiliary SkyPoint since we need the * source referred to the J2000 equinox and we do not want to overwrite * the current values */ SkyPoint aux(RA0, Dec0); aux.precessFromAnyEpoch(jd0, J2000); aux.ra().SinCos(sinRA, cosRA); aux.dec().SinCos(sinDec, cosDec); /* vEarth is referred to the J2000 equinox, hence we need that the source coordinates are also in the same reference system. */ KSNumbers num(jd0); return num.vEarth(0) * cosDec * cosRA + num.vEarth(1) * cosDec * sinRA + num.vEarth(2) * sinDec; } double SkyPoint::vGeocentric(double vhelio, long double jd0) { return vhelio - vREarth(jd0); } double SkyPoint::vGeoToVHelio(double vgeo, long double jd0) { return vgeo + vREarth(jd0); } double SkyPoint::vRSite(double vsite[3]) { double sinRA, sinDec, cosRA, cosDec; RA.SinCos(sinRA, cosRA); Dec.SinCos(sinDec, cosDec); return vsite[0] * cosDec * cosRA + vsite[1] * cosDec * sinRA + vsite[2] * sinDec; } double SkyPoint::vTopoToVGeo(double vtopo, double vsite[3]) { return vtopo + vRSite(vsite); } double SkyPoint::vTopocentric(double vgeo, double vsite[3]) { return vgeo - vRSite(vsite); } bool SkyPoint::checkCircumpolar(const dms *gLat) const { return fabs(dec().Degrees()) > (90 - fabs(gLat->Degrees())); } dms SkyPoint::altRefracted() const { if (Options::useRefraction()) return refract(Alt); else return Alt; } double SkyPoint::refractionCorr(double alt) { return 1.02 / tan(dms::DegToRad * (alt + 10.3 / (alt + 5.11))) / 60; } double SkyPoint::refract(const double alt) { static double corrCrit = SkyPoint::refractionCorr(SkyPoint::altCrit); if (alt > SkyPoint::altCrit) return (alt + SkyPoint::refractionCorr(alt)); else return (alt + corrCrit * (alt + 90) / (SkyPoint::altCrit + 90)); // Linear extrapolation from corrCrit at altCrit to 0 at -90 degrees } // Found uncorrected value by solving equation. This is OK since // unrefract is never called in loops. // // Convergence is quite fast just a few iterations. double SkyPoint::unrefract(const double alt) { double h0 = alt; double h1 = alt - (refract(h0) - h0); // It's probably okay to add h0 in refract() and subtract it here, since refract() is called way more frequently. while (fabs(h1 - h0) > 1e-4) { h0 = h1; h1 = alt - (refract(h0) - h0); } return h1; } dms SkyPoint::findAltitude(const SkyPoint *p, const KStarsDateTime &dt, const GeoLocation *geo, const double hour) { Q_ASSERT(p); if (!p) return dms(NaN::d); // Jasem 2015-08-24 Using correct procedure to find altitude return SkyPoint::timeTransformed(p, dt, geo, hour).alt(); } SkyPoint SkyPoint::timeTransformed(const SkyPoint *p, const KStarsDateTime &dt, const GeoLocation *geo, const double hour) { Q_ASSERT(p); if (!p) return SkyPoint(NaN::d, NaN::d); // Jasem 2015-08-24 Using correct procedure to find altitude SkyPoint sp = *p; // make a copy KStarsDateTime targetDateTime = dt.addSecs(hour * 3600.0); dms LST = geo->GSTtoLST(targetDateTime.gst()); sp.EquatorialToHorizontal(&LST, geo->lat()); return sp; } double SkyPoint::maxAlt(const dms &lat) const { double retval = (lat.Degrees() + 90. - dec().Degrees()); if (retval > 90.) retval = 180. - retval; return retval; } double SkyPoint::minAlt(const dms &lat) const { double retval = (lat.Degrees() - 90. + dec().Degrees()); if (retval < -90.) retval = 180. + retval; return retval; } + +#ifndef KSTARS_LITE +QDBusArgument &operator<<(QDBusArgument &argument, const SkyPoint &source) +{ + argument.beginStructure(); + argument << source.ra().Hours() << source.dec().Degrees(); + argument.endStructure(); + return argument; +} + +const QDBusArgument &operator>>(const QDBusArgument &argument, SkyPoint &dest) +{ + double ra, dec; + argument.beginStructure(); + argument >> ra >> dec; + argument.endStructure(); + dest = SkyPoint(ra, dec); + return argument; +} +#endif + diff --git a/kstars/skyobjects/skypoint.h b/kstars/skyobjects/skypoint.h index 5153ddbe6..34a62725d 100644 --- a/kstars/skyobjects/skypoint.h +++ b/kstars/skyobjects/skypoint.h @@ -1,650 +1,659 @@ /*************************************************************************** skypoint.h - K Desktop Planetarium ------------------- begin : Sun Feb 11 2001 copyright : (C) 2001-2005 by Jason Harris email : jharris@30doradus.org copyright : (C) 2004-2005 by Pablo de Vicente email : p.devicente@wanadoo.es ***************************************************************************/ /*************************************************************************** * * * This program is free software; you can redistribute it and/or modify * * it under the terms of the GNU General Public License as published by * * the Free Software Foundation; either version 2 of the License, or * * (at your option) any later version. * * * ***************************************************************************/ #pragma once #include "cachingdms.h" #include "kstarsdatetime.h" #include +#ifndef KSTARS_LITE +#include +#endif //#define PROFILE_COORDINATE_CONVERSION class KSNumbers; class KSSun; class GeoLocation; /** * @class SkyPoint * * The sky coordinates of a point in the sky. The * coordinates are stored in both Equatorial (Right Ascension, * Declination) and Horizontal (Azimuth, Altitude) coordinate systems. * Provides set/get functions for each coordinate angle, and functions * to convert between the Equatorial and Horizon coordinate systems. * * Because the coordinate values change slowly over time (due to * precession, nutation), the "catalog coordinates" are stored * (RA0, Dec0), which were the true coordinates on Jan 1, 2000. * The true coordinates (RA, Dec) at any other epoch can be found * from the catalog coordinates using updateCoords(). * @short Stores dms coordinates for a point in the sky. * for converting between coordinate systems. * * @author Jason Harris * @version 1.0 */ class SkyPoint { public: /** * Default constructor: Sets RA, Dec and RA0, Dec0 according * to arguments. Does not set Altitude or Azimuth. * * @param r Right Ascension * @param d Declination */ SkyPoint(const dms &r, const dms &d) : RA0(r), Dec0(d), RA(r), Dec(d), lastPrecessJD(J2000) {} SkyPoint(const CachingDms &r, const CachingDms &d) : RA0(r), Dec0(d), RA(r), Dec(d), lastPrecessJD(J2000) {} /** * Alternate constructor using double arguments, for convenience. * It behaves essentially like the default constructor. * * @param r Right Ascension, expressed as a double * @param d Declination, expressed as a double * @note This also sets RA0 and Dec0 */ //FIXME: this (*15.0) thing is somewhat hacky. explicit SkyPoint(double r, double d) : RA0(r * 15.0), Dec0(d), RA(r * 15.0), Dec(d), lastPrecessJD(J2000) {} /** @short Default constructor. Sets nonsense values for RA, Dec etc */ SkyPoint(); virtual ~SkyPoint() = default; //// //// 1. Setting Coordinates //// ======================= /** * @short Sets RA, Dec and RA0, Dec0 according to arguments. * Does not set Altitude or Azimuth. * * @param r Right Ascension * @param d Declination * @note This function also sets RA0 and Dec0 to the same values, so call at your own peril! * @note FIXME: This method must be removed, or an epoch argument must be added. */ void set(const dms &r, const dms &d); /** * Sets RA0, the catalog Right Ascension. * * @param r catalog Right Ascension. */ inline void setRA0(dms r) { RA0 = r; } inline void setRA0(CachingDms r) { RA0 = r; } /** * Overloaded member function, provided for convenience. * It behaves essentially like the above function. * * @param r Right Ascension, expressed as a double. */ inline void setRA0(double r) { RA0.setH(r); } /** * Sets Dec0, the catalog Declination. * * @param d catalog Declination. */ inline void setDec0(dms d) { Dec0 = d; } inline void setDec0(const CachingDms &d) { Dec0 = d; } /** * Overloaded member function, provided for convenience. * It behaves essentially like the above function. * * @param d Declination, expressed as a double. */ inline void setDec0(double d) { Dec0.setD(d); } /** * Sets RA, the current Right Ascension. * * @param r Right Ascension. */ inline void setRA(dms& r) { RA = r; } inline void setRA(const CachingDms &r) { RA = r; } /** * Overloaded member function, provided for convenience. * It behaves essentially like the above function. * * @param r Right Ascension, expressed as a double. */ inline void setRA(double r) { RA.setH(r); } /** * Sets Dec, the current Declination * * @param d Declination. */ inline void setDec(dms d) { Dec = d; } inline void setDec(const CachingDms &d) { Dec = d; } /** * Overloaded member function, provided for convenience. * It behaves essentially like the above function. * * @param d Declination, expressed as a double. */ inline void setDec(double d) { Dec.setD(d); } /** * Sets Alt, the Altitude. * * @param alt Altitude. */ inline void setAlt(dms alt) { Alt = alt; } /** * Overloaded member function, provided for convenience. * It behaves essentially like the above function. * * @param alt Altitude, expressed as a double. */ inline void setAlt(double alt) { Alt.setD(alt); } /** * Sets Az, the Azimuth. * * @param az Azimuth. */ inline void setAz(dms az) { Az = az; } /** * Overloaded member function, provided for convenience. * It behaves essentially like the above function. * * @param az Azimuth, expressed as a double. */ inline void setAz(double az) { Az.setD(az); } //// //// 2. Returning coordinates. //// ========================= /** @return a pointer to the catalog Right Ascension. */ inline const CachingDms &ra0() const { return RA0; } /** @return a pointer to the catalog Declination. */ inline const CachingDms &dec0() const { return Dec0; } /** @returns a pointer to the current Right Ascension. */ inline const CachingDms &ra() const { return RA; } /** @return a pointer to the current Declination. */ inline const CachingDms &dec() const { return Dec; } /** @return a pointer to the current Azimuth. */ inline const dms &az() const { return Az; } /** @return a pointer to the current Altitude. */ inline const dms &alt() const { return Alt; } /** * @return refracted altitude. This function uses * Options::useRefraction to determine whether refraction * correction should be applied */ dms altRefracted() const; /** @return the JD for the precessed coordinates */ inline double getLastPrecessJD() const { return lastPrecessJD; } /** * @return the airmass of the point. Convenience method. * @note Question: is it better to use alt or refracted alt? Minor difference, probably doesn't matter. */ inline double airmass() const { return 1. / sin(alt().radians()); } //// //// 3. Coordinate conversions. //// ========================== /** * Determine the (Altitude, Azimuth) coordinates of the * SkyPoint from its (RA, Dec) coordinates, given the local * sidereal time and the observer's latitude. * @param LST pointer to the local sidereal time * @param lat pointer to the geographic latitude */ void EquatorialToHorizontal(const CachingDms *LST, const CachingDms *lat); // Deprecated method provided for compatibility void EquatorialToHorizontal(const dms *LST, const dms *lat); /** * Determine the (RA, Dec) coordinates of the * SkyPoint from its (Altitude, Azimuth) coordinates, given the local * sidereal time and the observer's latitude. * * @param LST pointer to the local sidereal time * @param lat pointer to the geographic latitude */ void HorizontalToEquatorial(const dms *LST, const dms *lat); /** * Determine the Ecliptic coordinates of the SkyPoint, given the Julian Date. * The ecliptic coordinates are returned as reference arguments (since * they are not stored internally) */ void findEcliptic(const CachingDms *Obliquity, dms &EcLong, dms &EcLat); /** * Set the current (RA, Dec) coordinates of the * SkyPoint, given pointers to its Ecliptic (Long, Lat) coordinates, and * to the current obliquity angle (the angle between the equator and ecliptic). */ void setFromEcliptic(const CachingDms *Obliquity, const dms &EcLong, const dms &EcLat); /** * Computes galactic coordinates from equatorial coordinates referred to * epoch 1950. RA and Dec are, therefore assumed to be B1950 coordinates. */ void Equatorial1950ToGalactic(dms &galLong, dms &galLat); /** * Computes equatorial coordinates referred to 1950 from galactic ones referred to * epoch B1950. RA and Dec are, therefore assumed to be B1950 coordinates. */ void GalacticToEquatorial1950(const dms *galLong, const dms *galLat); //// //// 4. Coordinate update/corrections. //// ================================= /** * Determine the current coordinates (RA, Dec) from the catalog * coordinates (RA0, Dec0), accounting for both precession and nutation. * @param num pointer to KSNumbers object containing current values of time-dependent variables. * @param includePlanets does nothing in this implementation (see KSPlanetBase::updateCoords()). * @param lat does nothing in this implementation (see KSPlanetBase::updateCoords()). * @param LST does nothing in this implementation (see KSPlanetBase::updateCoords()). * @param forceRecompute reapplies precession, nutation and aberration even if the time passed * since the last computation is not significant. */ virtual void updateCoords(const KSNumbers *num, bool includePlanets = true, const CachingDms *lat = nullptr, const CachingDms *LST = nullptr, bool forceRecompute = false); /** * @brief updateCoordsNow Shortcut for updateCoords( const KSNumbers *num, false, nullptr, nullptr, true) * * @param num pointer to KSNumbers object containing current values of time-dependent variables. */ virtual void updateCoordsNow(const KSNumbers *num) { updateCoords(num, false, nullptr, nullptr, true); } /** * Computes the apparent coordinates for this SkyPoint for any epoch, * accounting for the effects of precession, nutation, and aberration. * Similar to updateCoords(), but the starting epoch need not be * J2000, and the target epoch need not be the present time. * * @param jd0 Julian Day which identifies the original epoch * @param jdf Julian Day which identifies the final epoch */ void apparentCoord(long double jd0, long double jdf); /** * Determine the effects of nutation for this SkyPoint. * * @param num pointer to KSNumbers object containing current values of * time-dependent variables. */ void nutate(const KSNumbers *num); /** * @short Check if this sky point is close enough to the sun for * gravitational lensing to be significant */ bool checkBendLight(); /** * Correct for the effect of "bending" of light around the sun for * positions near the sun. * * General Relativity tells us that a photon with an impact * parameter b is deflected through an angle 1.75" (Rs / b) where * Rs is the solar radius. * * @return: true if the light was bent, false otherwise */ bool bendlight(); /** * @short Obtain a Skypoint with RA0 and Dec0 set from the RA, Dec * of this skypoint. Also set the RA0, Dec0 of this SkyPoint if not * set already and the target epoch is J2000. */ SkyPoint deprecess(const KSNumbers *num, long double epoch = J2000); /** * Determine the effects of aberration for this SkyPoint. * * @param num pointer to KSNumbers object containing current values of * time-dependent variables. */ void aberrate(const KSNumbers *num); /** * General case of precession. It precess from an original epoch to a * final epoch. In this case RA0, and Dec0 from SkyPoint object represent * the coordinates for the original epoch and not for J2000, as usual. * * @param jd0 Julian Day which identifies the original epoch * @param jdf Julian Day which identifies the final epoch */ void precessFromAnyEpoch(long double jd0, long double jdf); /** * Determine the E-terms of aberration * In the past, the mean places of stars published in catalogs included * the contribution to the aberration due to the ellipticity of the orbit * of the Earth. These terms, known as E-terms were almost constant, and * in the newer catalogs (FK5) are not included. Therefore to convert from * FK4 to FK5 one has to compute these E-terms. */ SkyPoint Eterms(void); /** * Exact precession from Besselian epoch 1950 to epoch J2000. The * coordinates referred to the first epoch are in the * FK4 catalog, while the latter are in the Fk5 one. * * Reference: Smith, C. A.; Kaplan, G. H.; Hughes, J. A.; Seidelmann, * P. K.; Yallop, B. D.; Hohenkerk, C. Y. * Astronomical Journal, vol. 97, Jan. 1989, p. 265-279 * * This transformation requires 4 steps: * - Correct E-terms * - Precess from B1950 to 1984, January 1st, 0h, using Newcomb expressions * - Add zero point correction in right ascension for 1984 * - Precess from 1984, January 1st, 0h to J2000 */ void B1950ToJ2000(void); /** * Exact precession from epoch J2000 Besselian epoch 1950. The coordinates * referred to the first epoch are in the FK4 catalog, while the * latter are in the Fk5 one. * * Reference: Smith, C. A.; Kaplan, G. H.; Hughes, J. A.; Seidelmann, * P. K.; Yallop, B. D.; Hohenkerk, C. Y. * Astronomical Journal, vol. 97, Jan. 1989, p. 265-279 * * This transformation requires 4 steps: * - Precess from J2000 to 1984, January 1st, 0h * - Add zero point correction in right ascension for 1984 * - Precess from 1984, January 1st, 0h, to B1950 using Newcomb expressions * - Correct E-terms */ void J2000ToB1950(void); /** * Coordinates in the FK4 catalog include the effect of aberration due * to the ellipticity of the orbit of the Earth. Coordinates in the FK5 * catalog do not include these terms. In order to convert from B1950 (FK4) * to actual mean places one has to use this function. */ void addEterms(void); /** * Coordinates in the FK4 catalog include the effect of aberration due * to the ellipticity of the orbit of the Earth. Coordinates in the FK5 * catalog do not include these terms. In order to convert from * FK5 coordinates to B1950 (FK4) one has to use this function. */ void subtractEterms(void); /** * Computes the angular distance between two SkyObjects. The algorithm * to compute this distance is: * cos(distance) = sin(d1)*sin(d2) + cos(d1)*cos(d2)*cos(a1-a2) * where a1,d1 are the coordinates of the first object and a2,d2 are * the coordinates of the second object. * However this algorithm is not accurate when the angular separation is small. * Meeus provides a different algorithm in page 111 which we implement here. * * @param sp SkyPoint to which distance is to be calculated * @param positionAngle if a non-null pointer is passed, the position angle [E of N] * in degrees from this SkyPoint to sp is computed and stored in the passed variable. * @return dms angle representing angular separation. **/ dms angularDistanceTo(const SkyPoint *sp, double *const positionAngle = nullptr) const; /** @return returns true if _current_ epoch RA / Dec match */ inline bool operator==(SkyPoint &p) const { return (ra() == p.ra() && dec() == p.dec()); } /** * Computes the velocity of the Sun projected on the direction of the source. * * @param jd Epoch expressed as julian day to which the source coordinates refer to. * @return Radial velocity of the source referred to the barycenter of the solar system in km/s **/ double vRSun(long double jd); /** * Computes the radial velocity of a source referred to the solar system barycenter * from the radial velocity referred to the * Local Standard of Rest, aka known as VLSR. To compute it we need the coordinates of the * source the VLSR and the epoch for the source coordinates. * * @param vlsr radial velocity of the source referred to the LSR in km/s * @param jd Epoch expressed as julian day to which the source coordinates refer to. * @return Radial velocity of the source referred to the barycenter of the solar system in km/s **/ double vHeliocentric(double vlsr, long double jd); /** * Computes the radial velocity of a source referred to the Local Standard of Rest, also known as VLSR * from the radial velocity referred to the solar system barycenter * * @param vhelio radial velocity of the source referred to the LSR in km/s * @param jd Epoch expressed as julian day to which the source coordinates refer to. * @return Radial velocity of the source referred to the barycenter of the solar system in km/s **/ double vHelioToVlsr(double vhelio, long double jd); /** * Computes the velocity of any object projected on the direction of the source. * * @param jd0 Julian day for which we compute the direction of the source * @return velocity of the Earth projected on the direction of the source kms-1 */ double vREarth(long double jd0); /** * Computes the radial velocity of a source referred to the center of the earth * from the radial velocity referred to the solar system barycenter * * @param vhelio radial velocity of the source referred to the barycenter of the * solar system in km/s * @param jd Epoch expressed as julian day to which the source coordinates refer to. * @return Radial velocity of the source referred to the center of the Earth in km/s **/ double vGeocentric(double vhelio, long double jd); /** * Computes the radial velocity of a source referred to the solar system barycenter * from the velocity referred to the center of the earth * * @param vgeo radial velocity of the source referred to the center of the Earth [km/s] * @param jd Epoch expressed as julian day to which the source coordinates refer to. * @return Radial velocity of the source referred to the solar system barycenter in km/s **/ double vGeoToVHelio(double vgeo, long double jd); /** * Computes the velocity of any object (observer's site) projected on the * direction of the source. * * @param vsite velocity of that object in cartesian coordinates * @return velocity of the object projected on the direction of the source kms-1 */ double vRSite(double vsite[3]); /** * Computes the radial velocity of a source referred to the observer site on the surface * of the earth from the geocentric velocity and the velocity of the site referred to the center * of the Earth. * * @param vgeo radial velocity of the source referred to the center of the earth in km/s * @param vsite Velocity at which the observer moves referred to the center of the earth. * @return Radial velocity of the source referred to the observer's site in km/s **/ double vTopocentric(double vgeo, double vsite[3]); /** * Computes the radial velocity of a source referred to the center of the Earth from * the radial velocity referred to an observer site on the surface of the earth * * @param vtopo radial velocity of the source referred to the observer's site in km/s * @param vsite Velocity at which the observer moves referred to the center of the earth. * @return Radial velocity of the source referred the center of the earth in km/s **/ double vTopoToVGeo(double vtopo, double vsite[3]); /** * Find the SkyPoint obtained by moving distance dist * (arcseconds) away from the givenSkyPoint * * @param dist Distance to move through in arcseconds * @param from The SkyPoint to move away from * @return a SkyPoint that is at the dist away from this SkyPoint in the direction away from */ SkyPoint moveAway(const SkyPoint &from, double dist) const; /** @short Check if this point is circumpolar at the given geographic latitude */ bool checkCircumpolar(const dms *gLat) const; /** Calculate refraction correction. Parameter and return value are in degrees */ static double refractionCorr(double alt); /** * @short Apply refraction correction to altitude. * * @param alt altitude to be corrected, in degrees * @return altitude after refraction correction, in degrees */ static double refract(const double alt); /** * @short Remove refraction correction. * * @param alt altitude from which refraction correction must be removed, in degrees * @return altitude without refraction correction, in degrees */ static double unrefract(const double alt); /** * @short Apply refraction correction to altitude. Overloaded method using * dms provided for convenience * @see SkyPoint::refract( const double alt ) */ static inline dms refract(const dms alt) { return dms(refract(alt.Degrees())); } /** * @short Remove refraction correction. Overloaded method using * dms provided for convenience * @see SkyPoint::unrefract( const double alt ) */ static inline dms unrefract(const dms alt) { return dms(unrefract(alt.Degrees())); } /** * @short Compute the altitude of a given skypoint hour hours from the given date/time * * @param p SkyPoint whose altitude is to be computed (const pointer, the method works on a clone) * @param dt Date/time that corresponds to 0 hour * @param geo GeoLocation object specifying the location * @param hour double specifying offset in hours from dt for which altitude is to be found * @return a dms containing (unrefracted?) altitude of the object at dt + hour hours at the given location * * @note This method is used in multiple places across KStars * @todo Fix code duplication in AltVsTime and KSAlmanac by using this method instead! FIXME. */ static dms findAltitude(const SkyPoint *p, const KStarsDateTime &dt, const GeoLocation *geo, const double hour = 0); /** * @short returns a time-transformed SkyPoint. See SkyPoint::findAltitude() for details * @todo Fix this documentation. */ static SkyPoint timeTransformed(const SkyPoint *p, const KStarsDateTime &dt, const GeoLocation *geo, const double hour = 0); /** * @short Critical height for atmospheric refraction * corrections. Below this, the height formula produces meaningless * results and the correction value is just interpolated. */ static const double altCrit; /** * @short Return the object's altitude at the upper culmination for the given latitude * * @return the maximum altitude in degrees */ double maxAlt(const dms &lat) const; /** * @short Return the object's altitude at the lower culmination for the given latitude * * @return the minimum altitude in degrees */ double minAlt(const dms &lat) const; #ifdef PROFILE_COORDINATE_CONVERSION static double cpuTime_EqToHz; static long unsigned eqToHzCalls; #endif protected: /** * Precess this SkyPoint's catalog coordinates to the epoch described by the * given KSNumbers object. * * @param num pointer to a KSNumbers object describing the target epoch. */ void precess(const KSNumbers *num); #ifdef UNIT_TEST friend class TestSkyPoint; // Test class #endif private: CachingDms RA0, Dec0; //catalog coordinates CachingDms RA, Dec; //current true sky coordinates dms Alt, Az; static KSSun *m_Sun; protected: double lastPrecessJD { 0 }; // JD at which the last coordinate (see updateCoords) for this SkyPoint was done }; + +#ifndef KSTARS_LITE +Q_DECLARE_METATYPE(SkyPoint) +QDBusArgument &operator<<(QDBusArgument &argument, const SkyPoint &source); +const QDBusArgument &operator>>(const QDBusArgument &argument, SkyPoint &dest); +#endif