diff --git a/kstars/ekos/capture/capture.cpp b/kstars/ekos/capture/capture.cpp index e4407afa5..7fde76d49 100644 --- a/kstars/ekos/capture/capture.cpp +++ b/kstars/ekos/capture/capture.cpp @@ -1,6832 +1,6832 @@ /* Ekos Copyright (C) 2012 Jasem Mutlaq This application is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version. */ #include "capture.h" #include "captureadaptor.h" #include "kstars.h" #include "kstarsdata.h" #include "Options.h" #include "rotatorsettings.h" #include "sequencejob.h" #include "skymap.h" #include "ui_calibrationoptions.h" #include "auxiliary/QProgressIndicator.h" #include "auxiliary/ksmessagebox.h" #include "ekos/manager.h" #include "ekos/auxiliary/darklibrary.h" #include "fitsviewer/fitsdata.h" #include "fitsviewer/fitsview.h" #include "indi/driverinfo.h" #include "indi/indifilter.h" #include "indi/clientmanager.h" #include "oal/observeradd.h" #include #include #define MF_TIMER_TIMEOUT 90000 #define GD_TIMER_TIMEOUT 60000 #define MF_RA_DIFF_LIMIT 4 // Wait 3-minutes as maximum beyond exposure // value. #define CAPTURE_TIMEOUT_THRESHOLD 180000 // Current Sequence File Format: #define SQ_FORMAT_VERSION 2.0 // We accept file formats with version back to: #define SQ_COMPAT_VERSION 2.0 namespace Ekos { Capture::Capture() { setupUi(this); qRegisterMetaType("Ekos::CaptureState"); qDBusRegisterMetaType(); new CaptureAdaptor(this); QDBusConnection::sessionBus().registerObject("/KStars/Ekos/Capture", this); QPointer ekosInterface = new QDBusInterface("org.kde.kstars", "/KStars/Ekos", "org.kde.kstars.Ekos", QDBusConnection::sessionBus(), this); // Connecting DBus signals QDBusConnection::sessionBus().connect("org.kde.kstars", "/KStars/Ekos", "org.kde.kstars.Ekos", "newModule", this, SLOT(registerNewModule(QString))); //connect(ekosInterface, SIGNAL(newModule(QString)), this, SLOT(registerNewModule(QString))); // ensure that the mount interface is present registerNewModule("Mount"); KStarsData::Instance()->userdb()->GetAllDSLRInfos(DSLRInfos); if (DSLRInfos.count() > 0) { qCDebug(KSTARS_EKOS_CAPTURE) << "DSLR Cameras Info:"; qCDebug(KSTARS_EKOS_CAPTURE) << DSLRInfos; } dirPath = QUrl::fromLocalFile(QDir::homePath()); //isAutoGuiding = false; rotatorSettings.reset(new RotatorSettings(this)); pi = new QProgressIndicator(this); progressLayout->addWidget(pi, 0, 4, 1, 1); seqFileCount = 0; //seqWatcher = new KDirWatch(); seqTimer = new QTimer(this); connect(seqTimer, &QTimer::timeout, this, &Ekos::Capture::captureImage); connect(startB, &QPushButton::clicked, this, &Ekos::Capture::toggleSequence); connect(pauseB, &QPushButton::clicked, this, &Ekos::Capture::pause); startB->setIcon(QIcon::fromTheme("media-playback-start")); startB->setAttribute(Qt::WA_LayoutUsesWidgetRect); pauseB->setIcon(QIcon::fromTheme("media-playback-pause")); pauseB->setAttribute(Qt::WA_LayoutUsesWidgetRect); filterManagerB->setIcon(QIcon::fromTheme("view-filter")); filterManagerB->setAttribute(Qt::WA_LayoutUsesWidgetRect); FilterDevicesCombo->addItem("--"); connect(binXIN, static_cast(&QSpinBox::valueChanged), binYIN, &QSpinBox::setValue); connect(CCDCaptureCombo, static_cast(&QComboBox::activated), this, &Ekos::Capture::setDefaultCCD); connect(CCDCaptureCombo, static_cast(&QComboBox::activated), this, &Ekos::Capture::checkCCD); connect(liveVideoB, &QPushButton::clicked, this, &Ekos::Capture::toggleVideo); guideDeviationTimer.setInterval(GD_TIMER_TIMEOUT); connect(&guideDeviationTimer, &QTimer::timeout, this, &Ekos::Capture::checkGuideDeviationTimeout); connect(clearConfigurationB, &QPushButton::clicked, this, &Ekos::Capture::clearCameraConfiguration); connect(FilterDevicesCombo, static_cast(&QComboBox::activated), this, &Ekos::Capture::setDefaultFilterWheel); connect(FilterDevicesCombo, static_cast(&QComboBox::activated), this, &Ekos::Capture::checkFilter); connect(temperatureCheck, &QCheckBox::toggled, [this](bool toggled) { if (currentCCD) { QVariantMap auxInfo = currentCCD->getDriverInfo()->getAuxInfo(); auxInfo[QString("%1_TC").arg(currentCCD->getDeviceName())] = toggled; currentCCD->getDriverInfo()->setAuxInfo(auxInfo); } }); connect(FilterPosCombo, static_cast(&QComboBox::currentIndexChanged), [ = ]() { updateHFRThreshold(); }); connect(previewB, &QPushButton::clicked, this, &Ekos::Capture::captureOne); connect(loopB, &QPushButton::clicked, this, &Ekos::Capture::startFraming); //connect( seqWatcher, SIGNAL(dirty(QString)), this, &Ekos::Capture::checkSeqFile(QString))); connect(addToQueueB, &QPushButton::clicked, this, &Ekos::Capture::addJob); connect(removeFromQueueB, &QPushButton::clicked, this, &Ekos::Capture::removeJobFromQueue); connect(queueUpB, &QPushButton::clicked, this, &Ekos::Capture::moveJobUp); connect(queueDownB, &QPushButton::clicked, this, &Ekos::Capture::moveJobDown); connect(selectFITSDirB, &QPushButton::clicked, this, &Ekos::Capture::saveFITSDirectory); connect(queueSaveB, &QPushButton::clicked, this, static_cast(&Ekos::Capture::saveSequenceQueue)); connect(queueSaveAsB, &QPushButton::clicked, this, &Ekos::Capture::saveSequenceQueueAs); connect(queueLoadB, &QPushButton::clicked, this, static_cast(&Ekos::Capture::loadSequenceQueue)); connect(resetB, &QPushButton::clicked, this, &Ekos::Capture::resetJobs); connect(queueTable->selectionModel(), &QItemSelectionModel::currentRowChanged, this, &Ekos::Capture::selectedJobChanged); connect(queueTable, &QAbstractItemView::doubleClicked, this, &Ekos::Capture::editJob); connect(queueTable, &QTableWidget::itemSelectionChanged, this, &Ekos::Capture::resetJobEdit); connect(setTemperatureB, &QPushButton::clicked, [&]() { if (currentCCD) currentCCD->setTemperature(temperatureIN->value()); }); connect(coolerOnB, &QPushButton::clicked, [&]() { if (currentCCD) currentCCD->setCoolerControl(true); }); connect(coolerOffB, &QPushButton::clicked, [&]() { if (currentCCD) currentCCD->setCoolerControl(false); }); connect(temperatureIN, &QDoubleSpinBox::editingFinished, setTemperatureB, static_cast(&QPushButton::setFocus)); connect(frameTypeCombo, static_cast(&QComboBox::activated), this, &Ekos::Capture::checkFrameType); connect(resetFrameB, &QPushButton::clicked, this, &Ekos::Capture::resetFrame); connect(calibrationB, &QPushButton::clicked, this, &Ekos::Capture::openCalibrationDialog); connect(rotatorB, &QPushButton::clicked, rotatorSettings.get(), &Ekos::Capture::show); addToQueueB->setIcon(QIcon::fromTheme("list-add")); addToQueueB->setAttribute(Qt::WA_LayoutUsesWidgetRect); removeFromQueueB->setIcon(QIcon::fromTheme("list-remove")); removeFromQueueB->setAttribute(Qt::WA_LayoutUsesWidgetRect); queueUpB->setIcon(QIcon::fromTheme("go-up")); queueUpB->setAttribute(Qt::WA_LayoutUsesWidgetRect); queueDownB->setIcon(QIcon::fromTheme("go-down")); queueDownB->setAttribute(Qt::WA_LayoutUsesWidgetRect); selectFITSDirB->setIcon( QIcon::fromTheme("document-open-folder")); selectFITSDirB->setAttribute(Qt::WA_LayoutUsesWidgetRect); queueLoadB->setIcon(QIcon::fromTheme("document-open")); queueLoadB->setAttribute(Qt::WA_LayoutUsesWidgetRect); queueSaveB->setIcon(QIcon::fromTheme("document-save")); queueSaveB->setAttribute(Qt::WA_LayoutUsesWidgetRect); queueSaveAsB->setIcon(QIcon::fromTheme("document-save-as")); queueSaveAsB->setAttribute(Qt::WA_LayoutUsesWidgetRect); resetB->setIcon(QIcon::fromTheme("system-reboot")); resetB->setAttribute(Qt::WA_LayoutUsesWidgetRect); resetFrameB->setIcon(QIcon::fromTheme("view-refresh")); resetFrameB->setAttribute(Qt::WA_LayoutUsesWidgetRect); calibrationB->setIcon(QIcon::fromTheme("run-build")); calibrationB->setAttribute(Qt::WA_LayoutUsesWidgetRect); rotatorB->setIcon(QIcon::fromTheme("kstars_solarsystem")); rotatorB->setAttribute(Qt::WA_LayoutUsesWidgetRect); addToQueueB->setToolTip(i18n("Add job to sequence queue")); removeFromQueueB->setToolTip(i18n("Remove job from sequence queue")); fitsDir->setText(Options::fitsDir()); for (auto &filter : FITSViewer::filterTypes) filterCombo->addItem(filter); //////////////////////////////////////////////////////////////////////// /// Settings //////////////////////////////////////////////////////////////////////// // #1 Guide Deviation Check guideDeviationCheck->setChecked(Options::enforceGuideDeviation()); connect(guideDeviationCheck, &QCheckBox::toggled, [ = ](bool checked) { Options::setEnforceGuideDeviation(checked); }); // #2 Guide Deviation Value guideDeviation->setValue(Options::guideDeviation()); connect(guideDeviation, &QDoubleSpinBox::editingFinished, [ = ]() { Options::setGuideDeviation(guideDeviation->value()); }); // 3. Autofocus HFR Check autofocusCheck->setChecked(Options::enforceAutofocus()); connect(autofocusCheck, &QCheckBox::toggled, [ = ](bool checked) { Options::setEnforceAutofocus(checked); if (checked == false) isInSequenceFocus = false; }); // 4. Autofocus HFR Deviation HFRPixels->setValue(Options::hFRDeviation()); connect(HFRPixels, &QDoubleSpinBox::editingFinished, [ = ]() { Options::setHFRDeviation(HFRPixels->value()); }); // 5. Refocus Every Check refocusEveryNCheck->setChecked(Options::enforceRefocusEveryN()); connect(refocusEveryNCheck, &QCheckBox::toggled, [ = ](bool checked) { Options::setEnforceRefocusEveryN(checked); }); // 6. Refocus Every Value refocusEveryN->setValue(Options::refocusEveryN()); connect(refocusEveryN, &QDoubleSpinBox::editingFinished, [ = ]() { Options::setRefocusEveryN(refocusEveryN->value()); }); // 7. File settings: filter name filterCheck->setChecked(Options::fileSettingsUseFilter()); connect(filterCheck, &QCheckBox::toggled, [ = ](bool checked) { Options::setFileSettingsUseFilter(checked); }); // 8. File settings: duration expDurationCheck->setChecked(Options::fileSettingsUseDuration()); connect(expDurationCheck, &QCheckBox::toggled, [ = ](bool checked) { Options::setFileSettingsUseDuration(checked); }); // 9. File settings: timestamp ISOCheck->setChecked(Options::fileSettingsUseTimestamp()); connect(ISOCheck, &QCheckBox::toggled, [ = ](bool checked) { Options::setFileSettingsUseTimestamp(checked); }); QCheckBox * const checkBoxes[] = { guideDeviationCheck, refocusEveryNCheck, guideDeviationCheck, }; for (const QCheckBox * control : checkBoxes) connect(control, &QCheckBox::toggled, this, &Ekos::Capture::setDirty); QDoubleSpinBox * const dspinBoxes[] { HFRPixels, guideDeviation, }; for (const QDoubleSpinBox * control : dspinBoxes) connect(control, static_cast(&QDoubleSpinBox::valueChanged), this, &Ekos::Capture::setDirty); connect(uploadModeCombo, static_cast(&QComboBox::activated), this, &Ekos::Capture::setDirty); connect(remoteDirIN, &QLineEdit::editingFinished, this, &Ekos::Capture::setDirty); m_ObserverName = Options::defaultObserver(); observerB->setIcon(QIcon::fromTheme("im-user")); observerB->setAttribute(Qt::WA_LayoutUsesWidgetRect); connect(observerB, &QPushButton::clicked, this, &Ekos::Capture::showObserverDialog); // Exposure Timeout captureTimeout.setSingleShot(true); connect(&captureTimeout, &QTimer::timeout, this, &Ekos::Capture::processCaptureTimeout); // Post capture script connect(&postCaptureScript, static_cast(&QProcess::finished), this, &Ekos::Capture::postScriptFinished); // Remote directory connect(uploadModeCombo, static_cast(&QComboBox::activated), this, [&](int index) { remoteDirIN->setEnabled(index != 0); }); customPropertiesDialog.reset(new CustomProperties()); connect(customValuesB, &QPushButton::clicked, [&]() { customPropertiesDialog.get()->show(); customPropertiesDialog.get()->raise(); }); connect(customPropertiesDialog.get(), &CustomProperties::valueChanged, [&]() { const double newGain = getGain(); if (GainSpin && newGain >= 0) GainSpin->setValue(newGain); }); flatFieldSource = static_cast(Options::calibrationFlatSourceIndex()); flatFieldDuration = static_cast(Options::calibrationFlatDurationIndex()); wallCoord.setAz(Options::calibrationWallAz()); wallCoord.setAlt(Options::calibrationWallAlt()); targetADU = Options::calibrationADUValue(); targetADUTolerance = Options::calibrationADUValueTolerance(); fitsDir->setText(Options::captureDirectory()); connect(fitsDir, &QLineEdit::textChanged, [&]() { Options::setCaptureDirectory(fitsDir->text()); }); if (Options::remoteCaptureDirectory().isEmpty() == false) { remoteDirIN->setText(Options::remoteCaptureDirectory()); } connect(remoteDirIN, &QLineEdit::editingFinished, [&]() { Options::setRemoteCaptureDirectory(remoteDirIN->text()); }); // Keep track of TARGET transfer format when changing CCDs (FITS or NATIVE). Actual format is not changed until capture connect( transferFormatCombo, static_cast(&QComboBox::activated), this, [&](int index) { if (currentCCD) currentCCD->setTargetTransferFormat(static_cast(index)); Options::setCaptureFormatIndex(index); }); // Load FIlter Offets //loadFilterOffsets(); //Note: This is to prevent a button from being called the default button //and then executing when the user hits the enter key such as when on a Text Box QList qButtons = findChildren(); for (auto &button : qButtons) button->setAutoDefault(false); //This Timer will update the Exposure time in the capture module to display the estimated download time left //It will also update the Exposure time left in the Summary Screen. //It fires every 100 ms while images are downloading. downloadProgressTimer.setInterval(100); connect(&downloadProgressTimer, &QTimer::timeout, this, &Ekos::Capture::setDownloadProgress); } Capture::~Capture() { qDeleteAll(jobs); } void Capture::setDefaultCCD(QString ccd) { Options::setDefaultCaptureCCD(ccd); } void Capture::setDefaultFilterWheel(QString filterWheel) { Options::setDefaultCaptureFilterWheel(filterWheel); } void Capture::addCCD(ISD::GDInterface * newCCD) { ISD::CCD * ccd = static_cast(newCCD); if (CCDs.contains(ccd)) return; CCDs.append(ccd); CCDCaptureCombo->addItem(ccd->getDeviceName()); if (Filters.count() > 0) syncFilterInfo(); checkCCD(); emit settingsUpdated(getSettings()); } void Capture::addGuideHead(ISD::GDInterface * newCCD) { QString guiderName = newCCD->getDeviceName() + QString(" Guider"); if (CCDCaptureCombo->findText(guiderName) == -1) { CCDCaptureCombo->addItem(guiderName); CCDs.append(static_cast(newCCD)); } } void Capture::addFilter(ISD::GDInterface * newFilter) { foreach (ISD::GDInterface * filter, Filters) { if (filter->getDeviceName() == newFilter->getDeviceName()) return; } FilterDevicesCombo->addItem(newFilter->getDeviceName()); Filters.append(static_cast(newFilter)); filterManagerB->setEnabled(true); int filterWheelIndex = 1; if (Options::defaultCaptureFilterWheel().isEmpty() == false) filterWheelIndex = FilterDevicesCombo->findText(Options::defaultCaptureFilterWheel()); if (filterWheelIndex < 1) filterWheelIndex = 1; checkFilter(filterWheelIndex); FilterDevicesCombo->setCurrentIndex(filterWheelIndex); emit settingsUpdated(getSettings()); } void Capture::pause() { if (m_State != CAPTURE_CAPTURING) { // Ensure that the pause function is only called during frame capturing // Handling it this way is by far easier than trying to enable/disable the pause button // Fixme: make pausing possible at all stages. This makes it necessary to separate the pausing states from CaptureState. appendLogText(i18n("Pausing only possible while frame capture is running.")); qCInfo(KSTARS_EKOS_CAPTURE) << "Pause button pressed while not capturing."; return; } pauseFunction = nullptr; m_State = CAPTURE_PAUSE_PLANNED; emit newStatus(Ekos::CAPTURE_PAUSE_PLANNED); appendLogText(i18n("Sequence shall be paused after current exposure is complete.")); pauseB->setEnabled(false); startB->setIcon(QIcon::fromTheme("media-playback-start")); startB->setToolTip(i18n("Resume Sequence")); } void Capture::toggleSequence() { if (m_State == CAPTURE_PAUSE_PLANNED || m_State == CAPTURE_PAUSED) { startB->setIcon( QIcon::fromTheme("media-playback-stop")); startB->setToolTip(i18n("Stop Sequence")); pauseB->setEnabled(true); m_State = CAPTURE_CAPTURING; emit newStatus(Ekos::CAPTURE_CAPTURING); appendLogText(i18n("Sequence resumed.")); // Call from where ever we have left of when we paused if (pauseFunction) (this->*pauseFunction)(); } else if (m_State == CAPTURE_IDLE || m_State == CAPTURE_ABORTED || m_State == CAPTURE_COMPLETE) { start(); } else { abort(); } } void Capture::registerNewModule(const QString &name) { qCDebug(KSTARS_EKOS_CAPTURE) << "Registering new Module (" << name << ")"; if (name == "Mount") { delete mountInterface; mountInterface = new QDBusInterface("org.kde.kstars", "/KStars/Ekos/Mount", "org.kde.kstars.Ekos.Mount", QDBusConnection::sessionBus(), this); } } void Capture::start() { if (darkSubCheck->isChecked()) { KSNotification::error(i18n("Auto dark subtract is not supported in batch mode.")); return; } // Reset progress option if there is no captured frame map set at the time of start - fixes the end-user setting the option just before starting ignoreJobProgress = !capturedFramesMap.count() && Options::alwaysResetSequenceWhenStarting(); if (queueTable->rowCount() == 0) { if (addJob() == false) return; } SequenceJob * first_job = nullptr; foreach (SequenceJob * job, jobs) { if (job->getStatus() == SequenceJob::JOB_IDLE || job->getStatus() == SequenceJob::JOB_ABORTED) { first_job = job; break; } } // If there are no idle nor aborted jobs, question is whether to reset and restart // Scheduler will start a non-empty new job each time and doesn't use this execution path if (first_job == nullptr) { // If we have at least one job that are in error, bail out, even if ignoring job progress foreach (SequenceJob * job, jobs) { if (job->getStatus() != SequenceJob::JOB_DONE) { appendLogText(i18n("No pending jobs found. Please add a job to the sequence queue.")); return; } } // If we only have completed jobs and we don't ignore job progress, ask the end-user what to do if (!ignoreJobProgress) if(KMessageBox::warningContinueCancel( nullptr, i18n("All jobs are complete. Do you want to reset the status of all jobs and restart capturing?"), i18n("Reset job status"), KStandardGuiItem::cont(), KStandardGuiItem::cancel(), "reset_job_complete_status_warning") != KMessageBox::Continue) return; // If the end-user accepted to reset, reset all jobs and restart foreach (SequenceJob * job, jobs) job->resetStatus(); first_job = jobs.first(); } // If we need to ignore job progress, systematically reset all jobs and restart // Scheduler will never ignore job progress and doesn't use this path else if (ignoreJobProgress) { appendLogText(i18n("Warning: option \"Always Reset Sequence When Starting\" is enabled and resets the sequence counts.")); foreach (SequenceJob * job, jobs) job->resetStatus(); } // Refocus timer should not be reset on deviation error if (m_DeviationDetected == false && m_State != CAPTURE_SUSPENDED) { // start timer to measure time until next forced refocus startRefocusEveryNTimer(); } // Only reset these counters if we are NOT restarting from deviation errors // So when starting a new job or fresh then we reset them. if (m_DeviationDetected == false) { ditherCounter = Options::ditherFrames(); inSequenceFocusCounter = Options::inSequenceCheckFrames(); } m_DeviationDetected = false; m_SpikeDetected = false; m_State = CAPTURE_PROGRESS; emit newStatus(Ekos::CAPTURE_PROGRESS); startB->setIcon(QIcon::fromTheme("media-playback-stop")); startB->setToolTip(i18n("Stop Sequence")); pauseB->setEnabled(true); setBusy(true); if (guideDeviationCheck->isChecked() && autoGuideReady == false) appendLogText(i18n("Warning: Guide deviation is selected but autoguide process was not started.")); if (autofocusCheck->isChecked() && m_AutoFocusReady == false) appendLogText(i18n("Warning: in-sequence focusing is selected but autofocus process was not started.")); prepareJob(first_job); } void Capture::stop(CaptureState targetState) { retries = 0; //seqTotalCount = 0; //seqCurrentCount = 0; captureTimeout.stop(); ADURaw.clear(); ExpRaw.clear(); if (activeJob) { if (activeJob->getStatus() == SequenceJob::JOB_BUSY) { QString stopText; switch (targetState) { case CAPTURE_IDLE: stopText = i18n("CCD capture stopped"); break; case CAPTURE_SUSPENDED: stopText = i18n("CCD capture suspended"); break; default: stopText = i18n("CCD capture aborted"); break; } KSNotification::event(QLatin1String("CaptureFailed"), stopText); appendLogText(stopText); activeJob->abort(); if (activeJob->isPreview() == false) { int index = jobs.indexOf(activeJob); QJsonObject oneSequence = m_SequenceArray[index].toObject(); oneSequence["Status"] = "Aborted"; m_SequenceArray.replace(index, oneSequence); emit sequenceChanged(m_SequenceArray); } } // In case of batch job if (activeJob->isPreview() == false) { activeJob->disconnect(this); activeJob->reset(); } // or preview job in calibration stage else if (calibrationStage == CAL_CALIBRATION) { activeJob->disconnect(this); activeJob->reset(); activeJob->setPreview(false); currentCCD->setUploadMode(rememberUploadMode); } // or regular preview job else { currentCCD->setUploadMode(rememberUploadMode); jobs.removeOne(activeJob); // Delete preview job delete (activeJob); activeJob = nullptr; emit newStatus(targetState); } } // Only emit a new status if there is an active job or if capturing is suspended. // The latter is necessary since suspending clears the active job, but the Capture // module keeps the control. if (activeJob != nullptr || m_State == CAPTURE_SUSPENDED) emit newStatus(targetState); calibrationStage = CAL_NONE; m_State = targetState; // Turn off any calibration light, IF they were turned on by Capture module if (currentDustCap && dustCapLightEnabled) { dustCapLightEnabled = false; currentDustCap->SetLightEnabled(false); } if (currentLightBox && lightBoxLightEnabled) { lightBoxLightEnabled = false; currentLightBox->SetLightEnabled(false); } if (meridianFlipStage == MF_NONE) secondsLabel->clear(); disconnect(currentCCD, &ISD::CCD::BLOBUpdated, this, &Ekos::Capture::newFITS); disconnect(currentCCD, &ISD::CCD::newExposureValue, this, &Ekos::Capture::setExposureProgress); disconnect(currentCCD, &ISD::CCD::previewFITSGenerated, this, &Ekos::Capture::setGeneratedPreviewFITS); disconnect(currentCCD, &ISD::CCD::ready, this, &Ekos::Capture::ready); currentCCD->setFITSDir(""); // In case of exposure looping, let's abort if (currentCCD->isLooping()) targetChip->abortExposure(); imgProgress->reset(); imgProgress->setEnabled(false); fullImgCountOUT->setText(QString()); currentImgCountOUT->setText(QString()); exposeOUT->setText(QString()); m_isLooping = false; setBusy(false); if (m_State == CAPTURE_ABORTED || m_State == CAPTURE_SUSPENDED) { startB->setIcon( QIcon::fromTheme("media-playback-start")); startB->setToolTip(i18n("Start Sequence")); pauseB->setEnabled(false); } //foreach (QAbstractButton *button, queueEditButtonGroup->buttons()) //button->setEnabled(true); seqTimer->stop(); activeJob = nullptr; // meridian flip may take place if requested setMeridianFlipStage(MF_READY); } void Capture::sendNewImage(const QString &filename, ISD::CCDChip * myChip) { if (activeJob && (myChip == nullptr || myChip == targetChip)) { activeJob->setProperty("filename", filename); emit newImage(activeJob); // We only emit this for client/both images since remote images already send this automatically if (currentCCD->getUploadMode() != ISD::CCD::UPLOAD_LOCAL && activeJob->isPreview() == false) { emit newSequenceImage(filename, m_GeneratedPreviewFITS); m_GeneratedPreviewFITS.clear(); } } } bool Capture::setCamera(const QString &device) { for (int i = 0; i < CCDCaptureCombo->count(); i++) if (device == CCDCaptureCombo->itemText(i)) { CCDCaptureCombo->setCurrentIndex(i); checkCCD(i); return true; } return false; } QString Capture::camera() { if (currentCCD) return currentCCD->getDeviceName(); return QString(); } void Capture::checkCCD(int ccdNum) { if (ccdNum == -1) { ccdNum = CCDCaptureCombo->currentIndex(); if (ccdNum == -1) return; } if (ccdNum < CCDs.count()) { // Check whether main camera or guide head only currentCCD = CCDs.at(ccdNum); if (CCDCaptureCombo->itemText(ccdNum).right(6) == QString("Guider")) { useGuideHead = true; targetChip = currentCCD->getChip(ISD::CCDChip::GUIDE_CCD); } else { currentCCD = CCDs.at(ccdNum); targetChip = currentCCD->getChip(ISD::CCDChip::PRIMARY_CCD); useGuideHead = false; } // Make sure we have a valid chip and valid base device. // Make sure we are not in capture process. if (!targetChip || !targetChip->getCCD() || !targetChip->getCCD()->getBaseDevice() || targetChip->isCapturing()) return; for (auto &ccd : CCDs) { disconnect(ccd, &ISD::CCD::numberUpdated, this, &Ekos::Capture::processCCDNumber); disconnect(ccd, &ISD::CCD::newTemperatureValue, this, &Ekos::Capture::updateCCDTemperature); disconnect(ccd, &ISD::CCD::coolerToggled, this, &Ekos::Capture::setCoolerToggled); disconnect(ccd, &ISD::CCD::newRemoteFile, this, &Ekos::Capture::setNewRemoteFile); disconnect(ccd, &ISD::CCD::videoStreamToggled, this, &Ekos::Capture::setVideoStreamEnabled); disconnect(ccd, &ISD::CCD::ready, this, &Ekos::Capture::ready); } if (currentCCD->hasCoolerControl()) { coolerOnB->setEnabled(true); coolerOffB->setEnabled(true); coolerOnB->setChecked(currentCCD->isCoolerOn()); coolerOffB->setChecked(!currentCCD->isCoolerOn()); } else { coolerOnB->setEnabled(false); coolerOnB->setChecked(false); coolerOffB->setEnabled(false); coolerOffB->setChecked(false); } if (currentCCD->hasCooler()) { temperatureCheck->setEnabled(true); temperatureIN->setEnabled(true); if (currentCCD->getBaseDevice()->getPropertyPermission("CCD_TEMPERATURE") != IP_RO) { double min, max, step; setTemperatureB->setEnabled(true); temperatureIN->setReadOnly(false); temperatureCheck->setEnabled(true); currentCCD->getMinMaxStep("CCD_TEMPERATURE", "CCD_TEMPERATURE_VALUE", &min, &max, &step); temperatureIN->setMinimum(min); temperatureIN->setMaximum(max); temperatureIN->setSingleStep(1); bool isChecked = currentCCD->getDriverInfo()->getAuxInfo().value(QString("%1_TC").arg(currentCCD->getDeviceName()), false).toBool(); temperatureCheck->setChecked(isChecked); } else { setTemperatureB->setEnabled(false); temperatureIN->setReadOnly(true); temperatureCheck->setEnabled(false); temperatureCheck->setChecked(false); } double temperature = 0; if (currentCCD->getTemperature(&temperature)) { temperatureOUT->setText(QString("%L1").arg(temperature, 0, 'f', 2)); if (temperatureIN->cleanText().isEmpty()) temperatureIN->setValue(temperature); } } else { temperatureCheck->setEnabled(false); temperatureIN->setEnabled(false); temperatureIN->clear(); temperatureOUT->clear(); setTemperatureB->setEnabled(false); } updateFrameProperties(); QStringList frameTypes = targetChip->getFrameTypes(); frameTypeCombo->clear(); if (frameTypes.isEmpty()) frameTypeCombo->setEnabled(false); else { frameTypeCombo->setEnabled(true); frameTypeCombo->addItems(frameTypes); frameTypeCombo->setCurrentIndex(targetChip->getFrameType()); } QStringList isoList = targetChip->getISOList(); //ISOCombo->clear(); transferFormatCombo->blockSignals(true); transferFormatCombo->clear(); delete (ISOCombo); delete (GainSpin); - ISOLabel->hide(); + //ISOLabel->hide(); if (isoList.isEmpty()) { // Only one transfer format transferFormatCombo->addItem(i18n("FITS")); if (currentCCD->hasGain()) { ISOLabel->setText(QString("%1:").arg(i18nc("Camera Gain", "Gain"))); - ISOLabel->show(); + //ISOLabel->show(); GainSpin = new QDoubleSpinBox(CCDFWGroup); double min, max, step, value, targetCustomGain; currentCCD->getGainMinMaxStep(&min, &max, &step); // Allow the possibility of no gain value at all. GainSpinSpecialValue = min - step; GainSpin->setRange(GainSpinSpecialValue, max); GainSpin->setSpecialValueText(i18n("--")); GainSpin->setSingleStep(step); currentCCD->getGain(&value); targetCustomGain = getGain(); // Set the custom gain if we have one // otherwise it will not have an effect. if (targetCustomGain > 0) GainSpin->setValue(targetCustomGain); else GainSpin->setValue(GainSpinSpecialValue); GainSpin->setReadOnly(currentCCD->getGainPermission() == IP_RO); connect(GainSpin, &QDoubleSpinBox::editingFinished, [this]() { if (GainSpin->value() != GainSpinSpecialValue) setGain(GainSpin->value()); }); gridLayout->addWidget(GainSpin, 4, 5, 1, 2); } } else { ISOLabel->setText(QString("%1:").arg(i18nc("Camera ISO", "ISO"))); - ISOLabel->show(); + //ISOLabel->show(); ISOCombo = new QComboBox(CCDFWGroup); ISOCombo->addItems(isoList); ISOCombo->setCurrentIndex(targetChip->getISOIndex()); gridLayout->addWidget(ISOCombo, 4, 5, 1, 2); // DSLRs have two transfer formats transferFormatCombo->addItem(i18n("FITS")); transferFormatCombo->addItem(i18n("Native")); //transferFormatCombo->setCurrentIndex(currentCCD->getTargetTransferFormat()); // 2018-05-07 JM: Set value to the value in options transferFormatCombo->setCurrentIndex(Options::captureFormatIndex()); uint16_t w, h; uint8_t bbp {8}; double pixelX = 0, pixelY = 0; bool rc = targetChip->getImageInfo(w, h, pixelX, pixelY, bbp); bool isModelInDB = isModelinDSLRInfo(QString(currentCCD->getDeviceName())); // If rc == true, then the property has been defined by the driver already // Only then we check if the pixels are zero if (rc == true && (pixelX == 0 || pixelY == 0 || isModelInDB == false)) { // If model is already in database, no need to show dialog // The zeros above are the initial packets so we can safely ignore them if (isModelInDB == false) { createDSLRDialog(); } else { QString model = QString(currentCCD->getDeviceName()); auto pos = std::find_if(DSLRInfos.begin(), DSLRInfos.end(), [model](QMap &oneDSLRInfo) { return (oneDSLRInfo["Model"] == model); }); // Sync Pixel Size if (pos != DSLRInfos.end()) { auto camera = *pos; targetChip->setImageInfo(camera["Width"].toDouble(), camera["Height"].toDouble(), camera["PixelW"].toDouble(), camera["PixelH"].toDouble(), 8); } } } } transferFormatCombo->blockSignals(false); customPropertiesDialog->setCCD(currentCCD); liveVideoB->setEnabled(currentCCD->hasVideoStream()); if (currentCCD->hasVideoStream()) setVideoStreamEnabled(currentCCD->isStreamingEnabled()); else liveVideoB->setIcon(QIcon::fromTheme("camera-off")); connect(currentCCD, &ISD::CCD::numberUpdated, this, &Ekos::Capture::processCCDNumber, Qt::UniqueConnection); connect(currentCCD, &ISD::CCD::newTemperatureValue, this, &Ekos::Capture::updateCCDTemperature, Qt::UniqueConnection); connect(currentCCD, &ISD::CCD::coolerToggled, this, &Ekos::Capture::setCoolerToggled, Qt::UniqueConnection); connect(currentCCD, &ISD::CCD::newRemoteFile, this, &Ekos::Capture::setNewRemoteFile); connect(currentCCD, &ISD::CCD::videoStreamToggled, this, &Ekos::Capture::setVideoStreamEnabled); connect(currentCCD, &ISD::CCD::ready, this, &Ekos::Capture::ready); } } void Capture::setGuideChip(ISD::CCDChip * chip) { guideChip = chip; // We should suspend guide in two scenarios: // 1. If guide chip is within the primary CCD, then we cannot download any data from guide chip while primary CCD is downloading. // 2. If we have two CCDs running from ONE driver (Multiple-Devices-Per-Driver mpdp is true). Same issue as above, only one download // at a time. // After primary CCD download is complete, we resume guiding. suspendGuideOnDownload = (currentCCD->getChip(ISD::CCDChip::GUIDE_CCD) == guideChip) || (guideChip->getCCD() == currentCCD && currentCCD->getDriverInfo()->getAuxInfo().value("mdpd", false).toBool()); } void Capture::resetFrameToZero() { frameXIN->setMinimum(0); frameXIN->setMaximum(0); frameXIN->setValue(0); frameYIN->setMinimum(0); frameYIN->setMaximum(0); frameYIN->setValue(0); frameWIN->setMinimum(0); frameWIN->setMaximum(0); frameWIN->setValue(0); frameHIN->setMinimum(0); frameHIN->setMaximum(0); frameHIN->setValue(0); } void Capture::updateFrameProperties(int reset) { int binx = 1, biny = 1; double min, max, step; int xstep = 0, ystep = 0; QString frameProp = useGuideHead ? QString("GUIDER_FRAME") : QString("CCD_FRAME"); QString exposureProp = useGuideHead ? QString("GUIDER_EXPOSURE") : QString("CCD_EXPOSURE"); QString exposureElem = useGuideHead ? QString("GUIDER_EXPOSURE_VALUE") : QString("CCD_EXPOSURE_VALUE"); targetChip = useGuideHead ? currentCCD->getChip(ISD::CCDChip::GUIDE_CCD) : currentCCD->getChip(ISD::CCDChip::PRIMARY_CCD); frameWIN->setEnabled(targetChip->canSubframe()); frameHIN->setEnabled(targetChip->canSubframe()); frameXIN->setEnabled(targetChip->canSubframe()); frameYIN->setEnabled(targetChip->canSubframe()); binXIN->setEnabled(targetChip->canBin()); binYIN->setEnabled(targetChip->canBin()); QList exposureValues; exposureValues << 0.01 << 0.02 << 0.05 << 0.1 << 0.2 << 0.25 << 0.5 << 1 << 1.5 << 2 << 2.5 << 3 << 5 << 6 << 7 << 8 << 9 << 10 << 20 << 30 << 40 << 50 << 60 << 120 << 180 << 300 << 600 << 900 << 1200 << 1800; if (currentCCD->getMinMaxStep(exposureProp, exposureElem, &min, &max, &step)) { if (min < 0.001) exposureIN->setDecimals(6); else exposureIN->setDecimals(3); for(int i = 0; i < exposureValues.count(); i++) { double value = exposureValues.at(i); if(value < min || value > max) { exposureValues.removeAt(i); i--; //So we don't skip one } } exposureValues.prepend(min); exposureValues.append(max); } exposureIN->setRecommendedValues(exposureValues); if (currentCCD->getMinMaxStep(frameProp, "WIDTH", &min, &max, &step)) { if (min >= max) { resetFrameToZero(); return; } if (step == 0) xstep = static_cast(max * 0.05); else xstep = step; if (min >= 0 && max > 0) { frameWIN->setMinimum(min); frameWIN->setMaximum(max); frameWIN->setSingleStep(xstep); } } else return; if (currentCCD->getMinMaxStep(frameProp, "HEIGHT", &min, &max, &step)) { if (min >= max) { resetFrameToZero(); return; } if (step == 0) ystep = static_cast(max * 0.05); else ystep = step; if (min >= 0 && max > 0) { frameHIN->setMinimum(min); frameHIN->setMaximum(max); frameHIN->setSingleStep(ystep); } } else return; if (currentCCD->getMinMaxStep(frameProp, "X", &min, &max, &step)) { if (min >= max) { resetFrameToZero(); return; } if (step == 0) step = xstep; if (min >= 0 && max > 0) { frameXIN->setMinimum(min); frameXIN->setMaximum(max); frameXIN->setSingleStep(step); } } else return; if (currentCCD->getMinMaxStep(frameProp, "Y", &min, &max, &step)) { if (min >= max) { resetFrameToZero(); return; } if (step == 0) step = ystep; if (min >= 0 && max > 0) { frameYIN->setMinimum(min); frameYIN->setMaximum(max); frameYIN->setSingleStep(step); } } else return; // cull to camera limits, if there are any if (useGuideHead == false) cullToDSLRLimits(); if (reset == 1 || frameSettings.contains(targetChip) == false) { QVariantMap settings; settings["x"] = 0; settings["y"] = 0; settings["w"] = frameWIN->maximum(); settings["h"] = frameHIN->maximum(); settings["binx"] = 1; settings["biny"] = 1; frameSettings[targetChip] = settings; } else if (reset == 2 && frameSettings.contains(targetChip)) { QVariantMap settings = frameSettings[targetChip]; int x, y, w, h; x = settings["x"].toInt(); y = settings["y"].toInt(); w = settings["w"].toInt(); h = settings["h"].toInt(); // Bound them x = qBound(frameXIN->minimum(), x, frameXIN->maximum() - 1); y = qBound(frameYIN->minimum(), y, frameYIN->maximum() - 1); w = qBound(frameWIN->minimum(), w, frameWIN->maximum()); h = qBound(frameHIN->minimum(), h, frameHIN->maximum()); settings["x"] = x; settings["y"] = y; settings["w"] = w; settings["h"] = h; frameSettings[targetChip] = settings; } if (frameSettings.contains(targetChip)) { QVariantMap settings = frameSettings[targetChip]; int x = settings["x"].toInt(); int y = settings["y"].toInt(); int w = settings["w"].toInt(); int h = settings["h"].toInt(); if (targetChip->canBin()) { targetChip->getMaxBin(&binx, &biny); binXIN->setMaximum(binx); binYIN->setMaximum(biny); binXIN->setValue(settings["binx"].toInt()); binYIN->setValue(settings["biny"].toInt()); } else { binXIN->setValue(1); binYIN->setValue(1); } if (x >= 0) frameXIN->setValue(x); if (y >= 0) frameYIN->setValue(y); if (w > 0) frameWIN->setValue(w); if (h > 0) frameHIN->setValue(h); } } void Capture::processCCDNumber(INumberVectorProperty * nvp) { if (currentCCD == nullptr) return; if ((!strcmp(nvp->name, "CCD_FRAME") && useGuideHead == false) || (!strcmp(nvp->name, "GUIDER_FRAME") && useGuideHead)) updateFrameProperties(); else if ((!strcmp(nvp->name, "CCD_INFO") && useGuideHead == false) || (!strcmp(nvp->name, "GUIDER_INFO") && useGuideHead)) updateFrameProperties(2); } void Capture::resetFrame() { targetChip = useGuideHead ? currentCCD->getChip(ISD::CCDChip::GUIDE_CCD) : currentCCD->getChip(ISD::CCDChip::PRIMARY_CCD); targetChip->resetFrame(); updateFrameProperties(1); } void Capture::syncFrameType(ISD::GDInterface * ccd) { if (ccd->getDeviceName() != CCDCaptureCombo->currentText().toLatin1()) return; ISD::CCDChip * tChip = (static_cast(ccd))->getChip(ISD::CCDChip::PRIMARY_CCD); QStringList frameTypes = tChip->getFrameTypes(); frameTypeCombo->clear(); if (frameTypes.isEmpty()) frameTypeCombo->setEnabled(false); else { frameTypeCombo->setEnabled(true); frameTypeCombo->addItems(frameTypes); frameTypeCombo->setCurrentIndex(tChip->getFrameType()); } } bool Capture::setFilterWheel(const QString &device) { bool deviceFound = false; for (int i = 0; i < FilterDevicesCombo->count(); i++) if (device == FilterDevicesCombo->itemText(i)) { // Check Combo if it was set to something else. if (FilterDevicesCombo->currentIndex() != i) { FilterDevicesCombo->blockSignals(true); FilterDevicesCombo->setCurrentIndex(i); FilterDevicesCombo->blockSignals(false); } checkFilter(i); deviceFound = true; break; } if (deviceFound == false) return false; return true; } QString Capture::filterWheel() { if (FilterDevicesCombo->currentIndex() >= 1) return FilterDevicesCombo->currentText(); return QString(); } bool Capture::setFilter(const QString &filter) { if (FilterDevicesCombo->currentIndex() >= 1) { FilterPosCombo->setCurrentText(filter); return true; } return false; } QString Capture::filter() { return FilterPosCombo->currentText(); } void Capture::checkFilter(int filterNum) { if (filterNum == -1) { filterNum = FilterDevicesCombo->currentIndex(); if (filterNum == -1) return; } // "--" is no filter if (filterNum == 0) { currentFilter = nullptr; m_CurrentFilterPosition = -1; FilterPosCombo->clear(); syncFilterInfo(); return; } if (filterNum <= Filters.count()) currentFilter = Filters.at(filterNum - 1); filterManager->setCurrentFilterWheel(currentFilter); syncFilterInfo(); FilterPosCombo->clear(); FilterPosCombo->addItems(filterManager->getFilterLabels()); m_CurrentFilterPosition = filterManager->getFilterPosition(); FilterPosCombo->setCurrentIndex(m_CurrentFilterPosition - 1); /*if (activeJob && (activeJob->getStatus() == SequenceJob::JOB_ABORTED || activeJob->getStatus() == SequenceJob::JOB_IDLE)) activeJob->setCurrentFilter(currentFilterPosition);*/ } void Capture::syncFilterInfo() { if (currentCCD) { ITextVectorProperty * activeDevices = currentCCD->getBaseDevice()->getText("ACTIVE_DEVICES"); if (activeDevices) { IText * activeFilter = IUFindText(activeDevices, "ACTIVE_FILTER"); if (activeFilter) { if (currentFilter != nullptr && (activeFilter->text != currentFilter->getDeviceName())) { m_FilterOverride = true; activeFilter->aux0 = &m_FilterOverride; IUSaveText(activeFilter, currentFilter->getDeviceName().toLatin1().constData()); currentCCD->getDriverInfo()->getClientManager()->sendNewText(activeDevices); } // Reset filter name in CCD driver else if (currentFilter == nullptr && strlen(activeFilter->text) > 0) { m_FilterOverride = true; activeFilter->aux0 = &m_FilterOverride; IUSaveText(activeFilter, ""); currentCCD->getDriverInfo()->getClientManager()->sendNewText(activeDevices); } } } } } bool Capture::startNextExposure() { // check if pausing has been requested if (checkPausing() == true) { pauseFunction = &Capture::startNextExposure; return false; } if (checkMeridianFlip()) // execute flip before next capture return false; if (startFocusIfRequired()) // re-focus before next capture return false; if (seqDelay > 0) { secondsLabel->setText(i18n("Waiting...")); m_State = CAPTURE_WAITING; emit newStatus(Ekos::CAPTURE_WAITING); } seqTimer->start(seqDelay); return true; } void Capture::newFITS(IBLOB * bp) { ISD::CCDChip * tChip = nullptr; // If there is no active job, ignore if (activeJob == nullptr) { qCWarning(KSTARS_EKOS_CAPTURE) << "Ignoring received FITS" << bp->name << "as active job is null."; return; } if (meridianFlipStage >= MF_ALIGNING) { qCWarning(KSTARS_EKOS_CAPTURE) << "Ignoring Received FITS" << bp->name << "as meridian flip stage is" << meridianFlipStage; return; } if (currentCCD->getUploadMode() != ISD::CCD::UPLOAD_LOCAL) { if (bp == nullptr) { appendLogText(i18n("Failed to save file to %1", activeJob->getSignature())); abort(); return; } if (m_State == CAPTURE_IDLE || m_State == CAPTURE_ABORTED) { qCWarning(KSTARS_EKOS_CAPTURE) << "Ignoring Received FITS" << bp->name << "as current capture state is not active" << m_State; return; } if (!strcmp(bp->name, "CCD2")) tChip = currentCCD->getChip(ISD::CCDChip::GUIDE_CCD); else tChip = currentCCD->getChip(ISD::CCDChip::PRIMARY_CCD); if (tChip != targetChip) { if (guideState == GUIDE_IDLE) qCWarning(KSTARS_EKOS_CAPTURE) << "Ignoring Received FITS" << bp->name << "as it does not correspond to the target chip" << targetChip->getType(); return; } if (targetChip->getCaptureMode() == FITS_FOCUS || targetChip->getCaptureMode() == FITS_GUIDE) { qCWarning(KSTARS_EKOS_CAPTURE) << "Ignoring Received FITS" << bp->name << "as it has the wrong capture mode" << targetChip->getCaptureMode(); return; } // If the FITS is not for our device, simply ignore //if (QString(bp->bvp->device) != currentCCD->getDeviceName() || (startB->isEnabled() && previewB->isEnabled())) if (QString(bp->bvp->device) != currentCCD->getDeviceName()) { qCWarning(KSTARS_EKOS_CAPTURE) << "Ignoring Received FITS" << bp->name << "as the blob device name" << bp->bvp->device << "does not equal active camera" << currentCCD->getDeviceName(); return; } // If this is a preview job, make sure to enable preview button after // we receive the FITS if (activeJob->isPreview() && previewB->isEnabled() == false) previewB->setEnabled(true); // m_isLooping client-side looping (next capture starts after image is downloaded to client) // currentCCD->isLooping driver side looping (without any delays, next capture starts after driver reads data) if (m_isLooping == false && currentCCD->isLooping() == false) { disconnect(currentCCD, &ISD::CCD::BLOBUpdated, this, &Ekos::Capture::newFITS); if (useGuideHead == false && darkSubCheck->isChecked() && activeJob->isPreview()) { FITSView * currentImage = targetChip->getImageView(FITS_NORMAL); FITSData * darkData = DarkLibrary::Instance()->getDarkFrame(targetChip, activeJob->getExposure()); uint16_t offsetX = activeJob->getSubX() / activeJob->getXBin(); uint16_t offsetY = activeJob->getSubY() / activeJob->getYBin(); connect(DarkLibrary::Instance(), &DarkLibrary::darkFrameCompleted, this, [&](bool completed) { if (currentCCD->isLooping() == false) DarkLibrary::Instance()->disconnect(this); if (completed) setCaptureComplete(); else abort(); }); connect(DarkLibrary::Instance(), &DarkLibrary::newLog, this, &Ekos::Capture::appendLogText); if (darkData) DarkLibrary::Instance()->subtract(darkData, currentImage, activeJob->getCaptureFilter(), offsetX, offsetY); else DarkLibrary::Instance()->captureAndSubtract(targetChip, currentImage, activeJob->getExposure(), offsetX, offsetY); return; } } } blobChip = bp ? static_cast(bp->aux0) : nullptr; blobFilename = bp ? static_cast(bp->aux2) : QString(); setCaptureComplete(); } bool Capture::setCaptureComplete() { captureTimeout.stop(); m_CaptureTimeoutCounter = 0; downloadProgressTimer.stop(); // In case we're framing, let's start if (m_isLooping) { sendNewImage(blobFilename, blobChip); secondsLabel->setText(i18n("Framing...")); activeJob->capture(darkSubCheck->isChecked() ? true : false); return true; } if (currentCCD->isLooping() == false) { disconnect(currentCCD, &ISD::CCD::newExposureValue, this, &Ekos::Capture::setExposureProgress); DarkLibrary::Instance()->disconnect(this); } // Do not calculate download time for images stored on server. // Only calculate for longer exposures. if (currentCCD->getUploadMode() != ISD::CCD::UPLOAD_LOCAL) { //This determines the time since the image started downloading //Then it gets the estimated time left and displays it in the log. double currentDownloadTime = downloadTimer.elapsed() / 1000.0; downloadTimes << currentDownloadTime; QString dLTimeString = QString::number(currentDownloadTime, 'd', 2); QString estimatedTimeString = QString::number(getEstimatedDownloadTime(), 'd', 2); appendLogText(i18n("Download Time: %1 s, New Download Time Estimate: %2 s.", dLTimeString, estimatedTimeString)); } secondsLabel->setText(i18n("Complete.")); // Do not display notifications for very short captures if (activeJob->getExposure() >= 1) KSNotification::event(QLatin1String("EkosCaptureImageReceived"), i18n("Captured image received"), KSNotification::EVENT_INFO); // If it was initially set as pure preview job and NOT as preview for calibration if (activeJob->isPreview() && calibrationStage != CAL_CALIBRATION) { sendNewImage(blobFilename, blobChip); jobs.removeOne(activeJob); // Reset upload mode if it was changed by preview currentCCD->setUploadMode(rememberUploadMode); delete (activeJob); // Reset active job pointer activeJob = nullptr; abort(); if (guideState == GUIDE_SUSPENDED && suspendGuideOnDownload) emit resumeGuiding(); m_State = CAPTURE_IDLE; emit newStatus(Ekos::CAPTURE_IDLE); return true; } // check if pausing has been requested if (checkPausing() == true) { pauseFunction = &Capture::setCaptureComplete; return false; } if (! activeJob->isPreview()) { /* Increase the sequence's current capture count */ activeJob->setCompleted(activeJob->getCompleted() + 1); /* Decrease the counter for in-sequence focusing */ inSequenceFocusCounter--; } // Do not send new image if the image was stored on the server. if (currentCCD->getUploadMode() != ISD::CCD::UPLOAD_LOCAL) sendNewImage(blobFilename, blobChip); /* If we were assigned a captured frame map, also increase the relevant counter for prepareJob */ SchedulerJob::CapturedFramesMap::iterator frame_item = capturedFramesMap.find(activeJob->getSignature()); if (capturedFramesMap.end() != frame_item) frame_item.value()++; if (activeJob->getFrameType() != FRAME_LIGHT) { if (processPostCaptureCalibrationStage() == false) return true; if (calibrationStage == CAL_CALIBRATION_COMPLETE) calibrationStage = CAL_CAPTURING; } /* The image progress has now one more capture */ imgProgress->setValue(activeJob->getCompleted()); appendLogText(i18n("Received image %1 out of %2.", activeJob->getCompleted(), activeJob->getCount())); m_State = CAPTURE_IMAGE_RECEIVED; emit newStatus(Ekos::CAPTURE_IMAGE_RECEIVED); currentImgCountOUT->setText(QString("%L1").arg(activeJob->getCompleted())); // Check if we need to execute post capture script first if (activeJob->getPostCaptureScript().isEmpty() == false) { postCaptureScript.start(activeJob->getPostCaptureScript()); appendLogText(i18n("Executing post capture script %1", activeJob->getPostCaptureScript())); return true; } // if we're done if (activeJob->getCount() <= activeJob->getCompleted()) { processJobCompletion(); return true; } return resumeSequence(); } void Capture::processJobCompletion() { activeJob->done(); if (activeJob->isPreview() == false) { int index = jobs.indexOf(activeJob); QJsonObject oneSequence = m_SequenceArray[index].toObject(); oneSequence["Status"] = "Complete"; m_SequenceArray.replace(index, oneSequence); emit sequenceChanged(m_SequenceArray); } stop(); // Check if there are more pending jobs and execute them if (resumeSequence()) return; // Otherwise, we're done. We park if required and resume guiding if no parking is done and autoguiding was engaged before. else { //KNotification::event(QLatin1String("CaptureSuccessful"), i18n("CCD capture sequence completed")); KSNotification::event(QLatin1String("CaptureSuccessful"), i18n("CCD capture sequence completed"), KSNotification::EVENT_INFO); abort(); m_State = CAPTURE_COMPLETE; emit newStatus(Ekos::CAPTURE_COMPLETE); //Resume guiding if it was suspended before //if (isAutoGuiding && currentCCD->getChip(ISD::CCDChip::GUIDE_CCD) == guideChip) if (guideState == GUIDE_SUSPENDED && suspendGuideOnDownload) emit resumeGuiding(); } } bool Capture::resumeSequence() { if (m_State == CAPTURE_PAUSED) { pauseFunction = &Capture::resumeSequence; appendLogText(i18n("Sequence paused.")); secondsLabel->setText(i18n("Paused...")); return false; } // If no job is active, we have to find if there are more pending jobs in the queue if (!activeJob) { SequenceJob * next_job = nullptr; foreach (SequenceJob * job, jobs) { if (job->getStatus() == SequenceJob::JOB_IDLE || job->getStatus() == SequenceJob::JOB_ABORTED) { next_job = job; break; } } if (next_job) { prepareJob(next_job); //Resume guiding if it was suspended before //if (isAutoGuiding && currentCCD->getChip(ISD::CCDChip::GUIDE_CCD) == guideChip) if (guideState == GUIDE_SUSPENDED && suspendGuideOnDownload) { qCDebug(KSTARS_EKOS_CAPTURE) << "Resuming guiding..."; emit resumeGuiding(); } return true; } else { qCDebug(KSTARS_EKOS_CAPTURE) << "All capture jobs complete."; return false; } } // Otherwise, let's prepare for next exposure after making sure in-sequence focus and dithering are complete if applicable. else { isInSequenceFocus = (m_AutoFocusReady && autofocusCheck->isChecked()/* && HFRPixels->value() > 0*/); // if (isInSequenceFocus) // requiredAutoFocusStarted = false; // Reset HFR pixels to file value after meridian flip if (isInSequenceFocus && meridianFlipStage != MF_NONE && meridianFlipStage != MF_READY) { qCDebug(KSTARS_EKOS_CAPTURE) << "Resetting HFR value to file value of" << fileHFR << "pixels after meridian flip."; //firstAutoFocus = true; HFRPixels->setValue(fileHFR); } // If we suspended guiding due to primary chip download, resume guide chip guiding now if (guideState == GUIDE_SUSPENDED && suspendGuideOnDownload) { qCInfo(KSTARS_EKOS_CAPTURE) << "Resuming guiding..."; emit resumeGuiding(); } // Dither either when guiding or IF Non-Guide either option is enabled if ( (Options::ditherEnabled() || Options::ditherNoGuiding()) // 2017-09-20 Jasem: No need to dither after post meridian flip guiding && meridianFlipStage != MF_GUIDING // If CCD is looping, we cannot dither UNLESS a different camera and NOT a guide chip is doing the guiding for us. && (currentCCD->isLooping() == false || guideChip == nullptr) // We must be either in guide mode or if non-guide dither (via pulsing) is enabled && (guideState == GUIDE_GUIDING || Options::ditherNoGuiding()) // Must be only done for light frames && activeJob->getFrameType() == FRAME_LIGHT // Check dither counter && --ditherCounter == 0) { ditherCounter = Options::ditherFrames(); secondsLabel->setText(i18n("Dithering...")); qCInfo(KSTARS_EKOS_CAPTURE) << "Dithering..."; if (currentCCD->isLooping()) targetChip->abortExposure(); m_State = CAPTURE_DITHERING; emit newStatus(Ekos::CAPTURE_DITHERING); } #if 0 else if (isRefocus && activeJob->getFrameType() == FRAME_LIGHT) { appendLogText(i18n("Scheduled refocus starting after %1 seconds...", getRefocusEveryNTimerElapsedSec())); secondsLabel->setText(i18n("Focusing...")); if (currentCCD->isLooping()) targetChip->abortExposure(); // If we are over 30 mins since last autofocus, we'll reset frame. if (refocusEveryN->value() >= 30) emit resetFocus(); // force refocus qCDebug(KSTARS_EKOS_CAPTURE) << "Capture is triggering autofocus on line 1812."; emit checkFocus(0.1); m_State = CAPTURE_FOCUSING; emit newStatus(Ekos::CAPTURE_FOCUSING); } else if (isInSequenceFocus && activeJob->getFrameType() == FRAME_LIGHT && --inSequenceFocusCounter == 0) { inSequenceFocusCounter = Options::inSequenceCheckFrames(); // Post meridian flip we need to reset filter _before_ running in-sequence focusing // as it could have changed for whatever reason (e.g. alignment used a different filter). // Then when focus process begins with the _target_ filter in place, it should take all the necessary actions to make it // work for the next set of captures. This is direct reset to the filter device, not via Filter Manager. if (meridianFlipStage != MF_NONE && currentFilter) { int targetFilterPosition = activeJob->getTargetFilter(); int currentFilterPosition = filterManager->getFilterPosition(); if (targetFilterPosition > 0 && targetFilterPosition != currentFilterPosition) currentFilter->runCommand(INDI_SET_FILTER, &targetFilterPosition); } secondsLabel->setText(i18n("Focusing...")); if (currentCCD->isLooping()) targetChip->abortExposure(); if (HFRPixels->value() == 0) { qCDebug(KSTARS_EKOS_CAPTURE) << "Capture is triggering autofocus on line 1841."; emit checkFocus(0.1); } else { qCDebug(KSTARS_EKOS_CAPTURE) << "Capture is triggering autofocus on line 1846."; emit checkFocus(HFRPixels->value()); } qCDebug(KSTARS_EKOS_CAPTURE) << "In-sequence focusing started..."; m_State = CAPTURE_FOCUSING; emit newStatus(Ekos::CAPTURE_FOCUSING); } #endif // Check if we need to do autofocus, if not let's check if we need looping or start next exposure else if (startFocusIfRequired() == false) { // If looping, we just increment the file system image count if (currentCCD->isLooping()) { if (currentCCD->getUploadMode() != ISD::CCD::UPLOAD_LOCAL) { checkSeqBoundary(activeJob->getSignature()); currentCCD->setNextSequenceID(nextSequenceID); } } else startNextExposure(); } } return true; } bool Capture::startFocusIfRequired() { if (activeJob == nullptr || activeJob->getFrameType() != FRAME_LIGHT) return false; // check if time for forced refocus if (refocusEveryNCheck->isChecked()) { qCDebug(KSTARS_EKOS_CAPTURE) << "Focus elapsed time (secs): " << getRefocusEveryNTimerElapsedSec() << ". Requested Interval (secs): " << refocusEveryN->value() * 60; isRefocus = getRefocusEveryNTimerElapsedSec() >= refocusEveryN->value() * 60; } else isRefocus = false; if (isRefocus) { appendLogText(i18n("Scheduled refocus starting after %1 seconds...", getRefocusEveryNTimerElapsedSec())); secondsLabel->setText(i18n("Focusing...")); if (currentCCD->isLooping()) targetChip->abortExposure(); // If we are over 30 mins since last autofocus, we'll reset frame. if (refocusEveryN->value() >= 30) emit resetFocus(); // force refocus qCDebug(KSTARS_EKOS_CAPTURE) << "Capture is triggering autofocus on line 1904."; emit checkFocus(0.1); m_State = CAPTURE_FOCUSING; emit newStatus(Ekos::CAPTURE_FOCUSING); return true; } else if (isInSequenceFocus && inSequenceFocusCounter == 0) { inSequenceFocusCounter = Options::inSequenceCheckFrames(); // Post meridian flip we need to reset filter _before_ running in-sequence focusing // as it could have changed for whatever reason (e.g. alignment used a different filter). // Then when focus process begins with the _target_ filter in place, it should take all the necessary actions to make it // work for the next set of captures. This is direct reset to the filter device, not via Filter Manager. if (meridianFlipStage != MF_NONE && currentFilter) { int targetFilterPosition = activeJob->getTargetFilter(); int currentFilterPosition = filterManager->getFilterPosition(); if (targetFilterPosition > 0 && targetFilterPosition != currentFilterPosition) currentFilter->runCommand(INDI_SET_FILTER, &targetFilterPosition); } secondsLabel->setText(i18n("Focusing...")); if (currentCCD->isLooping()) targetChip->abortExposure(); if (HFRPixels->value() == 0) { qCDebug(KSTARS_EKOS_CAPTURE) << "Capture is triggering autofocus on line 1934."; emit checkFocus(0.1); } else { qCDebug(KSTARS_EKOS_CAPTURE) << "Capture is triggering autofocus on line 1939."; emit checkFocus(HFRPixels->value()); } qCDebug(KSTARS_EKOS_CAPTURE) << "In-sequence focusing started..."; m_State = CAPTURE_FOCUSING; emit newStatus(Ekos::CAPTURE_FOCUSING); return true; } return false; } void Capture::captureOne() { //if (currentCCD->getUploadMode() == ISD::CCD::UPLOAD_LOCAL) /*if (uploadModeCombo->currentIndex() != ISD::CCD::UPLOAD_CLIENT) { appendLogText(i18n("Cannot take preview image while CCD upload mode is set to local or both. Please change " "upload mode to client and try again.")); return; }*/ if (transferFormatCombo->currentIndex() == ISD::CCD::FORMAT_NATIVE && darkSubCheck->isChecked()) { appendLogText(i18n("Cannot perform auto dark subtraction of native DSLR formats.")); return; } if (addJob(true)) prepareJob(jobs.last()); } void Capture::startFraming() { m_isLooping = true; appendLogText(i18n("Starting framing...")); captureOne(); } void Capture::captureImage() { if (activeJob == nullptr) return; captureTimeout.stop(); seqTimer->stop(); SequenceJob::CAPTUREResult rc = SequenceJob::CAPTURE_OK; if (currentCCD->isConnected() == false) { appendLogText(i18n("Error: Lost connection to CCD.")); abort(); return; } // This test must be placed before the FOCUS_PROGRESS test, // as sometimes the FilterManager can cause an auto-focus. // If the filterManager is not IDLE, then try again in 1 second. switch (filterManagerState) { case FILTER_IDLE: secondsLabel->clear(); break; case FILTER_AUTOFOCUS: secondsLabel->setText(i18n("Focusing...")); QTimer::singleShot(1000, this, &Ekos::Capture::captureImage); return; case FILTER_CHANGE: secondsLabel->setText(i18n("Changing Filters...")); QTimer::singleShot(1000, this, &Ekos::Capture::captureImage); return; case FILTER_OFFSET: secondsLabel->setText(i18n("Adjusting Filter Offset...")); QTimer::singleShot(1000, this, &Ekos::Capture::captureImage); return; } if (focusState >= FOCUS_PROGRESS) { appendLogText(i18n("Cannot capture while focus module is busy.")); abort(); return; } /* if (filterSlot != nullptr) { currentFilterPosition = (int)filterSlot->np[0].value; activeJob->setCurrentFilter(currentFilterPosition); }*/ if (currentFilter != nullptr) { m_CurrentFilterPosition = filterManager->getFilterPosition(); activeJob->setCurrentFilter(m_CurrentFilterPosition); } if (currentCCD->hasCooler()) { double temperature = 0; currentCCD->getTemperature(&temperature); activeJob->setCurrentTemperature(temperature); } if (currentCCD->isLooping()) { int remaining = activeJob->getCount() - activeJob->getCompleted(); if (remaining > 1) currentCCD->setExposureLoopCount(remaining); } connect(currentCCD, &ISD::CCD::BLOBUpdated, this, &Ekos::Capture::newFITS, Qt::UniqueConnection); connect(currentCCD, &ISD::CCD::previewFITSGenerated, this, &Ekos::Capture::setGeneratedPreviewFITS, Qt::UniqueConnection); if (activeJob->getFrameType() == FRAME_FLAT) { // If we have to calibrate ADU levels, first capture must be preview and not in batch mode if (activeJob->isPreview() == false && activeJob->getFlatFieldDuration() == DURATION_ADU && calibrationStage == CAL_PRECAPTURE_COMPLETE) { if (currentCCD->getTransferFormat() == ISD::CCD::FORMAT_NATIVE) { appendLogText(i18n("Cannot calculate ADU levels in non-FITS images.")); abort(); return; } calibrationStage = CAL_CALIBRATION; // We need to be in preview mode and in client mode for this to work activeJob->setPreview(true); } } // Temporary change upload mode to client when requesting previews if (activeJob->isPreview()) { rememberUploadMode = activeJob->getUploadMode(); currentCCD->setUploadMode(ISD::CCD::UPLOAD_CLIENT); } if (currentCCD->getUploadMode() != ISD::CCD::UPLOAD_LOCAL) { checkSeqBoundary(activeJob->getSignature()); currentCCD->setNextSequenceID(nextSequenceID); } m_State = CAPTURE_CAPTURING; //if (activeJob->isPreview() == false) // NOTE: Why we didn't emit this before for preview? emit newStatus(Ekos::CAPTURE_CAPTURING); if (frameSettings.contains(activeJob->getActiveChip())) { QVariantMap settings; settings["x"] = activeJob->getSubX(); settings["y"] = activeJob->getSubY(); settings["w"] = activeJob->getSubW(); settings["h"] = activeJob->getSubH(); settings["binx"] = activeJob->getXBin(); settings["biny"] = activeJob->getYBin(); frameSettings[activeJob->getActiveChip()] = settings; } // If using DSLR, make sure it is set to correct transfer format currentCCD->setTransformFormat(activeJob->getTransforFormat()); connect(currentCCD, &ISD::CCD::newExposureValue, this, &Ekos::Capture::setExposureProgress, Qt::UniqueConnection); rc = activeJob->capture(darkSubCheck->isChecked() ? true : false); if (rc != SequenceJob::CAPTURE_OK) { disconnect(currentCCD, &ISD::CCD::newExposureValue, this, &Ekos::Capture::setExposureProgress); } switch (rc) { case SequenceJob::CAPTURE_OK: { appendLogText(i18n("Capturing %1-second %2 image...", QString("%L1").arg(activeJob->getExposure(), 0, 'f', 3), activeJob->getFilterName())); captureTimeout.start(activeJob->getExposure() * 1000 + CAPTURE_TIMEOUT_THRESHOLD); if (activeJob->isPreview() == false) { int index = jobs.indexOf(activeJob); QJsonObject oneSequence = m_SequenceArray[index].toObject(); oneSequence["Status"] = "In Progress"; m_SequenceArray.replace(index, oneSequence); emit sequenceChanged(m_SequenceArray); } } break; case SequenceJob::CAPTURE_FRAME_ERROR: appendLogText(i18n("Failed to set sub frame.")); abort(); break; case SequenceJob::CAPTURE_BIN_ERROR: appendLogText(i18n("Failed to set binning.")); abort(); break; case SequenceJob::CAPTURE_FILTER_BUSY: // Try again in 1 second if filter is busy secondsLabel->setText(i18n("Changing filter...")); QTimer::singleShot(1000, this, &Ekos::Capture::captureImage); break; case SequenceJob::CAPTURE_FOCUS_ERROR: appendLogText(i18n("Cannot capture while focus module is busy.")); abort(); break; } } bool Capture::resumeCapture() { if (m_State == CAPTURE_PAUSED) { pauseFunction = &Capture::resumeCapture; appendLogText(i18n("Sequence paused.")); secondsLabel->setText(i18n("Paused...")); return false; } #if 0 /* Refresh isRefocus when resuming */ if (autoFocusReady && refocusEveryNCheck->isChecked()) { qCDebug(KSTARS_EKOS_CAPTURE) << "NFocus Elapsed Time (secs): " << getRefocusEveryNTimerElapsedSec() << " Requested Interval (secs): " << refocusEveryN->value() * 60; isRefocus = getRefocusEveryNTimerElapsedSec() >= refocusEveryN->value() * 60; } // FIXME ought to be able to combine these - only different is value passed // to checkFocus() // 2018-08-23 Jasem: For now in-sequence-focusing takes precedence. if (isInSequenceFocus && requiredAutoFocusStarted == false) { requiredAutoFocusStarted = true; secondsLabel->setText(i18n("Focusing...")); qCDebug(KSTARS_EKOS_CAPTURE) << "Requesting focusing if HFR >" << HFRPixels->value(); qCDebug(KSTARS_EKOS_CAPTURE) << "Capture is triggering autofocus on line 2186."; emit checkFocus(HFRPixels->value()); m_State = CAPTURE_FOCUSING; emit newStatus(Ekos::CAPTURE_FOCUSING); return true; } else if (isRefocus) { appendLogText(i18n("Scheduled refocus started...")); secondsLabel->setText(i18n("Focusing...")); qCDebug(KSTARS_EKOS_CAPTURE) << "Capture is triggering autofocus on line 2197."; emit checkFocus(0.1); m_State = CAPTURE_FOCUSING; emit newStatus(Ekos::CAPTURE_FOCUSING); return true; } #endif if (m_State == CAPTURE_DITHERING && m_AutoFocusReady && startFocusIfRequired()) return true; startNextExposure(); return true; } /*******************************************************************************/ /* Update the prefix for the sequence of images to be captured */ /*******************************************************************************/ void Capture::updateSequencePrefix(const QString &newPrefix, const QString &dir) { seqPrefix = newPrefix; // If it doesn't exist, create it QDir().mkpath(dir); nextSequenceID = 1; } /*******************************************************************************/ /* Determine the next file number sequence. That is, if we have file1.png */ /* and file2.png, then the next sequence should be file3.png */ /*******************************************************************************/ void Capture::checkSeqBoundary(const QString &path) { int newFileIndex = -1; QFileInfo const path_info(path); QString const sig_dir(path_info.dir().path()); QString const sig_file(path_info.completeBaseName()); QString tempName; // seqFileCount = 0; // No updates during meridian flip if (meridianFlipStage >= MF_ALIGNING) return; QDirIterator it(sig_dir, QDir::Files); while (it.hasNext()) { tempName = it.next(); QFileInfo info(tempName); // This returns the filename without the extension tempName = info.completeBaseName(); // This remove any additional extension (e.g. m42_001.fits.fz) // the completeBaseName() would return m42_001.fits // and this remove .fits so we end up with m42_001 tempName = tempName.remove(".fits"); QString finalSeqPrefix = seqPrefix; finalSeqPrefix.remove(SequenceJob::ISOMarker); // find the prefix first if (tempName.startsWith(finalSeqPrefix, Qt::CaseInsensitive) == false) continue; /* Do not change the number of captures. * - If the sequence is required by the end-user, unconditionally run what each sequence item is requiring. * - If the sequence is required by the scheduler, use capturedFramesMap to determine when to stop capturing. */ //seqFileCount++; int lastUnderScoreIndex = tempName.lastIndexOf("_"); if (lastUnderScoreIndex > 0) { bool indexOK = false; newFileIndex = tempName.midRef(lastUnderScoreIndex + 1).toInt(&indexOK); if (indexOK && newFileIndex >= nextSequenceID) nextSequenceID = newFileIndex + 1; } } } void Capture::appendLogText(const QString &text) { m_LogText.insert(0, i18nc("log entry; %1 is the date, %2 is the text", "%1 %2", KStarsData::Instance()->lt().toString("yyyy-MM-ddThh:mm:ss"), text)); qCInfo(KSTARS_EKOS_CAPTURE) << text; emit newLog(text); } void Capture::clearLog() { m_LogText.clear(); emit newLog(QString()); } //This method will update the Capture Module and Summary Screen's estimate of how much time is left in the download void Capture::setDownloadProgress() { if (activeJob) { double downloadTimeLeft = getEstimatedDownloadTime() - downloadTimer.elapsed() / 1000.0; if(downloadTimeLeft > 0) { exposeOUT->setText(QString("%L1").arg(downloadTimeLeft, 0, 'd', 2)); emit newDownloadProgress(downloadTimeLeft); } } } void Capture::setExposureProgress(ISD::CCDChip * tChip, double value, IPState state) { if (targetChip != tChip || targetChip->getCaptureMode() != FITS_NORMAL || meridianFlipStage >= MF_ALIGNING) return; exposeOUT->setText(QString("%L1").arg(value, 0, 'd', 2)); if (activeJob) { activeJob->setExposeLeft(value); emit newExposureProgress(activeJob); } if (activeJob && state == IPS_ALERT) { int retries = activeJob->getCaptureRetires() + 1; activeJob->setCaptureRetires(retries); appendLogText(i18n("Capture failed. Check INDI Control Panel for details.")); if (retries == 3) { abort(); return; } appendLogText(i18n("Restarting capture attempt #%1", retries)); nextSequenceID = 1; captureImage(); return; } //qDebug() << "Exposure with value " << value << "state" << pstateStr(state); if (activeJob != nullptr && state == IPS_OK) { activeJob->setCaptureRetires(0); activeJob->setExposeLeft(0); if (currentCCD && currentCCD->getUploadMode() == ISD::CCD::UPLOAD_LOCAL) { if (activeJob && activeJob->getStatus() == SequenceJob::JOB_BUSY) { newFITS(nullptr); return; } } //if (isAutoGuiding && Options::useEkosGuider() && currentCCD->getChip(ISD::CCDChip::GUIDE_CCD) == guideChip) if (guideState == GUIDE_GUIDING && Options::guiderType() == 0 && suspendGuideOnDownload) { qCDebug(KSTARS_EKOS_CAPTURE) << "Autoguiding suspended until primary CCD chip completes downloading..."; emit suspendGuiding(); } secondsLabel->setText(i18n("Downloading...")); //This will start the clock to see how long the download takes. downloadTimer.start(); downloadProgressTimer.start(); //disconnect(currentCCD, &ISD::CCD::newExposureValue(ISD::CCDChip*,double,IPState)), this, &Ekos::Capture::updateCaptureProgress(ISD::CCDChip*,double,IPState))); } // JM: Don't change to i18np, value is DOUBLE, not Integer. else if (value <= 1) secondsLabel->setText(i18n("second left")); else secondsLabel->setText(i18n("seconds left")); } void Capture::updateCCDTemperature(double value) { if (temperatureCheck->isEnabled() == false) { if (currentCCD->getBaseDevice()->getPropertyPermission("CCD_TEMPERATURE") != IP_RO) checkCCD(); } temperatureOUT->setText(QString("%L1").arg(value, 0, 'f', 2)); if (temperatureIN->cleanText().isEmpty()) temperatureIN->setValue(value); //if (activeJob && (activeJob->getStatus() == SequenceJob::JOB_ABORTED || activeJob->getStatus() == SequenceJob::JOB_IDLE)) if (activeJob) activeJob->setCurrentTemperature(value); } void Capture::updateRotatorNumber(INumberVectorProperty * nvp) { if (!strcmp(nvp->name, "ABS_ROTATOR_ANGLE")) { // Update widget rotator position rotatorSettings->setCurrentAngle(nvp->np[0].value); //if (activeJob && (activeJob->getStatus() == SequenceJob::JOB_ABORTED || activeJob->getStatus() == SequenceJob::JOB_IDLE)) if (activeJob) activeJob->setCurrentRotation(rotatorSettings->getCurrentRotationPA()); } } bool Capture::addJob(bool preview) { if (m_State != CAPTURE_IDLE && m_State != CAPTURE_ABORTED && m_State != CAPTURE_COMPLETE) return false; SequenceJob * job = nullptr; QString imagePrefix; if (preview == false && darkSubCheck->isChecked()) { KSNotification::error(i18n("Auto dark subtract is not supported in batch mode.")); return false; } if (uploadModeCombo->currentIndex() != ISD::CCD::UPLOAD_CLIENT && remoteDirIN->text().isEmpty()) { KSNotification::error(i18n("You must set remote directory for Local & Both modes.")); return false; } if (uploadModeCombo->currentIndex() != ISD::CCD::UPLOAD_LOCAL && fitsDir->text().isEmpty()) { KSNotification::error(i18n("You must set local directory for Client & Both modes.")); return false; } if (m_JobUnderEdit) job = jobs.at(queueTable->currentRow()); else { job = new SequenceJob(); job->setFilterManager(filterManager); } if (job == nullptr) { qWarning() << "Job is nullptr!" << endl; return false; } if (ISOCombo) job->setISOIndex(ISOCombo->currentIndex()); if (getGain() >= 0) job->setGain(getGain()); job->setTransforFormat(static_cast(transferFormatCombo->currentIndex())); job->setPreview(preview); if (temperatureIN->isEnabled()) { double currentTemperature; currentCCD->getTemperature(¤tTemperature); job->setEnforceTemperature(temperatureCheck->isChecked()); job->setTargetTemperature(temperatureIN->value()); job->setCurrentTemperature(currentTemperature); } job->setCaptureFilter(static_cast(filterCombo->currentIndex())); job->setUploadMode(static_cast(uploadModeCombo->currentIndex())); job->setPostCaptureScript(postCaptureScriptIN->text()); job->setFlatFieldDuration(flatFieldDuration); job->setFlatFieldSource(flatFieldSource); job->setPreMountPark(preMountPark); job->setPreDomePark(preDomePark); job->setWallCoord(wallCoord); job->setTargetADU(targetADU); job->setTargetADUTolerance(targetADUTolerance); imagePrefix = prefixIN->text(); // JM 2019-11-26: In case there is no raw prefix set // BUT target name is set, we update the prefix to include // the target name, which is usually set by the scheduler. if (imagePrefix.isEmpty() && !m_TargetName.isEmpty()) { prefixIN->setText(m_TargetName); imagePrefix = m_TargetName; } constructPrefix(imagePrefix); job->setPrefixSettings(prefixIN->text(), filterCheck->isChecked(), expDurationCheck->isChecked(), ISOCheck->isChecked()); job->setFrameType(static_cast(frameTypeCombo->currentIndex())); job->setFullPrefix(imagePrefix); //if (filterSlot != nullptr && currentFilter != nullptr) if (FilterPosCombo->currentIndex() != -1 && currentFilter != nullptr) job->setTargetFilter(FilterPosCombo->currentIndex() + 1, FilterPosCombo->currentText()); job->setExposure(exposureIN->value()); job->setCount(countIN->value()); job->setBin(binXIN->value(), binYIN->value()); job->setDelay(delayIN->value() * 1000); /* in ms */ job->setActiveChip(targetChip); job->setActiveCCD(currentCCD); job->setActiveFilter(currentFilter); // Custom Properties job->setCustomProperties(customPropertiesDialog->getCustomProperties()); if (currentRotator && rotatorSettings->isRotationEnforced()) { job->setActiveRotator(currentRotator); job->setTargetRotation(rotatorSettings->getTargetRotationPA()); job->setCurrentRotation(rotatorSettings->getCurrentRotationPA()); } job->setFrame(frameXIN->value(), frameYIN->value(), frameWIN->value(), frameHIN->value()); job->setRemoteDir(remoteDirIN->text()); job->setLocalDir(fitsDir->text()); if (m_JobUnderEdit == false) { // JM 2018-09-24: If this is the first job added // We always ignore job progress by default. if (jobs.isEmpty() && preview == false) ignoreJobProgress = true; jobs.append(job); // Nothing more to do if preview if (preview) return true; } QJsonObject jsonJob = {{"Status", "Idle"}}; QString directoryPostfix; /* FIXME: Refactor directoryPostfix assignment, whose code is duplicated in scheduler.cpp */ if (m_TargetName.isEmpty()) directoryPostfix = QLatin1String("/") + frameTypeCombo->currentText(); else directoryPostfix = QLatin1String("/") + m_TargetName + QLatin1String("/") + frameTypeCombo->currentText(); if ((job->getFrameType() == FRAME_LIGHT || job->getFrameType() == FRAME_FLAT) && job->getFilterName().isEmpty() == false) directoryPostfix += QLatin1String("/") + job->getFilterName(); job->setDirectoryPostfix(directoryPostfix); int currentRow = 0; if (m_JobUnderEdit == false) { currentRow = queueTable->rowCount(); queueTable->insertRow(currentRow); } else currentRow = queueTable->currentRow(); QTableWidgetItem * status = m_JobUnderEdit ? queueTable->item(currentRow, 0) : new QTableWidgetItem(); job->setStatusCell(status); QTableWidgetItem * filter = m_JobUnderEdit ? queueTable->item(currentRow, 1) : new QTableWidgetItem(); filter->setText("--"); jsonJob.insert("Filter", "--"); /*if (frameTypeCombo->currentText().compare("Bias", Qt::CaseInsensitive) && frameTypeCombo->currentText().compare("Dark", Qt::CaseInsensitive) && FilterPosCombo->count() > 0)*/ if (FilterPosCombo->count() > 0 && (frameTypeCombo->currentIndex() == FRAME_LIGHT || frameTypeCombo->currentIndex() == FRAME_FLAT)) { filter->setText(FilterPosCombo->currentText()); jsonJob.insert("Filter", FilterPosCombo->currentText()); } filter->setTextAlignment(Qt::AlignHCenter); filter->setFlags(Qt::ItemIsSelectable | Qt::ItemIsEnabled); QTableWidgetItem * type = m_JobUnderEdit ? queueTable->item(currentRow, 2) : new QTableWidgetItem(); type->setText(frameTypeCombo->currentText()); type->setTextAlignment(Qt::AlignHCenter); type->setFlags(Qt::ItemIsSelectable | Qt::ItemIsEnabled); jsonJob.insert("Type", type->text()); QTableWidgetItem * bin = m_JobUnderEdit ? queueTable->item(currentRow, 3) : new QTableWidgetItem(); bin->setText(QString("%1x%2").arg(binXIN->value()).arg(binYIN->value())); bin->setTextAlignment(Qt::AlignHCenter); bin->setFlags(Qt::ItemIsSelectable | Qt::ItemIsEnabled); jsonJob.insert("Bin", bin->text()); QTableWidgetItem * exp = m_JobUnderEdit ? queueTable->item(currentRow, 4) : new QTableWidgetItem(); exp->setText(QString("%L1").arg(exposureIN->value(), 0, 'f', exposureIN->decimals())); exp->setTextAlignment(Qt::AlignHCenter); exp->setFlags(Qt::ItemIsSelectable | Qt::ItemIsEnabled); jsonJob.insert("Exp", exp->text()); QTableWidgetItem * iso = m_JobUnderEdit ? queueTable->item(currentRow, 5) : new QTableWidgetItem(); if (ISOCombo && ISOCombo->currentIndex() != -1) { iso->setText(ISOCombo->currentText()); jsonJob.insert("ISO/Gain", iso->text()); } else if (GainSpin && GainSpin->value() >= 0 && std::fabs(GainSpin->value() - GainSpinSpecialValue) > 0) { iso->setText(GainSpin->cleanText()); jsonJob.insert("ISO/Gain", iso->text()); } else { iso->setText("--"); jsonJob.insert("ISO/Gain", "--"); } iso->setTextAlignment(Qt::AlignHCenter); iso->setFlags(Qt::ItemIsSelectable | Qt::ItemIsEnabled); QTableWidgetItem * count = m_JobUnderEdit ? queueTable->item(currentRow, 6) : new QTableWidgetItem(); job->setCountCell(count); jsonJob.insert("Count", count->text()); if (m_JobUnderEdit == false) { queueTable->setItem(currentRow, 0, status); queueTable->setItem(currentRow, 1, filter); queueTable->setItem(currentRow, 2, type); queueTable->setItem(currentRow, 3, bin); queueTable->setItem(currentRow, 4, exp); queueTable->setItem(currentRow, 5, iso); queueTable->setItem(currentRow, 6, count); m_SequenceArray.append(jsonJob); emit sequenceChanged(m_SequenceArray); } removeFromQueueB->setEnabled(true); if (queueTable->rowCount() > 0) { queueSaveAsB->setEnabled(true); queueSaveB->setEnabled(true); resetB->setEnabled(true); m_Dirty = true; } if (queueTable->rowCount() > 1) { queueUpB->setEnabled(true); queueDownB->setEnabled(true); } if (m_JobUnderEdit) { m_JobUnderEdit = false; resetJobEdit(); appendLogText(i18n("Job #%1 changes applied.", currentRow + 1)); m_SequenceArray.replace(currentRow, jsonJob); emit sequenceChanged(m_SequenceArray); } return true; } void Capture::removeJobFromQueue() { int currentRow = queueTable->currentRow(); if (currentRow < 0) currentRow = queueTable->rowCount() - 1; removeJob(currentRow); // update selection if (queueTable->rowCount() == 0) return; if (currentRow > queueTable->rowCount()) queueTable->selectRow(queueTable->rowCount() - 1); else queueTable->selectRow(currentRow); } void Capture::removeJob(int index) { if (m_State != CAPTURE_IDLE && m_State != CAPTURE_ABORTED && m_State != CAPTURE_COMPLETE) return; if (m_JobUnderEdit) { resetJobEdit(); return; } if (index < 0) return; queueTable->removeRow(index); m_SequenceArray.removeAt(index); emit sequenceChanged(m_SequenceArray); SequenceJob * job = jobs.at(index); jobs.removeOne(job); if (job == activeJob) activeJob = nullptr; delete job; if (queueTable->rowCount() == 0) removeFromQueueB->setEnabled(false); if (queueTable->rowCount() == 1) { queueUpB->setEnabled(false); queueDownB->setEnabled(false); } for (int i = 0; i < jobs.count(); i++) jobs.at(i)->setStatusCell(queueTable->item(i, 0)); if (index < queueTable->rowCount()) queueTable->selectRow(index); else if (queueTable->rowCount() > 0) queueTable->selectRow(queueTable->rowCount() - 1); if (queueTable->rowCount() == 0) { queueSaveAsB->setEnabled(false); queueSaveB->setEnabled(false); resetB->setEnabled(false); } m_Dirty = true; } void Capture::moveJobUp() { int currentRow = queueTable->currentRow(); int columnCount = queueTable->columnCount(); if (currentRow <= 0 || queueTable->rowCount() == 1) return; int destinationRow = currentRow - 1; for (int i = 0; i < columnCount; i++) { QTableWidgetItem * downItem = queueTable->takeItem(currentRow, i); QTableWidgetItem * upItem = queueTable->takeItem(destinationRow, i); queueTable->setItem(destinationRow, i, downItem); queueTable->setItem(currentRow, i, upItem); } SequenceJob * job = jobs.takeAt(currentRow); jobs.removeOne(job); jobs.insert(destinationRow, job); QJsonObject currentJob = m_SequenceArray[currentRow].toObject(); m_SequenceArray.replace(currentRow, m_SequenceArray[destinationRow]); m_SequenceArray.replace(destinationRow, currentJob); emit sequenceChanged(m_SequenceArray); queueTable->selectRow(destinationRow); for (int i = 0; i < jobs.count(); i++) jobs.at(i)->setStatusCell(queueTable->item(i, 0)); m_Dirty = true; } void Capture::moveJobDown() { int currentRow = queueTable->currentRow(); int columnCount = queueTable->columnCount(); if (currentRow < 0 || queueTable->rowCount() == 1 || (currentRow + 1) == queueTable->rowCount()) return; int destinationRow = currentRow + 1; for (int i = 0; i < columnCount; i++) { QTableWidgetItem * downItem = queueTable->takeItem(currentRow, i); QTableWidgetItem * upItem = queueTable->takeItem(destinationRow, i); queueTable->setItem(destinationRow, i, downItem); queueTable->setItem(currentRow, i, upItem); } SequenceJob * job = jobs.takeAt(currentRow); jobs.removeOne(job); jobs.insert(destinationRow, job); QJsonObject currentJob = m_SequenceArray[currentRow].toObject(); m_SequenceArray.replace(currentRow, m_SequenceArray[destinationRow]); m_SequenceArray.replace(destinationRow, currentJob); emit sequenceChanged(m_SequenceArray); queueTable->selectRow(destinationRow); for (int i = 0; i < jobs.count(); i++) jobs.at(i)->setStatusCell(queueTable->item(i, 0)); m_Dirty = true; } void Capture::setBusy(bool enable) { isBusy = enable; enable ? pi->startAnimation() : pi->stopAnimation(); previewB->setEnabled(!enable); loopB->setEnabled(!enable); foreach (QAbstractButton * button, queueEditButtonGroup->buttons()) button->setEnabled(!enable); } void Capture::prepareJob(SequenceJob * job) { activeJob = job; if (m_isLooping == false) qCDebug(KSTARS_EKOS_CAPTURE) << "Preparing capture job" << job->getSignature() << "for execution."; int index = jobs.indexOf(job); if (index >= 0) queueTable->selectRow(index); if (activeJob->getActiveCCD() != currentCCD) { setCamera(activeJob->getActiveCCD()->getDeviceName()); } /*if (activeJob->isPreview()) seqTotalCount = -1; else seqTotalCount = activeJob->getCount();*/ seqDelay = activeJob->getDelay(); // seqCurrentCount = activeJob->getCompleted(); if (activeJob->isPreview() == false) { fullImgCountOUT->setText(QString("%L1").arg(activeJob->getCount())); currentImgCountOUT->setText(QString("%L1").arg(activeJob->getCompleted())); // set the progress info imgProgress->setEnabled(true); imgProgress->setMaximum(activeJob->getCount()); imgProgress->setValue(activeJob->getCompleted()); if (currentCCD->getUploadMode() != ISD::CCD::UPLOAD_LOCAL) updateSequencePrefix(activeJob->getFullPrefix(), QFileInfo(activeJob->getSignature()).path()); } // We check if the job is already fully or partially complete by checking how many files of its type exist on the file system if (activeJob->isPreview() == false) { // The signature is the unique identification path in the system for a particular job. Format is "///". // If the Scheduler is requesting the Capture tab to process a sequence job, a target name will be inserted after the sequence file storage field (e.g. /path/to/storage/target/Light/...) // If the end-user is requesting the Capture tab to process a sequence job, the sequence file storage will be used as is (e.g. /path/to/storage/Light/...) QString signature = activeJob->getSignature(); // Now check on the file system ALL the files that exist with the above signature // If 29 files exist for example, then nextSequenceID would be the NEXT file number (30) // Therefore, we know how to number the next file. // However, we do not deduce the number of captures to process from this function. checkSeqBoundary(signature); // Captured Frames Map contains a list of signatures:count of _already_ captured files in the file system. // This map is set by the Scheduler in order to complete efficiently the required captures. // When the end-user requests a sequence to be processed, that map is empty. // // Example with a 5xL-5xR-5xG-5xB sequence // // When the end-user loads and runs this sequence, each filter gets to capture 5 frames, then the procedure stops. // When the Scheduler executes a job with this sequence, the procedure depends on what is in the storage. // // Let's consider the Scheduler has 3 instances of this job to run. // // When the first job completes the sequence, there are 20 images in the file system (5 for each filter). // When the second job starts, Scheduler finds those 20 images but requires 20 more images, thus sets the frames map counters to 0 for all LRGB frames. // When the third job starts, Scheduler now has 40 images, but still requires 20 more, thus again sets the frames map counters to 0 for all LRGB frames. // // Now let's consider something went wrong, and the third job was aborted before getting to 60 images, say we have full LRG, but only 1xB. // When Scheduler attempts to run the aborted job again, it will count captures in storage, subtract previous job requirements, and set the frames map counters to 0 for LRG, and 4 for B. // When the sequence runs, the procedure will bypass LRG and proceed to capture 4xB. if (capturedFramesMap.contains(signature)) { // Get the current capture count from the map int count = capturedFramesMap[signature]; // Count how many captures this job has to process, given that previous jobs may have done some work already foreach (SequenceJob * a_job, jobs) if (a_job == activeJob) break; else if (a_job->getSignature() == activeJob->getSignature()) count -= a_job->getCompleted(); // This is the current completion count of the current job activeJob->setCompleted(count); } // JM 2018-09-24: Only set completed jobs to 0 IF the scheduler set captured frames map to begin with // If the map is empty, then no scheduler is used and it should proceed as normal. else if (capturedFramesMap.count() > 0) { // No preliminary information, we reset the job count and run the job unconditionally to clarify the behavior activeJob->setCompleted(0); } // JM 2018-09-24: In case ignoreJobProgress is enabled // We check if this particular job progress ignore flag is set. If not, // then we set it and reset completed to zero. Next time it is evaluated here again // It will maintain its count regardless else if (ignoreJobProgress && activeJob->getJobProgressIgnored() == false) { activeJob->setJobProgressIgnored(true); activeJob->setCompleted(0); } // We cannot rely on sequenceID to give us a count - if we don't ignore job progress, we leave the count as it was originally #if 0 // If we cannot ignore job progress, then we set completed job number according to what // was found on the file system. else if (ignoreJobProgress == false) { int count = nextSequenceID - 1; if (count < activeJob->getCount()) activeJob->setCompleted(count); else activeJob->setCompleted(activeJob->getCount()); } #endif // Check whether active job is complete by comparing required captures to what is already available if (activeJob->getCount() <= activeJob->getCompleted()) { activeJob->setCompleted(activeJob->getCount()); appendLogText(i18n("Job requires %1-second %2 images, has already %3/%4 captures and does not need to run.", QString("%L1").arg(job->getExposure(), 0, 'f', 3), job->getFilterName(), activeJob->getCompleted(), activeJob->getCount())); processJobCompletion(); /* FIXME: find a clearer way to exit here */ return; } else { // There are captures to process currentImgCountOUT->setText(QString("%L1").arg(activeJob->getCompleted())); appendLogText(i18n("Job requires %1-second %2 images, has %3/%4 frames captured and will be processed.", QString("%L1").arg(job->getExposure(), 0, 'f', 3), job->getFilterName(), activeJob->getCompleted(), activeJob->getCount())); // Emit progress update - done a few lines below // emit newImage(nullptr, activeJob); currentCCD->setNextSequenceID(nextSequenceID); } } if (currentCCD->isBLOBEnabled() == false) { // FIXME: Move this warning pop-up elsewhere, it will interfere with automation. // if (Options::guiderType() != Ekos::Guide::GUIDE_INTERNAL || KMessageBox::questionYesNo(nullptr, i18n("Image transfer is disabled for this camera. Would you like to enable it?")) == // KMessageBox::Yes) if (Options::guiderType() != Ekos::Guide::GUIDE_INTERNAL) { currentCCD->setBLOBEnabled(true); } else { connect(KSMessageBox::Instance(), &KSMessageBox::accepted, this, [this]() { //QObject::disconnect(KSMessageBox::Instance(), &KSMessageBox::accepted, this, nullptr); KSMessageBox::Instance()->disconnect(this); currentCCD->setBLOBEnabled(true); prepareActiveJob(); }); connect(KSMessageBox::Instance(), &KSMessageBox::rejected, this, [this]() { //QObject::disconnect(KSMessageBox::Instance(), &KSMessageBox::rejected, this, nullptr); KSMessageBox::Instance()->disconnect(this); currentCCD->setBLOBEnabled(true); setBusy(false); }); KSMessageBox::Instance()->questionYesNo(i18n("Image transfer is disabled for this camera. Would you like to enable it?"), i18n("Image Transfer"), 15); return; } } prepareActiveJob(); } void Capture::prepareActiveJob() { // Just notification of active job stating up emit newImage(activeJob); //connect(job, SIGNAL(checkFocus()), this, &Ekos::Capture::startPostFilterAutoFocus())); // Reset calibration stage if (calibrationStage == CAL_CAPTURING) { if (activeJob->getFrameType() != FRAME_LIGHT) calibrationStage = CAL_PRECAPTURE_COMPLETE; else calibrationStage = CAL_NONE; } /* Disable this restriction, let the sequence run even if focus did not run prior to the capture. * Besides, this locks up the Scheduler when the Capture module starts a sequence without any prior focus procedure done. * This is quite an old code block. The message "Manual scheduled" seems to even refer to some manual intervention? * With the new HFR threshold, it might be interesting to prevent the execution because we actually need an HFR value to * begin capturing, but even there, on one hand it makes sense for the end-user to know what HFR to put in the edit box, * and on the other hand the focus procedure will deduce the next HFR automatically. * But in the end, it's not entirely clear what the intent was. Note there is still a warning that a preliminary autofocus * procedure is important to avoid any surprise that could make the whole schedule ineffective. */ #if 0 // If we haven't performed a single autofocus yet, we stop //if (!job->isPreview() && Options::enforceRefocusEveryN() && autoFocusReady && isInSequenceFocus == false && firstAutoFocus == true) if (!job->isPreview() && Options::enforceRefocusEveryN() && autoFocusReady == false && isInSequenceFocus == false) { appendLogText(i18n("Manual scheduled focusing is not supported. Run Autofocus process before trying again.")); abort(); return; } #endif #if 0 if (currentFilterPosition > 0) { // If we haven't performed a single autofocus yet, we stop if (!job->isPreview() && Options::autoFocusOnFilterChange() && (isInSequenceFocus == false && firstAutoFocus == true)) { appendLogText(i18n( "Manual focusing post filter change is not supported. Run Autofocus process before trying again.")); abort(); return; } /* if (currentFilterPosition != activeJob->getTargetFilter() && filterFocusOffsets.empty() == false) { int16_t targetFilterOffset = 0; foreach (FocusOffset *offset, filterFocusOffsets) { if (offset->filter == activeJob->getFilterName()) { targetFilterOffset = offset->offset - lastFilterOffset; lastFilterOffset = offset->offset; break; } } if (targetFilterOffset != 0 && (activeJob->getFrameType() == FRAME_LIGHT || activeJob->getFrameType() == FRAME_FLAT)) { appendLogText(i18n("Adjust focus offset by %1 steps", targetFilterOffset)); secondsLabel->setText(i18n("Adjusting filter offset")); if (activeJob->isPreview() == false) { state = CAPTURE_FILTER_FOCUS; emit newStatus(Ekos::CAPTURE_FILTER_FOCUS); } setBusy(true); emit newFocusOffset(targetFilterOffset); return; } } */ } #endif preparePreCaptureActions(); } void Capture::preparePreCaptureActions() { // Update position if (m_CurrentFilterPosition > 0) activeJob->setCurrentFilter(m_CurrentFilterPosition); // update temperature if (currentCCD->hasCooler() && activeJob->getEnforceTemperature()) { double temperature = 0; currentCCD->getTemperature(&temperature); activeJob->setCurrentTemperature(temperature); } // update rotator angle if (currentRotator != nullptr && activeJob->getTargetRotation() != Ekos::INVALID_VALUE) activeJob->setCurrentRotation(rotatorSettings->getCurrentRotationPA()); setBusy(true); if (activeJob->isPreview()) { startB->setIcon( QIcon::fromTheme("media-playback-stop")); startB->setToolTip(i18n("Stop")); } connect(activeJob, &SequenceJob::prepareState, this, &Ekos::Capture::updatePrepareState); connect(activeJob, &SequenceJob::prepareComplete, this, &Ekos::Capture::executeJob); activeJob->prepareCapture(); } void Capture::updatePrepareState(Ekos::CaptureState prepareState) { m_State = prepareState; emit newStatus(prepareState); switch (prepareState) { case CAPTURE_SETTING_TEMPERATURE: appendLogText(i18n("Setting temperature to %1 C...", activeJob->getTargetTemperature())); secondsLabel->setText(i18n("Set %1 C...", activeJob->getTargetTemperature())); break; case CAPTURE_SETTING_ROTATOR: appendLogText(i18n("Setting rotation to %1 degrees E of N...", activeJob->getTargetRotation())); secondsLabel->setText(i18n("Set Rotator %1...", activeJob->getTargetRotation())); break; default: break; } } void Capture::executeJob() { activeJob->disconnect(this); QMap FITSHeader; QString rawPrefix = activeJob->property("rawPrefix").toString(); if (m_ObserverName.isEmpty() == false) FITSHeader["FITS_OBSERVER"] = m_ObserverName; if (m_TargetName.isEmpty() == false) FITSHeader["FITS_OBJECT"] = m_TargetName; else if (rawPrefix.isEmpty() == false) { FITSHeader["FITS_OBJECT"] = rawPrefix; } if (FITSHeader.count() > 0) currentCCD->setFITSHeader(FITSHeader); // Update button status setBusy(true); useGuideHead = (activeJob->getActiveChip()->getType() == ISD::CCDChip::PRIMARY_CCD) ? false : true; syncGUIToJob(activeJob); calibrationCheckType = CAL_CHECK_TASK; updatePreCaptureCalibrationStatus(); // Check calibration frame requirements #if 0 if (activeJob->getFrameType() != FRAME_LIGHT && activeJob->isPreview() == false) { updatePreCaptureCalibrationStatus(); return; } captureImage(); #endif } void Capture::updatePreCaptureCalibrationStatus() { // If process was aborted or stopped by the user if (isBusy == false) { appendLogText(i18n("Warning: Calibration process was prematurely terminated.")); return; } IPState rc = processPreCaptureCalibrationStage(); if (rc == IPS_ALERT) return; else if (rc == IPS_BUSY) { // Clear the label if we are neither executing a meridian flip nor re-focusing if ((meridianFlipStage == MF_NONE || meridianFlipStage == MF_READY) && m_State != CAPTURE_FOCUSING) secondsLabel->clear(); QTimer::singleShot(1000, this, &Ekos::Capture::updatePreCaptureCalibrationStatus); return; } captureImage(); } void Capture::setGuideDeviation(double delta_ra, double delta_dec) { // if (activeJob == nullptr) // { // if (deviationDetected == false) // return; // // Try to find first job that was aborted due to deviation // for(SequenceJob *job : jobs) // { // if (job->getStatus() == SequenceJob::JOB_ABORTED) // { // activeJob = job; // break; // } // } // if (activeJob == nullptr) // return; // } // If guiding is started after a meridian flip we will start getting guide deviations again // if the guide deviations are within our limits, we resume the sequence if (meridianFlipStage == MF_GUIDING) { double deviation_rms = sqrt(delta_ra * delta_ra + delta_dec * delta_dec); // If the user didn't select any guiding deviation, we fall through // otherwise we can for deviation RMS if (guideDeviationCheck->isChecked() == false || deviation_rms < guideDeviation->value()) { appendLogText(i18n("Post meridian flip calibration completed successfully.")); resumeSequence(); // N.B. Set meridian flip stage AFTER resumeSequence() always setMeridianFlipStage(MF_NONE); return; } } // We don't enforce limit on previews if (guideDeviationCheck->isChecked() == false || (activeJob && (activeJob->isPreview() || activeJob->getExposeLeft() == 0))) return; double deviation_rms = sqrt(delta_ra * delta_ra + delta_dec * delta_dec); QString deviationText = QString("%1").arg(deviation_rms, 0, 'f', 3); // If we have an active busy job, let's abort it if guiding deviation is exceeded. // And we accounted for the spike if (activeJob && activeJob->getStatus() == SequenceJob::JOB_BUSY && activeJob->getFrameType() == FRAME_LIGHT) { if (deviation_rms > guideDeviation->value()) { // Ignore spikes ONCE if (m_SpikeDetected == false) { m_SpikeDetected = true; return; } appendLogText(i18n("Guiding deviation %1 exceeded limit value of %2 arcsecs, " "suspending exposure and waiting for guider up to %3 seconds.", deviationText, guideDeviation->value(), QString("%L1").arg(guideDeviationTimer.interval() / 1000.0, 0, 'f', 3))); suspend(); m_SpikeDetected = false; // Check if we need to start meridian flip if (checkMeridianFlip()) return; m_DeviationDetected = true; guideDeviationTimer.start(); } return; } // Find the first aborted job SequenceJob * abortedJob = nullptr; for(SequenceJob * job : jobs) { if (job->getStatus() == SequenceJob::JOB_ABORTED) { abortedJob = job; break; } } if (abortedJob && m_DeviationDetected) { if (deviation_rms <= guideDeviation->value()) { guideDeviationTimer.stop(); if (seqDelay == 0) appendLogText(i18n("Guiding deviation %1 is now lower than limit value of %2 arcsecs, " "resuming exposure.", deviationText, guideDeviation->value())); else appendLogText(i18n("Guiding deviation %1 is now lower than limit value of %2 arcsecs, " "resuming exposure in %3 seconds.", deviationText, guideDeviation->value(), seqDelay / 1000.0)); QTimer::singleShot(seqDelay, this, &Ekos::Capture::start); return; } else appendLogText(i18n("Guiding deviation %1 is still higher than limit value of %2 arcsecs.", deviationText, guideDeviation->value())); } } void Capture::setFocusStatus(FocusState state) { qCDebug(KSTARS_EKOS_CAPTURE) << "setFocusStatus: " << state; focusState = state; if (focusState > FOCUS_ABORTED) return; if (focusState == FOCUS_COMPLETE) { // enable option to have a refocus event occur if HFR goes over threshold m_AutoFocusReady = true; //if (HFRPixels->value() == 0.0 && fileHFR == 0.0) if (fileHFR == 0.0) { QList filterHFRList; if (m_CurrentFilterPosition > 0) { // If we are using filters, then we retrieve which filter is currently active. // We check if filter lock is used, and store that instead of the current filter. // e.g. If current filter HA, but lock filter is L, then the HFR value is stored for L filter. // If no lock filter exists, then we store as is (HA) QString currentFilterText = FilterPosCombo->itemText(m_CurrentFilterPosition - 1); //QString filterLock = filterManager.data()->getFilterLock(currentFilterText); //QString finalFilter = (filterLock == "--" ? currentFilterText : filterLock); //filterHFRList = HFRMap[finalFilter]; filterHFRList = HFRMap[currentFilterText]; filterHFRList.append(focusHFR); //HFRMap[finalFilter] = filterHFRList; HFRMap[currentFilterText] = filterHFRList; } // No filters else { filterHFRList = HFRMap["--"]; filterHFRList.append(focusHFR); HFRMap["--"] = filterHFRList; } double median = focusHFR; int count = filterHFRList.size(); if (Options::useMedianFocus() && count > 1) median = (count % 2) ? filterHFRList[count / 2] : (filterHFRList[count / 2 - 1] + filterHFRList[count / 2]) / 2.0; // Add 2.5% (default) to the automatic initial HFR value to allow for minute changes in HFR without need to refocus // in case in-sequence-focusing is used. HFRPixels->setValue(median + (median * (Options::hFRThresholdPercentage() / 100.0))); } #if 0 if (focusHFR > 0 && firstAutoFocus && HFRPixels->value() == 0 && fileHFR == 0) { firstAutoFocus = false; // Add 2.5% (default) to the automatic initial HFR value to allow for minute changes in HFR without need to refocus // in case in-sequence-focusing is used. HFRPixels->setValue(focusHFR + (focusHFR * (Options::hFRThresholdPercentage() / 100.0))); } #endif // successful focus so reset elapsed time restartRefocusEveryNTimer(); } #if 0 if (activeJob && (activeJob->getStatus() == SequenceJob::JOB_ABORTED || activeJob->getStatus() == SequenceJob::JOB_IDLE)) { if (focusState == FOCUS_COMPLETE) { //HFRPixels->setValue(focusHFR + (focusHFR * 0.025)); appendLogText(i18n("Focus complete.")); } else if (focusState == FOCUS_FAILED) { appendLogText(i18n("Autofocus failed. Aborting exposure...")); secondsLabel->setText(""); abort(); } return; } #endif if ((isRefocus || isInSequenceFocus) && activeJob && activeJob->getStatus() == SequenceJob::JOB_BUSY) { // if the focusing has been started during the post-calibration, return to the calibration if (calibrationStage < CAL_PRECAPTURE_COMPLETE && m_State == CAPTURE_FOCUSING) { if (focusState == FOCUS_COMPLETE) { appendLogText(i18n("Focus complete.")); secondsLabel->setText(i18n("Focus complete.")); m_State = CAPTURE_PROGRESS; } else if (focusState == FOCUS_FAILED) { appendLogText(i18n("Autofocus failed.")); secondsLabel->setText(i18n("Autofocus failed.")); abort(); } } else if (focusState == FOCUS_COMPLETE) { appendLogText(i18n("Focus complete.")); secondsLabel->setText(i18n("Focus complete.")); startNextExposure(); } else if (focusState == FOCUS_FAILED) { appendLogText(i18n("Autofocus failed. Aborting exposure...")); secondsLabel->setText(i18n("Autofocus failed.")); abort(); } } } void Capture::updateHFRThreshold() { if (fileHFR != 0.0) return; QList filterHFRList; if (FilterPosCombo->currentIndex() != -1) { // If we are using filters, then we retrieve which filter is currently active. // We check if filter lock is used, and store that instead of the current filter. // e.g. If current filter HA, but lock filter is L, then the HFR value is stored for L filter. // If no lock filter exists, then we store as is (HA) QString currentFilterText = FilterPosCombo->currentText(); QString filterLock = filterManager.data()->getFilterLock(currentFilterText); QString finalFilter = (filterLock == "--" ? currentFilterText : filterLock); filterHFRList = HFRMap[finalFilter]; } // No filters else { filterHFRList = HFRMap["--"]; } if (filterHFRList.empty()) { HFRPixels->setValue(Options::hFRDeviation()); return; } double median = 0; int count = filterHFRList.size(); if (count > 1) median = (count % 2) ? filterHFRList[count / 2] : (filterHFRList[count / 2 - 1] + filterHFRList[count / 2]) / 2.0; else if (count == 1) median = filterHFRList[0]; // Add 2.5% (default) to the automatic initial HFR value to allow for minute changes in HFR without need to refocus // in case in-sequence-focusing is used. HFRPixels->setValue(median + (median * (Options::hFRThresholdPercentage() / 100.0))); } void Capture::setMeridianFlipStage(MFStage status) { qCDebug(KSTARS_EKOS_CAPTURE) << "setMeridianFlipStage: " << status; if (meridianFlipStage != status) { switch (status) { case MF_NONE: if (m_State == CAPTURE_PAUSED) // paused after meridian flip secondsLabel->setText(i18n("Paused...")); /* disabled since the focusing label will be overwritten else secondsLabel->setText(""); */ meridianFlipStage = status; break; case MF_READY: if (meridianFlipStage == MF_REQUESTED) { // we keep the stage on requested until the mount starts the meridian flip emit newMeridianFlipStatus(Mount::FLIP_ACCEPTED); } else if (m_State == CAPTURE_PAUSED) { // paused after meridian flip requested secondsLabel->setText(i18n("Paused...")); meridianFlipStage = status; emit newMeridianFlipStatus(Mount::FLIP_ACCEPTED); } // in any other case, ignore it break; case MF_INITIATED: meridianFlipStage = MF_INITIATED; emit meridianFlipStarted(); secondsLabel->setText(i18n("Meridian Flip...")); KSNotification::event(QLatin1String("MeridianFlipStarted"), i18n("Meridian flip started"), KSNotification::EVENT_INFO); break; case MF_REQUESTED: if (m_State == CAPTURE_PAUSED) // paused before meridian flip requested emit newMeridianFlipStatus(Mount::FLIP_ACCEPTED); else emit newMeridianFlipStatus(Mount::FLIP_WAITING); meridianFlipStage = status; break; case MF_COMPLETED: secondsLabel->setText(i18n("Flip complete.")); break; default: meridianFlipStage = status; break; } } } void Capture::meridianFlipStatusChanged(Mount::MeridianFlipStatus status) { qCDebug(KSTARS_EKOS_CAPTURE) << "meridianFlipStatusChanged: " << status; switch (status) { case Mount::FLIP_NONE: // MF_NONE as external signal ignored so that re-alignment and guiding are processed first if (meridianFlipStage < MF_COMPLETED) setMeridianFlipStage(MF_NONE); break; case Mount::FLIP_PLANNED: if (meridianFlipStage > MF_NONE) { // it seems like the meridian flip had been postponed resumeSequence(); return; } else { // If we are autoguiding, we should resume autoguiding after flip resumeGuidingAfterFlip = isGuidingActive(); if (m_State == CAPTURE_IDLE || m_State == CAPTURE_ABORTED || m_State == CAPTURE_COMPLETE || m_State == CAPTURE_PAUSED) { setMeridianFlipStage(MF_INITIATED); emit newMeridianFlipStatus(Mount::FLIP_ACCEPTED); } else setMeridianFlipStage(MF_REQUESTED); } break; case Mount::FLIP_RUNNING: setMeridianFlipStage(MF_INITIATED); emit newStatus(Ekos::CAPTURE_MERIDIAN_FLIP); break; case Mount::FLIP_COMPLETED: setMeridianFlipStage(MF_COMPLETED); emit newStatus(Ekos::CAPTURE_IDLE); processFlipCompleted(); break; default: break; } } int Capture::getTotalFramesCount(QString signature) { int result = 0; bool found = false; foreach (SequenceJob * job, jobs) { // FIXME: this should be part of SequenceJob QString sig = job->getSignature(); if (sig == signature) { result += job->getCount(); found = true; } } if (found) return result; else return -1; } void Capture::setRotator(ISD::GDInterface * newRotator) { currentRotator = newRotator; connect(currentRotator, &ISD::GDInterface::numberUpdated, this, &Ekos::Capture::updateRotatorNumber, Qt::UniqueConnection); rotatorB->setEnabled(true); rotatorSettings->setRotator(newRotator); INumberVectorProperty * nvp = newRotator->getBaseDevice()->getNumber("ABS_ROTATOR_ANGLE"); rotatorSettings->setCurrentAngle(nvp->np[0].value); } void Capture::setTelescope(ISD::GDInterface * newTelescope) { currentTelescope = static_cast(newTelescope); currentTelescope->disconnect(this); connect(currentTelescope, &ISD::GDInterface::numberUpdated, this, &Ekos::Capture::processTelescopeNumber); connect(currentTelescope, &ISD::Telescope::newTarget, [&](const QString & target) { if (m_State == CAPTURE_IDLE || m_State == CAPTURE_COMPLETE) { QString sanitized = target; // Remove illegal characters that can be problematic sanitized = sanitized.replace( QRegularExpression("\\s|/|\\(|\\)|:|\\*|~|\"" ), "_" ) // Remove any two or more __ .replace( QRegularExpression("_{2,}"), "_") // Remove any _ at the end .replace( QRegularExpression("_$"), ""); prefixIN->setText(sanitized); } }); syncTelescopeInfo(); } void Capture::syncTelescopeInfo() { if (currentTelescope && currentTelescope->isConnected()) { // Sync ALL CCDs to current telescope for (ISD::CCD * oneCCD : CCDs) { ITextVectorProperty * activeDevices = oneCCD->getBaseDevice()->getText("ACTIVE_DEVICES"); if (activeDevices) { IText * activeTelescope = IUFindText(activeDevices, "ACTIVE_TELESCOPE"); if (activeTelescope) { IUSaveText(activeTelescope, currentTelescope->getDeviceName().toLatin1().constData()); oneCCD->getDriverInfo()->getClientManager()->sendNewText(activeDevices); } } } } } void Capture::saveFITSDirectory() { QString dir = QFileDialog::getExistingDirectory(KStars::Instance(), i18n("FITS Save Directory"), dirPath.toLocalFile()); if (dir.isEmpty()) return; fitsDir->setText(dir); } void Capture::loadSequenceQueue() { QUrl fileURL = QFileDialog::getOpenFileUrl(KStars::Instance(), i18n("Open Ekos Sequence Queue"), dirPath, "Ekos Sequence Queue (*.esq)"); if (fileURL.isEmpty()) return; if (fileURL.isValid() == false) { QString message = i18n("Invalid URL: %1", fileURL.toLocalFile()); KSNotification::sorry(message, i18n("Invalid URL")); return; } dirPath = QUrl(fileURL.url(QUrl::RemoveFilename)); loadSequenceQueue(fileURL.toLocalFile()); } bool Capture::loadSequenceQueue(const QString &fileURL) { QFile sFile(fileURL); if (!sFile.open(QIODevice::ReadOnly)) { QString message = i18n("Unable to open file %1", fileURL); KSNotification::sorry(message, i18n("Could Not Open File")); return false; } capturedFramesMap.clear(); clearSequenceQueue(); LilXML * xmlParser = newLilXML(); char errmsg[MAXRBUF]; XMLEle * root = nullptr; XMLEle * ep = nullptr; char c; // We expect all data read from the XML to be in the C locale - QLocale::c(). QLocale cLocale = QLocale::c(); while (sFile.getChar(&c)) { root = readXMLEle(xmlParser, c, errmsg); if (root) { double sqVersion = cLocale.toFloat(findXMLAttValu(root, "version")); if (sqVersion < SQ_COMPAT_VERSION) { appendLogText(i18n("Deprecated sequence file format version %1. Please construct a new sequence file.", sqVersion)); return false; } for (ep = nextXMLEle(root, 1); ep != nullptr; ep = nextXMLEle(root, 0)) { if (!strcmp(tagXMLEle(ep), "Observer")) { m_ObserverName = QString(pcdataXMLEle(ep)); } else if (!strcmp(tagXMLEle(ep), "GuideDeviation")) { guideDeviationCheck->setChecked(!strcmp(findXMLAttValu(ep, "enabled"), "true")); guideDeviation->setValue(cLocale.toDouble(pcdataXMLEle(ep))); } else if (!strcmp(tagXMLEle(ep), "Autofocus")) { autofocusCheck->setChecked(!strcmp(findXMLAttValu(ep, "enabled"), "true")); double const HFRValue = cLocale.toDouble(pcdataXMLEle(ep)); // Set the HFR value from XML, or reset it to zero, don't let another unrelated older HFR be used // Note that HFR value will only be serialized to XML when option "Save Sequence HFR to File" is enabled fileHFR = HFRValue > 0.0 ? HFRValue : 0.0; HFRPixels->setValue(fileHFR); } else if (!strcmp(tagXMLEle(ep), "RefocusEveryN")) { refocusEveryNCheck->setChecked(!strcmp(findXMLAttValu(ep, "enabled"), "true")); int const minutesValue = cLocale.toInt(pcdataXMLEle(ep)); // Set the refocus period from XML, or reset it to zero, don't let another unrelated older refocus period be used. refocusEveryNMinutesValue = minutesValue > 0 ? minutesValue : 0; refocusEveryN->setValue(refocusEveryNMinutesValue); } else if (!strcmp(tagXMLEle(ep), "MeridianFlip")) { // meridian flip is managed by the mount only // older files might nevertheless contain MF settings if (! strcmp(findXMLAttValu(ep, "enabled"), "true")) appendLogText( i18n("Meridian flip configuration has been shifted to the mount module. Please configure the meridian flip there.")); } else if (!strcmp(tagXMLEle(ep), "CCD")) { CCDCaptureCombo->setCurrentText(pcdataXMLEle(ep)); // Signal "activated" of QComboBox does not fire when changing the text programmatically setCamera(pcdataXMLEle(ep)); } else if (!strcmp(tagXMLEle(ep), "FilterWheel")) { FilterDevicesCombo->setCurrentText(pcdataXMLEle(ep)); checkFilter(); } else { processJobInfo(ep); } } delXMLEle(root); } else if (errmsg[0]) { appendLogText(QString(errmsg)); delLilXML(xmlParser); return false; } } m_SequenceURL = QUrl::fromLocalFile(fileURL); m_Dirty = false; delLilXML(xmlParser); // update save button tool tip queueSaveB->setToolTip("Save to " + sFile.fileName()); return true; } bool Capture::processJobInfo(XMLEle * root) { XMLEle * ep; XMLEle * subEP; rotatorSettings->setRotationEnforced(false); QLocale cLocale = QLocale::c(); for (ep = nextXMLEle(root, 1); ep != nullptr; ep = nextXMLEle(root, 0)) { if (!strcmp(tagXMLEle(ep), "Exposure")) exposureIN->setValue(cLocale.toDouble(pcdataXMLEle(ep))); else if (!strcmp(tagXMLEle(ep), "Binning")) { subEP = findXMLEle(ep, "X"); if (subEP) binXIN->setValue(cLocale.toInt(pcdataXMLEle(subEP))); subEP = findXMLEle(ep, "Y"); if (subEP) binYIN->setValue(cLocale.toInt(pcdataXMLEle(subEP))); } else if (!strcmp(tagXMLEle(ep), "Frame")) { subEP = findXMLEle(ep, "X"); if (subEP) frameXIN->setValue(cLocale.toInt(pcdataXMLEle(subEP))); subEP = findXMLEle(ep, "Y"); if (subEP) frameYIN->setValue(cLocale.toInt(pcdataXMLEle(subEP))); subEP = findXMLEle(ep, "W"); if (subEP) frameWIN->setValue(cLocale.toInt(pcdataXMLEle(subEP))); subEP = findXMLEle(ep, "H"); if (subEP) frameHIN->setValue(cLocale.toInt(pcdataXMLEle(subEP))); } else if (!strcmp(tagXMLEle(ep), "Temperature")) { if (temperatureIN->isEnabled()) temperatureIN->setValue(cLocale.toDouble(pcdataXMLEle(ep))); // If force attribute exist, we change temperatureCheck, otherwise do nothing. if (!strcmp(findXMLAttValu(ep, "force"), "true")) temperatureCheck->setChecked(true); else if (!strcmp(findXMLAttValu(ep, "force"), "false")) temperatureCheck->setChecked(false); } else if (!strcmp(tagXMLEle(ep), "Filter")) { //FilterPosCombo->setCurrentIndex(atoi(pcdataXMLEle(ep))-1); FilterPosCombo->setCurrentText(pcdataXMLEle(ep)); } else if (!strcmp(tagXMLEle(ep), "Type")) { frameTypeCombo->setCurrentText(pcdataXMLEle(ep)); } else if (!strcmp(tagXMLEle(ep), "Prefix")) { subEP = findXMLEle(ep, "RawPrefix"); if (subEP) prefixIN->setText(pcdataXMLEle(subEP)); subEP = findXMLEle(ep, "FilterEnabled"); if (subEP) filterCheck->setChecked(!strcmp("1", pcdataXMLEle(subEP))); subEP = findXMLEle(ep, "ExpEnabled"); if (subEP) expDurationCheck->setChecked(!strcmp("1", pcdataXMLEle(subEP))); subEP = findXMLEle(ep, "TimeStampEnabled"); if (subEP) ISOCheck->setChecked(!strcmp("1", pcdataXMLEle(subEP))); } else if (!strcmp(tagXMLEle(ep), "Count")) { countIN->setValue(cLocale.toInt(pcdataXMLEle(ep))); } else if (!strcmp(tagXMLEle(ep), "Delay")) { delayIN->setValue(cLocale.toInt(pcdataXMLEle(ep))); } else if (!strcmp(tagXMLEle(ep), "PostCaptureScript")) { postCaptureScriptIN->setText(pcdataXMLEle(ep)); } else if (!strcmp(tagXMLEle(ep), "FITSDirectory")) { fitsDir->setText(pcdataXMLEle(ep)); } else if (!strcmp(tagXMLEle(ep), "RemoteDirectory")) { remoteDirIN->setText(pcdataXMLEle(ep)); } else if (!strcmp(tagXMLEle(ep), "UploadMode")) { uploadModeCombo->setCurrentIndex(cLocale.toInt(pcdataXMLEle(ep))); } else if (!strcmp(tagXMLEle(ep), "ISOIndex")) { if (ISOCombo) ISOCombo->setCurrentIndex(cLocale.toInt(pcdataXMLEle(ep))); } else if (!strcmp(tagXMLEle(ep), "FormatIndex")) { transferFormatCombo->setCurrentIndex(cLocale.toInt(pcdataXMLEle(ep))); } else if (!strcmp(tagXMLEle(ep), "Rotation")) { rotatorSettings->setRotationEnforced(true); rotatorSettings->setTargetRotationPA(cLocale.toDouble(pcdataXMLEle(ep))); } else if (!strcmp(tagXMLEle(ep), "Properties")) { QMap> propertyMap; for (subEP = nextXMLEle(ep, 1); subEP != nullptr; subEP = nextXMLEle(ep, 0)) { QMap numbers; XMLEle * oneNumber = nullptr; for (oneNumber = nextXMLEle(subEP, 1); oneNumber != nullptr; oneNumber = nextXMLEle(subEP, 0)) { const char * name = findXMLAttValu(oneNumber, "name"); numbers[name] = cLocale.toDouble(pcdataXMLEle(oneNumber)); } const char * name = findXMLAttValu(subEP, "name"); propertyMap[name] = numbers; } customPropertiesDialog->setCustomProperties(propertyMap); const double gain = getGain(); if (GainSpin && gain >= 0) GainSpin->setValue(gain); } else if (!strcmp(tagXMLEle(ep), "Calibration")) { subEP = findXMLEle(ep, "FlatSource"); if (subEP) { XMLEle * typeEP = findXMLEle(subEP, "Type"); if (typeEP) { if (!strcmp(pcdataXMLEle(typeEP), "Manual")) flatFieldSource = SOURCE_MANUAL; else if (!strcmp(pcdataXMLEle(typeEP), "FlatCap")) flatFieldSource = SOURCE_FLATCAP; else if (!strcmp(pcdataXMLEle(typeEP), "DarkCap")) flatFieldSource = SOURCE_DARKCAP; else if (!strcmp(pcdataXMLEle(typeEP), "Wall")) { XMLEle * azEP = findXMLEle(subEP, "Az"); XMLEle * altEP = findXMLEle(subEP, "Alt"); if (azEP && altEP) { flatFieldSource = SOURCE_WALL; wallCoord.setAz(cLocale.toDouble(pcdataXMLEle(azEP))); wallCoord.setAlt(cLocale.toDouble(pcdataXMLEle(altEP))); } } else flatFieldSource = SOURCE_DAWN_DUSK; } } subEP = findXMLEle(ep, "FlatDuration"); if (subEP) { XMLEle * typeEP = findXMLEle(subEP, "Type"); if (typeEP) { if (!strcmp(pcdataXMLEle(typeEP), "Manual")) flatFieldDuration = DURATION_MANUAL; } XMLEle * aduEP = findXMLEle(subEP, "Value"); if (aduEP) { flatFieldDuration = DURATION_ADU; targetADU = cLocale.toDouble(pcdataXMLEle(aduEP)); } aduEP = findXMLEle(subEP, "Tolerance"); if (aduEP) { targetADUTolerance = cLocale.toDouble(pcdataXMLEle(aduEP)); } } subEP = findXMLEle(ep, "PreMountPark"); if (subEP) { if (!strcmp(pcdataXMLEle(subEP), "True")) preMountPark = true; else preMountPark = false; } subEP = findXMLEle(ep, "PreDomePark"); if (subEP) { if (!strcmp(pcdataXMLEle(subEP), "True")) preDomePark = true; else preDomePark = false; } } } addJob(false); return true; } void Capture::saveSequenceQueue() { QUrl backupCurrent = m_SequenceURL; if (m_SequenceURL.toLocalFile().startsWith(QLatin1String("/tmp/")) || m_SequenceURL.toLocalFile().contains("/Temp")) m_SequenceURL.clear(); // If no changes made, return. if (m_Dirty == false && !m_SequenceURL.isEmpty()) return; if (m_SequenceURL.isEmpty()) { m_SequenceURL = QFileDialog::getSaveFileUrl(KStars::Instance(), i18n("Save Ekos Sequence Queue"), dirPath, "Ekos Sequence Queue (*.esq)"); // if user presses cancel if (m_SequenceURL.isEmpty()) { m_SequenceURL = backupCurrent; return; } dirPath = QUrl(m_SequenceURL.url(QUrl::RemoveFilename)); if (m_SequenceURL.toLocalFile().endsWith(QLatin1String(".esq")) == false) m_SequenceURL.setPath(m_SequenceURL.toLocalFile() + ".esq"); /*if (QFile::exists(sequenceURL.toLocalFile())) { int r = KMessageBox::warningContinueCancel(0, i18n("A file named \"%1\" already exists. " "Overwrite it?", sequenceURL.fileName()), i18n("Overwrite File?"), KStandardGuiItem::overwrite()); if (r == KMessageBox::Cancel) return; }*/ } if (m_SequenceURL.isValid()) { if ((saveSequenceQueue(m_SequenceURL.toLocalFile())) == false) { KSNotification::error(i18n("Failed to save sequence queue"), i18n("Save")); return; } m_Dirty = false; } else { QString message = i18n("Invalid URL: %1", m_SequenceURL.url()); KSNotification::sorry(message, i18n("Invalid URL")); } } void Capture::saveSequenceQueueAs() { m_SequenceURL.clear(); saveSequenceQueue(); } bool Capture::saveSequenceQueue(const QString &path) { QFile file; QString rawPrefix; bool filterEnabled, expEnabled, tsEnabled; const QMap frameTypes = { { "Light", FRAME_LIGHT }, { "Dark", FRAME_DARK }, { "Bias", FRAME_BIAS }, { "Flat", FRAME_FLAT } }; file.setFileName(path); if (!file.open(QIODevice::WriteOnly)) { QString message = i18n("Unable to write to file %1", path); KSNotification::sorry(message, i18n("Could not open file")); return false; } QTextStream outstream(&file); // We serialize sequence data to XML using the C locale QLocale cLocale = QLocale::c(); outstream << "" << endl; outstream << "" << endl; if (m_ObserverName.isEmpty() == false) outstream << "" << m_ObserverName << "" << endl; outstream << "" << CCDCaptureCombo->currentText() << "" << endl; outstream << "" << FilterDevicesCombo->currentText() << "" << endl; outstream << "" << cLocale.toString(guideDeviation->value()) << "" << endl; // Issue a warning when autofocus is enabled but Ekos options prevent HFR value from being written if (autofocusCheck->isChecked() && !Options::saveHFRToFile()) appendLogText(i18n( "Warning: HFR-based autofocus is set but option \"Save Sequence HFR Value to File\" is not enabled. " "Current HFR value will not be written to sequence file.")); outstream << "" << cLocale.toString(Options::saveHFRToFile() ? HFRPixels->value() : 0) << "" << endl; outstream << "" << cLocale.toString(refocusEveryN->value()) << "" << endl; foreach (SequenceJob * job, jobs) { job->getPrefixSettings(rawPrefix, filterEnabled, expEnabled, tsEnabled); outstream << "" << endl; outstream << "" << cLocale.toString(job->getExposure()) << "" << endl; outstream << "" << endl; outstream << "" << cLocale.toString(job->getXBin()) << "" << endl; outstream << "" << cLocale.toString(job->getXBin()) << "" << endl; outstream << "" << endl; outstream << "" << endl; outstream << "" << cLocale.toString(job->getSubX()) << "" << endl; outstream << "" << cLocale.toString(job->getSubY()) << "" << endl; outstream << "" << cLocale.toString(job->getSubW()) << "" << endl; outstream << "" << cLocale.toString(job->getSubH()) << "" << endl; outstream << "" << endl; if (job->getTargetTemperature() != Ekos::INVALID_VALUE) outstream << "" << cLocale.toString(job->getTargetTemperature()) << "" << endl; if (job->getTargetFilter() >= 0) //outstream << "" << job->getTargetFilter() << "" << endl; outstream << "" << job->getFilterName() << "" << endl; outstream << "" << frameTypes.key(job->getFrameType()) << "" << endl; outstream << "" << endl; //outstream << "" << job->getPrefix() << "" << endl; outstream << "" << rawPrefix << "" << endl; outstream << "" << (filterEnabled ? 1 : 0) << "" << endl; outstream << "" << (expEnabled ? 1 : 0) << "" << endl; outstream << "" << (tsEnabled ? 1 : 0) << "" << endl; outstream << "" << endl; outstream << "" << cLocale.toString(job->getCount()) << "" << endl; // ms to seconds outstream << "" << cLocale.toString(job->getDelay() / 1000.0) << "" << endl; if (job->getPostCaptureScript().isEmpty() == false) outstream << "" << job->getPostCaptureScript() << "" << endl; outstream << "" << job->getLocalDir() << "" << endl; outstream << "" << job->getUploadMode() << "" << endl; if (job->getRemoteDir().isEmpty() == false) outstream << "" << job->getRemoteDir() << "" << endl; if (job->getISOIndex() != -1) outstream << "" << (job->getISOIndex()) << "" << endl; outstream << "" << (job->getTransforFormat()) << "" << endl; if (job->getTargetRotation() != Ekos::INVALID_VALUE) outstream << "" << (job->getTargetRotation()) << "" << endl; QMapIterator> customIter(job->getCustomProperties()); outstream << "" << endl; while (customIter.hasNext()) { customIter.next(); outstream << "" << endl; QMap numbers = customIter.value(); QMapIterator numberIter(numbers); while (numberIter.hasNext()) { numberIter.next(); outstream << "" << cLocale.toString(numberIter.value()) << "" << endl; } outstream << "" << endl; } outstream << "" << endl; outstream << "" << endl; outstream << "" << endl; if (job->getFlatFieldSource() == SOURCE_MANUAL) outstream << "Manual" << endl; else if (job->getFlatFieldSource() == SOURCE_FLATCAP) outstream << "FlatCap" << endl; else if (job->getFlatFieldSource() == SOURCE_DARKCAP) outstream << "DarkCap" << endl; else if (job->getFlatFieldSource() == SOURCE_WALL) { outstream << "Wall" << endl; outstream << "" << cLocale.toString(job->getWallCoord().az().Degrees()) << "" << endl; outstream << "" << cLocale.toString(job->getWallCoord().alt().Degrees()) << "" << endl; } else outstream << "DawnDust" << endl; outstream << "" << endl; outstream << "" << endl; if (job->getFlatFieldDuration() == DURATION_MANUAL) outstream << "Manual" << endl; else { outstream << "ADU" << endl; outstream << "" << cLocale.toString(job->getTargetADU()) << "" << endl; outstream << "" << cLocale.toString(job->getTargetADUTolerance()) << "" << endl; } outstream << "" << endl; outstream << "" << (job->isPreMountPark() ? "True" : "False") << "" << endl; outstream << "" << (job->isPreDomePark() ? "True" : "False") << "" << endl; outstream << "" << endl; outstream << "" << endl; } outstream << "" << endl; appendLogText(i18n("Sequence queue saved to %1", path)); file.flush(); file.close(); // update save button tool tip queueSaveB->setToolTip("Save to " + file.fileName()); return true; } void Capture::resetJobs() { // Stop any running capture stop(); // If a job is selected for edit, reset only that job if (m_JobUnderEdit == true) { SequenceJob * job = jobs.at(queueTable->currentRow()); if (nullptr != job) job->resetStatus(); } else { if (KMessageBox::warningContinueCancel( nullptr, i18n("Are you sure you want to reset status of all jobs?"), i18n("Reset job status"), KStandardGuiItem::cont(), KStandardGuiItem::cancel(), "reset_job_status_warning") != KMessageBox::Continue) { return; } foreach (SequenceJob * job, jobs) job->resetStatus(); } // Also reset the storage count for all jobs capturedFramesMap.clear(); // We're not controlled by the Scheduler, restore progress option ignoreJobProgress = Options::alwaysResetSequenceWhenStarting(); } void Capture::ignoreSequenceHistory() { // This function is called independently from the Scheduler or the UI, so honor the change ignoreJobProgress = true; } void Capture::syncGUIToJob(SequenceJob * job) { QString rawPrefix; bool filterEnabled, expEnabled, tsEnabled; job->getPrefixSettings(rawPrefix, filterEnabled, expEnabled, tsEnabled); exposureIN->setValue(job->getExposure()); binXIN->setValue(job->getXBin()); binYIN->setValue(job->getYBin()); frameXIN->setValue(job->getSubX()); frameYIN->setValue(job->getSubY()); frameWIN->setValue(job->getSubW()); frameHIN->setValue(job->getSubH()); FilterPosCombo->setCurrentIndex(job->getTargetFilter() - 1); frameTypeCombo->setCurrentIndex(job->getFrameType()); prefixIN->setText(rawPrefix); filterCheck->setChecked(filterEnabled); expDurationCheck->setChecked(expEnabled); ISOCheck->setChecked(tsEnabled); countIN->setValue(job->getCount()); delayIN->setValue(job->getDelay() / 1000); postCaptureScriptIN->setText(job->getPostCaptureScript()); uploadModeCombo->setCurrentIndex(job->getUploadMode()); remoteDirIN->setEnabled(uploadModeCombo->currentIndex() != 0); remoteDirIN->setText(job->getRemoteDir()); fitsDir->setText(job->getLocalDir()); // Temperature Options temperatureCheck->setChecked(job->getEnforceTemperature()); if (job->getEnforceTemperature()) temperatureIN->setValue(job->getTargetTemperature()); // Flat field options calibrationB->setEnabled(job->getFrameType() != FRAME_LIGHT); flatFieldDuration = job->getFlatFieldDuration(); flatFieldSource = job->getFlatFieldSource(); targetADU = job->getTargetADU(); targetADUTolerance = job->getTargetADUTolerance(); wallCoord = job->getWallCoord(); preMountPark = job->isPreMountPark(); preDomePark = job->isPreDomePark(); // Custom Properties customPropertiesDialog->setCustomProperties(job->getCustomProperties()); if (ISOCombo) ISOCombo->setCurrentIndex(job->getISOIndex()); if (GainSpin) { double value = getGain(); if (value >= 0) GainSpin->setValue(value); else GainSpin->setValue(GainSpinSpecialValue); } transferFormatCombo->setCurrentIndex(job->getTransforFormat()); if (job->getTargetRotation() != Ekos::INVALID_VALUE) { rotatorSettings->setRotationEnforced(true); rotatorSettings->setTargetRotationPA(job->getTargetRotation()); } else rotatorSettings->setRotationEnforced(false); emit settingsUpdated(getSettings()); } QJsonObject Capture::getSettings() { QJsonObject settings; // Try to get settings value // if not found, fallback to camera value double gain = -1; if (GainSpin && GainSpin->value() != GainSpinSpecialValue) gain = GainSpin->value(); else if (currentCCD && currentCCD->hasGain()) currentCCD->getGain(&gain); int iso = -1; if (ISOCombo) iso = ISOCombo->currentIndex(); else if (currentCCD) iso = currentCCD->getChip(ISD::CCDChip::PRIMARY_CCD)->getISOIndex(); settings.insert("camera", CCDCaptureCombo->currentText()); settings.insert("fw", FilterDevicesCombo->currentText()); settings.insert("filter", FilterPosCombo->currentText()); settings.insert("exp", exposureIN->value()); settings.insert("bin", binXIN->value()); settings.insert("iso", iso); settings.insert("frameType", frameTypeCombo->currentIndex()); settings.insert("format", transferFormatCombo->currentIndex()); settings.insert("gain", gain); settings.insert("temperature", temperatureIN->value()); return settings; } void Capture::selectedJobChanged(QModelIndex current, QModelIndex previous) { Q_UNUSED(previous) selectJob(current); } void Capture::selectJob(QModelIndex i) { if (i.row() < 0 || (i.row() + 1) > jobs.size()) return; SequenceJob * job = jobs.at(i.row()); if (job == nullptr) return; syncGUIToJob(job); if (isBusy || jobs.size() < 2) return; queueUpB->setEnabled(i.row() > 0); queueDownB->setEnabled(i.row() + 1 < jobs.size()); } void Capture::editJob(QModelIndex i) { selectJob(i); appendLogText(i18n("Editing job #%1...", i.row() + 1)); addToQueueB->setIcon(QIcon::fromTheme("dialog-ok-apply")); addToQueueB->setToolTip(i18n("Apply job changes.")); removeFromQueueB->setToolTip(i18n("Cancel job changes.")); m_JobUnderEdit = true; } void Capture::resetJobEdit() { if (m_JobUnderEdit) appendLogText(i18n("Editing job canceled.")); m_JobUnderEdit = false; addToQueueB->setIcon(QIcon::fromTheme("list-add")); addToQueueB->setToolTip(i18n("Add job to sequence queue")); removeFromQueueB->setToolTip(i18n("Remove job from sequence queue")); } void Capture::constructPrefix(QString &imagePrefix) { if (imagePrefix.isEmpty() == false) imagePrefix += '_'; imagePrefix += frameTypeCombo->currentText(); /*if (filterCheck->isChecked() && FilterPosCombo->currentText().isEmpty() == false && frameTypeCombo->currentText().compare("Bias", Qt::CaseInsensitive) && frameTypeCombo->currentText().compare("Dark", Qt::CaseInsensitive))*/ if (filterCheck->isChecked() && FilterPosCombo->currentText().isEmpty() == false && (frameTypeCombo->currentIndex() == FRAME_LIGHT || frameTypeCombo->currentIndex() == FRAME_FLAT)) { imagePrefix += '_'; imagePrefix += FilterPosCombo->currentText(); } if (expDurationCheck->isChecked()) { //if (imagePrefix.isEmpty() == false || frameTypeCheck->isChecked()) imagePrefix += '_'; double exposureValue = exposureIN->value(); // Don't use the locale for exposure value in the capture file name, so that we get a "." as decimal separator if (exposureValue == static_cast(exposureValue)) // Whole number imagePrefix += QString::number(exposureIN->value(), 'd', 0) + QString("_secs"); else { // Decimal if (exposureIN->value() >= 0.001) imagePrefix += QString::number(exposureIN->value(), 'f', 3) + QString("_secs"); else imagePrefix += QString::number(exposureIN->value(), 'f', 6) + QString("_secs"); } } if (ISOCheck->isChecked()) { imagePrefix += SequenceJob::ISOMarker; } } double Capture::getProgressPercentage() { int totalImageCount = 0; int totalImageCompleted = 0; foreach (SequenceJob * job, jobs) { totalImageCount += job->getCount(); totalImageCompleted += job->getCompleted(); } if (totalImageCount != 0) return ((static_cast(totalImageCompleted) / totalImageCount) * 100.0); else return -1; } int Capture::getActiveJobID() { if (activeJob == nullptr) return -1; for (int i = 0; i < jobs.count(); i++) { if (activeJob == jobs[i]) return i; } return -1; } int Capture::getPendingJobCount() { int completedJobs = 0; foreach (SequenceJob * job, jobs) { if (job->getStatus() == SequenceJob::JOB_DONE) completedJobs++; } return (jobs.count() - completedJobs); } QString Capture::getJobState(int id) { if (id < jobs.count()) { SequenceJob * job = jobs.at(id); return job->getStatusString(); } return QString(); } int Capture::getJobImageProgress(int id) { if (id < jobs.count()) { SequenceJob * job = jobs.at(id); return job->getCompleted(); } return -1; } int Capture::getJobImageCount(int id) { if (id < jobs.count()) { SequenceJob * job = jobs.at(id); return job->getCount(); } return -1; } double Capture::getJobExposureProgress(int id) { if (id < jobs.count()) { SequenceJob * job = jobs.at(id); return job->getExposeLeft(); } return -1; } double Capture::getJobExposureDuration(int id) { if (id < jobs.count()) { SequenceJob * job = jobs.at(id); return job->getExposure(); } return -1; } int Capture::getJobRemainingTime(SequenceJob * job) { int remaining = (job->getExposure() + getEstimatedDownloadTime() + job->getDelay() / 1000) * (job->getCount() - job->getCompleted()); if (job->getStatus() == SequenceJob::JOB_BUSY) remaining += job->getExposeLeft() + getEstimatedDownloadTime(); return remaining; } int Capture::getOverallRemainingTime() { double remaining = 0; foreach (SequenceJob * job, jobs) remaining += getJobRemainingTime(job); return remaining; } int Capture::getActiveJobRemainingTime() { if (activeJob == nullptr) return -1; return getJobRemainingTime(activeJob); } void Capture::setMaximumGuidingDeviation(bool enable, double value) { guideDeviationCheck->setChecked(enable); if (enable) guideDeviation->setValue(value); } void Capture::setInSequenceFocus(bool enable, double HFR) { autofocusCheck->setChecked(enable); if (enable) HFRPixels->setValue(HFR); } void Capture::setTargetTemperature(double temperature) { temperatureIN->setValue(temperature); } void Capture::clearSequenceQueue() { activeJob = nullptr; //m_TargetName.clear(); //stop(); while (queueTable->rowCount() > 0) queueTable->removeRow(0); qDeleteAll(jobs); jobs.clear(); } QString Capture::getSequenceQueueStatus() { if (jobs.count() == 0) return "Invalid"; if (isBusy) return "Running"; int idle = 0, error = 0, complete = 0, aborted = 0, running = 0; foreach (SequenceJob * job, jobs) { switch (job->getStatus()) { case SequenceJob::JOB_ABORTED: aborted++; break; case SequenceJob::JOB_BUSY: running++; break; case SequenceJob::JOB_DONE: complete++; break; case SequenceJob::JOB_ERROR: error++; break; case SequenceJob::JOB_IDLE: idle++; break; } } if (error > 0) return "Error"; if (aborted > 0) { if (m_State == CAPTURE_SUSPENDED) return "Suspended"; else return "Aborted"; } if (running > 0) return "Running"; if (idle == jobs.count()) return "Idle"; if (complete == jobs.count()) return "Complete"; return "Invalid"; } void Capture::processTelescopeNumber(INumberVectorProperty * nvp) { // If it is not ours, return. if (nvp->device != currentTelescope->getDeviceName() || strstr(nvp->name, "EQUATORIAL_") == nullptr) return; switch (meridianFlipStage) { case MF_NONE: break; case MF_INITIATED: { if (nvp->s == IPS_BUSY) setMeridianFlipStage(MF_FLIPPING); } break; case MF_FLIPPING: { if (currentTelescope != nullptr && currentTelescope->isSlewing()) setMeridianFlipStage(MF_SLEWING); } break; default: break; } } void Capture::processFlipCompleted() { // If dome is syncing, wait until it stops if (currentDome && currentDome->isMoving()) return; appendLogText(i18n("Telescope completed the meridian flip.")); //KNotification::event(QLatin1String("MeridianFlipCompleted"), i18n("Meridian flip is successfully completed")); KSNotification::event(QLatin1String("MeridianFlipCompleted"), i18n("Meridian flip is successfully completed"), KSNotification::EVENT_INFO); // resume only if capturing was running if (m_State == CAPTURE_IDLE || m_State == CAPTURE_ABORTED || m_State == CAPTURE_COMPLETE || m_State == CAPTURE_PAUSED) return; if (resumeAlignmentAfterFlip == true) { appendLogText(i18n("Performing post flip re-alignment...")); secondsLabel->setText(i18n("Aligning...")); retries = 0; m_State = CAPTURE_ALIGNING; emit newStatus(Ekos::CAPTURE_ALIGNING); setMeridianFlipStage(MF_ALIGNING); //QTimer::singleShot(Options::settlingTime(), [this]() {emit meridialFlipTracked();}); //emit meridialFlipTracked(); return; } retries = 0; checkGuidingAfterFlip(); } void Capture::checkGuidingAfterFlip() { // If we're not autoguiding then we're done if (resumeGuidingAfterFlip == false) { resumeSequence(); // N.B. Set meridian flip stage AFTER resumeSequence() always setMeridianFlipStage(MF_NONE); } else { appendLogText(i18n("Performing post flip re-calibration and guiding...")); secondsLabel->setText(i18n("Calibrating...")); m_State = CAPTURE_CALIBRATING; emit newStatus(Ekos::CAPTURE_CALIBRATING); setMeridianFlipStage(MF_GUIDING); emit meridianFlipCompleted(); } } bool Capture::checkPausing() { if (m_State == CAPTURE_PAUSE_PLANNED) { appendLogText(i18n("Sequence paused.")); secondsLabel->setText(i18n("Paused...")); m_State = CAPTURE_PAUSED; // handle a requested meridian flip if (meridianFlipStage != MF_NONE) setMeridianFlipStage(MF_READY); // pause return true; } // no pause return false; } bool Capture::checkMeridianFlip() { if (currentTelescope == nullptr) return false; // If active job is taking flat field image at a wall source // then do not flip. if (activeJob && activeJob->getFrameType() == FRAME_FLAT && activeJob->getFlatFieldSource() == SOURCE_WALL) return false; if (meridianFlipStage != MF_REQUESTED) // if no flip has been requested or is already ongoing return false; // meridian flip requested or already in action // Reset frame if we need to do focusing later on if (isInSequenceFocus || (refocusEveryNCheck->isChecked() && getRefocusEveryNTimerElapsedSec() > 0)) emit resetFocus(); // signal that meridian flip may take place if (meridianFlipStage == MF_REQUESTED) setMeridianFlipStage(MF_READY); return true; } void Capture::checkGuideDeviationTimeout() { if (activeJob && activeJob->getStatus() == SequenceJob::JOB_ABORTED && m_DeviationDetected) { appendLogText(i18n("Guide module timed out.")); m_DeviationDetected = false; // If capture was suspended, it should be aborted (failed) now. if (m_State == CAPTURE_SUSPENDED) { m_State = CAPTURE_ABORTED; emit newStatus(m_State); } } } void Capture::setAlignStatus(AlignState state) { qCDebug(KSTARS_EKOS_CAPTURE) << "setAlignStatus: " << state; alignState = state; resumeAlignmentAfterFlip = true; switch (state) { case ALIGN_COMPLETE: if (meridianFlipStage == MF_ALIGNING) { appendLogText(i18n("Post flip re-alignment completed successfully.")); retries = 0; checkGuidingAfterFlip(); } break; case ALIGN_ABORTED: case ALIGN_FAILED: // TODO run it 3 times before giving up if (meridianFlipStage == MF_ALIGNING) { if (++retries == 3) { appendLogText(i18n("Post-flip alignment failed.")); abort(); } else { appendLogText(i18n("Post-flip alignment failed. Retrying...")); secondsLabel->setText(i18n("Aligning...")); this->m_State = CAPTURE_ALIGNING; emit newStatus(Ekos::CAPTURE_ALIGNING); setMeridianFlipStage(MF_ALIGNING); } } break; default: break; } } void Capture::setGuideStatus(GuideState state) { if (state != guideState) qCDebug(KSTARS_EKOS_CAPTURE) << "Guiding state changed from" << getGuideStatusString(guideState) << "to" << getGuideStatusString(state); switch (state) { case GUIDE_IDLE: case GUIDE_ABORTED: // If Autoguiding was started before and now stopped, let's abort (unless we're doing a meridian flip) if (isGuidingActive() && meridianFlipStage == MF_NONE && ((activeJob && activeJob->getStatus() == SequenceJob::JOB_BUSY) || this->m_State == CAPTURE_SUSPENDED || this->m_State == CAPTURE_PAUSED)) { appendLogText(i18n("Autoguiding stopped. Aborting...")); abort(); } break; case GUIDE_GUIDING: case GUIDE_CALIBRATION_SUCESS: autoGuideReady = true; break; case GUIDE_CALIBRATION_ERROR: // TODO try restarting calibration a couple of times before giving up if (meridianFlipStage == MF_GUIDING) { if (++retries == 3) { appendLogText(i18n("Post meridian flip calibration error. Aborting...")); abort(); } else { appendLogText(i18n("Post meridian flip calibration error. Restarting...")); checkGuidingAfterFlip(); } } autoGuideReady = false; break; case GUIDE_DITHERING_SUCCESS: qCInfo(KSTARS_EKOS_CAPTURE) << "Dithering succeeded, capture state" << getCaptureStatusString(m_State); // do nothing if something happened during dithering if (m_State != CAPTURE_DITHERING) break; if (Options::guidingSettle() > 0) { // N.B. Do NOT convert to i18np since guidingRate is DOUBLE value (e.g. 1.36) so we always use plural with that. appendLogText(i18n("Dither complete. Resuming capture in %1 seconds...", Options::guidingSettle())); QTimer::singleShot(Options::guidingSettle() * 1000, this, &Ekos::Capture::resumeCapture); } else { appendLogText(i18n("Dither complete.")); resumeCapture(); } break; case GUIDE_DITHERING_ERROR: qCInfo(KSTARS_EKOS_CAPTURE) << "Dithering failed, capture state" << getCaptureStatusString(m_State); if (m_State != CAPTURE_DITHERING) break; if (Options::guidingSettle() > 0) { // N.B. Do NOT convert to i18np since guidingRate is DOUBLE value (e.g. 1.36) so we always use plural with that. appendLogText(i18n("Warning: Dithering failed. Resuming capture in %1 seconds...", Options::guidingSettle())); QTimer::singleShot(Options::guidingSettle() * 1000, this, &Ekos::Capture::resumeCapture); } else { appendLogText(i18n("Warning: Dithering failed.")); resumeCapture(); } break; default: break; } guideState = state; } void Capture::checkFrameType(int index) { if (index == FRAME_LIGHT) calibrationB->setEnabled(false); else calibrationB->setEnabled(true); } double Capture::setCurrentADU(double value) { double nextExposure = 0; double targetADU = activeJob->getTargetADU(); std::vector coeff; // Check if saturated, then take shorter capture and discard value ExpRaw.append(activeJob->getExposure()); ADURaw.append(value); qCDebug(KSTARS_EKOS_CAPTURE) << "Capture: Current ADU = " << value << " targetADU = " << targetADU << " Exposure Count: " << ExpRaw.count(); // Most CCDs are quite linear so 1st degree polynomial is quite sufficient // But DSLRs can exhibit non-linear response curve and so a 2nd degree polynomial is more appropriate if (ExpRaw.count() >= 2) { if (ExpRaw.count() >= 5) { double chisq = 0; coeff = gsl_polynomial_fit(ADURaw.data(), ExpRaw.data(), ExpRaw.count(), 2, chisq); qCDebug(KSTARS_EKOS_CAPTURE) << "Running polynomial fitting. Found " << coeff.size() << " coefficients."; if (std::isnan(coeff[0]) || std::isinf(coeff[0])) { qCDebug(KSTARS_EKOS_CAPTURE) << "Coefficients are invalid."; targetADUAlgorithm = ADU_LEAST_SQUARES; } else { nextExposure = coeff[0] + (coeff[1] * targetADU) + (coeff[2] * pow(targetADU, 2)); // If exposure is not valid or does not make sense, then we fall back to least squares if (nextExposure < 0 || (nextExposure > ExpRaw.last() || targetADU < ADURaw.last()) || (nextExposure < ExpRaw.last() || targetADU > ADURaw.last())) { nextExposure = 0; targetADUAlgorithm = ADU_LEAST_SQUARES; } else { targetADUAlgorithm = ADU_POLYNOMIAL; for (size_t i = 0; i < coeff.size(); i++) qCDebug(KSTARS_EKOS_CAPTURE) << "Coeff #" << i << "=" << coeff[i]; } } } bool looping = false; if (ExpRaw.count() >= 10) { int size = ExpRaw.count(); looping = (std::fabs(ExpRaw[size - 1] - ExpRaw[size - 2] < 0.01)) && (std::fabs(ExpRaw[size - 2] - ExpRaw[size - 3] < 0.01)); if (looping && targetADUAlgorithm == ADU_POLYNOMIAL) { qWarning(KSTARS_EKOS_CAPTURE) << "Detected looping in polynomial results. Falling back to llsqr."; targetADUAlgorithm = ADU_LEAST_SQUARES; } } // If we get invalid data, let's fall back to llsq // Since polyfit can be unreliable at low counts, let's only use it at the 5th exposure // if we don't have results already. if (targetADUAlgorithm == ADU_LEAST_SQUARES) { double a = 0, b = 0; llsq(ExpRaw, ADURaw, a, b); // If we have valid results, let's calculate next exposure if (a != 0) { nextExposure = (targetADU - b) / a; // If we get invalid value, let's just proceed iteratively if (nextExposure < 0) nextExposure = 0; } } } if (nextExposure == 0) { if (value < targetADU) nextExposure = activeJob->getExposure() * 1.25; else nextExposure = activeJob->getExposure() * .75; } qCDebug(KSTARS_EKOS_CAPTURE) << "next flat exposure is" << nextExposure; return nextExposure; } // Based on John Burkardt LLSQ (LGPL) void Capture::llsq(QVector x, QVector y, double &a, double &b) { double bot; int i; double top; double xbar; double ybar; int n = x.count(); // // Special case. // if (n == 1) { a = 0.0; b = y[0]; return; } // // Average X and Y. // xbar = 0.0; ybar = 0.0; for (i = 0; i < n; i++) { xbar = xbar + x[i]; ybar = ybar + y[i]; } xbar = xbar / static_cast(n); ybar = ybar / static_cast(n); // // Compute Beta. // top = 0.0; bot = 0.0; for (i = 0; i < n; i++) { top = top + (x[i] - xbar) * (y[i] - ybar); bot = bot + (x[i] - xbar) * (x[i] - xbar); } a = top / bot; b = ybar - a * xbar; } void Capture::setDirty() { m_Dirty = true; } bool Capture::hasCoolerControl() { if (currentCCD && currentCCD->hasCoolerControl()) return true; return false; } bool Capture::setCoolerControl(bool enable) { if (currentCCD && currentCCD->hasCoolerControl()) return currentCCD->setCoolerControl(enable); return false; } void Capture::clearAutoFocusHFR() { // If HFR limit was set from file, we cannot override it. if (fileHFR > 0) return; HFRPixels->setValue(0); //firstAutoFocus = true; } void Capture::openCalibrationDialog() { QDialog calibrationDialog; Ui_calibrationOptions calibrationOptions; calibrationOptions.setupUi(&calibrationDialog); if (currentTelescope) { calibrationOptions.parkMountC->setEnabled(currentTelescope->canPark()); calibrationOptions.parkMountC->setChecked(preMountPark); } else calibrationOptions.parkMountC->setEnabled(false); if (currentDome) { calibrationOptions.parkDomeC->setEnabled(currentDome->canPark()); calibrationOptions.parkDomeC->setChecked(preDomePark); } else calibrationOptions.parkDomeC->setEnabled(false); //connect(calibrationOptions.wallSourceC, SIGNAL(toggled(bool)), calibrationOptions.parkC, &Ekos::Capture::setDisabled(bool))); switch (flatFieldSource) { case SOURCE_MANUAL: calibrationOptions.manualSourceC->setChecked(true); break; case SOURCE_FLATCAP: calibrationOptions.flatDeviceSourceC->setChecked(true); break; case SOURCE_DARKCAP: calibrationOptions.darkDeviceSourceC->setChecked(true); break; case SOURCE_WALL: calibrationOptions.wallSourceC->setChecked(true); calibrationOptions.azBox->setText(wallCoord.az().toDMSString()); calibrationOptions.altBox->setText(wallCoord.alt().toDMSString()); break; case SOURCE_DAWN_DUSK: calibrationOptions.dawnDuskFlatsC->setChecked(true); break; } switch (flatFieldDuration) { case DURATION_MANUAL: calibrationOptions.manualDurationC->setChecked(true); break; case DURATION_ADU: calibrationOptions.ADUC->setChecked(true); calibrationOptions.ADUValue->setValue(targetADU); calibrationOptions.ADUTolerance->setValue(targetADUTolerance); break; } if (calibrationDialog.exec() == QDialog::Accepted) { if (calibrationOptions.manualSourceC->isChecked()) flatFieldSource = SOURCE_MANUAL; else if (calibrationOptions.flatDeviceSourceC->isChecked()) flatFieldSource = SOURCE_FLATCAP; else if (calibrationOptions.darkDeviceSourceC->isChecked()) flatFieldSource = SOURCE_DARKCAP; else if (calibrationOptions.wallSourceC->isChecked()) { dms wallAz, wallAlt; bool azOk = false, altOk = false; wallAz = calibrationOptions.azBox->createDms(true, &azOk); wallAlt = calibrationOptions.altBox->createDms(true, &altOk); if (azOk && altOk) { flatFieldSource = SOURCE_WALL; wallCoord.setAz(wallAz); wallCoord.setAlt(wallAlt); } else { calibrationOptions.manualSourceC->setChecked(true); KSNotification::error(i18n("Wall coordinates are invalid.")); } } else flatFieldSource = SOURCE_DAWN_DUSK; if (calibrationOptions.manualDurationC->isChecked()) flatFieldDuration = DURATION_MANUAL; else { flatFieldDuration = DURATION_ADU; targetADU = calibrationOptions.ADUValue->value(); targetADUTolerance = calibrationOptions.ADUTolerance->value(); } preMountPark = calibrationOptions.parkMountC->isChecked(); preDomePark = calibrationOptions.parkDomeC->isChecked(); setDirty(); Options::setCalibrationFlatSourceIndex(flatFieldSource); Options::setCalibrationFlatDurationIndex(flatFieldDuration); Options::setCalibrationWallAz(wallCoord.az().Degrees()); Options::setCalibrationWallAlt(wallCoord.alt().Degrees()); Options::setCalibrationADUValue(targetADU); Options::setCalibrationADUValueTolerance(targetADUTolerance); } } IPState Capture::checkLightFrameAuxiliaryTasks() { // step 2: check if meridian flip already is ongoing if (meridianFlipStage != MF_NONE && meridianFlipStage != MF_READY) return IPS_BUSY; // step 3: check if meridian flip is required if (checkMeridianFlip()) return IPS_BUSY; // step 4: check if re-focusing is required if (m_State == CAPTURE_FOCUSING || startFocusIfRequired()) { m_State = CAPTURE_FOCUSING; return IPS_BUSY; } if (guideState == GUIDE_SUSPENDED) { appendLogText(i18n("Autoguiding resumed.")); emit resumeGuiding(); } calibrationStage = CAL_PRECAPTURE_COMPLETE; return IPS_OK; } IPState Capture::checkLightFramePendingTasks() { switch (activeJob->getFlatFieldSource()) { // All these are considered MANUAL when it comes to light frames case SOURCE_MANUAL: case SOURCE_DAWN_DUSK: case SOURCE_WALL: // If telescopes were MANUALLY covered before // we need to manually uncover them. if (m_TelescopeCoveredDarkExposure || m_TelescopeCoveredFlatExposure) { // Uncover telescope // N.B. This operation cannot be autonomous // if (KMessageBox::warningContinueCancel( // nullptr, i18n("Remove cover from the telescope in order to continue."), i18n("Telescope Covered"), // KStandardGuiItem::cont(), KStandardGuiItem::cancel(), // "uncover_scope_dialog_notification", KMessageBox::WindowModal | KMessageBox::Notify) == KMessageBox::Cancel) // { // return IPS_ALERT; // } // If we already asked for confirmation and waiting for it // let us see if the confirmation is fulfilled // otherwise we return. if (calibrationCheckType == CAL_CHECK_CONFIRMATION) return IPS_BUSY; // Otherwise, we ask user to confirm manually calibrationCheckType = CAL_CHECK_CONFIRMATION; connect(KSMessageBox::Instance(), &KSMessageBox::accepted, this, [this]() { //QObject::disconnect(KSMessageBox::Instance(), &KSMessageBox::accepted, this, nullptr); KSMessageBox::Instance()->disconnect(this); m_TelescopeCoveredDarkExposure = false; m_TelescopeCoveredFlatExposure = false; calibrationCheckType = CAL_CHECK_TASK; }); KSMessageBox::Instance()->warningContinueCancel(i18n("Remove cover from the telescope in order to continue."), i18n("Telescope Covered"), Options::manualCoverTimeout()); return IPS_BUSY; } break; case SOURCE_FLATCAP: case SOURCE_DARKCAP: if (currentDustCap == nullptr) { qCWarning(KSTARS_EKOS_CAPTURE) << "Skipping flat/dark cap since it is not connected."; break; } // { // appendLogText(i18n("Cap device is missing but the job requires flat or dark cap device.")); // return IPS_ALERT; // } // If dust cap HAS light and light is ON, then turn it off. if (currentDustCap->hasLight() && currentDustCap->isLightOn() == true) { dustCapLightEnabled = false; currentDustCap->SetLightEnabled(false); } // If cap is parked, we need to unpark it if (calibrationStage < CAL_DUSTCAP_UNPARKING && currentDustCap->isParked()) { if (currentDustCap->UnPark()) { calibrationStage = CAL_DUSTCAP_UNPARKING; appendLogText(i18n("Unparking dust cap...")); return IPS_BUSY; } else { appendLogText(i18n("Unparking dust cap failed, aborting...")); abort(); return IPS_ALERT; } } // Wait until cap is unparked if (calibrationStage == CAL_DUSTCAP_UNPARKING) { if (currentDustCap->isUnParked() == false) return IPS_BUSY; else { calibrationStage = CAL_DUSTCAP_UNPARKED; appendLogText(i18n("Dust cap unparked.")); } } break; } return checkLightFrameAuxiliaryTasks(); } IPState Capture::checkDarkFramePendingTasks() { QStringList shutterfulCCDs = Options::shutterfulCCDs(); QStringList shutterlessCCDs = Options::shutterlessCCDs(); QString deviceName = currentCCD->getDeviceName(); bool hasShutter = shutterfulCCDs.contains(deviceName); bool hasNoShutter = shutterlessCCDs.contains(deviceName) || (ISOCombo && ISOCombo->count() > 0); // If we have no information, we ask before we proceed. if (hasShutter == false && hasNoShutter == false) { // Awaiting user input if (calibrationCheckType == CAL_CHECK_CONFIRMATION) return IPS_BUSY; // // This action cannot be autonomous // if (KMessageBox::questionYesNo(nullptr, i18n("Does %1 have a shutter?", deviceName), // i18n("Dark Exposure")) == KMessageBox::Yes) // { // hasNoShutter = false; // shutterfulCCDs.append(deviceName); // Options::setShutterfulCCDs(shutterfulCCDs); // } // else // { // hasNoShutter = true; // shutterlessCCDs.append(deviceName); // Options::setShutterlessCCDs(shutterlessCCDs); // } connect(KSMessageBox::Instance(), &KSMessageBox::accepted, this, [&]() { //QObject::disconnect(KSMessageBox::Instance(), &KSMessageBox::accepted, this, nullptr); KSMessageBox::Instance()->disconnect(this); QStringList shutterfulCCDs = Options::shutterfulCCDs(); QString deviceName = currentCCD->getDeviceName(); shutterfulCCDs.append(deviceName); Options::setShutterfulCCDs(shutterfulCCDs); calibrationCheckType = CAL_CHECK_TASK; }); connect(KSMessageBox::Instance(), &KSMessageBox::rejected, this, [&]() { //QObject::disconnect(KSMessageBox::Instance(), &KSMessageBox::rejected, this, nullptr); KSMessageBox::Instance()->disconnect(this); QStringList shutterlessCCDs = Options::shutterlessCCDs(); QString deviceName = currentCCD->getDeviceName(); shutterlessCCDs.append(deviceName); Options::setShutterlessCCDs(shutterlessCCDs); calibrationCheckType = CAL_CHECK_TASK; }); calibrationCheckType = CAL_CHECK_CONFIRMATION; KSMessageBox::Instance()->questionYesNo(i18n("Does %1 have a shutter?", deviceName), i18n("Dark Exposure")); return IPS_BUSY; } switch (activeJob->getFlatFieldSource()) { // All these are manual when it comes to dark frames case SOURCE_MANUAL: case SOURCE_DAWN_DUSK: // For cameras without a shutter, we need to ask the user to cover the telescope // if the telescope is not already covered. if (hasNoShutter && !m_TelescopeCoveredDarkExposure) { if (calibrationCheckType == CAL_CHECK_CONFIRMATION) return IPS_BUSY; // Continue connect(KSMessageBox::Instance(), &KSMessageBox::accepted, this, [&]() { //QObject::disconnect(KSMessageBox::Instance(), &KSMessageBox::accepted, this, nullptr); KSMessageBox::Instance()->disconnect(this); m_TelescopeCoveredDarkExposure = true; m_TelescopeCoveredFlatExposure = false; calibrationCheckType = CAL_CHECK_TASK; }); // Cancel connect(KSMessageBox::Instance(), &KSMessageBox::rejected, this, [&]() { //QObject::disconnect(KSMessageBox::Instance(), &KSMessageBox::rejected, this, nullptr); KSMessageBox::Instance()->disconnect(this); calibrationCheckType = CAL_CHECK_TASK; abort(); }); // if (KMessageBox::warningContinueCancel( // nullptr, i18n("Cover the telescope in order to take a dark exposure."), i18n("Dark Exposure"), // KStandardGuiItem::cont(), KStandardGuiItem::cancel(), // "cover_scope_dialog_notification", KMessageBox::WindowModal | KMessageBox::Notify) == KMessageBox::Cancel) // { // abort(); // return IPS_ALERT; // } calibrationCheckType = CAL_CHECK_CONFIRMATION; KSMessageBox::Instance()->warningContinueCancel(i18n("Cover the telescope in order to take a dark exposure.") , i18n("Dark Exposure"), Options::manualCoverTimeout()); return IPS_BUSY; } break; case SOURCE_FLATCAP: case SOURCE_DARKCAP: if (currentDustCap == nullptr) { qCWarning(KSTARS_EKOS_CAPTURE) << "Skipping flat/dark cap since it is not connected."; break; } // When using a cap, we need to park, if not already parked. // Need to turn off light, if light exists and was on. // if (!currentDustCap) // { // appendLogText(i18n("Cap device is missing but the job requires flat or dark cap device.")); // abort(); // return IPS_ALERT; // } // If cap is not park, park it if (calibrationStage < CAL_DUSTCAP_PARKING && currentDustCap->isParked() == false) { if (currentDustCap->Park()) { calibrationStage = CAL_DUSTCAP_PARKING; appendLogText(i18n("Parking dust cap...")); return IPS_BUSY; } else { appendLogText(i18n("Parking dust cap failed, aborting...")); abort(); return IPS_ALERT; } } // Wait until cap is parked if (calibrationStage == CAL_DUSTCAP_PARKING) { if (currentDustCap->isParked() == false) return IPS_BUSY; else { calibrationStage = CAL_DUSTCAP_PARKED; appendLogText(i18n("Dust cap parked.")); } } // Turn off light if it exists and was on. if (currentDustCap->hasLight() && currentDustCap->isLightOn() == true) { dustCapLightEnabled = false; currentDustCap->SetLightEnabled(false); } break; case SOURCE_WALL: if (currentTelescope) { if (calibrationStage < CAL_SLEWING) { wallCoord = activeJob->getWallCoord(); wallCoord.HorizontalToEquatorial(KStarsData::Instance()->lst(), KStarsData::Instance()->geo()->lat()); currentTelescope->Slew(&wallCoord); appendLogText(i18n("Mount slewing to wall position...")); calibrationStage = CAL_SLEWING; return IPS_BUSY; } // Check if slewing is complete if (calibrationStage == CAL_SLEWING) { if (currentTelescope->isSlewing() == false) { // Disable mount tracking if supported by the driver. currentTelescope->setTrackEnabled(false); calibrationStage = CAL_SLEWING_COMPLETE; appendLogText(i18n("Slew to wall position complete.")); } else return IPS_BUSY; } if (currentLightBox && currentLightBox->isLightOn() == true) { lightBoxLightEnabled = false; currentLightBox->SetLightEnabled(false); } } break; } calibrationStage = CAL_PRECAPTURE_COMPLETE; return IPS_OK; } IPState Capture::checkFlatFramePendingTasks() { switch (activeJob->getFlatFieldSource()) { case SOURCE_MANUAL: // Manual mode we need to cover mount with evenly illuminated field. if (m_TelescopeCoveredFlatExposure == false) { if (calibrationCheckType == CAL_CHECK_CONFIRMATION) return IPS_BUSY; // This action cannot be autonomous // if (KMessageBox::warningContinueCancel( // nullptr, i18n("Cover telescope with evenly illuminated light source."), i18n("Flat Frame"), // KStandardGuiItem::cont(), KStandardGuiItem::cancel(), // "flat_light_cover_dialog_notification", KMessageBox::WindowModal | KMessageBox::Notify) == KMessageBox::Cancel) // { // abort(); // return IPS_ALERT; // } // m_TelescopeCoveredFlatExposure = true; // m_TelescopeCoveredDarkExposure = false; // Continue connect(KSMessageBox::Instance(), &KSMessageBox::accepted, this, [&]() { //QObject::disconnect(KSMessageBox::Instance(), &KSMessageBox::accepted, this, nullptr); KSMessageBox::Instance()->disconnect(this); m_TelescopeCoveredFlatExposure = true; m_TelescopeCoveredDarkExposure = false; calibrationCheckType = CAL_CHECK_TASK; }); // Cancel connect(KSMessageBox::Instance(), &KSMessageBox::rejected, this, [&]() { //QObject::disconnect(KSMessageBox::Instance(), &KSMessageBox::rejected, this, nullptr); KSMessageBox::Instance()->disconnect(this); calibrationCheckType = CAL_CHECK_TASK; abort(); }); calibrationCheckType = CAL_CHECK_CONFIRMATION; KSMessageBox::Instance()->warningContinueCancel(i18n("Cover telescope with evenly illuminated light source."), i18n("Flat Frame"), Options::manualCoverTimeout()); return IPS_BUSY; } break; // Not implemented. case SOURCE_DAWN_DUSK: break; case SOURCE_FLATCAP: if (currentDustCap == nullptr) { qCWarning(KSTARS_EKOS_CAPTURE) << "Skipping flat/dark cap since it is not connected."; break; } // if (!currentDustCap) // { // appendLogText(i18n("Cap device is missing but the job requires flat cap device.")); // abort(); // return IPS_ALERT; // } // If cap is not park, park it if (calibrationStage < CAL_DUSTCAP_PARKING && currentDustCap->isParked() == false) { if (currentDustCap->Park()) { calibrationStage = CAL_DUSTCAP_PARKING; appendLogText(i18n("Parking dust cap...")); return IPS_BUSY; } else { appendLogText(i18n("Parking dust cap failed, aborting...")); abort(); return IPS_ALERT; } } // Wait until cap is parked if (calibrationStage == CAL_DUSTCAP_PARKING) { if (currentDustCap->isParked() == false) return IPS_BUSY; else { calibrationStage = CAL_DUSTCAP_PARKED; appendLogText(i18n("Dust cap parked.")); } } // If light is not on, turn it on. if (currentDustCap->hasLight() && currentDustCap->isLightOn() == false) { dustCapLightEnabled = true; currentDustCap->SetLightEnabled(true); } break; case SOURCE_WALL: if (currentTelescope) { if (calibrationStage < CAL_SLEWING) { wallCoord = activeJob->getWallCoord(); wallCoord.HorizontalToEquatorial(KStarsData::Instance()->lst(), KStarsData::Instance()->geo()->lat()); currentTelescope->Slew(&wallCoord); appendLogText(i18n("Mount slewing to wall position...")); calibrationStage = CAL_SLEWING; return IPS_BUSY; } // Check if slewing is complete if (calibrationStage == CAL_SLEWING) { if (currentTelescope->isSlewing() == false) { // Disable mount tracking if supported by the driver. currentTelescope->setTrackEnabled(false); calibrationStage = CAL_SLEWING_COMPLETE; appendLogText(i18n("Slew to wall position complete.")); } else return IPS_BUSY; } if (currentLightBox) { // Check if we have a light box to turn on if (activeJob->getFrameType() == FRAME_FLAT && currentLightBox->isLightOn() == false) { lightBoxLightEnabled = true; currentLightBox->SetLightEnabled(true); } else if (activeJob->getFrameType() != FRAME_FLAT && currentLightBox->isLightOn() == true) { lightBoxLightEnabled = false; currentLightBox->SetLightEnabled(false); } } } break; case SOURCE_DARKCAP: if (currentDustCap == nullptr) { qCWarning(KSTARS_EKOS_CAPTURE) << "Skipping flat/dark cap since it is not connected."; break; } // if (!currentDustCap) // { // appendLogText(i18n("Cap device is missing but the job requires dark cap device.")); // abort(); // return IPS_ALERT; // } // If cap is parked, unpark it since dark cap uses external light source. if (calibrationStage < CAL_DUSTCAP_UNPARKING && currentDustCap->isParked() == true) { if (currentDustCap->UnPark()) { calibrationStage = CAL_DUSTCAP_UNPARKING; appendLogText(i18n("UnParking dust cap...")); return IPS_BUSY; } else { appendLogText(i18n("UnParking dust cap failed, aborting...")); abort(); return IPS_ALERT; } } // Wait until cap is unparked if (calibrationStage == CAL_DUSTCAP_UNPARKING) { if (currentDustCap->isUnParked() == false) return IPS_BUSY; else { calibrationStage = CAL_DUSTCAP_UNPARKED; appendLogText(i18n("Dust cap unparked.")); } } // If light is off, turn it on. if (currentDustCap->hasLight() && currentDustCap->isLightOn() == false) { dustCapLightEnabled = true; currentDustCap->SetLightEnabled(true); } break; } // Check if we need to perform mount prepark if (preMountPark && currentTelescope && activeJob->getFlatFieldSource() != SOURCE_WALL) { if (calibrationStage < CAL_MOUNT_PARKING && currentTelescope->isParked() == false) { if (currentTelescope->Park()) { calibrationStage = CAL_MOUNT_PARKING; //emit mountParking(); appendLogText(i18n("Parking mount prior to calibration frames capture...")); return IPS_BUSY; } else { appendLogText(i18n("Parking mount failed, aborting...")); abort(); return IPS_ALERT; } } if (calibrationStage == CAL_MOUNT_PARKING) { // If not parked yet, check again in 1 second // Otherwise proceed to the rest of the algorithm if (currentTelescope->isParked() == false) return IPS_BUSY; else { calibrationStage = CAL_MOUNT_PARKED; appendLogText(i18n("Mount parked.")); } } } // Check if we need to perform dome prepark if (preDomePark && currentDome) { if (calibrationStage < CAL_DOME_PARKING && currentDome->isParked() == false) { if (currentDome->Park()) { calibrationStage = CAL_DOME_PARKING; //emit mountParking(); appendLogText(i18n("Parking dome...")); return IPS_BUSY; } else { appendLogText(i18n("Parking dome failed, aborting...")); abort(); return IPS_ALERT; } } if (calibrationStage == CAL_DOME_PARKING) { // If not parked yet, check again in 1 second // Otherwise proceed to the rest of the algorithm if (currentDome->isParked() == false) return IPS_BUSY; else { calibrationStage = CAL_DOME_PARKED; appendLogText(i18n("Dome parked.")); } } } // If we used AUTOFOCUS before for a specific frame (e.g. Lum) // then the absolute focus position for Lum is recorded in the filter manager // when we take flats again, we always go back to the same focus position as the light frames to ensure // near identical focus for both frames. if (activeJob->getFrameType() == FRAME_FLAT && m_AutoFocusReady && currentFilter != nullptr && Options::flatSyncFocus()) { if (filterManager->syncAbsoluteFocusPosition(activeJob->getTargetFilter() - 1) == false) return IPS_BUSY; } calibrationStage = CAL_PRECAPTURE_COMPLETE; return IPS_OK; } IPState Capture::processPreCaptureCalibrationStage() { // If we are currently guide and the frame is NOT a light frame, then we shopld suspend. // N.B. The guide camera could be on its own scope unaffected but it doesn't hurt to stop // guiding since it is no longer used anyway. if (activeJob->getFrameType() != FRAME_LIGHT && guideState == GUIDE_GUIDING) { appendLogText(i18n("Autoguiding suspended.")); emit suspendGuiding(); } // Run necessary tasks for each frame type switch (activeJob->getFrameType()) { case FRAME_LIGHT: return checkLightFramePendingTasks(); case FRAME_BIAS: case FRAME_DARK: return checkDarkFramePendingTasks(); case FRAME_FLAT: return checkFlatFramePendingTasks(); } return IPS_OK; } bool Capture::processPostCaptureCalibrationStage() { // If there are no more images to capture, do not bother calculating next exposure if (calibrationStage == CAL_CALIBRATION_COMPLETE) if (activeJob && activeJob->getCount() <= activeJob->getCompleted()) return true; // Check if we need to do flat field slope calculation if the user specified a desired ADU value if (activeJob->getFrameType() == FRAME_FLAT && activeJob->getFlatFieldDuration() == DURATION_ADU && activeJob->getTargetADU() > 0) { if (Options::useFITSViewer() == false) { Options::setUseFITSViewer(true); qCInfo(KSTARS_EKOS_CAPTURE) << "Enabling FITS Viewer..."; } FITSData * image_data = nullptr; FITSView * currentImage = targetChip->getImageView(FITS_NORMAL); if (currentImage) { image_data = currentImage->getImageData(); double currentADU = image_data->getADU(); bool outOfRange = false, saturated = false; switch (image_data->bpp()) { case 8: if (activeJob->getTargetADU() > UINT8_MAX) outOfRange = true; else if (currentADU / UINT8_MAX > 0.95) saturated = true; break; case 16: if (activeJob->getTargetADU() > UINT16_MAX) outOfRange = true; else if (currentADU / UINT16_MAX > 0.95) saturated = true; break; case 32: if (activeJob->getTargetADU() > UINT32_MAX) outOfRange = true; else if (currentADU / UINT32_MAX > 0.95) saturated = true; break; default: break; } if (outOfRange) { appendLogText(i18n("Flat calibration failed. Captured image is only %1-bit while requested ADU is %2.", QString::number(image_data->bpp()) , QString::number(activeJob->getTargetADU(), 'f', 2))); abort(); return false; } else if (saturated) { double nextExposure = activeJob->getExposure() * 0.1; nextExposure = qBound(exposureIN->minimum(), nextExposure, exposureIN->maximum()); appendLogText(i18n("Current image is saturated (%1). Next exposure is %2 seconds.", QString::number(currentADU, 'f', 0), QString("%L1").arg(nextExposure, 0, 'f', 6))); calibrationStage = CAL_CALIBRATION; activeJob->setExposure(nextExposure); activeJob->setPreview(true); rememberUploadMode = activeJob->getUploadMode(); currentCCD->setUploadMode(ISD::CCD::UPLOAD_CLIENT); startNextExposure(); return false; } double ADUDiff = fabs(currentADU - activeJob->getTargetADU()); // If it is within tolerance range of target ADU if (ADUDiff <= targetADUTolerance) { if (calibrationStage == CAL_CALIBRATION) { appendLogText( i18n("Current ADU %1 within target ADU tolerance range.", QString::number(currentADU, 'f', 0))); activeJob->setPreview(false); currentCCD->setUploadMode(rememberUploadMode); // Get raw prefix exposureIN->setValue(activeJob->getExposure()); QString imagePrefix = activeJob->property("rawPrefix").toString(); constructPrefix(imagePrefix); activeJob->setFullPrefix(imagePrefix); seqPrefix = imagePrefix; currentCCD->setSeqPrefix(imagePrefix); currentCCD->updateUploadSettings(activeJob->getRemoteDir() + activeJob->getDirectoryPostfix()); calibrationStage = CAL_CALIBRATION_COMPLETE; startNextExposure(); return false; } return true; } double nextExposure = -1; // If value is saturated, try to reduce it to valid range first if (std::fabs(image_data->getMax(0) - image_data->getMin(0)) < 10) nextExposure = activeJob->getExposure() * 0.5; else nextExposure = setCurrentADU(currentADU); if (nextExposure <= 0 || std::isnan(nextExposure)) { appendLogText( i18n("Unable to calculate optimal exposure settings, please capture the flats manually.")); //activeJob->setTargetADU(0); //targetADU = 0; abort(); return false; } // Limit to minimum and maximum values nextExposure = qBound(exposureIN->minimum(), nextExposure, exposureIN->maximum()); appendLogText(i18n("Current ADU is %1 Next exposure is %2 seconds.", QString::number(currentADU, 'f', 0), QString("%L1").arg(nextExposure, 0, 'f', 6))); calibrationStage = CAL_CALIBRATION; activeJob->setExposure(nextExposure); activeJob->setPreview(true); rememberUploadMode = activeJob->getUploadMode(); currentCCD->setUploadMode(ISD::CCD::UPLOAD_CLIENT); startNextExposure(); return false; // Start next exposure in case ADU Slope is not calculated yet /*if (currentSlope == 0) { startNextExposure(); return; }*/ } else { appendLogText(i18n("An empty image is received, aborting...")); abort(); return false; } } calibrationStage = CAL_CALIBRATION_COMPLETE; return true; } void Capture::setNewRemoteFile(QString file) { appendLogText(i18n("Remote image saved to %1", file)); emit newSequenceImage(file, QString()); } /* void Capture::startPostFilterAutoFocus() { if (focusState >= FOCUS_PROGRESS || state == CAPTURE_FOCUSING) return; secondsLabel->setText(i18n("Focusing...")); state = CAPTURE_FOCUSING; emit newStatus(Ekos::CAPTURE_FOCUSING); appendLogText(i18n("Post filter change Autofocus...")); // Force it to always run autofocus routine emit checkFocus(0.1); } */ void Capture::postScriptFinished(int exitCode, QProcess::ExitStatus status) { Q_UNUSED(status) appendLogText(i18n("Post capture script finished with code %1.", exitCode)); // If we're done, proceed to completion. if (activeJob->getCount() <= activeJob->getCompleted()) { processJobCompletion(); } // Else check if meridian condition is met. else if (checkMeridianFlip()) { appendLogText(i18n("Processing meridian flip...")); } // Then if nothing else, just resume sequence. else { appendLogText(i18n("Resuming sequence...")); resumeSequence(); } } void Capture::toggleVideo(bool enabled) { if (currentCCD == nullptr) return; if (currentCCD->isBLOBEnabled() == false) { if (Options::guiderType() != Ekos::Guide::GUIDE_INTERNAL) currentCCD->setBLOBEnabled(true); else { connect(KSMessageBox::Instance(), &KSMessageBox::accepted, this, [this, enabled]() { //QObject::disconnect(KSMessageBox::Instance(), &KSMessageBox::accepted, this, nullptr); KSMessageBox::Instance()->disconnect(this); currentCCD->setBLOBEnabled(true); currentCCD->setVideoStreamEnabled(enabled); }); KSMessageBox::Instance()->questionYesNo(i18n("Image transfer is disabled for this camera. Would you like to enable it?"), i18n("Image Transfer"), 15); return; } } currentCCD->setVideoStreamEnabled(enabled); } void Capture::setVideoStreamEnabled(bool enabled) { if (enabled) { liveVideoB->setChecked(true); liveVideoB->setIcon(QIcon::fromTheme("camera-on")); //liveVideoB->setStyleSheet("color:red;"); } else { liveVideoB->setChecked(false); liveVideoB->setIcon(QIcon::fromTheme("camera-ready")); //liveVideoB->setStyleSheet(QString()); } } void Capture::setMountStatus(ISD::Telescope::Status newState) { switch (newState) { case ISD::Telescope::MOUNT_PARKING: case ISD::Telescope::MOUNT_SLEWING: case ISD::Telescope::MOUNT_MOVING: previewB->setEnabled(false); liveVideoB->setEnabled(false); // Only disable when button is "Start", and not "Stopped" // If mount is in motion, Stopped button should always be enabled to terminate // the sequence if (pi->isAnimated() == false) startB->setEnabled(false); break; default: if (pi->isAnimated() == false) { previewB->setEnabled(true); if (currentCCD) liveVideoB->setEnabled(currentCCD->hasVideoStream()); startB->setEnabled(true); } break; } } void Capture::showObserverDialog() { QList m_observerList; KStars::Instance()->data()->userdb()->GetAllObservers(m_observerList); QStringList observers; for (auto &o : m_observerList) observers << QString("%1 %2").arg(o->name(), o->surname()); QDialog observersDialog(this); observersDialog.setWindowTitle(i18n("Select Current Observer")); QLabel label(i18n("Current Observer:")); QComboBox observerCombo(&observersDialog); observerCombo.addItems(observers); observerCombo.setCurrentText(m_ObserverName); observerCombo.setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Fixed); QPushButton manageObserver(&observersDialog); manageObserver.setFixedSize(QSize(32, 32)); manageObserver.setIcon(QIcon::fromTheme("document-edit")); manageObserver.setAttribute(Qt::WA_LayoutUsesWidgetRect); manageObserver.setToolTip(i18n("Manage Observers")); connect(&manageObserver, &QPushButton::clicked, this, [&]() { ObserverAdd add; add.exec(); QList m_observerList; KStars::Instance()->data()->userdb()->GetAllObservers(m_observerList); QStringList observers; for (auto &o : m_observerList) observers << QString("%1 %2").arg(o->name(), o->surname()); observerCombo.clear(); observerCombo.addItems(observers); observerCombo.setCurrentText(m_ObserverName); }); QHBoxLayout * layout = new QHBoxLayout; layout->addWidget(&label); layout->addWidget(&observerCombo); layout->addWidget(&manageObserver); observersDialog.setLayout(layout); observersDialog.exec(); m_ObserverName = observerCombo.currentText(); Options::setDefaultObserver(m_ObserverName); } void Capture::startRefocusTimer(bool forced) { /* If refocus is requested, only restart timer if not already running in order to keep current elapsed time since last refocus */ if (refocusEveryNCheck->isChecked()) { // How much time passed since we last started the time uint32_t elapsedSecs = refocusEveryNTimer.elapsed() / 1000; // How many seconds do we wait for between focusing (60 mins ==> 3600 secs) uint32_t totalSecs = refocusEveryN->value() * 60; if (!refocusEveryNTimer.isValid() || forced) { appendLogText(i18n("Ekos will refocus in %1 seconds.", totalSecs)); refocusEveryNTimer.restart(); } else if (elapsedSecs < totalSecs) { //appendLogText(i18n("Ekos will refocus in %1 seconds, last procedure was %2 seconds ago.", refocusEveryNTimer.elapsed()/1000-refocusEveryNTimer.elapsed()*60, refocusEveryNTimer.elapsed()/1000)); appendLogText(i18n("Ekos will refocus in %1 seconds, last procedure was %2 seconds ago.", totalSecs - elapsedSecs, elapsedSecs)); } else { appendLogText(i18n("Ekos will refocus as soon as possible, last procedure was %1 seconds ago.", elapsedSecs)); } } } int Capture::getRefocusEveryNTimerElapsedSec() { /* If timer isn't valid, consider there is no focus to be done, that is, that focus was just done */ return refocusEveryNTimer.isValid() ? refocusEveryNTimer.elapsed() / 1000 : 0; } void Capture::setAlignResults(double orientation, double ra, double de, double pixscale) { Q_UNUSED(orientation) Q_UNUSED(ra) Q_UNUSED(de) Q_UNUSED(pixscale) if (currentRotator == nullptr) return; rotatorSettings->refresh(); } void Capture::setFilterManager(const QSharedPointer &manager) { filterManager = manager; connect(filterManagerB, &QPushButton::clicked, [this]() { filterManager->show(); filterManager->raise(); }); connect(filterManager.data(), &FilterManager::ready, [this]() { m_CurrentFilterPosition = filterManager->getFilterPosition(); // Due to race condition, focusState = FOCUS_IDLE; if (activeJob) activeJob->setCurrentFilter(m_CurrentFilterPosition); } ); connect(filterManager.data(), &FilterManager::failed, [this]() { if (activeJob) { appendLogText(i18n("Filter operation failed.")); abort(); } } ); connect(filterManager.data(), &FilterManager::newStatus, [this](Ekos::FilterState filterState) { filterManagerState = filterState; if (m_State == CAPTURE_CHANGING_FILTER) { secondsLabel->setText(Ekos::getFilterStatusString(filterState)); switch (filterState) { case FILTER_OFFSET: appendLogText(i18n("Changing focus offset by %1 steps...", filterManager->getTargetFilterOffset())); break; case FILTER_CHANGE: appendLogText(i18n("Changing filter to %1...", FilterPosCombo->itemText(filterManager->getTargetFilterPosition() - 1))); break; case FILTER_AUTOFOCUS: appendLogText(i18n("Auto focus on filter change...")); clearAutoFocusHFR(); break; default: break; } } }); connect(filterManager.data(), &FilterManager::labelsChanged, this, [this]() { FilterPosCombo->clear(); FilterPosCombo->addItems(filterManager->getFilterLabels()); m_CurrentFilterPosition = filterManager->getFilterPosition(); FilterPosCombo->setCurrentIndex(m_CurrentFilterPosition - 1); }); connect(filterManager.data(), &FilterManager::positionChanged, this, [this]() { m_CurrentFilterPosition = filterManager->getFilterPosition(); FilterPosCombo->setCurrentIndex(m_CurrentFilterPosition - 1); }); } void Capture::addDSLRInfo(const QString &model, uint32_t maxW, uint32_t maxH, double pixelW, double pixelH) { // Check if model already exists auto pos = std::find_if(DSLRInfos.begin(), DSLRInfos.end(), [model](QMap &oneDSLRInfo) { return (oneDSLRInfo["Model"] == model); }); if (pos != DSLRInfos.end()) { KStarsData::Instance()->userdb()->DeleteDSLRInfo(model); DSLRInfos.removeOne(*pos); } QMap oneDSLRInfo; oneDSLRInfo["Model"] = model; oneDSLRInfo["Width"] = maxW; oneDSLRInfo["Height"] = maxH; oneDSLRInfo["PixelW"] = pixelW; oneDSLRInfo["PixelH"] = pixelH; KStarsData::Instance()->userdb()->AddDSLRInfo(oneDSLRInfo); KStarsData::Instance()->userdb()->GetAllDSLRInfos(DSLRInfos); updateFrameProperties(); resetFrame(); // In case the dialog was opened, let's close it if (dslrInfoDialog) dslrInfoDialog.reset(); } bool Capture::isModelinDSLRInfo(const QString &model) { auto pos = std::find_if(DSLRInfos.begin(), DSLRInfos.end(), [model](QMap &oneDSLRInfo) { return (oneDSLRInfo["Model"] == model); }); return (pos != DSLRInfos.end()); } #if 0 void Capture::syncDriverToDSLRLimits() { if (targetChip == nullptr) return; QString model(currentCCD->getDeviceName()); // Check if model already exists auto pos = std::find_if(DSLRInfos.begin(), DSLRInfos.end(), [model](QMap &oneDSLRInfo) { return (oneDSLRInfo["Model"] == model); }); if (pos != DSLRInfos.end()) targetChip->setImageInfo((*pos)["Width"].toInt(), (*pos)["Height"].toInt(), (*pos)["PixelW"].toDouble(), (*pos)["PixelH"].toDouble(), 8); } #endif void Capture::cullToDSLRLimits() { QString model(currentCCD->getDeviceName()); // Check if model already exists auto pos = std::find_if(DSLRInfos.begin(), DSLRInfos.end(), [model](QMap &oneDSLRInfo) { return (oneDSLRInfo["Model"] == model); }); if (pos != DSLRInfos.end()) { if (frameWIN->maximum() == 0 || frameWIN->maximum() > (*pos)["Width"].toInt()) { frameWIN->setValue((*pos)["Width"].toInt()); frameWIN->setMaximum((*pos)["Width"].toInt()); } if (frameHIN->maximum() == 0 || frameHIN->maximum() > (*pos)["Height"].toInt()) { frameHIN->setValue((*pos)["Height"].toInt()); frameHIN->setMaximum((*pos)["Height"].toInt()); } } } void Capture::setCapturedFramesMap(const QString &signature, int count) { capturedFramesMap[signature] = count; qCDebug(KSTARS_EKOS_CAPTURE) << QString("Client module indicates that storage for '%1' has already %2 captures processed.").arg(signature).arg(count); // Scheduler's captured frame map overrides the progress option of the Capture module ignoreJobProgress = false; } void Capture::setSettings(const QJsonObject &settings) { // FIXME: QComboBox signal "activated" does not trigger when setting text programmatically. const QString targetCamera = settings["camera"].toString(); const QString targetFW = settings["fw"].toString(); const QString targetFilter = settings["filter"].toString(); if (CCDCaptureCombo->currentText() != targetCamera) { const int index = CCDCaptureCombo->findText(targetCamera); CCDCaptureCombo->setCurrentIndex(index); checkCCD(index); } if (!targetFW.isEmpty() && FilterDevicesCombo->currentText() != targetFW) { const int index = FilterDevicesCombo->findText(targetFW); FilterDevicesCombo->setCurrentIndex(index); checkFilter(index); } if (!targetFilter.isEmpty() && FilterPosCombo->currentText() != targetFilter) { FilterPosCombo->setCurrentIndex(FilterPosCombo->findText(targetFilter)); } exposureIN->setValue(settings["exp"].toDouble(1)); int bin = settings["bin"].toInt(1); setBinning(bin, bin); double temperature = settings["temperature"].toDouble(100); if (temperature < 100 && currentCCD && currentCCD->hasCoolerControl()) { setForceTemperature(true); setTargetTemperature(temperature); } double gain = settings["gain"].toDouble(-1); if (gain >= 0 && currentCCD && currentCCD->hasGain()) { setGain(gain); } int format = settings["format"].toInt(-1); if (format >= 0) { transferFormatCombo->setCurrentIndex(format); } frameTypeCombo->setCurrentIndex(qMax(0, settings["frameType"].toInt(0))); // ISO int isoIndex = settings["iso"].toInt(-1); if (isoIndex >= 0) setISO(isoIndex); } void Capture::clearCameraConfiguration() { //if (!Options::autonomousMode() && KMessageBox::questionYesNo(nullptr, i18n("Reset %1 configuration to default?", currentCCD->getDeviceName()), i18n("Confirmation")) == KMessageBox::No) // return; connect(KSMessageBox::Instance(), &KSMessageBox::accepted, this, [this]() { //QObject::disconnect(KSMessageBox::Instance(), &KSMessageBox::accepted, this, nullptr); KSMessageBox::Instance()->disconnect(this); currentCCD->setConfig(PURGE_CONFIG); KStarsData::Instance()->userdb()->DeleteDSLRInfo(currentCCD->getDeviceName()); QStringList shutterfulCCDs = Options::shutterfulCCDs(); QStringList shutterlessCCDs = Options::shutterlessCCDs(); // Remove camera from shutterful and shutterless CCDs if (shutterfulCCDs.contains(currentCCD->getDeviceName())) { shutterfulCCDs.removeOne(currentCCD->getDeviceName()); Options::setShutterfulCCDs(shutterfulCCDs); } if (shutterlessCCDs.contains(currentCCD->getDeviceName())) { shutterlessCCDs.removeOne(currentCCD->getDeviceName()); Options::setShutterlessCCDs(shutterlessCCDs); } // For DSLRs, immediately ask them to enter the values again. if (ISOCombo && ISOCombo->count() > 0) { createDSLRDialog(); } }); KSMessageBox::Instance()->questionYesNo( i18n("Reset %1 configuration to default?", currentCCD->getDeviceName()), i18n("Confirmation"), 30); } void Capture::setCoolerToggled(bool enabled) { coolerOnB->blockSignals(true); coolerOnB->setChecked(enabled); coolerOnB->blockSignals(false); coolerOffB->blockSignals(true); coolerOffB->setChecked(!enabled); coolerOffB->blockSignals(false); appendLogText(enabled ? i18n("Cooler is on") : i18n("Cooler is off")); } void Capture::processCaptureTimeout() { auto restartExposure = [&]() { appendLogText(i18n("Exposure timeout. Restarting exposure...")); currentCCD->setTransformFormat(ISD::CCD::FORMAT_FITS); ISD::CCDChip *targetChip = currentCCD->getChip(useGuideHead ? ISD::CCDChip::GUIDE_CCD : ISD::CCDChip::PRIMARY_CCD); targetChip->abortExposure(); targetChip->capture(exposureIN->value()); captureTimeout.start(exposureIN->value() * 1000 + CAPTURE_TIMEOUT_THRESHOLD); }; m_CaptureTimeoutCounter++; if (m_DeviceRestartCounter >= 3) { m_CaptureTimeoutCounter = 0; m_DeviceRestartCounter = 0; appendLogText(i18n("Exposure timeout. Aborting...")); abort(); return; } if (m_CaptureTimeoutCounter > 1) { QString camera = currentCCD->getDeviceName(); QString fw = currentFilter ? currentFilter->getDeviceName() : ""; emit driverTimedout(camera); QTimer::singleShot(5000, [ &, camera, fw]() { m_DeviceRestartCounter++; reconnectDriver(camera, fw); }); return; } else restartExposure(); } void Capture::setGeneratedPreviewFITS(const QString &previewFITS) { m_GeneratedPreviewFITS = previewFITS; } void Capture::createDSLRDialog() { dslrInfoDialog.reset(new DSLRInfo(this, currentCCD)); connect(dslrInfoDialog.get(), &DSLRInfo::infoChanged, [this]() { addDSLRInfo(QString(currentCCD->getDeviceName()), dslrInfoDialog->sensorMaxWidth, dslrInfoDialog->sensorMaxHeight, dslrInfoDialog->sensorPixelW, dslrInfoDialog->sensorPixelH); }); dslrInfoDialog->show(); emit dslrInfoRequested(currentCCD->getDeviceName()); } void Capture::removeDevice(ISD::GDInterface *device) { device->disconnect(this); if (currentTelescope && currentTelescope->getDeviceName() == device->getDeviceName()) { currentTelescope = nullptr; } else if (currentDome && currentDome->getDeviceName() == device->getDeviceName()) { currentDome = nullptr; } else if (currentRotator && currentRotator->getDeviceName() == device->getDeviceName()) { currentRotator = nullptr; rotatorB->setEnabled(false); } if (CCDs.contains(static_cast(device))) { ISD::CCD *oneCCD = static_cast(device); CCDs.removeAll(oneCCD); CCDCaptureCombo->removeItem(CCDCaptureCombo->findText(device->getDeviceName())); CCDCaptureCombo->removeItem(CCDCaptureCombo->findText(device->getDeviceName() + QString(" Guider"))); if (CCDs.empty()) { currentCCD = nullptr; CCDCaptureCombo->setCurrentIndex(-1); } else CCDCaptureCombo->setCurrentIndex(0); checkCCD(); } if (Filters.contains(static_cast(device))) { Filters.removeOne(static_cast(device)); filterManager->removeDevice(device); FilterDevicesCombo->removeItem(FilterDevicesCombo->findText(device->getDeviceName())); if (Filters.empty()) { currentFilter = nullptr; FilterDevicesCombo->setCurrentIndex(-1); } else FilterDevicesCombo->setCurrentIndex(0); checkFilter(); } } void Capture::setGain(double value) { QMap > customProps = customPropertiesDialog->getCustomProperties(); // Gain is manifested in two forms // Property CCD_GAIN and // Part of CCD_CONTROLS properties. // Therefore, we have to find what the currently camera supports first. if (currentCCD->getProperty("CCD_GAIN")) { QMap ccdGain; ccdGain["GAIN"] = value; customProps["CCD_GAIN"] = ccdGain; } else if (currentCCD->getProperty("CCD_CONTROLS")) { QMap ccdGain; ccdGain["Gain"] = value; customProps["CCD_CONTROLS"] = ccdGain; } customPropertiesDialog->setCustomProperties(customProps); } double Capture::getGain() { if (!GainSpin) return -1; QMap > customProps = customPropertiesDialog->getCustomProperties(); // Gain is manifested in two forms // Property CCD_GAIN and // Part of CCD_CONTROLS properties. // Therefore, we have to find what the currently camera supports first. if (currentCCD->getProperty("CCD_GAIN")) { return customProps["CCD_GAIN"].value("GAIN", -1); } else if (currentCCD->getProperty("CCD_CONTROLS")) { return customProps["CCD_CONTROLS"].value("Gain", -1); } return -1; } double Capture::getEstimatedDownloadTime() { double total = 0; foreach(double dlTime, downloadTimes) total += dlTime; if(downloadTimes.count() == 0) return 0; else return total / downloadTimes.count(); } void Capture::reconnectDriver(const QString &camera, const QString &filterWheel) { for (auto &oneCamera : CCDs) { if (oneCamera->getDeviceName() == camera) { // Set camera again to the one we restarted CCDCaptureCombo->setCurrentIndex(CCDCaptureCombo->findText(camera)); FilterDevicesCombo->setCurrentIndex(FilterDevicesCombo->findText(filterWheel)); checkCCD(); // restart capture m_CaptureTimeoutCounter = 0; if (activeJob) { activeJob->setActiveChip(targetChip); activeJob->setActiveCCD(currentCCD); activeJob->setActiveFilter(currentFilter); captureImage(); } return; } } QTimer::singleShot(5000, this, [ &, camera, filterWheel]() { reconnectDriver(camera, filterWheel); }); } bool Capture::isGuidingActive() { // In case we are doing non guiding dither, then we are not performing autoguiding. if (Options::ditherNoGuiding()) return false; return (guideState == GUIDE_GUIDING || guideState == GUIDE_CALIBRATING || guideState == GUIDE_CALIBRATION_SUCESS || guideState == GUIDE_REACQUIRE || guideState == GUIDE_DITHERING || guideState == GUIDE_DITHERING_SUCCESS || guideState == GUIDE_DITHERING_ERROR || guideState == GUIDE_DITHERING_SETTLE); } } diff --git a/kstars/ekos/guide/guide.cpp b/kstars/ekos/guide/guide.cpp index 0fe224f2a..a899c7b48 100644 --- a/kstars/ekos/guide/guide.cpp +++ b/kstars/ekos/guide/guide.cpp @@ -1,3626 +1,3626 @@ /* Ekos Copyright (C) 2012 Jasem Mutlaq This application is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version. */ #include "guide.h" #include "guideadaptor.h" #include "kstars.h" #include "ksmessagebox.h" #include "ksnotification.h" #include "kstarsdata.h" #include "opscalibration.h" #include "opsguide.h" #include "Options.h" #include "auxiliary/QProgressIndicator.h" #include "ekos/auxiliary/darklibrary.h" #include "externalguide/linguider.h" #include "externalguide/phd2.h" #include "fitsviewer/fitsdata.h" #include "fitsviewer/fitsview.h" #include "fitsviewer/fitsviewer.h" #include "internalguide/internalguider.h" #include #include #include #include "ui_manualdither.h" #define CAPTURE_TIMEOUT_THRESHOLD 30000 namespace Ekos { Guide::Guide() : QWidget() { // #1 Setup UI setupUi(this); // #2 Register DBus qRegisterMetaType("Ekos::GuideState"); qDBusRegisterMetaType(); new GuideAdaptor(this); QDBusConnection::sessionBus().registerObject("/KStars/Ekos/Guide", this); // #3 Init Plots initPlots(); // #4 Init View initView(); // #5 Load all settings loadSettings(); // #6 Init Connections initConnections(); // Image Filters for (auto &filter : FITSViewer::filterTypes) filterCombo->addItem(filter); // Progress Indicator pi = new QProgressIndicator(this); controlLayout->addWidget(pi, 1, 2, 1, 1); showFITSViewerB->setIcon( QIcon::fromTheme("kstars_fitsviewer")); connect(showFITSViewerB, &QPushButton::clicked, this, &Ekos::Guide::showFITSViewer); showFITSViewerB->setAttribute(Qt::WA_LayoutUsesWidgetRect); guideAutoScaleGraphB->setIcon( QIcon::fromTheme("zoom-fit-best")); connect(guideAutoScaleGraphB, &QPushButton::clicked, this, &Ekos::Guide::slotAutoScaleGraphs); guideAutoScaleGraphB->setAttribute(Qt::WA_LayoutUsesWidgetRect); guideSaveDataB->setIcon( QIcon::fromTheme("document-save")); connect(guideSaveDataB, &QPushButton::clicked, this, &Ekos::Guide::exportGuideData); guideSaveDataB->setAttribute(Qt::WA_LayoutUsesWidgetRect); guideDataClearB->setIcon( QIcon::fromTheme("application-exit")); connect(guideDataClearB, &QPushButton::clicked, this, &Ekos::Guide::clearGuideGraphs); guideDataClearB->setAttribute(Qt::WA_LayoutUsesWidgetRect); // Exposure //Should we set the range for the spin box here? QList exposureValues; exposureValues << 0.02 << 0.05 << 0.1 << 0.2 << 0.5 << 1 << 1.5 << 2 << 2.5 << 3 << 3.5 << 4 << 4.5 << 5 << 6 << 7 << 8 << 9 << 10 << 15 << 30; exposureIN->setRecommendedValues(exposureValues); connect(exposureIN, &NonLinearDoubleSpinBox::editingFinished, this, &Ekos::Guide::saveDefaultGuideExposure); // Init Internal Guider always internalGuider = new InternalGuider(); KConfigDialog *dialog = new KConfigDialog(this, "guidesettings", Options::self()); opsCalibration = new OpsCalibration(internalGuider); KPageWidgetItem *page = dialog->addPage(opsCalibration, i18n("Calibration")); page->setIcon(QIcon::fromTheme("tool-measure")); opsGuide = new OpsGuide(); connect(opsGuide, &OpsGuide::settingsUpdated, [this]() { onThresholdChanged(Options::guideAlgorithm()); configurePHD2Camera(); }); page = dialog->addPage(opsGuide, i18n("Guide")); page->setIcon(QIcon::fromTheme("kstars_guides")); internalGuider->setGuideView(guideView); // Set current guide type setGuiderType(-1); //This allows the current guideSubframe option to be loaded. if(guiderType == GUIDE_PHD2) setExternalGuiderBLOBEnabled(!Options::guideSubframeEnabled()); //Note: This is to prevent a button from being called the default button //and then executing when the user hits the enter key such as when on a Text Box QList qButtons = findChildren(); for (auto &button : qButtons) button->setAutoDefault(false); } Guide::~Guide() { delete guider; } void Guide::handleHorizontalPlotSizeChange() { driftPlot->xAxis->setScaleRatio(driftPlot->yAxis, 1.0); driftPlot->replot(); } void Guide::handleVerticalPlotSizeChange() { driftPlot->yAxis->setScaleRatio(driftPlot->xAxis, 1.0); driftPlot->replot(); } void Guide::guideAfterMeridianFlip() { //This will clear the tracking box selection //The selected guide star is no longer valid due to the flip guideView->setTrackingBoxEnabled(false); starCenter = QVector3D(); if (Options::resetGuideCalibration()) clearCalibration(); guide(); } void Guide::resizeEvent(QResizeEvent *event) { if (event->oldSize().width() != -1) { if (event->oldSize().width() != size().width()) handleHorizontalPlotSizeChange(); else if (event->oldSize().height() != size().height()) handleVerticalPlotSizeChange(); } else { QTimer::singleShot(10, this, &Ekos::Guide::handleHorizontalPlotSizeChange); } } void Guide::buildTarget() { double accuracyRadius = accuracyRadiusSpin->value(); Options::setGuiderAccuracyThreshold(accuracyRadius); if (centralTarget) { concentricRings->data()->clear(); redTarget->data()->clear(); yellowTarget->data()->clear(); centralTarget->data()->clear(); } else { concentricRings = new QCPCurve(driftPlot->xAxis, driftPlot->yAxis); redTarget = new QCPCurve(driftPlot->xAxis, driftPlot->yAxis); yellowTarget = new QCPCurve(driftPlot->xAxis, driftPlot->yAxis); centralTarget = new QCPCurve(driftPlot->xAxis, driftPlot->yAxis); } const int pointCount = 200; QVector circleRings( pointCount * (5)); //Have to multiply by the number of rings, Rings at : 25%, 50%, 75%, 125%, 175% QVector circleCentral(pointCount); QVector circleYellow(pointCount); QVector circleRed(pointCount); int circleRingPt = 0; for (int i = 0; i < pointCount; i++) { double theta = i / static_cast(pointCount) * 2 * M_PI; for (double ring = 1; ring < 8; ring++) { if (ring != 4 && ring != 6) { if (i % (9 - static_cast(ring)) == 0) //This causes fewer points to draw on the inner circles. { circleRings[circleRingPt] = QCPCurveData(circleRingPt, accuracyRadius * ring * 0.25 * qCos(theta), accuracyRadius * ring * 0.25 * qSin(theta)); circleRingPt++; } } } circleCentral[i] = QCPCurveData(i, accuracyRadius * qCos(theta), accuracyRadius * qSin(theta)); circleYellow[i] = QCPCurveData(i, accuracyRadius * 1.5 * qCos(theta), accuracyRadius * 1.5 * qSin(theta)); circleRed[i] = QCPCurveData(i, accuracyRadius * 2 * qCos(theta), accuracyRadius * 2 * qSin(theta)); } concentricRings->setLineStyle(QCPCurve::lsNone); concentricRings->setScatterSkip(0); concentricRings->setScatterStyle(QCPScatterStyle(QCPScatterStyle::ssDisc, QColor(255, 255, 255, 150), 1)); concentricRings->data()->set(circleRings, true); redTarget->data()->set(circleRed, true); yellowTarget->data()->set(circleYellow, true); centralTarget->data()->set(circleCentral, true); concentricRings->setPen(QPen(Qt::white)); redTarget->setPen(QPen(Qt::red)); yellowTarget->setPen(QPen(Qt::yellow)); centralTarget->setPen(QPen(Qt::green)); concentricRings->setBrush(Qt::NoBrush); redTarget->setBrush(QBrush(QColor(255, 0, 0, 50))); yellowTarget->setBrush( QBrush(QColor(0, 255, 0, 50))); //Note this is actually yellow. It is green on top of red with equal opacity. centralTarget->setBrush(QBrush(QColor(0, 255, 0, 50))); if (driftPlot->size().width() > 0) driftPlot->replot(); } void Guide::clearGuideGraphs() { driftGraph->graph(0)->data()->clear(); //RA data driftGraph->graph(1)->data()->clear(); //DEC data driftGraph->graph(2)->data()->clear(); //RA highlighted point driftGraph->graph(3)->data()->clear(); //DEC highlighted point driftGraph->graph(4)->data()->clear(); //RA Pulses driftGraph->graph(5)->data()->clear(); //DEC Pulses driftPlot->graph(0)->data()->clear(); //Guide data driftPlot->graph(1)->data()->clear(); //Guide highlighted point driftGraph->clearItems(); //Clears dither text items from the graph driftGraph->replot(); driftPlot->replot(); //Since the labels got cleared with clearItems above. setupNSEWLabels(); } void Guide::setupNSEWLabels() { //Labels for N/S/E/W QColor raLabelColor(KStarsData::Instance()->colorScheme()->colorNamed("RAGuideError")); QColor deLabelColor(KStarsData::Instance()->colorScheme()->colorNamed("DEGuideError")); //DriftGraph { QCPItemText *northLabel = new QCPItemText(driftGraph); northLabel->setColor(deLabelColor); northLabel->setText(i18nc("North", "N")); northLabel->position->setType(QCPItemPosition::ptViewportRatio); northLabel->position->setCoords(0.6, 0.1); northLabel->setVisible(true); QCPItemText *southLabel = new QCPItemText(driftGraph); southLabel->setColor(deLabelColor); southLabel->setText(i18nc("South", "S")); southLabel->position->setType(QCPItemPosition::ptViewportRatio); southLabel->position->setCoords(0.6, 0.8); southLabel->setVisible(true); QCPItemText *westLabel = new QCPItemText(driftGraph); westLabel->setColor(raLabelColor); westLabel->setText(i18nc("West", "W")); westLabel->position->setType(QCPItemPosition::ptViewportRatio); westLabel->position->setCoords(0.8, 0.1); westLabel->setVisible(true); QCPItemText *eastLabel = new QCPItemText(driftGraph); eastLabel->setColor(raLabelColor); eastLabel->setText(i18nc("East", "E")); eastLabel->position->setType(QCPItemPosition::ptViewportRatio); eastLabel->position->setCoords(0.8, 0.8); eastLabel->setVisible(true); } //DriftPlot { QCPItemText *northLabel = new QCPItemText(driftPlot); northLabel->setColor(deLabelColor); northLabel->setText(i18nc("North", "N")); northLabel->position->setType(QCPItemPosition::ptViewportRatio); northLabel->position->setCoords(0.25, 0.2); northLabel->setVisible(true); QCPItemText *southLabel = new QCPItemText(driftPlot); southLabel->setColor(deLabelColor); southLabel->setText(i18nc("South", "S")); southLabel->position->setType(QCPItemPosition::ptViewportRatio); southLabel->position->setCoords(0.25, 0.7); southLabel->setVisible(true); QCPItemText *westLabel = new QCPItemText(driftPlot); westLabel->setColor(raLabelColor); westLabel->setText(i18nc("West", "W")); westLabel->position->setType(QCPItemPosition::ptViewportRatio); westLabel->position->setCoords(0.8, 0.75); westLabel->setVisible(true); QCPItemText *eastLabel = new QCPItemText(driftPlot); eastLabel->setColor(raLabelColor); eastLabel->setText(i18nc("East", "E")); eastLabel->position->setType(QCPItemPosition::ptViewportRatio); eastLabel->position->setCoords(0.3, 0.75); eastLabel->setVisible(true); } } void Guide::slotAutoScaleGraphs() { double accuracyRadius = accuracyRadiusSpin->value(); double key = guideTimer.elapsed() / 1000.0; driftGraph->xAxis->setRange(key - 60, key); driftGraph->yAxis->setRange(-3, 3); driftGraph->graph(0)->rescaleValueAxis(true); driftGraph->replot(); driftPlot->xAxis->setRange(-accuracyRadius * 3, accuracyRadius * 3); driftPlot->yAxis->setRange(-accuracyRadius * 3, accuracyRadius * 3); driftPlot->graph(0)->rescaleAxes(true); driftPlot->yAxis->setScaleRatio(driftPlot->xAxis, 1.0); driftPlot->xAxis->setScaleRatio(driftPlot->yAxis, 1.0); driftPlot->replot(); } void Guide::guideHistory() { int sliderValue = guideSlider->value(); latestCheck->setChecked(sliderValue == guideSlider->maximum() - 1 || sliderValue == guideSlider->maximum()); driftGraph->graph(2)->data()->clear(); //Clear RA highlighted point driftGraph->graph(3)->data()->clear(); //Clear DEC highlighted point driftPlot->graph(1)->data()->clear(); //Clear Guide highlighted point double t = driftGraph->graph(0)->dataMainKey(sliderValue); //Get time from RA data double ra = driftGraph->graph(0)->dataMainValue(sliderValue); //Get RA from RA data double de = driftGraph->graph(1)->dataMainValue(sliderValue); //Get DEC from DEC data double raPulse = driftGraph->graph(4)->dataMainValue(sliderValue); //Get RA Pulse from RA pulse data double dePulse = driftGraph->graph(5)->dataMainValue(sliderValue); //Get DEC Pulse from DEC pulse data driftGraph->graph(2)->addData(t, ra); //Set RA highlighted point driftGraph->graph(3)->addData(t, de); //Set DEC highlighted point //This will allow the graph to scroll left and right along with the guide slider if (driftGraph->xAxis->range().contains(t) == false) { if(t < driftGraph->xAxis->range().lower) { driftGraph->xAxis->setRange(t, t + driftGraph->xAxis->range().size()); } if(t > driftGraph->xAxis->range().upper) { driftGraph->xAxis->setRange(t - driftGraph->xAxis->range().size(), t); } } driftGraph->replot(); driftPlot->graph(1)->addData(ra, de); //Set guide highlighted point driftPlot->replot(); if(!graphOnLatestPt) { QTime localTime = guideTimer; localTime = localTime.addSecs(t); QPoint localTooltipCoordinates = driftGraph->graph(0)->dataPixelPosition(sliderValue).toPoint(); QPoint globalTooltipCoordinates = driftGraph->mapToGlobal(localTooltipCoordinates); if(raPulse == 0 && dePulse == 0) { QToolTip::showText( globalTooltipCoordinates, i18nc("Drift graphics tooltip; %1 is local time; %2 is RA deviation; %3 is DE deviation in arcseconds", "" "" "" "" "
LT: %1
RA: %2 \"
DE: %3 \"
", localTime.toString("hh:mm:ss AP"), QString::number(ra, 'f', 2), QString::number(de, 'f', 2))); } else { QToolTip::showText( globalTooltipCoordinates, i18nc("Drift graphics tooltip; %1 is local time; %2 is RA deviation; %3 is DE deviation in arcseconds; %4 is RA Pulse in ms; %5 is DE Pulse in ms", "" "" "" "" "" "" "
LT: %1
RA: %2 \"
DE: %3 \"
RA Pulse: %4 ms
DE Pulse: %5 ms
", localTime.toString("hh:mm:ss AP"), QString::number(ra, 'f', 2), QString::number(de, 'f', 2), QString::number(raPulse, 'f', 2), QString::number(dePulse, 'f', 2))); //The pulses were divided by 100 before they were put on the graph. } } } void Guide::setLatestGuidePoint(bool isChecked) { graphOnLatestPt = isChecked; if(isChecked) guideSlider->setValue(guideSlider->maximum()); } void Guide::toggleShowRAPlot(bool isChecked) { Options::setRADisplayedOnGuideGraph(isChecked); driftGraph->graph(0)->setVisible(isChecked); driftGraph->graph(2)->setVisible(isChecked); driftGraph->replot(); } void Guide::toggleShowDEPlot(bool isChecked) { Options::setDEDisplayedOnGuideGraph(isChecked); driftGraph->graph(1)->setVisible(isChecked); driftGraph->graph(3)->setVisible(isChecked); driftGraph->replot(); } void Guide::toggleRACorrectionsPlot(bool isChecked) { Options::setRACorrDisplayedOnGuideGraph(isChecked); driftGraph->graph(4)->setVisible(isChecked); updateCorrectionsScaleVisibility(); } void Guide::toggleDECorrectionsPlot(bool isChecked) { Options::setDECorrDisplayedOnGuideGraph(isChecked); driftGraph->graph(5)->setVisible(isChecked); updateCorrectionsScaleVisibility(); } void Guide::updateCorrectionsScaleVisibility() { bool isVisible = (Options::rACorrDisplayedOnGuideGraph() || Options::dECorrDisplayedOnGuideGraph()); driftGraph->yAxis2->setVisible(isVisible); correctionSlider->setVisible(isVisible); driftGraph->replot(); } void Guide::setCorrectionGraphScale() { driftGraph->yAxis2->setRange(driftGraph->yAxis->range().lower * correctionSlider->value(), driftGraph->yAxis->range().upper * correctionSlider->value()); driftGraph->replot(); } void Guide::exportGuideData() { int numPoints = driftGraph->graph(0)->dataCount(); if (numPoints == 0) return; QUrl exportFile = QFileDialog::getSaveFileUrl(KStars::Instance(), i18n("Export Guide Data"), guideURLPath, "CSV File (*.csv)"); if (exportFile.isEmpty()) // if user presses cancel return; if (exportFile.toLocalFile().endsWith(QLatin1String(".csv")) == false) exportFile.setPath(exportFile.toLocalFile() + ".csv"); QString path = exportFile.toLocalFile(); if (QFile::exists(path)) { int r = KMessageBox::warningContinueCancel(nullptr, i18n("A file named \"%1\" already exists. " "Overwrite it?", exportFile.fileName()), i18n("Overwrite File?"), KStandardGuiItem::overwrite()); if (r == KMessageBox::Cancel) return; } if (!exportFile.isValid()) { QString message = i18n("Invalid URL: %1", exportFile.url()); KSNotification::sorry(message, i18n("Invalid URL")); return; } QFile file; file.setFileName(path); if (!file.open(QIODevice::WriteOnly)) { QString message = i18n("Unable to write to file %1", path); KSNotification::sorry(message, i18n("Could Not Open File")); return; } QTextStream outstream(&file); outstream << "Frame #, Time Elapsed (sec), Local Time (HMS), RA Error (arcsec), DE Error (arcsec), RA Pulse (ms), DE Pulse (ms)" << endl; for (int i = 0; i < numPoints; i++) { double t = driftGraph->graph(0)->dataMainKey(i); double ra = driftGraph->graph(0)->dataMainValue(i); double de = driftGraph->graph(1)->dataMainValue(i); double raPulse = driftGraph->graph(4)->dataMainValue(i); double dePulse = driftGraph->graph(5)->dataMainValue(i); QTime localTime = guideTimer; localTime = localTime.addSecs(t); outstream << i << ',' << t << ',' << localTime.toString("hh:mm:ss AP") << ',' << ra << ',' << de << ',' << raPulse << ',' << dePulse << ',' << endl; } appendLogText(i18n("Guide Data Saved as: %1", path)); file.close(); } QString Guide::setRecommendedExposureValues(QList values) { exposureIN->setRecommendedValues(values); return exposureIN->getRecommendedValuesString(); } void Guide::addCamera(ISD::GDInterface *newCCD) { ISD::CCD *ccd = static_cast(newCCD); if (CCDs.contains(ccd)) return; if(guiderType != GUIDE_INTERNAL) { connect(ccd, &ISD::CCD::newBLOBManager, [ccd, this](INDI::Property * prop) { if (!strcmp(prop->getName(), "CCD1") || !strcmp(prop->getName(), "CCD2")) { ccd->setBLOBEnabled(false); //This will disable PHD2 external guide frames until it is properly connected. currentCCD = ccd; } }); guiderCombo->clear(); guiderCombo->setEnabled(false); if (guiderType == GUIDE_PHD2) guiderCombo->addItem("PHD2"); else guiderCombo->addItem("LinGuider"); } else { guiderCombo->setEnabled(true); guiderCombo->addItem(ccd->getDeviceName()); } CCDs.append(ccd); checkCCD(); configurePHD2Camera(); } void Guide::configurePHD2Camera() { //Maybe something like this can be done for Linguider? //But for now, Linguider doesn't support INDI Cameras if(guiderType != GUIDE_PHD2) return; //This prevents a crash if phd2guider is null if(!phd2Guider) return; //This way it doesn't check if the equipment isn't connected yet. //It will check again when the equipment is connected. if(!phd2Guider->isConnected()) return; //This way it doesn't check if the equipment List has not been received yet. //It will ask for the list. When the list is received it will check again. if(phd2Guider->getCurrentCamera().isEmpty()) { phd2Guider->requestCurrentEquipmentUpdate(); return; } //this checks to see if a CCD in the list matches the name of PHD2's camera ISD::CCD *ccdMatch = nullptr; QString currentPHD2CameraName = "None"; foreach(ISD::CCD *ccd, CCDs) { if(phd2Guider->getCurrentCamera().contains(ccd->getDeviceName())) { ccdMatch = ccd; currentPHD2CameraName = (phd2Guider->getCurrentCamera()); break; } } //If this method gives the same result as last time, no need to update the Camera info again. //That way the user doesn't see a ton of messages printing about the PHD2 external camera. //But lets make sure the blob is set correctly every time. if(lastPHD2CameraName == currentPHD2CameraName) { setExternalGuiderBLOBEnabled(!Options::guideSubframeEnabled()); return; } //This means that a Guide Camera was connected before but it changed. if(currentCCD) setExternalGuiderBLOBEnabled(false); //Updating the currentCCD currentCCD = ccdMatch; //This updates the last camera name for the next time it is checked. lastPHD2CameraName = currentPHD2CameraName; //This sets a boolean that allows you to tell if the PHD2 camera is in Ekos phd2Guider->setCurrentCameraIsNotInEkos(currentCCD == nullptr); if(phd2Guider->isCurrentCameraNotInEkos()) { appendLogText( i18n("PHD2's current camera: %1, is NOT connected to Ekos. The PHD2 Guide Star Image will be received, but the full external guide frames cannot.", phd2Guider->getCurrentCamera())); subFrameCheck->setEnabled(false); //We don't want to actually change the user's subFrame Setting for when a camera really is connected, just check the box to tell the user. disconnect(subFrameCheck, &QCheckBox::toggled, this, &Ekos::Guide::setSubFrameEnabled); subFrameCheck->setChecked(true); return; } appendLogText( i18n("PHD2's current camera: %1, IS connected to Ekos. You can select whether to use the full external guide frames or just receive the PHD2 Guide Star Image using the SubFrame checkbox.", phd2Guider->getCurrentCamera())); subFrameCheck->setEnabled(true); connect(subFrameCheck, &QCheckBox::toggled, this, &Ekos::Guide::setSubFrameEnabled); subFrameCheck->setChecked(Options::guideSubframeEnabled()); } void Guide::addGuideHead(ISD::GDInterface *newCCD) { if (guiderType != GUIDE_INTERNAL) return; ISD::CCD *ccd = static_cast(newCCD); CCDs.append(ccd); QString guiderName = ccd->getDeviceName() + QString(" Guider"); if (guiderCombo->findText(guiderName) == -1) { guiderCombo->addItem(guiderName); //CCDs.append(static_cast (newCCD)); } //checkCCD(CCDs.count()-1); //guiderCombo->setCurrentIndex(CCDs.count()-1); //setGuiderProcess(Options::useEkosGuider() ? GUIDE_INTERNAL : GUIDE_PHD2); } void Guide::setTelescope(ISD::GDInterface *newTelescope) { currentTelescope = dynamic_cast(newTelescope); syncTelescopeInfo(); } bool Guide::setCamera(const QString &device) { if (guiderType != GUIDE_INTERNAL) return true; for (int i = 0; i < guiderCombo->count(); i++) if (device == guiderCombo->itemText(i)) { guiderCombo->setCurrentIndex(i); checkCCD(i); return true; } return false; } QString Guide::camera() { if (currentCCD) return currentCCD->getDeviceName(); return QString(); } void Guide::checkCCD(int ccdNum) { if (guiderType != GUIDE_INTERNAL) return; if (ccdNum == -1) { ccdNum = guiderCombo->currentIndex(); if (ccdNum == -1) return; } if (ccdNum <= CCDs.count()) { currentCCD = CCDs.at(ccdNum); if (currentCCD->hasGuideHead() && guiderCombo->currentText().contains("Guider")) useGuideHead = true; else useGuideHead = false; ISD::CCDChip *targetChip = currentCCD->getChip(useGuideHead ? ISD::CCDChip::GUIDE_CCD : ISD::CCDChip::PRIMARY_CCD); if (targetChip && targetChip->isCapturing()) return; if (guiderType != GUIDE_INTERNAL) { syncCCDInfo(); return; } //connect(currentCCD, SIGNAL(FITSViewerClosed()), this, &Ekos::Guide::viewerClosed()), Qt::UniqueConnection); connect(currentCCD, &ISD::CCD::numberUpdated, this, &Ekos::Guide::processCCDNumber, Qt::UniqueConnection); connect(currentCCD, &ISD::CCD::newExposureValue, this, &Ekos::Guide::checkExposureValue, Qt::UniqueConnection); targetChip->setImageView(guideView, FITS_GUIDE); syncCCDInfo(); } } void Guide::syncCCDInfo() { INumberVectorProperty *nvp = nullptr; if (currentCCD == nullptr) return; if (useGuideHead) nvp = currentCCD->getBaseDevice()->getNumber("GUIDER_INFO"); else nvp = currentCCD->getBaseDevice()->getNumber("CCD_INFO"); if (nvp) { INumber *np = IUFindNumber(nvp, "CCD_PIXEL_SIZE_X"); if (np) ccdPixelSizeX = np->value; np = IUFindNumber(nvp, "CCD_PIXEL_SIZE_Y"); if (np) ccdPixelSizeY = np->value; np = IUFindNumber(nvp, "CCD_PIXEL_SIZE_Y"); if (np) ccdPixelSizeY = np->value; } updateGuideParams(); } void Guide::setTelescopeInfo(double primaryFocalLength, double primaryAperture, double guideFocalLength, double guideAperture) { if (primaryFocalLength > 0) focal_length = primaryFocalLength; if (primaryAperture > 0) aperture = primaryAperture; // If we have guide scope info, always prefer that over primary if (guideFocalLength > 0) focal_length = guideFocalLength; if (guideAperture > 0) aperture = guideAperture; updateGuideParams(); } void Guide::syncTelescopeInfo() { if (currentTelescope == nullptr || currentTelescope->isConnected() == false) return; INumberVectorProperty *nvp = currentTelescope->getBaseDevice()->getNumber("TELESCOPE_INFO"); if (nvp) { INumber *np = IUFindNumber(nvp, "TELESCOPE_APERTURE"); if (np && np->value > 0) primaryAperture = np->value; np = IUFindNumber(nvp, "GUIDER_APERTURE"); if (np && np->value > 0) guideAperture = np->value; aperture = primaryAperture; //if (currentCCD && currentCCD->getTelescopeType() == ISD::CCD::TELESCOPE_GUIDE) if (FOVScopeCombo->currentIndex() == ISD::CCD::TELESCOPE_GUIDE) aperture = guideAperture; np = IUFindNumber(nvp, "TELESCOPE_FOCAL_LENGTH"); if (np && np->value > 0) primaryFL = np->value; np = IUFindNumber(nvp, "GUIDER_FOCAL_LENGTH"); if (np && np->value > 0) guideFL = np->value; focal_length = primaryFL; //if (currentCCD && currentCCD->getTelescopeType() == ISD::CCD::TELESCOPE_GUIDE) if (FOVScopeCombo->currentIndex() == ISD::CCD::TELESCOPE_GUIDE) focal_length = guideFL; } updateGuideParams(); } void Guide::updateGuideParams() { if (currentCCD == nullptr) return; if (currentCCD->hasGuideHead() == false) useGuideHead = false; ISD::CCDChip *targetChip = currentCCD->getChip(useGuideHead ? ISD::CCDChip::GUIDE_CCD : ISD::CCDChip::PRIMARY_CCD); if (targetChip == nullptr) { appendLogText(i18n("Connection to the guide CCD is lost.")); return; } if (targetChip->getFrameType() != FRAME_LIGHT) return; if(guiderType == GUIDE_INTERNAL) binningCombo->setEnabled(targetChip->canBin()); int subBinX = 1, subBinY = 1; if (targetChip->canBin()) { int maxBinX, maxBinY; targetChip->getBinning(&subBinX, &subBinY); targetChip->getMaxBin(&maxBinX, &maxBinY); binningCombo->blockSignals(true); binningCombo->clear(); for (int i = 1; i <= maxBinX; i++) binningCombo->addItem(QString("%1x%2").arg(i).arg(i)); binningCombo->setCurrentIndex(subBinX - 1); binningCombo->blockSignals(false); } if (frameSettings.contains(targetChip) == false) { int x, y, w, h; if (targetChip->getFrame(&x, &y, &w, &h)) { if (w > 0 && h > 0) { int minX, maxX, minY, maxY, minW, maxW, minH, maxH; targetChip->getFrameMinMax(&minX, &maxX, &minY, &maxY, &minW, &maxW, &minH, &maxH); QVariantMap settings; settings["x"] = Options::guideSubframeEnabled() ? x : minX; settings["y"] = Options::guideSubframeEnabled() ? y : minY; settings["w"] = Options::guideSubframeEnabled() ? w : maxW; settings["h"] = Options::guideSubframeEnabled() ? h : maxH; settings["binx"] = subBinX; settings["biny"] = subBinY; frameSettings[targetChip] = settings; } } } if (ccdPixelSizeX != -1 && ccdPixelSizeY != -1 && aperture != -1 && focal_length != -1) { FOVScopeCombo->setItemData( ISD::CCD::TELESCOPE_PRIMARY, i18nc("F-Number, Focal Length, Aperture", "F%1 Focal Length: %2 mm Aperture: %3 mm2", QString::number(primaryFL / primaryAperture, 'f', 1), QString::number(primaryFL, 'f', 2), QString::number(primaryAperture, 'f', 2)), Qt::ToolTipRole); FOVScopeCombo->setItemData( ISD::CCD::TELESCOPE_GUIDE, i18nc("F-Number, Focal Length, Aperture", "F%1 Focal Length: %2 mm Aperture: %3 mm2", QString::number(guideFL / guideAperture, 'f', 1), QString::number(guideFL, 'f', 2), QString::number(guideAperture, 'f', 2)), Qt::ToolTipRole); guider->setGuiderParams(ccdPixelSizeX, ccdPixelSizeY, aperture, focal_length); emit guideChipUpdated(targetChip); int x, y, w, h; if (targetChip->getFrame(&x, &y, &w, &h)) { guider->setFrameParams(x, y, w, h, subBinX, subBinY); } l_Focal->setText(QString::number(focal_length, 'f', 1)); l_Aperture->setText(QString::number(aperture, 'f', 1)); if (aperture == 0) { l_FbyD->setText("0"); // Pixel scale in arcsec/pixel pixScaleX = 0; pixScaleY = 0; } else { l_FbyD->setText(QString::number(focal_length / aperture, 'f', 1)); // Pixel scale in arcsec/pixel pixScaleX = 206264.8062470963552 * ccdPixelSizeX / 1000.0 / focal_length; pixScaleY = 206264.8062470963552 * ccdPixelSizeY / 1000.0 / focal_length; } // FOV in arcmin double fov_w = (w * pixScaleX) / 60.0; double fov_h = (h * pixScaleY) / 60.0; l_FOV->setText(QString("%1x%2").arg(QString::number(fov_w, 'f', 1), QString::number(fov_h, 'f', 1))); } } void Guide::addST4(ISD::ST4 *newST4) { if (guiderType != GUIDE_INTERNAL) return; - foreach (ISD::ST4 *guidePort, ST4List) + for (auto &guidePort : ST4List) { - if (!strcmp(guidePort->getDeviceName(), newST4->getDeviceName())) + if (guidePort->getDeviceName() == newST4->getDeviceName()) return; } ST4List.append(newST4); ST4Combo->addItem(newST4->getDeviceName()); setST4(0); } bool Guide::setST4(const QString &device) { if (guiderType != GUIDE_INTERNAL) return true; for (int i = 0; i < ST4List.count(); i++) if (ST4List.at(i)->getDeviceName() == device) { ST4Combo->setCurrentIndex(i); setST4(i); return true; } return false; } QString Guide::st4() { if (guiderType != GUIDE_INTERNAL || ST4Combo->currentIndex() == -1) return QString(); return ST4Combo->currentText(); } void Guide::setST4(int index) { if (ST4List.empty() || index >= ST4List.count() || guiderType != GUIDE_INTERNAL) return; ST4Driver = ST4List.at(index); GuideDriver = ST4Driver; } void Guide::setAO(ISD::ST4 *newAO) { AODriver = newAO; //guider->setAO(true); } bool Guide::capture() { buildOperationStack(GUIDE_CAPTURE); return executeOperationStack(); } bool Guide::captureOneFrame() { captureTimeout.stop(); if (currentCCD == nullptr) return false; if (currentCCD->isConnected() == false) { appendLogText(i18n("Error: lost connection to CCD.")); return false; } // If CCD Telescope Type does not match desired scope type, change it if (currentCCD->getTelescopeType() != FOVScopeCombo->currentIndex()) currentCCD->setTelescopeType(static_cast(FOVScopeCombo->currentIndex())); double seqExpose = exposureIN->value(); ISD::CCDChip *targetChip = currentCCD->getChip(useGuideHead ? ISD::CCDChip::GUIDE_CCD : ISD::CCDChip::PRIMARY_CCD); targetChip->setCaptureMode(FITS_GUIDE); targetChip->setFrameType(FRAME_LIGHT); if (darkFrameCheck->isChecked()) targetChip->setCaptureFilter(FITS_NONE); else targetChip->setCaptureFilter(static_cast(filterCombo->currentIndex())); guideView->setBaseSize(guideWidget->size()); setBusy(true); if (frameSettings.contains(targetChip)) { QVariantMap settings = frameSettings[targetChip]; targetChip->setFrame(settings["x"].toInt(), settings["y"].toInt(), settings["w"].toInt(), settings["h"].toInt()); } currentCCD->setTransformFormat(ISD::CCD::FORMAT_FITS); connect(currentCCD, &ISD::CCD::BLOBUpdated, this, &Ekos::Guide::newFITS, Qt::UniqueConnection); qCDebug(KSTARS_EKOS_GUIDE) << "Capturing frame..."; double finalExposure = seqExpose; // Increase exposure for calibration frame if we need auto-select a star // To increase chances we detect one. if (operationStack.contains(GUIDE_STAR_SELECT) && Options::guideAutoStarEnabled()) finalExposure *= 3; // Timeout is exposure duration + timeout threshold in seconds captureTimeout.start(finalExposure * 1000 + CAPTURE_TIMEOUT_THRESHOLD); targetChip->capture(finalExposure); return true; } bool Guide::abort() { if (currentCCD && guiderType == GUIDE_INTERNAL) { captureTimeout.stop(); pulseTimer.stop(); ISD::CCDChip *targetChip = currentCCD->getChip(useGuideHead ? ISD::CCDChip::GUIDE_CCD : ISD::CCDChip::PRIMARY_CCD); if (targetChip->isCapturing()) targetChip->abortExposure(); } manualDitherB->setEnabled(false); setBusy(false); switch (state) { case GUIDE_IDLE: case GUIDE_CONNECTED: case GUIDE_DISCONNECTED: break; case GUIDE_CALIBRATING: case GUIDE_DITHERING: case GUIDE_STAR_SELECT: case GUIDE_CAPTURE: case GUIDE_GUIDING: case GUIDE_LOOPING: guider->abort(); break; default: break; } return true; } void Guide::setBusy(bool enable) { if (enable && pi->isAnimated()) return; else if (enable == false && pi->isAnimated() == false) return; if (enable) { clearCalibrationB->setEnabled(false); guideB->setEnabled(false); captureB->setEnabled(false); loopB->setEnabled(false); darkFrameCheck->setEnabled(false); subFrameCheck->setEnabled(false); autoStarCheck->setEnabled(false); stopB->setEnabled(true); pi->startAnimation(); //disconnect(guideView, SIGNAL(trackingStarSelected(int,int)), this, &Ekos::Guide::setTrackingStar(int,int))); } else { if(guiderType != GUIDE_LINGUIDER) { captureB->setEnabled(true); loopB->setEnabled(true); autoStarCheck->setEnabled(true); if(currentCCD) subFrameCheck->setEnabled(true); } if (guiderType == GUIDE_INTERNAL) darkFrameCheck->setEnabled(true); if (calibrationComplete) clearCalibrationB->setEnabled(true); guideB->setEnabled(true); stopB->setEnabled(false); pi->stopAnimation(); connect(guideView, &FITSView::trackingStarSelected, this, &Ekos::Guide::setTrackingStar, Qt::UniqueConnection); } } void Guide::processCaptureTimeout() { auto restartExposure = [&]() { appendLogText(i18n("Exposure timeout. Restarting exposure...")); currentCCD->setTransformFormat(ISD::CCD::FORMAT_FITS); ISD::CCDChip *targetChip = currentCCD->getChip(useGuideHead ? ISD::CCDChip::GUIDE_CCD : ISD::CCDChip::PRIMARY_CCD); targetChip->abortExposure(); targetChip->capture(exposureIN->value()); captureTimeout.start(exposureIN->value() * 1000 + CAPTURE_TIMEOUT_THRESHOLD); }; m_CaptureTimeoutCounter++; if (m_DeviceRestartCounter >= 3) { m_CaptureTimeoutCounter = 0; m_DeviceRestartCounter = 0; if (state == GUIDE_GUIDING) appendLogText(i18n("Exposure timeout. Aborting Autoguide.")); else if (state == GUIDE_DITHERING) appendLogText(i18n("Exposure timeout. Aborting Dithering.")); else if (state == GUIDE_CALIBRATING) appendLogText(i18n("Exposure timeout. Aborting Calibration.")); abort(); return; } if (m_CaptureTimeoutCounter > 1) { QString camera = currentCCD->getDeviceName(); QString via = ST4Driver ? ST4Driver->getDeviceName() : ""; emit driverTimedout(camera); QTimer::singleShot(5000, [ &, camera, via]() { m_DeviceRestartCounter++; reconnectDriver(camera, via); }); return; } else restartExposure(); } void Guide::reconnectDriver(const QString &camera, const QString &via) { for (auto &oneCamera : CCDs) { if (oneCamera->getDeviceName() == camera) { // Set camera again to the one we restarted guiderCombo->setCurrentIndex(guiderCombo->findText(camera)); ST4Combo->setCurrentIndex(ST4Combo->findText(via)); checkCCD(); // restart capture m_CaptureTimeoutCounter = 0; captureOneFrame(); return; } } QTimer::singleShot(5000, this, [ &, camera, via]() { reconnectDriver(camera, via); }); } void Guide::newFITS(IBLOB *bp) { INDI_UNUSED(bp); captureTimeout.stop(); m_CaptureTimeoutCounter = 0; disconnect(currentCCD, &ISD::CCD::BLOBUpdated, this, &Ekos::Guide::newFITS); qCDebug(KSTARS_EKOS_GUIDE) << "Received guide frame."; ISD::CCDChip *targetChip = currentCCD->getChip(useGuideHead ? ISD::CCDChip::GUIDE_CCD : ISD::CCDChip::PRIMARY_CCD); int subBinX = 1, subBinY = 1; targetChip->getBinning(&subBinX, &subBinY); if (starCenter.x() == 0 && starCenter.y() == 0) { int x = 0, y = 0, w = 0, h = 0; if (frameSettings.contains(targetChip)) { QVariantMap settings = frameSettings[targetChip]; x = settings["x"].toInt(); y = settings["y"].toInt(); w = settings["w"].toInt(); h = settings["h"].toInt(); } else targetChip->getFrame(&x, &y, &w, &h); starCenter.setX(w / (2 * subBinX)); starCenter.setY(h / (2 * subBinY)); starCenter.setZ(subBinX); } syncTrackingBoxPosition(); setCaptureComplete(); } void Guide::setCaptureComplete() { DarkLibrary::Instance()->disconnect(this); if (operationStack.isEmpty() == false) { executeOperationStack(); return; } switch (state) { case GUIDE_IDLE: case GUIDE_ABORTED: case GUIDE_CONNECTED: case GUIDE_DISCONNECTED: case GUIDE_CALIBRATION_SUCESS: case GUIDE_CALIBRATION_ERROR: case GUIDE_DITHERING_ERROR: setBusy(false); break; case GUIDE_CAPTURE: state = GUIDE_IDLE; emit newStatus(state); setBusy(false); break; case GUIDE_LOOPING: capture(); break; case GUIDE_CALIBRATING: guider->calibrate(); break; case GUIDE_GUIDING: guider->guide(); break; case GUIDE_DITHERING: guider->dither(Options::ditherPixels()); break; // Feature only of internal guider case GUIDE_MANUAL_DITHERING: dynamic_cast(guider)->processManualDithering(); break; case GUIDE_REACQUIRE: guider->reacquire(); break; case GUIDE_DITHERING_SETTLE: if (Options::ditherNoGuiding()) return; capture(); break; default: break; } emit newStarPixmap(guideView->getTrackingBoxPixmap(10)); } void Guide::appendLogText(const QString &text) { m_LogText.insert(0, i18nc("log entry; %1 is the date, %2 is the text", "%1 %2", KStarsData::Instance()->lt().toString("yyyy-MM-ddThh:mm:ss"), text)); qCInfo(KSTARS_EKOS_GUIDE) << text; emit newLog(text); } void Guide::clearLog() { m_LogText.clear(); emit newLog(QString()); } void Guide::setDECSwap(bool enable) { if (ST4Driver == nullptr || guider == nullptr) return; if (guiderType == GUIDE_INTERNAL) { dynamic_cast(guider)->setDECSwap(enable); ST4Driver->setDECSwap(enable); } } bool Guide::sendPulse(GuideDirection ra_dir, int ra_msecs, GuideDirection dec_dir, int dec_msecs) { if (GuideDriver == nullptr || (ra_dir == NO_DIR && dec_dir == NO_DIR)) return false; if (state == GUIDE_CALIBRATING) pulseTimer.start((ra_msecs > dec_msecs ? ra_msecs : dec_msecs) + 100); return GuideDriver->doPulse(ra_dir, ra_msecs, dec_dir, dec_msecs); } bool Guide::sendPulse(GuideDirection dir, int msecs) { if (GuideDriver == nullptr || dir == NO_DIR) return false; if (state == GUIDE_CALIBRATING) pulseTimer.start(msecs + 100); return GuideDriver->doPulse(dir, msecs); } QStringList Guide::getST4Devices() { QStringList devices; foreach (ISD::ST4 *driver, ST4List) devices << driver->getDeviceName(); return devices; } bool Guide::calibrate() { // Set status to idle and let the operations change it as they get executed state = GUIDE_IDLE; emit newStatus(state); if (guiderType == GUIDE_INTERNAL) { ISD::CCDChip *targetChip = currentCCD->getChip(useGuideHead ? ISD::CCDChip::GUIDE_CCD : ISD::CCDChip::PRIMARY_CCD); if (frameSettings.contains(targetChip)) { targetChip->resetFrame(); int x, y, w, h; targetChip->getFrame(&x, &y, &w, &h); QVariantMap settings = frameSettings[targetChip]; settings["x"] = x; settings["y"] = y; settings["w"] = w; settings["h"] = h; frameSettings[targetChip] = settings; subFramed = false; } } saveSettings(); buildOperationStack(GUIDE_CALIBRATING); executeOperationStack(); qCDebug(KSTARS_EKOS_GUIDE) << "Starting calibration using CCD:" << currentCCD->getDeviceName() << "via" << ST4Combo->currentText(); return true; } bool Guide::guide() { auto executeGuide = [this]() { if(guiderType != GUIDE_PHD2) { if (calibrationComplete == false) { calibrate(); return; } } saveSettings(); guider->guide(); //If PHD2 gets a Guide command and it is looping, it will accept a lock position //but if it was not looping it will ignore the lock position and do an auto star automatically //This is not the default behavior in Ekos if auto star is not selected. //This gets around that by noting the position of the tracking box, and enforcing it after the state switches to guide. if(!Options::guideAutoStarEnabled()) { if(guiderType == GUIDE_PHD2 && guideView->isTrackingBoxEnabled()) { double x = starCenter.x(); double y = starCenter.y(); if(guideView->getImageData() != nullptr) { if(guideView->getImageData()->width() > 50) { guideConnect = connect(this, &Guide::newStatus, this, [this, x, y](Ekos::GuideState newState) { if(newState == GUIDE_GUIDING) { phd2Guider->setLockPosition(x, y); disconnect(guideConnect); } }); } } } } }; if (Options::defaultCaptureCCD() == guiderCombo->currentText()) { connect(KSMessageBox::Instance(), &KSMessageBox::accepted, this, [this, executeGuide]() { //QObject::disconnect(KSMessageBox::Instance(), &KSMessageBox::accepted, this, nullptr); KSMessageBox::Instance()->disconnect(this); executeGuide(); }); KSMessageBox::Instance()->questionYesNo( i18n("The guide camera is identical to the primary imaging camera. Are you sure you want to continue?")); return false; } if (m_MountStatus == ISD::Telescope::MOUNT_PARKED) { KSMessageBox::Instance()->sorry(i18n("The mount is parked. Unpark to start guiding.")); return false; } executeGuide(); return true; } bool Guide::dither() { if (Options::ditherNoGuiding() && state == GUIDE_IDLE) { ditherDirectly(); return true; } if (state == GUIDE_DITHERING || state == GUIDE_DITHERING_SETTLE) return true; //This adds a dither text item to the graph where dithering occurred. double time = guideTimer.elapsed() / 1000.0; QCPItemText *ditherLabel = new QCPItemText(driftGraph); ditherLabel->setPositionAlignment(Qt::AlignVCenter | Qt::AlignLeft); ditherLabel->position->setType(QCPItemPosition::ptPlotCoords); ditherLabel->position->setCoords(time, 1.5); ditherLabel->setColor(Qt::white); ditherLabel->setBrush(Qt::NoBrush); ditherLabel->setPen(Qt::NoPen); ditherLabel->setText("Dither"); ditherLabel->setFont(QFont(font().family(), 10)); if (guiderType == GUIDE_INTERNAL) { if (state != GUIDE_GUIDING) capture(); setStatus(GUIDE_DITHERING); return true; } else return guider->dither(Options::ditherPixels()); } bool Guide::suspend() { if (state == GUIDE_SUSPENDED) return true; else if (state >= GUIDE_CAPTURE) return guider->suspend(); else return false; } bool Guide::resume() { if (state == GUIDE_GUIDING) return true; else if (state == GUIDE_SUSPENDED) return guider->resume(); else return false; } void Guide::setCaptureStatus(CaptureState newState) { switch (newState) { case CAPTURE_DITHERING: dither(); break; default: break; } } void Guide::setPierSide(ISD::Telescope::PierSide newSide) { Q_UNUSED(newSide); // If pier side changes in internal guider // and calibration was already done // then let's swap if (guiderType == GUIDE_INTERNAL && state != GUIDE_GUIDING && state != GUIDE_CALIBRATING && calibrationComplete) { clearCalibration(); appendLogText(i18n("Pier side change detected. Clearing calibration.")); } } void Guide::setMountStatus(ISD::Telescope::Status newState) { m_MountStatus = newState; if (newState == ISD::Telescope::MOUNT_PARKING || newState == ISD::Telescope::MOUNT_SLEWING) { // reset the calibration if "Always reset calibration" is selected and the mount moves if (Options::resetGuideCalibration()) { appendLogText(i18n("Mount is moving. Resetting calibration...")); clearCalibration(); } // If we're guiding, and the mount either slews or parks, then we abort. if (state == GUIDE_GUIDING || state == GUIDE_DITHERING) { if (newState == ISD::Telescope::MOUNT_PARKING) appendLogText(i18n("Mount is parking. Aborting guide...")); else appendLogText(i18n("Mount is slewing. Aborting guide...")); abort(); } } if (guiderType != GUIDE_INTERNAL) return; switch (newState) { case ISD::Telescope::MOUNT_SLEWING: case ISD::Telescope::MOUNT_PARKING: case ISD::Telescope::MOUNT_MOVING: captureB->setEnabled(false); loopB->setEnabled(false); clearCalibrationB->setEnabled(false); break; default: if (pi->isAnimated() == false) { captureB->setEnabled(true); loopB->setEnabled(true); clearCalibrationB->setEnabled(true); } } } void Guide::setExposure(double value) { exposureIN->setValue(value); } void Guide::setImageFilter(const QString &value) { for (int i = 0; i < filterCombo->count(); i++) if (filterCombo->itemText(i) == value) { filterCombo->setCurrentIndex(i); break; } } void Guide::setCalibrationTwoAxis(bool enable) { Options::setTwoAxisEnabled(enable); } void Guide::setCalibrationAutoStar(bool enable) { autoStarCheck->setChecked(enable); } void Guide::setCalibrationAutoSquareSize(bool enable) { Options::setGuideAutoSquareSizeEnabled(enable); } void Guide::setCalibrationPulseDuration(int pulseDuration) { Options::setCalibrationPulseDuration(pulseDuration); } void Guide::setGuideBoxSizeIndex(int index) { Options::setGuideSquareSizeIndex(index); } void Guide::setGuideAlgorithmIndex(int index) { Options::setGuideAlgorithm(index); } void Guide::setSubFrameEnabled(bool enable) { Options::setGuideSubframeEnabled(enable); if (subFrameCheck->isChecked() != enable) subFrameCheck->setChecked(enable); if(guiderType == GUIDE_PHD2) setExternalGuiderBLOBEnabled(!enable); } void Guide::setDitherSettings(bool enable, double value) { Options::setDitherEnabled(enable); Options::setDitherPixels(value); } void Guide::clearCalibration() { calibrationComplete = false; guider->clearCalibration(); appendLogText(i18n("Calibration is cleared.")); } void Guide::setStatus(Ekos::GuideState newState) { if (newState == state) { // pass through the aborted state if (newState == GUIDE_ABORTED) emit newStatus(state); return; } GuideState previousState = state; state = newState; emit newStatus(state); switch (state) { case GUIDE_CONNECTED: appendLogText(i18n("External guider connected.")); externalConnectB->setEnabled(false); externalDisconnectB->setEnabled(true); clearCalibrationB->setEnabled(true); guideB->setEnabled(true); if(guiderType == GUIDE_PHD2) { captureB->setEnabled(true); loopB->setEnabled(true); autoStarCheck->setEnabled(true); configurePHD2Camera(); setExternalGuiderBLOBEnabled(!Options::guideSubframeEnabled()); boxSizeCombo->setEnabled(true); } break; case GUIDE_DISCONNECTED: appendLogText(i18n("External guider disconnected.")); setBusy(false); //This needs to come before caputureB since it will set it to enabled again. externalConnectB->setEnabled(true); externalDisconnectB->setEnabled(false); clearCalibrationB->setEnabled(false); guideB->setEnabled(false); captureB->setEnabled(false); loopB->setEnabled(false); autoStarCheck->setEnabled(false); boxSizeCombo->setEnabled(false); //setExternalGuiderBLOBEnabled(true); #ifdef Q_OS_OSX repaint(); //This is a band-aid for a bug in QT 5.10.0 #endif break; case GUIDE_CALIBRATION_SUCESS: appendLogText(i18n("Calibration completed.")); calibrationComplete = true; /*if (autoCalibrateGuide) { autoCalibrateGuide = false; guide(); } else setBusy(false);*/ if(guiderType != GUIDE_PHD2) //PHD2 will take care of this. If this command is executed for PHD2, it might start guiding when it is first connected, if the calibration was completed already. guide(); break; case GUIDE_IDLE: case GUIDE_CALIBRATION_ERROR: setBusy(false); manualDitherB->setEnabled(false); break; case GUIDE_CALIBRATING: appendLogText(i18n("Calibration started.")); setBusy(true); break; case GUIDE_GUIDING: if (previousState == GUIDE_SUSPENDED || previousState == GUIDE_DITHERING_SUCCESS) appendLogText(i18n("Guiding resumed.")); else { appendLogText(i18n("Autoguiding started.")); setBusy(true); clearGuideGraphs(); guideTimer = QTime::currentTime(); refreshColorScheme(); } manualDitherB->setEnabled(true); break; case GUIDE_ABORTED: appendLogText(i18n("Autoguiding aborted.")); setBusy(false); break; case GUIDE_SUSPENDED: appendLogText(i18n("Guiding suspended.")); break; case GUIDE_REACQUIRE: capture(); break; case GUIDE_MANUAL_DITHERING: appendLogText(i18n("Manual dithering in progress.")); break; case GUIDE_DITHERING: appendLogText(i18n("Dithering in progress.")); break; case GUIDE_DITHERING_SETTLE: if (Options::ditherSettle() > 0) appendLogText(i18np("Post-dither settling for %1 second...", "Post-dither settling for %1 seconds...", Options::ditherSettle())); capture(); break; case GUIDE_DITHERING_ERROR: appendLogText(i18n("Dithering failed.")); // LinGuider guide continue after dithering failure if (guiderType != GUIDE_LINGUIDER) { //state = GUIDE_IDLE; state = GUIDE_ABORTED; setBusy(false); } break; case GUIDE_DITHERING_SUCCESS: appendLogText(i18n("Dithering completed successfully.")); // Go back to guiding state immediately if using regular guider if (Options::ditherNoGuiding() == false) { setStatus(GUIDE_GUIDING); // Only capture again if we are using internal guider if (guiderType == GUIDE_INTERNAL) capture(); } break; default: break; } } void Guide::updateCCDBin(int index) { if (currentCCD == nullptr || guiderType != GUIDE_INTERNAL) return; ISD::CCDChip *targetChip = currentCCD->getChip(useGuideHead ? ISD::CCDChip::GUIDE_CCD : ISD::CCDChip::PRIMARY_CCD); targetChip->setBinning(index + 1, index + 1); QVariantMap settings = frameSettings[targetChip]; settings["binx"] = index + 1; settings["biny"] = index + 1; frameSettings[targetChip] = settings; guider->setFrameParams(settings["x"].toInt(), settings["y"].toInt(), settings["w"].toInt(), settings["h"].toInt(), settings["binx"].toInt(), settings["biny"].toInt()); } void Guide::processCCDNumber(INumberVectorProperty *nvp) { if (currentCCD == nullptr || (nvp->device != currentCCD->getDeviceName()) || guiderType != GUIDE_INTERNAL) return; if ((!strcmp(nvp->name, "CCD_BINNING") && useGuideHead == false) || (!strcmp(nvp->name, "GUIDER_BINNING") && useGuideHead)) { binningCombo->disconnect(); binningCombo->setCurrentIndex(nvp->np[0].value - 1); connect(binningCombo, static_cast(&QComboBox::activated), this, &Ekos::Guide::updateCCDBin); } } void Guide::checkExposureValue(ISD::CCDChip *targetChip, double exposure, IPState expState) { if (guiderType != GUIDE_INTERNAL) return; INDI_UNUSED(exposure); if (expState == IPS_ALERT && ((state == GUIDE_GUIDING) || (state == GUIDE_DITHERING) || (state == GUIDE_CALIBRATING))) { appendLogText(i18n("Exposure failed. Restarting exposure...")); currentCCD->setTransformFormat(ISD::CCD::FORMAT_FITS); targetChip->capture(exposureIN->value()); } } void Guide::setDarkFrameEnabled(bool enable) { Options::setGuideDarkFrameEnabled(enable); if (darkFrameCheck->isChecked() != enable) darkFrameCheck->setChecked(enable); } void Guide::saveDefaultGuideExposure() { Options::setGuideExposure(exposureIN->value()); if(guiderType == GUIDE_PHD2) phd2Guider->requestSetExposureTime(exposureIN->value() * 1000); } void Guide::setStarPosition(const QVector3D &newCenter, bool updateNow) { starCenter.setX(newCenter.x()); starCenter.setY(newCenter.y()); if (newCenter.z() > 0) starCenter.setZ(newCenter.z()); if (updateNow) syncTrackingBoxPosition(); } void Guide::syncTrackingBoxPosition() { if(!currentCCD || guiderType == GUIDE_LINGUIDER) return; if(guiderType == GUIDE_PHD2) { //This way it won't set the tracking box on the Guide Star Image. if(guideView->getImageData() != nullptr) { if(guideView->getImageData()->width() < 50) { guideView->setTrackingBoxEnabled(false); return; } } } ISD::CCDChip *targetChip = currentCCD->getChip(useGuideHead ? ISD::CCDChip::GUIDE_CCD : ISD::CCDChip::PRIMARY_CCD); Q_ASSERT(targetChip); int subBinX = 1, subBinY = 1; targetChip->getBinning(&subBinX, &subBinY); if (starCenter.isNull() == false) { double boxSize = boxSizeCombo->currentText().toInt(); int x, y, w, h; targetChip->getFrame(&x, &y, &w, &h); // If box size is larger than image size, set it to lower index if (boxSize / subBinX >= w || boxSize / subBinY >= h) { int newIndex = boxSizeCombo->currentIndex() - 1; if (newIndex >= 0) boxSizeCombo->setCurrentIndex(newIndex); return; } // If binning changed, update coords accordingly if (subBinX != starCenter.z()) { if (starCenter.z() > 0) { starCenter.setX(starCenter.x() * (starCenter.z() / subBinX)); starCenter.setY(starCenter.y() * (starCenter.z() / subBinY)); } starCenter.setZ(subBinX); } QRect starRect = QRect(starCenter.x() - boxSize / (2 * subBinX), starCenter.y() - boxSize / (2 * subBinY), boxSize / subBinX, boxSize / subBinY); guideView->setTrackingBoxEnabled(true); guideView->setTrackingBox(starRect); } } bool Guide::setGuiderType(int type) { // Use default guider option if (type == -1) type = Options::guiderType(); else if (type == guiderType) return true; if (state == GUIDE_CALIBRATING || state == GUIDE_GUIDING || state == GUIDE_DITHERING) { appendLogText(i18n("Cannot change guider type while active.")); return false; } if (guider != nullptr) { // Disconnect from host if (guider->isConnected()) guider->Disconnect(); // Disconnect signals guider->disconnect(); } guiderType = static_cast(type); switch (type) { case GUIDE_INTERNAL: { connect(internalGuider, SIGNAL(newPulse(GuideDirection, int)), this, SLOT(sendPulse(GuideDirection, int))); connect(internalGuider, SIGNAL(newPulse(GuideDirection, int, GuideDirection, int)), this, SLOT(sendPulse(GuideDirection, int, GuideDirection, int))); connect(internalGuider, SIGNAL(DESwapChanged(bool)), swapCheck, SLOT(setChecked(bool))); connect(internalGuider, SIGNAL(newStarPixmap(QPixmap &)), this, SIGNAL(newStarPixmap(QPixmap &))); guider = internalGuider; internalGuider->setSquareAlgorithm(opsGuide->kcfg_GuideAlgorithm->currentIndex()); internalGuider->setRegionAxis(opsGuide->kcfg_GuideRegionAxis->currentText().toInt()); clearCalibrationB->setEnabled(true); guideB->setEnabled(true); captureB->setEnabled(true); loopB->setEnabled(true); darkFrameCheck->setEnabled(true); subFrameCheck->setEnabled(true); autoStarCheck->setEnabled(true); guiderCombo->setEnabled(true); ST4Combo->setEnabled(true); exposureIN->setEnabled(true); binningCombo->setEnabled(true); boxSizeCombo->setEnabled(true); filterCombo->setEnabled(true); externalConnectB->setEnabled(false); externalDisconnectB->setEnabled(false); controlGroup->setEnabled(true); infoGroup->setEnabled(true); label_6->setEnabled(true); FOVScopeCombo->setEnabled(true); l_3->setEnabled(true); spinBox_GuideRate->setEnabled(true); l_RecommendedGain->setEnabled(true); l_5->setEnabled(true); l_6->setEnabled(true); l_7->setEnabled(true); l_8->setEnabled(true); l_Aperture->setEnabled(true); l_FOV->setEnabled(true); l_FbyD->setEnabled(true); l_Focal->setEnabled(true); driftGraphicsGroup->setEnabled(true); guiderCombo->setToolTip(i18n("Select guide camera.")); updateGuideParams(); } break; case GUIDE_PHD2: if (phd2Guider.isNull()) phd2Guider = new PHD2(); guider = phd2Guider; phd2Guider->setGuideView(guideView); connect(phd2Guider, SIGNAL(newStarPixmap(QPixmap &)), this, SIGNAL(newStarPixmap(QPixmap &))); clearCalibrationB->setEnabled(true); captureB->setEnabled(false); loopB->setEnabled(false); darkFrameCheck->setEnabled(false); subFrameCheck->setEnabled(false); autoStarCheck->setEnabled(false); guideB->setEnabled(false); //This will be enabled later when equipment connects (or not) externalConnectB->setEnabled(false); checkBox_DirRA->setEnabled(false); eastControlCheck->setEnabled(false); westControlCheck->setEnabled(false); swapCheck->setEnabled(false); controlGroup->setEnabled(false); infoGroup->setEnabled(true); label_6->setEnabled(false); FOVScopeCombo->setEnabled(false); l_3->setEnabled(false); spinBox_GuideRate->setEnabled(false); l_RecommendedGain->setEnabled(false); l_5->setEnabled(false); l_6->setEnabled(false); l_7->setEnabled(false); l_8->setEnabled(false); l_Aperture->setEnabled(false); l_FOV->setEnabled(false); l_FbyD->setEnabled(false); l_Focal->setEnabled(false); driftGraphicsGroup->setEnabled(true); ST4Combo->setEnabled(false); exposureIN->setEnabled(true); binningCombo->setEnabled(false); boxSizeCombo->setEnabled(false); filterCombo->setEnabled(false); guiderCombo->setEnabled(false); if (Options::resetGuideCalibration()) appendLogText(i18n("Warning: Reset Guiding Calibration is enabled. It is recommended to turn this option off for PHD2.")); updateGuideParams(); break; case GUIDE_LINGUIDER: if (linGuider.isNull()) linGuider = new LinGuider(); guider = linGuider; clearCalibrationB->setEnabled(true); captureB->setEnabled(false); loopB->setEnabled(false); darkFrameCheck->setEnabled(false); subFrameCheck->setEnabled(false); autoStarCheck->setEnabled(false); guideB->setEnabled(true); externalConnectB->setEnabled(true); controlGroup->setEnabled(false); infoGroup->setEnabled(false); driftGraphicsGroup->setEnabled(false); ST4Combo->setEnabled(false); exposureIN->setEnabled(false); binningCombo->setEnabled(false); boxSizeCombo->setEnabled(false); filterCombo->setEnabled(false); guiderCombo->setEnabled(false); updateGuideParams(); break; } if (guider != nullptr) { connect(guider, &Ekos::GuideInterface::frameCaptureRequested, this, &Ekos::Guide::capture); connect(guider, &Ekos::GuideInterface::newLog, this, &Ekos::Guide::appendLogText); connect(guider, &Ekos::GuideInterface::newStatus, this, &Ekos::Guide::setStatus); connect(guider, &Ekos::GuideInterface::newStarPosition, this, &Ekos::Guide::setStarPosition); connect(guider, &Ekos::GuideInterface::newAxisDelta, this, &Ekos::Guide::setAxisDelta); connect(guider, &Ekos::GuideInterface::newAxisPulse, this, &Ekos::Guide::setAxisPulse); connect(guider, &Ekos::GuideInterface::newAxisSigma, this, &Ekos::Guide::setAxisSigma); connect(guider, &Ekos::GuideInterface::guideEquipmentUpdated, this, &Ekos::Guide::configurePHD2Camera); } externalConnectB->setEnabled(false); externalDisconnectB->setEnabled(false); if (guider != nullptr && guiderType != GUIDE_INTERNAL) { externalConnectB->setEnabled(!guider->isConnected()); externalDisconnectB->setEnabled(guider->isConnected()); } if (guider != nullptr) guider->Connect(); return true; } void Guide::updateTrackingBoxSize(int currentIndex) { if (currentIndex >= 0) { Options::setGuideSquareSizeIndex(currentIndex); if (guiderType == GUIDE_INTERNAL) dynamic_cast(guider)->setGuideBoxSize(boxSizeCombo->currentText().toInt()); syncTrackingBoxPosition(); } } /* void Guide::onXscaleChanged( int i ) { int rx, ry; driftGraphics->getVisibleRanges( &rx, &ry ); driftGraphics->setVisibleRanges( i*driftGraphics->getGridN(), ry ); driftGraphics->update(); } void Guide::onYscaleChanged( int i ) { int rx, ry; driftGraphics->getVisibleRanges( &rx, &ry ); driftGraphics->setVisibleRanges( rx, i*driftGraphics->getGridN() ); driftGraphics->update(); } */ void Guide::onThresholdChanged(int index) { switch (guiderType) { case GUIDE_INTERNAL: dynamic_cast(guider)->setSquareAlgorithm(index); break; default: break; } } void Guide::onInfoRateChanged(double val) { Options::setGuidingRate(val); double gain = 0; if (val > 0.01) gain = 1000.0 / (val * 15.0); l_RecommendedGain->setText(i18n("P: %1", QString().setNum(gain, 'f', 2))); } void Guide::onEnableDirRA(bool enable) { Options::setRAGuideEnabled(enable); } void Guide::onEnableDirDEC(bool enable) { Options::setDECGuideEnabled(enable); updatePHD2Directions(); } void Guide::syncSettings() { QSpinBox *pSB = nullptr; QDoubleSpinBox *pDSB = nullptr; QCheckBox *pCB = nullptr; QObject *obj = sender(); if ((pSB = qobject_cast(obj))) { if (pSB == spinBox_MaxPulseRA) Options::setRAMaximumPulse(pSB->value()); else if (pSB == spinBox_MaxPulseDEC) Options::setDECMaximumPulse(pSB->value()); else if (pSB == spinBox_MinPulseRA) Options::setRAMinimumPulse(pSB->value()); else if (pSB == spinBox_MinPulseDEC) Options::setDECMinimumPulse(pSB->value()); } else if ((pDSB = qobject_cast(obj))) { if (pDSB == spinBox_PropGainRA) Options::setRAProportionalGain(pDSB->value()); else if (pDSB == spinBox_PropGainDEC) Options::setDECProportionalGain(pDSB->value()); else if (pDSB == spinBox_IntGainRA) Options::setRAIntegralGain(pDSB->value()); else if (pDSB == spinBox_IntGainDEC) Options::setDECIntegralGain(pDSB->value()); else if (pDSB == spinBox_DerGainRA) Options::setRADerivativeGain(pDSB->value()); else if (pDSB == spinBox_DerGainDEC) Options::setDECDerivativeGain(pDSB->value()); } else if ((pCB = qobject_cast(obj))) { if (pCB == autoStarCheck) Options::setGuideAutoStarEnabled(pCB->isChecked()); } } void Guide::onControlDirectionChanged(bool enable) { QObject *obj = sender(); if (northControlCheck == dynamic_cast(obj)) { Options::setNorthDECGuideEnabled(enable); updatePHD2Directions(); } else if (southControlCheck == dynamic_cast(obj)) { Options::setSouthDECGuideEnabled(enable); updatePHD2Directions(); } else if (westControlCheck == dynamic_cast(obj)) { Options::setWestRAGuideEnabled(enable); } else if (eastControlCheck == dynamic_cast(obj)) { Options::setEastRAGuideEnabled(enable); } } void Guide::updatePHD2Directions() { if(guiderType == GUIDE_PHD2) phd2Guider -> requestSetDEGuideMode(checkBox_DirDEC->isChecked(), northControlCheck->isChecked(), southControlCheck->isChecked()); } void Guide::updateDirectionsFromPHD2(QString mode) { //disable connections disconnect(checkBox_DirDEC, &QCheckBox::toggled, this, &Ekos::Guide::onEnableDirDEC); disconnect(northControlCheck, &QCheckBox::toggled, this, &Ekos::Guide::onControlDirectionChanged); disconnect(southControlCheck, &QCheckBox::toggled, this, &Ekos::Guide::onControlDirectionChanged); if(mode == "Auto") { checkBox_DirDEC->setChecked(true); northControlCheck->setChecked(true); southControlCheck->setChecked(true); } else if(mode == "North") { checkBox_DirDEC->setChecked(true); northControlCheck->setChecked(true); southControlCheck->setChecked(false); } else if(mode == "South") { checkBox_DirDEC->setChecked(true); northControlCheck->setChecked(false); southControlCheck->setChecked(true); } else //Off { checkBox_DirDEC->setChecked(false); northControlCheck->setChecked(true); southControlCheck->setChecked(true); } //Re-enable connections connect(checkBox_DirDEC, &QCheckBox::toggled, this, &Ekos::Guide::onEnableDirDEC); connect(northControlCheck, &QCheckBox::toggled, this, &Ekos::Guide::onControlDirectionChanged); connect(southControlCheck, &QCheckBox::toggled, this, &Ekos::Guide::onControlDirectionChanged); } void Guide::loadSettings() { // Exposure exposureIN->setValue(Options::guideExposure()); // Box Size boxSizeCombo->setCurrentIndex(Options::guideSquareSizeIndex()); // Dark frame? darkFrameCheck->setChecked(Options::guideDarkFrameEnabled()); // Subframed? subFrameCheck->setChecked(Options::guideSubframeEnabled()); // Guiding Rate spinBox_GuideRate->setValue(Options::guidingRate()); // RA/DEC enabled? checkBox_DirRA->setChecked(Options::rAGuideEnabled()); checkBox_DirDEC->setChecked(Options::dECGuideEnabled()); // N/S enabled? northControlCheck->setChecked(Options::northDECGuideEnabled()); southControlCheck->setChecked(Options::southDECGuideEnabled()); // W/E enabled? westControlCheck->setChecked(Options::westRAGuideEnabled()); eastControlCheck->setChecked(Options::eastRAGuideEnabled()); // PID Control - Proportional Gain spinBox_PropGainRA->setValue(Options::rAProportionalGain()); spinBox_PropGainDEC->setValue(Options::dECProportionalGain()); // PID Control - Integral Gain spinBox_IntGainRA->setValue(Options::rAIntegralGain()); spinBox_IntGainDEC->setValue(Options::dECIntegralGain()); // PID Control - Derivative Gain spinBox_DerGainRA->setValue(Options::rADerivativeGain()); spinBox_DerGainDEC->setValue(Options::dECDerivativeGain()); // Max Pulse Duration (ms) spinBox_MaxPulseRA->setValue(Options::rAMaximumPulse()); spinBox_MaxPulseDEC->setValue(Options::dECMaximumPulse()); // Min Pulse Duration (ms) spinBox_MinPulseRA->setValue(Options::rAMinimumPulse()); spinBox_MinPulseDEC->setValue(Options::dECMinimumPulse()); // Autostar autoStarCheck->setChecked(Options::guideAutoStarEnabled()); } void Guide::saveSettings() { // Exposure Options::setGuideExposure(exposureIN->value()); // Box Size Options::setGuideSquareSizeIndex(boxSizeCombo->currentIndex()); // Dark frame? Options::setGuideDarkFrameEnabled(darkFrameCheck->isChecked()); // Subframed? Options::setGuideSubframeEnabled(subFrameCheck->isChecked()); // Guiding Rate? Options::setGuidingRate(spinBox_GuideRate->value()); // RA/DEC enabled? Options::setRAGuideEnabled(checkBox_DirRA->isChecked()); Options::setDECGuideEnabled(checkBox_DirDEC->isChecked()); // N/S enabled? Options::setNorthDECGuideEnabled(northControlCheck->isChecked()); Options::setSouthDECGuideEnabled(southControlCheck->isChecked()); // W/E enabled? Options::setWestRAGuideEnabled(westControlCheck->isChecked()); Options::setEastRAGuideEnabled(eastControlCheck->isChecked()); // PID Control - Proportional Gain Options::setRAProportionalGain(spinBox_PropGainRA->value()); Options::setDECProportionalGain(spinBox_PropGainDEC->value()); // PID Control - Integral Gain Options::setRAIntegralGain(spinBox_IntGainRA->value()); Options::setDECIntegralGain(spinBox_IntGainDEC->value()); // PID Control - Derivative Gain Options::setRADerivativeGain(spinBox_DerGainRA->value()); Options::setDECDerivativeGain(spinBox_DerGainDEC->value()); // Max Pulse Duration (ms) Options::setRAMaximumPulse(spinBox_MaxPulseRA->value()); Options::setDECMaximumPulse(spinBox_MaxPulseDEC->value()); // Min Pulse Duration (ms) Options::setRAMinimumPulse(spinBox_MinPulseRA->value()); Options::setDECMinimumPulse(spinBox_MinPulseDEC->value()); } void Guide::setTrackingStar(int x, int y) { QVector3D newStarPosition(x, y, -1); setStarPosition(newStarPosition, true); if(guiderType == GUIDE_PHD2) { //The Guide Star Image is 32 pixels across or less, so this guarantees it isn't that. if(guideView->getImageData() != nullptr) { if(guideView->getImageData()->width() > 50) phd2Guider->setLockPosition(starCenter.x(), starCenter.y()); } } /*if (state == GUIDE_STAR_SELECT) { guider->setStarPosition(newStarPosition); guider->calibrate(); }*/ if (operationStack.isEmpty() == false) executeOperationStack(); } void Guide::setAxisDelta(double ra, double de) { //If PHD2 starts guiding because somebody pusted the button remotely, we want to set the state to guiding. //If guide pulses start coming in, it must be guiding. if(guiderType == GUIDE_PHD2 && state != GUIDE_GUIDING) setStatus(GUIDE_GUIDING); // Time since timer started. double key = guideTimer.elapsed() / 1000.0; ra = -ra; //The ra is backwards in sign from how it should be displayed on the graph. driftGraph->graph(0)->addData(key, ra); driftGraph->graph(1)->addData(key, de); int currentNumPoints = driftGraph->graph(0)->dataCount(); guideSlider->setMaximum(currentNumPoints); if(graphOnLatestPt) guideSlider->setValue(currentNumPoints); // Expand range if it doesn't fit already if (driftGraph->yAxis->range().contains(ra) == false) driftGraph->yAxis->setRange(-1.25 * ra, 1.25 * ra); if (driftGraph->yAxis->range().contains(de) == false) driftGraph->yAxis->setRange(-1.25 * de, 1.25 * de); // Show last 120 seconds //driftGraph->xAxis->setRange(key, 120, Qt::AlignRight); if(graphOnLatestPt) { driftGraph->xAxis->setRange(key, driftGraph->xAxis->range().size(), Qt::AlignRight); driftGraph->graph(2)->data()->clear(); //Clear highlighted RA point driftGraph->graph(3)->data()->clear(); //Clear highlighted DEC point driftGraph->graph(2)->addData(key, ra); //Set highlighted RA point to latest point driftGraph->graph(3)->addData(key, de); //Set highlighted DEC point to latest point } driftGraph->replot(); //Add to Drift Plot driftPlot->graph(0)->addData(ra, de); if(graphOnLatestPt) { driftPlot->graph(1)->data()->clear(); //Clear highlighted point driftPlot->graph(1)->addData(ra, de); //Set highlighted point to latest point } if (driftPlot->xAxis->range().contains(ra) == false || driftPlot->yAxis->range().contains(de) == false) { driftPlot->setBackground(QBrush(Qt::gray)); QTimer::singleShot(300, this, [ = ]() { driftPlot->setBackground(QBrush(Qt::black)); driftPlot->replot(); }); } driftPlot->replot(); l_DeltaRA->setText(QString::number(ra, 'f', 2)); l_DeltaDEC->setText(QString::number(de, 'f', 2)); emit newAxisDelta(ra, de); profilePixmap = driftGraph->grab(); emit newProfilePixmap(profilePixmap); } void Guide::setAxisSigma(double ra, double de) { l_ErrRA->setText(QString::number(ra, 'f', 2)); l_ErrDEC->setText(QString::number(de, 'f', 2)); l_TotalRMS->setText(QString::number(sqrt(ra * ra + de * de), 'f', 2)); emit newAxisSigma(ra, de); } QList Guide::axisDelta() { QList delta; delta << l_DeltaRA->text().toDouble() << l_DeltaDEC->text().toDouble(); return delta; } QList Guide::axisSigma() { QList sigma; sigma << l_ErrRA->text().toDouble() << l_ErrDEC->text().toDouble(); return sigma; } void Guide::setAxisPulse(double ra, double de) { l_PulseRA->setText(QString::number(static_cast(ra))); l_PulseDEC->setText(QString::number(static_cast(de))); double key = guideTimer.elapsed() / 1000.0; driftGraph->graph(4)->addData(key, ra); driftGraph->graph(5)->addData(key, de); } void Guide::refreshColorScheme() { // Drift color legend if (driftGraph) { if (driftGraph->graph(0) && driftGraph->graph(1) && driftGraph->graph(2) && driftGraph->graph(3) && driftGraph->graph(4) && driftGraph->graph(5)) { driftGraph->graph(0)->setPen(QPen(KStarsData::Instance()->colorScheme()->colorNamed("RAGuideError"))); driftGraph->graph(1)->setPen(QPen(KStarsData::Instance()->colorScheme()->colorNamed("DEGuideError"))); driftGraph->graph(2)->setPen(QPen(KStarsData::Instance()->colorScheme()->colorNamed("RAGuideError"))); driftGraph->graph(2)->setScatterStyle(QCPScatterStyle(QCPScatterStyle::ssPlusCircle, QPen(KStarsData::Instance()->colorScheme()->colorNamed("RAGuideError"), 2), QBrush(), 10)); driftGraph->graph(3)->setPen(QPen(KStarsData::Instance()->colorScheme()->colorNamed("DEGuideError"))); driftGraph->graph(3)->setScatterStyle(QCPScatterStyle(QCPScatterStyle::ssPlusCircle, QPen(KStarsData::Instance()->colorScheme()->colorNamed("DEGuideError"), 2), QBrush(), 10)); QColor raPulseColor(KStarsData::Instance()->colorScheme()->colorNamed("RAGuideError")); raPulseColor.setAlpha(75); driftGraph->graph(4)->setPen(QPen(raPulseColor)); driftGraph->graph(4)->setBrush(QBrush(raPulseColor, Qt::Dense4Pattern)); QColor dePulseColor(KStarsData::Instance()->colorScheme()->colorNamed("DEGuideError")); dePulseColor.setAlpha(75); driftGraph->graph(5)->setPen(QPen(dePulseColor)); driftGraph->graph(5)->setBrush(QBrush(dePulseColor, Qt::Dense4Pattern)); } } } void Guide::driftMouseClicked(QMouseEvent *event) { if (event->buttons() & Qt::RightButton) { driftGraph->yAxis->setRange(-3, 3); } } void Guide::driftMouseOverLine(QMouseEvent *event) { double key = driftGraph->xAxis->pixelToCoord(event->localPos().x()); if (driftGraph->xAxis->range().contains(key)) { QCPGraph *graph = qobject_cast(driftGraph->plottableAt(event->pos(), false)); if (graph) { int raIndex = driftGraph->graph(0)->findBegin(key); int deIndex = driftGraph->graph(1)->findBegin(key); double raDelta = driftGraph->graph(0)->dataMainValue(raIndex); double deDelta = driftGraph->graph(1)->dataMainValue(deIndex); double raPulse = driftGraph->graph(4)->dataMainValue(raIndex); //Get RA Pulse from RA pulse data double dePulse = driftGraph->graph(5)->dataMainValue(deIndex); //Get DEC Pulse from DEC pulse data // Compute time value: QTime localTime = guideTimer; localTime = localTime.addSecs(key); QToolTip::hideText(); if(raPulse == 0 && dePulse == 0) { QToolTip::showText( event->globalPos(), i18nc("Drift graphics tooltip; %1 is local time; %2 is RA deviation; %3 is DE deviation in arcseconds;", "" "" "" "" "
LT: %1
RA: %2 \"
DE: %3 \"
", localTime.toString("hh:mm:ss AP"), QString::number(raDelta, 'f', 2), QString::number(deDelta, 'f', 2))); } else { QToolTip::showText( event->globalPos(), i18nc("Drift graphics tooltip; %1 is local time; %2 is RA deviation; %3 is DE deviation in arcseconds; %4 is RA Pulse in ms; %5 is DE Pulse in ms", "" "" "" "" "" "" "
LT: %1
RA: %2 \"
DE: %3 \"
RA Pulse: %4 ms
DE Pulse: %5 ms
", localTime.toString("hh:mm:ss AP"), QString::number(raDelta, 'f', 2), QString::number(deDelta, 'f', 2), QString::number(raPulse, 'f', 2), QString::number(dePulse, 'f', 2))); //The pulses were divided by 100 before they were put on the graph. } } else QToolTip::hideText(); driftGraph->replot(); } } void Guide::buildOperationStack(GuideState operation) { operationStack.clear(); switch (operation) { case GUIDE_CAPTURE: if (Options::guideDarkFrameEnabled()) operationStack.push(GUIDE_DARK); operationStack.push(GUIDE_CAPTURE); operationStack.push(GUIDE_SUBFRAME); break; case GUIDE_CALIBRATING: operationStack.push(GUIDE_CALIBRATING); if (guiderType == GUIDE_INTERNAL) { if (Options::guideDarkFrameEnabled()) operationStack.push(GUIDE_DARK); // Auto Star Selected Path if (Options::guideAutoStarEnabled()) { // If subframe is enabled and we need to auto select a star, then we need to make the final capture // of the subframed image. This is only done if we aren't already subframed. if (subFramed == false && Options::guideSubframeEnabled()) operationStack.push(GUIDE_CAPTURE); // Do not subframe and auto-select star on Image Guiding mode if (Options::imageGuidingEnabled() == false) { operationStack.push(GUIDE_SUBFRAME); operationStack.push(GUIDE_STAR_SELECT); } operationStack.push(GUIDE_CAPTURE); // If we are being ask to go full frame, let's do that first if (subFramed == true && Options::guideSubframeEnabled() == false) operationStack.push(GUIDE_SUBFRAME); } // Manual Star Selection Path else { // In Image Guiding, we never need to subframe if (Options::imageGuidingEnabled() == false) { // Final capture before we start calibrating if (subFramed == false && Options::guideSubframeEnabled()) operationStack.push(GUIDE_CAPTURE); // Subframe if required operationStack.push(GUIDE_SUBFRAME); } // First capture an image operationStack.push(GUIDE_CAPTURE); } } break; default: break; } } bool Guide::executeOperationStack() { if (operationStack.isEmpty()) return false; GuideState nextOperation = operationStack.pop(); bool actionRequired = false; switch (nextOperation) { case GUIDE_SUBFRAME: actionRequired = executeOneOperation(nextOperation); break; case GUIDE_DARK: actionRequired = executeOneOperation(nextOperation); break; case GUIDE_CAPTURE: actionRequired = captureOneFrame(); break; case GUIDE_STAR_SELECT: actionRequired = executeOneOperation(nextOperation); break; case GUIDE_CALIBRATING: if (guiderType == GUIDE_INTERNAL) { guider->setStarPosition(starCenter); dynamic_cast(guider)->setImageGuideEnabled(Options::imageGuidingEnabled()); // No need to calibrate if (Options::imageGuidingEnabled()) { setStatus(GUIDE_CALIBRATION_SUCESS); break; } // Tracking must be engaged if (currentTelescope && currentTelescope->canControlTrack() && currentTelescope->isTracking() == false) currentTelescope->setTrackEnabled(true); } if (guider->calibrate()) { if (guiderType == GUIDE_INTERNAL) disconnect(guideView, SIGNAL(trackingStarSelected(int, int)), this, SLOT(setTrackingStar(int, int))); setBusy(true); } else { emit newStatus(GUIDE_CALIBRATION_ERROR); state = GUIDE_IDLE; appendLogText(i18n("Calibration failed to start.")); setBusy(false); } break; default: break; } // If an additional action is required, return return and continue later if (actionRequired) return true; // Otherwise, continue processing the stack else return executeOperationStack(); } bool Guide::executeOneOperation(GuideState operation) { bool actionRequired = false; ISD::CCDChip *targetChip = currentCCD->getChip(useGuideHead ? ISD::CCDChip::GUIDE_CCD : ISD::CCDChip::PRIMARY_CCD); int subBinX, subBinY; targetChip->getBinning(&subBinX, &subBinY); switch (operation) { case GUIDE_SUBFRAME: { // Check if we need and can subframe if (subFramed == false && Options::guideSubframeEnabled() == true && targetChip->canSubframe()) { int minX, maxX, minY, maxY, minW, maxW, minH, maxH; targetChip->getFrameMinMax(&minX, &maxX, &minY, &maxY, &minW, &maxW, &minH, &maxH); int offset = boxSizeCombo->currentText().toInt() / subBinX; int x = starCenter.x(); int y = starCenter.y(); x = (x - offset * 2) * subBinX; y = (y - offset * 2) * subBinY; int w = offset * 4 * subBinX; int h = offset * 4 * subBinY; if (x < minX) x = minX; if (y < minY) y = minY; if ((x + w) > maxW) w = maxW - x; if ((y + h) > maxH) h = maxH - y; targetChip->setFrame(x, y, w, h); subFramed = true; QVariantMap settings = frameSettings[targetChip]; settings["x"] = x; settings["y"] = y; settings["w"] = w; settings["h"] = h; settings["binx"] = subBinX; settings["biny"] = subBinY; frameSettings[targetChip] = settings; starCenter.setX(w / (2 * subBinX)); starCenter.setY(h / (2 * subBinX)); } // Otherwise check if we are already subframed // and we need to go back to full frame // or if we need to go back to full frame since we need // to reaquire a star else if (subFramed && (Options::guideSubframeEnabled() == false || state == GUIDE_REACQUIRE)) { targetChip->resetFrame(); int x, y, w, h; targetChip->getFrame(&x, &y, &w, &h); QVariantMap settings; settings["x"] = x; settings["y"] = y; settings["w"] = w; settings["h"] = h; settings["binx"] = 1; settings["biny"] = 1; frameSettings[targetChip] = settings; subFramed = false; starCenter.setX(w / (2 * subBinX)); starCenter.setY(h / (2 * subBinX)); //starCenter.setX(0); //starCenter.setY(0); } } break; case GUIDE_DARK: { // Do we need to take a dark frame? if (Options::guideDarkFrameEnabled()) { QVariantMap settings = frameSettings[targetChip]; uint16_t offsetX = settings["x"].toInt() / settings["binx"].toInt(); uint16_t offsetY = settings["y"].toInt() / settings["biny"].toInt(); FITSData *darkData = DarkLibrary::Instance()->getDarkFrame(targetChip, exposureIN->value()); connect(DarkLibrary::Instance(), &DarkLibrary::darkFrameCompleted, this, [&](bool completed) { DarkLibrary::Instance()->disconnect(this); if (completed != darkFrameCheck->isChecked()) setDarkFrameEnabled(completed); if (completed) setCaptureComplete(); else abort(); }); connect(DarkLibrary::Instance(), &DarkLibrary::newLog, this, &Ekos::Guide::appendLogText); actionRequired = true; targetChip->setCaptureFilter(static_cast(filterCombo->currentIndex())); if (darkData) DarkLibrary::Instance()->subtract(darkData, guideView, targetChip->getCaptureFilter(), offsetX, offsetY); else { DarkLibrary::Instance()->captureAndSubtract(targetChip, guideView, exposureIN->value(), offsetX, offsetY); } } } break; case GUIDE_STAR_SELECT: { state = GUIDE_STAR_SELECT; emit newStatus(state); if (Options::guideAutoStarEnabled()) { bool autoStarCaptured = internalGuider->selectAutoStar(); if (autoStarCaptured) { appendLogText(i18n("Auto star selected.")); } else { appendLogText(i18n("Failed to select an auto star.")); actionRequired = true; state = GUIDE_CALIBRATION_ERROR; emit newStatus(state); setBusy(false); } } else { appendLogText(i18n("Select a guide star to calibrate.")); actionRequired = true; } } break; default: break; } return actionRequired; } void Guide::processGuideOptions() { if (Options::guiderType() != guiderType) { guiderType = static_cast(Options::guiderType()); setGuiderType(Options::guiderType()); } } void Guide::showFITSViewer() { FITSData *data = guideView->getImageData(); if (data) { QUrl url = QUrl::fromLocalFile(data->filename()); if (fv.isNull()) { if (Options::singleWindowCapturedFITS()) fv = KStars::Instance()->genericFITSViewer(); else { fv = new FITSViewer(Options::independentWindowFITS() ? nullptr : KStars::Instance()); KStars::Instance()->addFITSViewer(fv); } fv->addFITS(url); FITSView *currentView = fv->getCurrentView(); if (currentView) currentView->getImageData()->setAutoRemoveTemporaryFITS(false); } else fv->updateFITS(url, 0); fv->show(); } } void Guide::setExternalGuiderBLOBEnabled(bool enable) { // Nothing to do if guider is internal if (guiderType == GUIDE_INTERNAL) return; if(!currentCCD) return; currentCCD->setBLOBEnabled(enable); if(currentCCD->isBLOBEnabled()) { if (currentCCD->hasGuideHead() && guiderCombo->currentText().contains("Guider")) useGuideHead = true; else useGuideHead = false; ISD::CCDChip *targetChip = currentCCD->getChip(useGuideHead ? ISD::CCDChip::GUIDE_CCD : ISD::CCDChip::PRIMARY_CCD); if (targetChip) { targetChip->setImageView(guideView, FITS_GUIDE); targetChip->setCaptureMode(FITS_GUIDE); } syncCCDInfo(); } } void Guide::ditherDirectly() { double ditherPulse = Options::ditherNoGuidingPulse(); // Randomize pulse length. It is equal to 50% of pulse length + random value up to 50% // e.g. if ditherPulse is 500ms then final pulse is = 250 + rand(0 to 250) int ra_msec = static_cast((static_cast(rand()) / RAND_MAX) * ditherPulse / 2.0 + ditherPulse / 2.0); int ra_polarity = (rand() % 2 == 0) ? 1 : -1; int de_msec = static_cast((static_cast(rand()) / RAND_MAX) * ditherPulse / 2.0 + ditherPulse / 2.0); int de_polarity = (rand() % 2 == 0) ? 1 : -1; qCInfo(KSTARS_EKOS_GUIDE) << "Starting non-guiding dither..."; qCDebug(KSTARS_EKOS_GUIDE) << "dither ra_msec:" << ra_msec << "ra_polarity:" << ra_polarity << "de_msec:" << de_msec << "de_polarity:" << de_polarity; bool rc = sendPulse(ra_polarity > 0 ? RA_INC_DIR : RA_DEC_DIR, ra_msec, de_polarity > 0 ? DEC_INC_DIR : DEC_DEC_DIR, de_msec); if (rc) { qCInfo(KSTARS_EKOS_GUIDE) << "Non-guiding dither successful."; QTimer::singleShot( (ra_msec > de_msec ? ra_msec : de_msec) + Options::ditherSettle() * 1000 + 100, [this]() { emit newStatus(GUIDE_DITHERING_SUCCESS); state = GUIDE_IDLE; }); } else { qCWarning(KSTARS_EKOS_GUIDE) << "Non-guiding dither failed."; emit newStatus(GUIDE_DITHERING_ERROR); state = GUIDE_IDLE; } } void Guide::updateTelescopeType(int index) { if (currentCCD == nullptr) return; focal_length = (index == ISD::CCD::TELESCOPE_PRIMARY) ? primaryFL : guideFL; aperture = (index == ISD::CCD::TELESCOPE_PRIMARY) ? primaryAperture : guideAperture; Options::setGuideScopeType(index); syncTelescopeInfo(); } void Guide::setDefaultST4(const QString &driver) { Options::setDefaultST4Driver(driver); } void Guide::setDefaultCCD(const QString &ccd) { if (guiderType == GUIDE_INTERNAL) Options::setDefaultGuideCCD(ccd); } void Guide::handleManualDither() { ISD::CCDChip *targetChip = currentCCD->getChip(useGuideHead ? ISD::CCDChip::GUIDE_CCD : ISD::CCDChip::PRIMARY_CCD); if (targetChip == nullptr) return; Ui::ManualDither ditherDialog; QDialog container(this); ditherDialog.setupUi(&container); if (guiderType != GUIDE_INTERNAL) { ditherDialog.coordinatesR->setEnabled(false); ditherDialog.x->setEnabled(false); ditherDialog.y->setEnabled(false); } int minX, maxX, minY, maxY, minW, maxW, minH, maxH; targetChip->getFrameMinMax(&minX, &maxX, &minY, &maxY, &minW, &maxW, &minH, &maxH); ditherDialog.x->setMinimum(minX); ditherDialog.x->setMaximum(maxX); ditherDialog.y->setMinimum(minY); ditherDialog.y->setMaximum(maxY); ditherDialog.x->setValue(starCenter.x()); ditherDialog.y->setValue(starCenter.y()); if (container.exec() == QDialog::Accepted) { if (ditherDialog.magnitudeR->isChecked()) guider->dither(ditherDialog.magnitude->value()); else { dynamic_cast(guider)->ditherXY(ditherDialog.x->value(), ditherDialog.y->value()); } } } bool Guide::connectGuider() { return guider->Connect(); } bool Guide::disconnectGuider() { return guider->Disconnect(); } void Guide::initPlots() { // Drift Graph Color Settings driftGraph->setBackground(QBrush(Qt::black)); driftGraph->xAxis->setBasePen(QPen(Qt::white, 1)); driftGraph->yAxis->setBasePen(QPen(Qt::white, 1)); driftGraph->xAxis->grid()->setPen(QPen(QColor(140, 140, 140), 1, Qt::DotLine)); driftGraph->yAxis->grid()->setPen(QPen(QColor(140, 140, 140), 1, Qt::DotLine)); driftGraph->xAxis->grid()->setSubGridPen(QPen(QColor(80, 80, 80), 1, Qt::DotLine)); driftGraph->yAxis->grid()->setSubGridPen(QPen(QColor(80, 80, 80), 1, Qt::DotLine)); driftGraph->xAxis->grid()->setZeroLinePen(Qt::NoPen); driftGraph->yAxis->grid()->setZeroLinePen(QPen(Qt::white, 1)); driftGraph->xAxis->setBasePen(QPen(Qt::white, 1)); driftGraph->yAxis->setBasePen(QPen(Qt::white, 1)); driftGraph->yAxis2->setBasePen(QPen(Qt::white, 1)); driftGraph->xAxis->setTickPen(QPen(Qt::white, 1)); driftGraph->yAxis->setTickPen(QPen(Qt::white, 1)); driftGraph->yAxis2->setTickPen(QPen(Qt::white, 1)); driftGraph->xAxis->setSubTickPen(QPen(Qt::white, 1)); driftGraph->yAxis->setSubTickPen(QPen(Qt::white, 1)); driftGraph->yAxis2->setSubTickPen(QPen(Qt::white, 1)); driftGraph->xAxis->setTickLabelColor(Qt::white); driftGraph->yAxis->setTickLabelColor(Qt::white); driftGraph->yAxis2->setTickLabelColor(Qt::white); driftGraph->xAxis->setLabelColor(Qt::white); driftGraph->yAxis->setLabelColor(Qt::white); driftGraph->yAxis2->setLabelColor(Qt::white); //Horizontal Axis Time Ticker Settings QSharedPointer timeTicker(new QCPAxisTickerTime); timeTicker->setTimeFormat("%m:%s"); driftGraph->xAxis->setTicker(timeTicker); //Vertical Axis Labels Settings driftGraph->yAxis2->setVisible(true); driftGraph->yAxis2->setTickLabels(true); driftGraph->yAxis->setLabelFont(QFont(font().family(), 10)); driftGraph->yAxis2->setLabelFont(QFont(font().family(), 10)); driftGraph->yAxis->setTickLabelFont(QFont(font().family(), 9)); driftGraph->yAxis2->setTickLabelFont(QFont(font().family(), 9)); driftGraph->yAxis->setLabelPadding(1); driftGraph->yAxis2->setLabelPadding(1); driftGraph->yAxis->setLabel(i18n("drift (arcsec)")); driftGraph->yAxis2->setLabel(i18n("pulse (ms)")); setupNSEWLabels(); //Sets the default ranges driftGraph->xAxis->setRange(0, 60, Qt::AlignRight); driftGraph->yAxis->setRange(-3, 3); int scale = 50; //This is a scaling value between the left and the right axes of the driftGraph, it could be stored in kstars kcfg correctionSlider->setValue(scale); driftGraph->yAxis2->setRange(-3 * scale, 3 * scale); //This sets up the legend driftGraph->legend->setVisible(true); driftGraph->legend->setFont(QFont("Helvetica", 9)); driftGraph->legend->setTextColor(Qt::white); driftGraph->legend->setBrush(QBrush(Qt::black)); driftGraph->legend->setFillOrder(QCPLegend::foColumnsFirst); driftGraph->axisRect()->insetLayout()->setInsetAlignment(0, Qt::AlignLeft | Qt::AlignBottom); // RA Curve driftGraph->addGraph(driftGraph->xAxis, driftGraph->yAxis); driftGraph->graph(0)->setPen(QPen(KStarsData::Instance()->colorScheme()->colorNamed("RAGuideError"))); driftGraph->graph(0)->setName("RA"); driftGraph->graph(0)->setLineStyle(QCPGraph::lsStepLeft); // DE Curve driftGraph->addGraph(driftGraph->xAxis, driftGraph->yAxis); driftGraph->graph(1)->setPen(QPen(KStarsData::Instance()->colorScheme()->colorNamed("DEGuideError"))); driftGraph->graph(1)->setName("DE"); driftGraph->graph(1)->setLineStyle(QCPGraph::lsStepLeft); // RA highlighted Point driftGraph->addGraph(driftGraph->xAxis, driftGraph->yAxis); driftGraph->graph(2)->setLineStyle(QCPGraph::lsNone); driftGraph->graph(2)->setPen(QPen(KStarsData::Instance()->colorScheme()->colorNamed("RAGuideError"))); driftGraph->graph(2)->setScatterStyle(QCPScatterStyle(QCPScatterStyle::ssPlusCircle, QPen(KStarsData::Instance()->colorScheme()->colorNamed("RAGuideError"), 2), QBrush(), 10)); // DE highlighted Point driftGraph->addGraph(driftGraph->xAxis, driftGraph->yAxis); driftGraph->graph(3)->setLineStyle(QCPGraph::lsNone); driftGraph->graph(3)->setPen(QPen(KStarsData::Instance()->colorScheme()->colorNamed("DEGuideError"))); driftGraph->graph(3)->setScatterStyle(QCPScatterStyle(QCPScatterStyle::ssPlusCircle, QPen(KStarsData::Instance()->colorScheme()->colorNamed("DEGuideError"), 2), QBrush(), 10)); // RA Pulse driftGraph->addGraph(driftGraph->xAxis, driftGraph->yAxis2); QColor raPulseColor(KStarsData::Instance()->colorScheme()->colorNamed("RAGuideError")); raPulseColor.setAlpha(75); driftGraph->graph(4)->setPen(QPen(raPulseColor)); driftGraph->graph(4)->setBrush(QBrush(raPulseColor, Qt::Dense4Pattern)); driftGraph->graph(4)->setName("RA Pulse"); driftGraph->graph(4)->setLineStyle(QCPGraph::lsStepLeft); // DEC Pulse driftGraph->addGraph(driftGraph->xAxis, driftGraph->yAxis2); QColor dePulseColor(KStarsData::Instance()->colorScheme()->colorNamed("DEGuideError")); dePulseColor.setAlpha(75); driftGraph->graph(5)->setPen(QPen(dePulseColor)); driftGraph->graph(5)->setBrush(QBrush(dePulseColor, Qt::Dense4Pattern)); driftGraph->graph(5)->setName("DEC Pulse"); driftGraph->graph(5)->setLineStyle(QCPGraph::lsStepLeft); //This will prevent the highlighted points and Pulses from showing up in the legend. driftGraph->legend->removeItem(5); driftGraph->legend->removeItem(4); driftGraph->legend->removeItem(3); driftGraph->legend->removeItem(2); //Dragging and zooming settings // make bottom axis transfer its range to the top axis if the graph gets zoomed: connect(driftGraph->xAxis, static_cast(&QCPAxis::rangeChanged), driftGraph->xAxis2, static_cast(&QCPAxis::setRange)); // update the second vertical axis properly if the graph gets zoomed. connect(driftGraph->yAxis, static_cast(&QCPAxis::rangeChanged), this, &Ekos::Guide::setCorrectionGraphScale); driftGraph->setInteractions(QCP::iRangeZoom); driftGraph->setInteraction(QCP::iRangeDrag, true); connect(driftGraph, &QCustomPlot::mouseMove, this, &Ekos::Guide::driftMouseOverLine); connect(driftGraph, &QCustomPlot::mousePress, this, &Ekos::Guide::driftMouseClicked); //drift plot double accuracyRadius = 2; driftPlot->setBackground(QBrush(Qt::black)); driftPlot->setSelectionTolerance(10); driftPlot->xAxis->setBasePen(QPen(Qt::white, 1)); driftPlot->yAxis->setBasePen(QPen(Qt::white, 1)); driftPlot->xAxis->setTickPen(QPen(Qt::white, 1)); driftPlot->yAxis->setTickPen(QPen(Qt::white, 1)); driftPlot->xAxis->setSubTickPen(QPen(Qt::white, 1)); driftPlot->yAxis->setSubTickPen(QPen(Qt::white, 1)); driftPlot->xAxis->setTickLabelColor(Qt::white); driftPlot->yAxis->setTickLabelColor(Qt::white); driftPlot->xAxis->setLabelColor(Qt::white); driftPlot->yAxis->setLabelColor(Qt::white); driftPlot->xAxis->setLabelFont(QFont(font().family(), 10)); driftPlot->yAxis->setLabelFont(QFont(font().family(), 10)); driftPlot->xAxis->setTickLabelFont(QFont(font().family(), 9)); driftPlot->yAxis->setTickLabelFont(QFont(font().family(), 9)); driftPlot->xAxis->setLabelPadding(2); driftPlot->yAxis->setLabelPadding(2); driftPlot->xAxis->grid()->setPen(QPen(QColor(140, 140, 140), 1, Qt::DotLine)); driftPlot->yAxis->grid()->setPen(QPen(QColor(140, 140, 140), 1, Qt::DotLine)); driftPlot->xAxis->grid()->setSubGridPen(QPen(QColor(80, 80, 80), 1, Qt::DotLine)); driftPlot->yAxis->grid()->setSubGridPen(QPen(QColor(80, 80, 80), 1, Qt::DotLine)); driftPlot->xAxis->grid()->setZeroLinePen(QPen(Qt::gray)); driftPlot->yAxis->grid()->setZeroLinePen(QPen(Qt::gray)); driftPlot->xAxis->setLabel(i18n("dRA (arcsec)")); driftPlot->yAxis->setLabel(i18n("dDE (arcsec)")); driftPlot->xAxis->setRange(-accuracyRadius * 3, accuracyRadius * 3); driftPlot->yAxis->setRange(-accuracyRadius * 3, accuracyRadius * 3); driftPlot->setInteractions(QCP::iRangeZoom); driftPlot->setInteraction(QCP::iRangeDrag, true); driftPlot->addGraph(); driftPlot->graph(0)->setLineStyle(QCPGraph::lsNone); driftPlot->graph(0)->setScatterStyle(QCPScatterStyle(QCPScatterStyle::ssStar, Qt::gray, 5)); driftPlot->addGraph(); driftPlot->graph(1)->setLineStyle(QCPGraph::lsNone); driftPlot->graph(1)->setScatterStyle(QCPScatterStyle(QCPScatterStyle::ssPlusCircle, QPen(Qt::yellow, 2), QBrush(), 10)); connect(rightLayout, &QSplitter::splitterMoved, this, &Ekos::Guide::handleVerticalPlotSizeChange); connect(driftSplitter, &QSplitter::splitterMoved, this, &Ekos::Guide::handleHorizontalPlotSizeChange); //This sets the values of all the Graph Options that are stored. accuracyRadiusSpin->setValue(Options::guiderAccuracyThreshold()); showRAPlotCheck->setChecked(Options::rADisplayedOnGuideGraph()); showDECPlotCheck->setChecked(Options::dEDisplayedOnGuideGraph()); showRACorrectionsCheck->setChecked(Options::rACorrDisplayedOnGuideGraph()); showDECorrectionsCheck->setChecked(Options::dECorrDisplayedOnGuideGraph()); //This sets the visibility of graph components to the stored values. driftGraph->graph(0)->setVisible(Options::rADisplayedOnGuideGraph()); //RA data driftGraph->graph(1)->setVisible(Options::dEDisplayedOnGuideGraph()); //DEC data driftGraph->graph(2)->setVisible(Options::rADisplayedOnGuideGraph()); //RA highlighted point driftGraph->graph(3)->setVisible(Options::dEDisplayedOnGuideGraph()); //DEC highlighted point driftGraph->graph(4)->setVisible(Options::rACorrDisplayedOnGuideGraph()); //RA Pulses driftGraph->graph(5)->setVisible(Options::dECorrDisplayedOnGuideGraph()); //DEC Pulses updateCorrectionsScaleVisibility(); driftPlot->resize(190, 190); driftPlot->replot(); buildTarget(); } void Guide::initView() { guideView = new FITSView(guideWidget, FITS_GUIDE); guideView->setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Expanding); guideView->setBaseSize(guideWidget->size()); guideView->createFloatingToolBar(); QVBoxLayout *vlayout = new QVBoxLayout(); vlayout->addWidget(guideView); guideWidget->setLayout(vlayout); connect(guideView, &FITSView::trackingStarSelected, this, &Ekos::Guide::setTrackingStar); } void Guide::initConnections() { // Exposure Timeout captureTimeout.setSingleShot(true); connect(&captureTimeout, &QTimer::timeout, this, &Ekos::Guide::processCaptureTimeout); // Guiding Box Size connect(boxSizeCombo, static_cast(&QComboBox::currentIndexChanged), this, &Ekos::Guide::updateTrackingBoxSize); // Guider CCD Selection connect(guiderCombo, static_cast(&QComboBox::activated), this, &Ekos::Guide::setDefaultCCD); connect(guiderCombo, static_cast(&QComboBox::activated), this, [&](int index) { if (guiderType == GUIDE_INTERNAL) { starCenter = QVector3D(); checkCCD(index); } } ); FOVScopeCombo->setCurrentIndex(Options::guideScopeType()); connect(FOVScopeCombo, static_cast(&QComboBox::currentIndexChanged), this, &Ekos::Guide::updateTelescopeType); // Dark Frame Check connect(darkFrameCheck, &QCheckBox::toggled, this, &Ekos::Guide::setDarkFrameEnabled); // Subframe check if(guiderType != GUIDE_PHD2) //For PHD2, this is handled in the configurePHD2Camera method connect(subFrameCheck, &QCheckBox::toggled, this, &Ekos::Guide::setSubFrameEnabled); // ST4 Selection connect(ST4Combo, static_cast(&QComboBox::activated), [&](const QString & text) { setDefaultST4(text); setST4(text); }); // Binning Combo Selection connect(binningCombo, static_cast(&QComboBox::currentIndexChanged), this, &Ekos::Guide::updateCCDBin); // RA/DEC Enable directions connect(checkBox_DirRA, &QCheckBox::toggled, this, &Ekos::Guide::onEnableDirRA); connect(checkBox_DirDEC, &QCheckBox::toggled, this, &Ekos::Guide::onEnableDirDEC); // N/W and W/E direction enable connect(northControlCheck, &QCheckBox::toggled, this, &Ekos::Guide::onControlDirectionChanged); connect(southControlCheck, &QCheckBox::toggled, this, &Ekos::Guide::onControlDirectionChanged); connect(westControlCheck, &QCheckBox::toggled, this, &Ekos::Guide::onControlDirectionChanged); connect(eastControlCheck, &QCheckBox::toggled, this, &Ekos::Guide::onControlDirectionChanged); // Auto star check connect(autoStarCheck, &QCheckBox::toggled, this, &Ekos::Guide::syncSettings); // Declination Swap connect(swapCheck, &QCheckBox::toggled, this, &Ekos::Guide::setDECSwap); // PID Control - Proportional Gain connect(spinBox_PropGainRA, &QSpinBox::editingFinished, this, &Ekos::Guide::syncSettings); connect(spinBox_PropGainDEC, &QSpinBox::editingFinished, this, &Ekos::Guide::syncSettings); // PID Control - Integral Gain connect(spinBox_IntGainRA, &QSpinBox::editingFinished, this, &Ekos::Guide::syncSettings); connect(spinBox_IntGainDEC, &QSpinBox::editingFinished, this, &Ekos::Guide::syncSettings); // PID Control - Derivative Gain connect(spinBox_DerGainRA, &QSpinBox::editingFinished, this, &Ekos::Guide::syncSettings); connect(spinBox_DerGainDEC, &QSpinBox::editingFinished, this, &Ekos::Guide::syncSettings); // Max Pulse Duration (ms) connect(spinBox_MaxPulseRA, &QSpinBox::editingFinished, this, &Ekos::Guide::syncSettings); connect(spinBox_MaxPulseDEC, &QSpinBox::editingFinished, this, &Ekos::Guide::syncSettings); // Min Pulse Duration (ms) connect(spinBox_MinPulseRA, &QSpinBox::editingFinished, this, &Ekos::Guide::syncSettings); connect(spinBox_MinPulseDEC, &QSpinBox::editingFinished, this, &Ekos::Guide::syncSettings); // Capture connect(captureB, &QPushButton::clicked, this, [this]() { state = GUIDE_CAPTURE; emit newStatus(state); if(guiderType == GUIDE_PHD2) { configurePHD2Camera(); if(phd2Guider->isCurrentCameraNotInEkos()) appendLogText( i18n("The PHD2 camera is not available to Ekos, so you cannot see the captured images. But you will still see the Guide Star Image when you guide.")); else if(Options::guideSubframeEnabled()) { appendLogText( i18n("To receive PHD2 images other than the Guide Star Image, SubFrame must be unchecked. Unchecking it now to enable your image captures. You can re-enable it before Guiding")); subFrameCheck->setChecked(false); } phd2Guider->captureSingleFrame(); } else capture(); }); connect(loopB, &QPushButton::clicked, this, [this]() { state = GUIDE_LOOPING; emit newStatus(state); if(guiderType == GUIDE_PHD2) { configurePHD2Camera(); if(phd2Guider->isCurrentCameraNotInEkos()) appendLogText( i18n("The PHD2 camera is not available to Ekos, so you cannot see the captured images. But you will still see the Guide Star Image when you guide.")); else if(Options::guideSubframeEnabled()) { appendLogText( i18n("To receive PHD2 images other than the Guide Star Image, SubFrame must be unchecked. Unchecking it now to enable your image captures. You can re-enable it before Guiding")); subFrameCheck->setChecked(false); } phd2Guider->loop(); stopB->setEnabled(true); } else capture(); }); // Stop connect(stopB, &QPushButton::clicked, this, &Ekos::Guide::abort); // Clear Calibrate //connect(calibrateB, &QPushButton::clicked, this, &Ekos::Guide::calibrate())); connect(clearCalibrationB, &QPushButton::clicked, this, &Ekos::Guide::clearCalibration); // Guide connect(guideB, &QPushButton::clicked, this, &Ekos::Guide::guide); // Connect External Guide connect(externalConnectB, &QPushButton::clicked, this, [&]() { //setExternalGuiderBLOBEnabled(false); guider->Connect(); }); connect(externalDisconnectB, &QPushButton::clicked, this, [&]() { //setExternalGuiderBLOBEnabled(true); guider->Disconnect(); }); // Pulse Timer pulseTimer.setSingleShot(true); connect(&pulseTimer, &QTimer::timeout, this, &Ekos::Guide::capture); //This connects all the buttons and slider below the guide plots. connect(accuracyRadiusSpin, static_cast(&QDoubleSpinBox::valueChanged), this, &Ekos::Guide::buildTarget); connect(guideSlider, &QSlider::sliderMoved, this, &Ekos::Guide::guideHistory); connect(latestCheck, &QCheckBox::toggled, this, &Ekos::Guide::setLatestGuidePoint); connect(showRAPlotCheck, &QCheckBox::toggled, this, &Ekos::Guide::toggleShowRAPlot); connect(showDECPlotCheck, &QCheckBox::toggled, this, &Ekos::Guide::toggleShowDEPlot); connect(showRACorrectionsCheck, &QCheckBox::toggled, this, &Ekos::Guide::toggleRACorrectionsPlot); connect(showDECorrectionsCheck, &QCheckBox::toggled, this, &Ekos::Guide::toggleDECorrectionsPlot); connect(correctionSlider, &QSlider::sliderMoved, this, &Ekos::Guide::setCorrectionGraphScale); connect(showGuideRateToolTipB, &QPushButton::clicked, [this]() { QToolTip::showText(showGuideRateToolTipB->mapToGlobal(QPoint(10, 10)), showGuideRateToolTipB->toolTip(), showGuideRateToolTipB); }); connect(manualDitherB, &QPushButton::clicked, this, &Guide::handleManualDither); // Guiding Rate - Advisory only onInfoRateChanged(spinBox_GuideRate->value()); connect(spinBox_GuideRate, static_cast(&QDoubleSpinBox::valueChanged), this, &Ekos::Guide::onInfoRateChanged); } void Guide::removeDevice(ISD::GDInterface *device) { device->disconnect(this); if (currentTelescope && (currentTelescope->getDeviceName() == device->getDeviceName())) { currentTelescope = nullptr; } else if (CCDs.contains(static_cast(device))) { CCDs.removeAll(static_cast(device)); guiderCombo->removeItem(guiderCombo->findText(device->getDeviceName())); guiderCombo->removeItem(guiderCombo->findText(device->getDeviceName() + QString(" Guider"))); if (CCDs.empty()) { currentCCD = nullptr; guiderCombo->setCurrentIndex(-1); } else guiderCombo->setCurrentIndex(0); checkCCD(); } auto st4 = std::find_if(ST4List.begin(), ST4List.end(), [device](ISD::ST4 * st) { return (st->getDeviceName() == device->getDeviceName()); }); if (st4 != ST4List.end()) { ST4List.removeOne(*st4); if (AODriver && (device->getDeviceName() == AODriver->getDeviceName())) AODriver = nullptr; ST4Combo->removeItem(ST4Combo->findText(device->getDeviceName())); if (ST4List.empty()) { ST4Driver = GuideDriver = nullptr; } else { setST4(ST4Combo->currentText()); } } } } diff --git a/kstars/ekos/manager.cpp b/kstars/ekos/manager.cpp index d808dbbe1..a8e244ecb 100644 --- a/kstars/ekos/manager.cpp +++ b/kstars/ekos/manager.cpp @@ -1,3562 +1,3562 @@ /* Ekos Copyright (C) 2012 Jasem Mutlaq This application is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version. */ #include "manager.h" #include "ekosadaptor.h" #include "kstars.h" #include "kstarsdata.h" #include "opsekos.h" #include "Options.h" #include "profileeditor.h" #include "profilewizard.h" #include "indihub.h" #include "skymap.h" #include "auxiliary/darklibrary.h" #include "auxiliary/QProgressIndicator.h" #include "auxiliary/ksmessagebox.h" #include "capture/sequencejob.h" #include "fitsviewer/fitstab.h" #include "fitsviewer/fitsview.h" #include "fitsviewer/fitsdata.h" #include "indi/clientmanager.h" #include "indi/driverinfo.h" #include "indi/drivermanager.h" #include "indi/guimanager.h" #include "indi/indielement.h" #include "indi/indilistener.h" #include "indi/indiproperty.h" #include "indi/indiwebmanager.h" #include "ekoslive/ekosliveclient.h" #include "ekoslive/message.h" #include "ekoslive/media.h" #include #include #include #include #include #include #include #include #define MAX_REMOTE_INDI_TIMEOUT 15000 #define MAX_LOCAL_INDI_TIMEOUT 5000 namespace Ekos { Manager *Manager::_Manager = nullptr; Manager *Manager::Instance() { if (_Manager == nullptr) _Manager = new Manager(Options::independentWindowEkos() ? nullptr : KStars::Instance()); return _Manager; } void Manager::release() { delete _Manager; } Manager::Manager(QWidget * parent) : QDialog(parent) { #ifdef Q_OS_OSX if (Options::independentWindowEkos()) setWindowFlags(Qt::Window); else { setWindowFlags(Qt::Window | Qt::WindowStaysOnTopHint); connect(QApplication::instance(), SIGNAL(applicationStateChanged(Qt::ApplicationState)), this, SLOT(changeAlwaysOnTop(Qt::ApplicationState))); } #else // if (Options::independentWindowEkos()) // setWindowFlags(Qt::Window | Qt::WindowStaysOnTopHint); #endif setupUi(this); qRegisterMetaType("Ekos::CommunicationStatus"); qDBusRegisterMetaType(); new EkosAdaptor(this); QDBusConnection::sessionBus().registerObject("/KStars/Ekos", this); setWindowIcon(QIcon::fromTheme("kstars_ekos")); profileModel.reset(new QStandardItemModel(0, 4)); profileModel->setHorizontalHeaderLabels(QStringList() << "id" << "name" << "host" << "port"); captureProgress->setValue(0); sequenceProgress->setValue(0); sequenceProgress->setDecimals(0); sequenceProgress->setFormat("%v"); imageProgress->setValue(0); imageProgress->setDecimals(1); imageProgress->setFormat("%v"); imageProgress->setBarStyle(QRoundProgressBar::StyleLine); countdownTimer.setInterval(1000); connect(&countdownTimer, &QTimer::timeout, this, &Ekos::Manager::updateCaptureCountDown); toolsWidget->setIconSize(QSize(48, 48)); connect(toolsWidget, &QTabWidget::currentChanged, this, &Ekos::Manager::processTabChange, Qt::UniqueConnection); // Enable scheduler Tab toolsWidget->setTabEnabled(1, false); // Start/Stop INDI Server connect(processINDIB, &QPushButton::clicked, this, &Ekos::Manager::processINDI); processINDIB->setIcon(QIcon::fromTheme("media-playback-start")); processINDIB->setToolTip(i18n("Start")); // Connect/Disconnect INDI devices connect(connectB, &QPushButton::clicked, this, &Ekos::Manager::connectDevices); connect(disconnectB, &QPushButton::clicked, this, &Ekos::Manager::disconnectDevices); ekosLiveB->setAttribute(Qt::WA_LayoutUsesWidgetRect); ekosLiveClient.reset(new EkosLive::Client(this)); connect(ekosLiveClient.get(), &EkosLive::Client::connected, [this]() { emit ekosLiveStatusChanged(true); }); connect(ekosLiveClient.get(), &EkosLive::Client::disconnected, [this]() { emit ekosLiveStatusChanged(false); }); // INDI Control Panel //connect(controlPanelB, &QPushButton::clicked, GUIManager::Instance(), SLOT(show())); connect(ekosLiveB, &QPushButton::clicked, [&]() { ekosLiveClient.get()->show(); ekosLiveClient.get()->raise(); }); connect(this, &Manager::ekosStatusChanged, ekosLiveClient.get()->message(), &EkosLive::Message::setEkosStatingStatus); connect(ekosLiveClient.get()->message(), &EkosLive::Message::connected, [&]() { ekosLiveB->setIcon(QIcon(":/icons/cloud-online.svg")); }); connect(ekosLiveClient.get()->message(), &EkosLive::Message::disconnected, [&]() { ekosLiveB->setIcon(QIcon::fromTheme("folder-cloud")); }); connect(ekosLiveClient.get()->media(), &EkosLive::Media::newBoundingRect, ekosLiveClient.get()->message(), &EkosLive::Message::setBoundingRect); connect(ekosLiveClient.get()->message(), &EkosLive::Message::resetPolarView, ekosLiveClient.get()->media(), &EkosLive::Media::resetPolarView); connect(ekosLiveClient.get()->message(), &EkosLive::Message::previewJPEGGenerated, ekosLiveClient.get()->media(), &EkosLive::Media::sendPreviewJPEG); connect(KSMessageBox::Instance(), &KSMessageBox::newMessage, ekosLiveClient.get()->message(), &EkosLive::Message::sendDialog); // Serial Port Assistant connect(serialPortAssistantB, &QPushButton::clicked, [&]() { serialPortAssistant->show(); serialPortAssistant->raise(); }); connect(this, &Ekos::Manager::ekosStatusChanged, [&](Ekos::CommunicationStatus status) { indiControlPanelB->setEnabled(status == Ekos::Success); connectB->setEnabled(false); disconnectB->setEnabled(false); profileGroup->setEnabled(status == Ekos::Idle || status == Ekos::Error); m_isStarted = (status == Ekos::Success || status == Ekos::Pending); if (status == Ekos::Success) { processINDIB->setIcon(QIcon::fromTheme("media-playback-stop")); processINDIB->setToolTip(i18n("Stop")); setWindowTitle(i18n("Ekos - %1 Profile", currentProfile->name)); } else if (status == Ekos::Error || status == Ekos::Idle) { processINDIB->setIcon(QIcon::fromTheme("media-playback-start")); processINDIB->setToolTip(i18n("Start")); } else { processINDIB->setIcon(QIcon::fromTheme("call-stop")); processINDIB->setToolTip(i18n("Connection in progress. Click to abort.")); } }); connect(indiControlPanelB, &QPushButton::clicked, [&]() { KStars::Instance()->actionCollection()->action("show_control_panel")->trigger(); }); connect(optionsB, &QPushButton::clicked, [&]() { KStars::Instance()->actionCollection()->action("configure")->trigger(); }); // Save as above, but it appears in all modules connect(ekosOptionsB, &QPushButton::clicked, this, &Ekos::Manager::showEkosOptions); // Clear Ekos Log connect(clearB, &QPushButton::clicked, this, &Ekos::Manager::clearLog); // Logs KConfigDialog * dialog = new KConfigDialog(this, "logssettings", Options::self()); opsLogs = new Ekos::OpsLogs(); KPageWidgetItem * page = dialog->addPage(opsLogs, i18n("Logging")); page->setIcon(QIcon::fromTheme("configure")); connect(logsB, &QPushButton::clicked, dialog, &KConfigDialog::show); connect(dialog->button(QDialogButtonBox::Apply), &QPushButton::clicked, this, &Ekos::Manager::updateDebugInterfaces); connect(dialog->button(QDialogButtonBox::Ok), &QPushButton::clicked, this, &Ekos::Manager::updateDebugInterfaces); // Summary // previewPixmap = new QPixmap(QPixmap(":/images/noimage.png")); // Profiles connect(addProfileB, &QPushButton::clicked, this, &Ekos::Manager::addProfile); connect(editProfileB, &QPushButton::clicked, this, &Ekos::Manager::editProfile); connect(deleteProfileB, &QPushButton::clicked, this, &Ekos::Manager::deleteProfile); connect(profileCombo, static_cast(&QComboBox::currentTextChanged), [ = ](const QString & text) { Options::setProfile(text); if (text == "Simulators") { editProfileB->setEnabled(false); deleteProfileB->setEnabled(false); } else { editProfileB->setEnabled(true); deleteProfileB->setEnabled(true); } }); // Ekos Wizard connect(wizardProfileB, &QPushButton::clicked, this, &Ekos::Manager::wizardProfile); addProfileB->setAttribute(Qt::WA_LayoutUsesWidgetRect); editProfileB->setAttribute(Qt::WA_LayoutUsesWidgetRect); deleteProfileB->setAttribute(Qt::WA_LayoutUsesWidgetRect); // Set Profile icons addProfileB->setIcon(QIcon::fromTheme("list-add")); addProfileB->setAttribute(Qt::WA_LayoutUsesWidgetRect); editProfileB->setIcon(QIcon::fromTheme("document-edit")); editProfileB->setAttribute(Qt::WA_LayoutUsesWidgetRect); deleteProfileB->setIcon(QIcon::fromTheme("list-remove")); deleteProfileB->setAttribute(Qt::WA_LayoutUsesWidgetRect); wizardProfileB->setIcon(QIcon::fromTheme("tools-wizard")); wizardProfileB->setAttribute(Qt::WA_LayoutUsesWidgetRect); customDriversB->setIcon(QIcon::fromTheme("roll")); customDriversB->setAttribute(Qt::WA_LayoutUsesWidgetRect); connect(customDriversB, &QPushButton::clicked, DriverManager::Instance(), &DriverManager::showCustomDrivers); // Load all drivers loadDrivers(); // Load add driver profiles loadProfiles(); // INDI Control Panel and Ekos Options optionsB->setIcon(QIcon::fromTheme("configure", QIcon(":/icons/ekos_setup.png"))); optionsB->setAttribute(Qt::WA_LayoutUsesWidgetRect); // Setup Tab toolsWidget->tabBar()->setTabIcon(0, QIcon(":/icons/ekos_setup.png")); toolsWidget->tabBar()->setTabToolTip(0, i18n("Setup")); // Initialize Ekos Scheduler Module schedulerProcess.reset(new Ekos::Scheduler()); toolsWidget->addTab(schedulerProcess.get(), QIcon(":/icons/ekos_scheduler.png"), ""); toolsWidget->tabBar()->setTabToolTip(1, i18n("Scheduler")); connect(schedulerProcess.get(), &Scheduler::newLog, this, &Ekos::Manager::updateLog); //connect(schedulerProcess.get(), SIGNAL(newTarget(QString)), mountTarget, SLOT(setText(QString))); connect(schedulerProcess.get(), &Ekos::Scheduler::newTarget, [&](const QString & target) { mountTarget->setText(target); ekosLiveClient.get()->message()->updateMountStatus(QJsonObject({{"target", target}})); }); // Temporary fix. Not sure how to resize Ekos Dialog to fit contents of the various tabs in the QScrollArea which are added // dynamically. I used setMinimumSize() but it doesn't appear to make any difference. // Also set Layout policy to SetMinAndMaxSize as well. Any idea how to fix this? // FIXME //resize(1000,750); summaryPreview.reset(new FITSView(previewWidget, FITS_NORMAL)); previewWidget->setContentsMargins(0, 0, 0, 0); summaryPreview->setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Expanding); summaryPreview->setBaseSize(previewWidget->size()); summaryPreview->createFloatingToolBar(); summaryPreview->setCursorMode(FITSView::dragCursor); QVBoxLayout * vlayout = new QVBoxLayout(); vlayout->addWidget(summaryPreview.get()); previewWidget->setLayout(vlayout); // JM 2019-01-19: Why cloud images depend on summary preview? // connect(summaryPreview.get(), &FITSView::loaded, [&]() // { // // UUID binds the cloud & preview frames by a common key // QString uuid = QUuid::createUuid().toString(); // uuid = uuid.remove(QRegularExpression("[-{}]")); // //ekosLiveClient.get()->media()->sendPreviewImage(summaryPreview.get(), uuid); // ekosLiveClient.get()->cloud()->sendPreviewImage(summaryPreview.get(), uuid); // }); if (Options::ekosLeftIcons()) { toolsWidget->setTabPosition(QTabWidget::West); QTransform trans; trans.rotate(90); QIcon icon = toolsWidget->tabIcon(0); QPixmap pix = icon.pixmap(QSize(48, 48)); icon = QIcon(pix.transformed(trans)); toolsWidget->setTabIcon(0, icon); icon = toolsWidget->tabIcon(1); pix = icon.pixmap(QSize(48, 48)); icon = QIcon(pix.transformed(trans)); toolsWidget->setTabIcon(1, icon); } //Note: This is to prevent a button from being called the default button //and then executing when the user hits the enter key such as when on a Text Box QList qButtons = findChildren(); for (auto &button : qButtons) button->setAutoDefault(false); resize(Options::ekosWindowWidth(), Options::ekosWindowHeight()); } void Manager::changeAlwaysOnTop(Qt::ApplicationState state) { if (isVisible()) { if (state == Qt::ApplicationActive) setWindowFlags(Qt::Window | Qt::WindowStaysOnTopHint); else setWindowFlags(windowFlags() & ~Qt::WindowStaysOnTopHint); show(); } } Manager::~Manager() { toolsWidget->disconnect(this); } void Manager::closeEvent(QCloseEvent * event) { // QAction * a = KStars::Instance()->actionCollection()->action("show_ekos"); // a->setChecked(false); // 2019-02-14 JM: Close event, for some reason, make all the children disappear // when the widget is shown again. Applying a workaround here event->ignore(); hide(); } void Manager::hideEvent(QHideEvent * /*event*/) { Options::setEkosWindowWidth(width()); Options::setEkosWindowHeight(height()); QAction * a = KStars::Instance()->actionCollection()->action("show_ekos"); a->setChecked(false); } void Manager::showEvent(QShowEvent * /*event*/) { QAction * a = KStars::Instance()->actionCollection()->action("show_ekos"); a->setChecked(true); // Just show the profile wizard ONCE per session if (profileWizardLaunched == false && profiles.count() == 1) { profileWizardLaunched = true; wizardProfile(); } } void Manager::resizeEvent(QResizeEvent *) { //previewImage->setPixmap(previewPixmap->scaled(previewImage->width(), previewImage->height(), Qt::KeepAspectRatio, Qt::SmoothTransformation)); if (focusStarPixmap.get() != nullptr) focusStarImage->setPixmap(focusStarPixmap->scaled(focusStarImage->width(), focusStarImage->height(), Qt::KeepAspectRatio, Qt::SmoothTransformation)); //if (focusProfilePixmap) //focusProfileImage->setPixmap(focusProfilePixmap->scaled(focusProfileImage->width(), focusProfileImage->height(), Qt::KeepAspectRatio, Qt::SmoothTransformation)); if (guideStarPixmap.get() != nullptr) guideStarImage->setPixmap(guideStarPixmap->scaled(guideStarImage->width(), guideStarImage->height(), Qt::KeepAspectRatio, Qt::SmoothTransformation)); //if (guideProfilePixmap) //guideProfileImage->setPixmap(guideProfilePixmap->scaled(guideProfileImage->width(), guideProfileImage->height(), Qt::KeepAspectRatio, Qt::SmoothTransformation)); } void Manager::loadProfiles() { profiles.clear(); KStarsData::Instance()->userdb()->GetAllProfiles(profiles); profileModel->clear(); for (auto &pi : profiles) { QList info; info << new QStandardItem(pi->id) << new QStandardItem(pi->name) << new QStandardItem(pi->host) << new QStandardItem(pi->port); profileModel->appendRow(info); } profileModel->sort(0); profileCombo->blockSignals(true); profileCombo->setModel(profileModel.get()); profileCombo->setModelColumn(1); profileCombo->blockSignals(false); // Load last used profile from options int index = profileCombo->findText(Options::profile()); // If not found, set it to first item if (index == -1) index = 0; profileCombo->setCurrentIndex(index); } void Manager::loadDrivers() { foreach (DriverInfo * dv, DriverManager::Instance()->getDrivers()) { if (dv->getDriverSource() != HOST_SOURCE) driversList[dv->getLabel()] = dv; } } void Manager::reset() { qCDebug(KSTARS_EKOS) << "Resetting Ekos Manager..."; // Filter Manager filterManager.reset(new Ekos::FilterManager()); nDevices = 0; useGuideHead = false; useST4 = false; removeTabs(); genericDevices.clear(); managedDevices.clear(); captureProcess.reset(); focusProcess.reset(); guideProcess.reset(); domeProcess.reset(); alignProcess.reset(); mountProcess.reset(); weatherProcess.reset(); observatoryProcess.reset(); dustCapProcess.reset(); DarkLibrary::Instance()->reset(); Ekos::CommunicationStatus previousStatus = m_ekosStatus; m_ekosStatus = Ekos::Idle; if (previousStatus != m_ekosStatus) emit ekosStatusChanged(m_ekosStatus); previousStatus = m_indiStatus; m_indiStatus = Ekos::Idle; if (previousStatus != m_indiStatus) emit indiStatusChanged(m_indiStatus); connectB->setEnabled(false); disconnectB->setEnabled(false); //controlPanelB->setEnabled(false); processINDIB->setEnabled(true); mountGroup->setEnabled(false); focusGroup->setEnabled(false); captureGroup->setEnabled(false); guideGroup->setEnabled(false); sequenceLabel->setText(i18n("Sequence")); sequenceProgress->setValue(0); captureProgress->setValue(0); overallRemainingTime->setText("--:--:--"); sequenceRemainingTime->setText("--:--:--"); imageRemainingTime->setText("--:--:--"); mountStatus->setText(i18n("Idle")); mountStatus->setStyleSheet(QString()); captureStatus->setText(i18n("Idle")); focusStatus->setText(i18n("Idle")); guideStatus->setText(i18n("Idle")); if (capturePI) capturePI->stopAnimation(); if (mountPI) mountPI->stopAnimation(); if (focusPI) focusPI->stopAnimation(); if (guidePI) guidePI->stopAnimation(); m_isStarted = false; processINDIB->setIcon(QIcon::fromTheme("media-playback-start")); processINDIB->setToolTip(i18n("Start")); } void Manager::processINDI() { if (m_isStarted == false) start(); else stop(); } void Manager::stop() { cleanDevices(); serialPortAssistant.reset(); serialPortAssistantB->setEnabled(false); if (indiHubAgent) indiHubAgent->terminate(); profileGroup->setEnabled(true); setWindowTitle(i18n("Ekos")); } void Manager::start() { // Don't start if it is already started before if (m_ekosStatus == Ekos::Pending || m_ekosStatus == Ekos::Success) { qCWarning(KSTARS_EKOS) << "Ekos Manager start called but current Ekos Status is" << m_ekosStatus << "Ignoring request."; return; } if (m_LocalMode) qDeleteAll(managedDrivers); managedDrivers.clear(); // If clock was paused, unpaused it and sync time if (KStarsData::Instance()->clock()->isActive() == false) { KStarsData::Instance()->changeDateTime(KStarsDateTime::currentDateTimeUtc()); KStarsData::Instance()->clock()->start(); } // Reset Ekos Manager reset(); // Get Current Profile currentProfile = getCurrentProfile(); m_LocalMode = currentProfile->isLocal(); // Load profile location if one exists updateProfileLocation(currentProfile); bool haveCCD = false, haveGuider = false; // If external guide is specified in the profile, set the // corresponding options if (currentProfile->guidertype == Ekos::Guide::GUIDE_PHD2) { Options::setPHD2Host(currentProfile->guiderhost); Options::setPHD2Port(currentProfile->guiderport); } else if (currentProfile->guidertype == Ekos::Guide::GUIDE_LINGUIDER) { Options::setLinGuiderHost(currentProfile->guiderhost); Options::setLinGuiderPort(currentProfile->guiderport); } // For locally running INDI server if (m_LocalMode) { DriverInfo * drv = driversList.value(currentProfile->mount()); if (drv != nullptr) managedDrivers.append(drv->clone()); drv = driversList.value(currentProfile->ccd()); if (drv != nullptr) { managedDrivers.append(drv->clone()); haveCCD = true; } Options::setGuiderType(currentProfile->guidertype); drv = driversList.value(currentProfile->guider()); if (drv != nullptr) { haveGuider = true; // If the guider and ccd are the same driver, we have two cases: // #1 Drivers that only support ONE device per driver (such as sbig) // #2 Drivers that supports multiples devices per driver (such as sx) // For #1, we modify guider_di to make a unique label for the other device with postfix "Guide" // For #2, we set guider_di to nullptr and we prompt the user to select which device is primary ccd and which is guider // since this is the only way to find out in real time. if (haveCCD && currentProfile->guider() == currentProfile->ccd()) { if (checkUniqueBinaryDriver( driversList.value(currentProfile->ccd()), drv)) { drv = nullptr; } else { drv->setUniqueLabel(drv->getLabel() + " Guide"); } } if (drv) managedDrivers.append(drv->clone()); } drv = driversList.value(currentProfile->ao()); if (drv != nullptr) managedDrivers.append(drv->clone()); drv = driversList.value(currentProfile->filter()); if (drv != nullptr) managedDrivers.append(drv->clone()); drv = driversList.value(currentProfile->focuser()); if (drv != nullptr) managedDrivers.append(drv->clone()); drv = driversList.value(currentProfile->dome()); if (drv != nullptr) managedDrivers.append(drv->clone()); drv = driversList.value(currentProfile->weather()); if (drv != nullptr) managedDrivers.append(drv->clone()); drv = driversList.value(currentProfile->aux1()); if (drv != nullptr) { if (!checkUniqueBinaryDriver(driversList.value(currentProfile->ccd()), drv) && !checkUniqueBinaryDriver(driversList.value(currentProfile->guider()), drv)) managedDrivers.append(drv->clone()); } drv = driversList.value(currentProfile->aux2()); if (drv != nullptr) { if (!checkUniqueBinaryDriver(driversList.value(currentProfile->ccd()), drv) && !checkUniqueBinaryDriver(driversList.value(currentProfile->guider()), drv)) managedDrivers.append(drv->clone()); } drv = driversList.value(currentProfile->aux3()); if (drv != nullptr) { if (!checkUniqueBinaryDriver(driversList.value(currentProfile->ccd()), drv) && !checkUniqueBinaryDriver(driversList.value(currentProfile->guider()), drv)) managedDrivers.append(drv->clone()); } drv = driversList.value(currentProfile->aux4()); if (drv != nullptr) { if (!checkUniqueBinaryDriver(driversList.value(currentProfile->ccd()), drv) && !checkUniqueBinaryDriver(driversList.value(currentProfile->guider()), drv)) managedDrivers.append(drv->clone()); } // Add remote drivers if we have any if (currentProfile->remotedrivers.isEmpty() == false && currentProfile->remotedrivers.contains("@")) { for (auto remoteDriver : currentProfile->remotedrivers.split(",")) { QString name, label, host("localhost"), port("7624"); QStringList properties = remoteDriver.split(QRegExp("[@:]")); if (properties.length() > 1) { name = properties[0]; host = properties[1]; if (properties.length() > 2) port = properties[2]; } DriverInfo * dv = new DriverInfo(name); dv->setRemoteHost(host); dv->setRemotePort(port); label = name; // Remove extra quotes label.remove("\""); dv->setLabel(label); dv->setUniqueLabel(label); managedDrivers.append(dv); } } if (haveCCD == false && haveGuider == false) { KSNotification::error(i18n("Ekos requires at least one CCD or Guider to operate.")); managedDrivers.clear(); m_ekosStatus = Ekos::Error; emit ekosStatusChanged(m_ekosStatus); return; } nDevices = managedDrivers.count(); } else { DriverInfo * remote_indi = new DriverInfo(QString("Ekos Remote Host")); remote_indi->setHostParameters(currentProfile->host, QString::number(currentProfile->port)); remote_indi->setDriverSource(GENERATED_SOURCE); managedDrivers.append(remote_indi); haveCCD = currentProfile->drivers.contains("CCD"); haveGuider = currentProfile->drivers.contains("Guider"); Options::setGuiderType(currentProfile->guidertype); if (haveCCD == false && haveGuider == false) { KSNotification::error(i18n("Ekos requires at least one CCD or Guider to operate.")); delete (remote_indi); nDevices = 0; m_ekosStatus = Ekos::Error; emit ekosStatusChanged(m_ekosStatus); return; } nDevices = currentProfile->drivers.count(); } connect(INDIListener::Instance(), &INDIListener::newDevice, this, &Ekos::Manager::processNewDevice); connect(INDIListener::Instance(), &INDIListener::newTelescope, this, &Ekos::Manager::setTelescope); connect(INDIListener::Instance(), &INDIListener::newCCD, this, &Ekos::Manager::setCCD); connect(INDIListener::Instance(), &INDIListener::newFilter, this, &Ekos::Manager::setFilter); connect(INDIListener::Instance(), &INDIListener::newFocuser, this, &Ekos::Manager::setFocuser); connect(INDIListener::Instance(), &INDIListener::newDome, this, &Ekos::Manager::setDome); connect(INDIListener::Instance(), &INDIListener::newWeather, this, &Ekos::Manager::setWeather); connect(INDIListener::Instance(), &INDIListener::newDustCap, this, &Ekos::Manager::setDustCap); connect(INDIListener::Instance(), &INDIListener::newLightBox, this, &Ekos::Manager::setLightBox); connect(INDIListener::Instance(), &INDIListener::newST4, this, &Ekos::Manager::setST4); connect(INDIListener::Instance(), &INDIListener::deviceRemoved, this, &Ekos::Manager::removeDevice, Qt::DirectConnection); #ifdef Q_OS_OSX if (m_LocalMode || currentProfile->host == "localhost") { if (isRunning("PTPCamera")) { if (KMessageBox::Yes == (KMessageBox::questionYesNo(nullptr, i18n("Ekos detected that PTP Camera is running and may prevent a Canon or Nikon camera from connecting to Ekos. Do you want to quit PTP Camera now?"), i18n("PTP Camera"), KStandardGuiItem::yes(), KStandardGuiItem::no(), "ekos_shutdown_PTPCamera"))) { //TODO is there a better way to do this. QProcess p; p.start("killall PTPCamera"); p.waitForFinished(); } } } #endif if (m_LocalMode) { auto executeStartINDIServices = [this]() { appendLogText(i18n("Starting INDI services...")); connect(DriverManager::Instance(), &DriverManager::serverStarted, this, &Manager::processServerStarted, Qt::UniqueConnection); connect(DriverManager::Instance(), &DriverManager::serverTerminated, this, &Manager::processServerTerminated, Qt::UniqueConnection); if (DriverManager::Instance()->startDevices(managedDrivers) == false) { INDIListener::Instance()->disconnect(this); qDeleteAll(managedDrivers); managedDrivers.clear(); m_ekosStatus = Ekos::Error; emit ekosStatusChanged(m_ekosStatus); } m_ekosStatus = Ekos::Pending; emit ekosStatusChanged(m_ekosStatus); if (currentProfile->autoConnect) appendLogText(i18n("INDI services started on port %1.", managedDrivers.first()->getPort())); else appendLogText( i18n("INDI services started on port %1. Please connect devices.", managedDrivers.first()->getPort())); QTimer::singleShot(MAX_LOCAL_INDI_TIMEOUT, this, &Ekos::Manager::checkINDITimeout); }; // If INDI server is already running, let's see if we need to shut it down first if (isRunning("indiserver")) { connect(KSMessageBox::Instance(), &KSMessageBox::accepted, this, [this, executeStartINDIServices]() { KSMessageBox::Instance()->disconnect(this); DriverManager::Instance()->stopAllDevices(); //TODO is there a better way to do this. QProcess p; p.start("pkill indiserver"); p.waitForFinished(); QTimer::singleShot(500, executeStartINDIServices); }); connect(KSMessageBox::Instance(), &KSMessageBox::rejected, this, [this, executeStartINDIServices]() { KSMessageBox::Instance()->disconnect(this); executeStartINDIServices(); }); KSMessageBox::Instance()->questionYesNo(i18n("Ekos detected an instance of INDI server running. Do you wish to " "shut down the existing instance before starting a new one?"), i18n("INDI Server"), 5); } else executeStartINDIServices(); } else { auto runConnection = [this]() { // If it got cancelled by the user, return immediately. if (m_ekosStatus != Ekos::Pending) return; appendLogText( i18n("Connecting to remote INDI server at %1 on port %2 ...", currentProfile->host, currentProfile->port)); if (DriverManager::Instance()->connectRemoteHost(managedDrivers.first())) { connect(DriverManager::Instance(), &DriverManager::serverTerminated, this, &Manager::processServerTerminated, Qt::UniqueConnection); appendLogText( i18n("INDI services started. Connection to remote INDI server is successful. Waiting for devices...")); QTimer::singleShot(MAX_REMOTE_INDI_TIMEOUT, this, &Ekos::Manager::checkINDITimeout); } else { appendLogText(i18n("Failed to connect to remote INDI server.")); INDIListener::Instance()->disconnect(this); qDeleteAll(managedDrivers); managedDrivers.clear(); m_ekosStatus = Ekos::Error; emit ekosStatusChanged(m_ekosStatus); return; } }; auto runProfile = [this, runConnection]() { // If it got cancelled by the user, return immediately. if (m_ekosStatus != Ekos::Pending) return; INDI::WebManager::syncCustomDrivers(currentProfile); currentProfile->isStellarMate = INDI::WebManager::isStellarMate(currentProfile); if (INDI::WebManager::areDriversRunning(currentProfile) == false) { INDI::WebManager::stopProfile(currentProfile); if (INDI::WebManager::startProfile(currentProfile) == false) { appendLogText(i18n("Failed to start profile on remote INDI Web Manager.")); return; } appendLogText(i18n("Starting profile on remote INDI Web Manager...")); m_RemoteManagerStart = true; } runConnection(); }; m_ekosStatus = Ekos::Pending; emit ekosStatusChanged(m_ekosStatus); // If we need to use INDI Web Manager if (currentProfile->INDIWebManagerPort > 0) { appendLogText(i18n("Establishing communication with remote INDI Web Manager...")); m_RemoteManagerStart = false; QFutureWatcher *watcher = new QFutureWatcher(); connect(watcher, &QFutureWatcher::finished, [this, runConnection, runProfile, watcher]() { watcher->deleteLater(); // If it got cancelled by the user, return immediately. if (m_ekosStatus != Ekos::Pending) return; // If web manager is online, try to run the profile in it if (watcher->result()) { runProfile(); } // Else, try to connect directly to INDI server as there could be a chance // that it is already running. else { appendLogText(i18n("Warning: INDI Web Manager is not online.")); runConnection(); } }); QFuture result = INDI::AsyncWebManager::isOnline(currentProfile); watcher->setFuture(result); } else { runConnection(); } } } void Manager::checkINDITimeout() { // Don't check anything unless we're still pending if (m_ekosStatus != Ekos::Pending) return; if (nDevices <= 0) { m_ekosStatus = Ekos::Success; emit ekosStatusChanged(m_ekosStatus); return; } if (m_LocalMode) { QStringList remainingDevices; foreach (DriverInfo * drv, managedDrivers) { if (drv->getDevices().count() == 0) remainingDevices << QString("+ %1").arg( drv->getUniqueLabel().isEmpty() == false ? drv->getUniqueLabel() : drv->getName()); } if (remainingDevices.count() == 1) { appendLogText(i18n("Unable to establish:\n%1\nPlease ensure the device is connected and powered on.", remainingDevices.at(0))); KNotification::beep(i18n("Ekos startup error")); } else { appendLogText(i18n("Unable to establish the following devices:\n%1\nPlease ensure each device is connected " "and powered on.", remainingDevices.join("\n"))); KNotification::beep(i18n("Ekos startup error")); } } else { QStringList remainingDevices; for (auto &driver : currentProfile->drivers.values()) { bool driverFound = false; for (auto &device : genericDevices) { if (device->getBaseDevice()->getDriverName() == driver) { driverFound = true; break; } } if (driverFound == false) remainingDevices << QString("+ %1").arg(driver); } if (remainingDevices.count() == 1) { appendLogText(i18n("Unable to establish remote device:\n%1\nPlease ensure remote device name corresponds " "to actual device name.", remainingDevices.at(0))); KNotification::beep(i18n("Ekos startup error")); } else { appendLogText(i18n("Unable to establish remote devices:\n%1\nPlease ensure remote device name corresponds " "to actual device name.", remainingDevices.join("\n"))); KNotification::beep(i18n("Ekos startup error")); } } m_ekosStatus = Ekos::Error; } void Manager::connectDevices() { // Check if already connected int nConnected = 0; Ekos::CommunicationStatus previousStatus = m_indiStatus; for (auto &device : genericDevices) { if (device->isConnected()) nConnected++; } if (genericDevices.count() == nConnected) { m_indiStatus = Ekos::Success; emit indiStatusChanged(m_indiStatus); return; } m_indiStatus = Ekos::Pending; if (previousStatus != m_indiStatus) emit indiStatusChanged(m_indiStatus); for (auto &device : genericDevices) { qCDebug(KSTARS_EKOS) << "Connecting " << device->getDeviceName(); device->Connect(); } connectB->setEnabled(false); disconnectB->setEnabled(true); appendLogText(i18n("Connecting INDI devices...")); } void Manager::disconnectDevices() { for (auto &device : genericDevices) { qCDebug(KSTARS_EKOS) << "Disconnecting " << device->getDeviceName(); device->Disconnect(); } appendLogText(i18n("Disconnecting INDI devices...")); } void Manager::processServerTerminated(const QString &host, const QString &port) { if ((m_LocalMode && managedDrivers.first()->getPort() == port) || (currentProfile->host == host && currentProfile->port == port.toInt())) { cleanDevices(false); if (indiHubAgent) indiHubAgent->terminate(); } } void Manager::processServerStarted(const QString &host, const QString &port) { if (m_LocalMode && currentProfile->indihub != INDIHub::None) { if (QFile(Options::iNDIHubAgent()).exists()) { indiHubAgent = new QProcess(); QStringList args; args << "--indi-server" << QString("%1:%2").arg(host).arg(port); if (currentProfile->guidertype == Ekos::Guide::GUIDE_PHD2) args << "--phd2-server" << QString("%1:%2").arg(currentProfile->guiderhost).arg(currentProfile->guiderport); args << "--mode" << INDIHub::toString(currentProfile->indihub); indiHubAgent->start(Options::iNDIHubAgent(), args); qCDebug(KSTARS_EKOS) << "Started INDIHub agent."; } } } void Manager::cleanDevices(bool stopDrivers) { if (m_ekosStatus == Ekos::Idle) return; INDIListener::Instance()->disconnect(this); DriverManager::Instance()->disconnect(this); if (managedDrivers.isEmpty() == false) { if (m_LocalMode) { if (stopDrivers) DriverManager::Instance()->stopDevices(managedDrivers); } else { if (stopDrivers) DriverManager::Instance()->disconnectRemoteHost(managedDrivers.first()); if (m_RemoteManagerStart && currentProfile->INDIWebManagerPort != -1) { INDI::WebManager::stopProfile(currentProfile); m_RemoteManagerStart = false; } } } reset(); profileGroup->setEnabled(true); appendLogText(i18n("INDI services stopped.")); } void Manager::processNewDevice(ISD::GDInterface * devInterface) { qCInfo(KSTARS_EKOS) << "Ekos received a new device: " << devInterface->getDeviceName(); Ekos::CommunicationStatus previousStatus = m_indiStatus; for(auto &device : genericDevices) { if (device->getDeviceName() == devInterface->getDeviceName()) { qCWarning(KSTARS_EKOS) << "Found duplicate device, ignoring..."; return; } } // Always reset INDI Connection status if we receive a new device m_indiStatus = Ekos::Idle; if (previousStatus != m_indiStatus) emit indiStatusChanged(m_indiStatus); genericDevices.append(devInterface); nDevices--; connect(devInterface, &ISD::GDInterface::Connected, this, &Ekos::Manager::deviceConnected); connect(devInterface, &ISD::GDInterface::Disconnected, this, &Ekos::Manager::deviceDisconnected); connect(devInterface, &ISD::GDInterface::propertyDefined, this, &Ekos::Manager::processNewProperty); connect(devInterface, &ISD::GDInterface::interfaceDefined, this, &Ekos::Manager::syncActiveDevices); connect(devInterface, &ISD::GDInterface::numberUpdated, this, &Ekos::Manager::processNewNumber); connect(devInterface, &ISD::GDInterface::switchUpdated, this, &Ekos::Manager::processNewSwitch); connect(devInterface, &ISD::GDInterface::textUpdated, this, &Ekos::Manager::processNewText); connect(devInterface, &ISD::GDInterface::lightUpdated, this, &Ekos::Manager::processNewLight); connect(devInterface, &ISD::GDInterface::BLOBUpdated, this, &Ekos::Manager::processNewBLOB); if (currentProfile->isStellarMate) { connect(devInterface, &ISD::GDInterface::systemPortDetected, [this, devInterface]() { if (!serialPortAssistant) { serialPortAssistant.reset(new SerialPortAssistant(currentProfile, this)); serialPortAssistantB->setEnabled(true); } uint32_t driverInterface = devInterface->getDriverInterface(); // Ignore CCD interface if (driverInterface & INDI::BaseDevice::CCD_INTERFACE) return; if (driverInterface & INDI::BaseDevice::TELESCOPE_INTERFACE || driverInterface & INDI::BaseDevice::FOCUSER_INTERFACE || driverInterface & INDI::BaseDevice::FILTER_INTERFACE || driverInterface & INDI::BaseDevice::AUX_INTERFACE || driverInterface & INDI::BaseDevice::GPS_INTERFACE) serialPortAssistant->addDevice(devInterface); if (Options::autoLoadSerialAssistant()) serialPortAssistant->show(); }); } if (nDevices <= 0) { m_ekosStatus = Ekos::Success; emit ekosStatusChanged(m_ekosStatus); connectB->setEnabled(true); disconnectB->setEnabled(false); //controlPanelB->setEnabled(true); if (m_LocalMode == false && nDevices == 0) { if (currentProfile->autoConnect) appendLogText(i18n("Remote devices established.")); else appendLogText(i18n("Remote devices established. Please connect devices.")); } } } void Manager::deviceConnected() { connectB->setEnabled(false); disconnectB->setEnabled(true); processINDIB->setEnabled(false); Ekos::CommunicationStatus previousStatus = m_indiStatus; if (Options::verboseLogging()) { ISD::GDInterface * device = qobject_cast(sender()); qCInfo(KSTARS_EKOS) << device->getDeviceName() << "Version:" << device->getDriverVersion() << "Interface:" << device->getDriverInterface() << "is connected."; } int nConnectedDevices = 0; foreach (ISD::GDInterface * device, genericDevices) { if (device->isConnected()) nConnectedDevices++; } qCDebug(KSTARS_EKOS) << nConnectedDevices << " devices connected out of " << genericDevices.count(); if (nConnectedDevices >= currentProfile->drivers.count()) //if (nConnectedDevices >= genericDevices.count()) { m_indiStatus = Ekos::Success; qCInfo(KSTARS_EKOS) << "All INDI devices are now connected."; } else m_indiStatus = Ekos::Pending; if (previousStatus != m_indiStatus) emit indiStatusChanged(m_indiStatus); ISD::GDInterface * dev = static_cast(sender()); if (dev->getDriverInterface() & INDI::BaseDevice::TELESCOPE_INTERFACE) { if (mountProcess.get() != nullptr) { mountProcess->setEnabled(true); if (alignProcess.get() != nullptr) alignProcess->setEnabled(true); } } else if (dev->getDriverInterface() & INDI::BaseDevice::CCD_INTERFACE) { if (captureProcess.get() != nullptr) captureProcess->setEnabled(true); if (focusProcess.get() != nullptr) focusProcess->setEnabled(true); if (alignProcess.get() != nullptr) { if (mountProcess.get() && mountProcess->isEnabled()) alignProcess->setEnabled(true); else alignProcess->setEnabled(false); } if (guideProcess.get() != nullptr) guideProcess->setEnabled(true); } else if (dev->getDriverInterface() & INDI::BaseDevice::FOCUSER_INTERFACE) { if (focusProcess.get() != nullptr) focusProcess->setEnabled(true); } if (Options::neverLoadConfig()) return; INDIConfig tConfig = Options::loadConfigOnConnection() ? LOAD_LAST_CONFIG : LOAD_DEFAULT_CONFIG; foreach (ISD::GDInterface * device, genericDevices) { if (device == dev) { connect(dev, &ISD::GDInterface::switchUpdated, this, &Ekos::Manager::watchDebugProperty); ISwitchVectorProperty * configProp = device->getBaseDevice()->getSwitch("CONFIG_PROCESS"); if (configProp && configProp->s == IPS_IDLE) device->setConfig(tConfig); break; } } } void Manager::deviceDisconnected() { ISD::GDInterface * dev = static_cast(sender()); Ekos::CommunicationStatus previousStatus = m_indiStatus; if (dev != nullptr) { if (dev->getState("CONNECTION") == IPS_ALERT) m_indiStatus = Ekos::Error; else if (dev->getState("CONNECTION") == IPS_BUSY) m_indiStatus = Ekos::Pending; else m_indiStatus = Ekos::Idle; if (Options::verboseLogging()) qCDebug(KSTARS_EKOS) << dev->getDeviceName() << " is disconnected."; appendLogText(i18n("%1 is disconnected.", dev->getDeviceName())); } else m_indiStatus = Ekos::Idle; if (previousStatus != m_indiStatus) emit indiStatusChanged(m_indiStatus); connectB->setEnabled(true); disconnectB->setEnabled(false); processINDIB->setEnabled(true); if (dev != nullptr && dev->getBaseDevice() && (dev->getDriverInterface() & INDI::BaseDevice::TELESCOPE_INTERFACE)) { if (mountProcess.get() != nullptr) mountProcess->setEnabled(false); } // Do not disable modules on device connection loss, let them handle it /* else if (dev->getDriverInterface() & INDI::BaseDevice::CCD_INTERFACE) { if (captureProcess.get() != nullptr) captureProcess->setEnabled(false); if (focusProcess.get() != nullptr) focusProcess->setEnabled(false); if (alignProcess.get() != nullptr) alignProcess->setEnabled(false); if (guideProcess.get() != nullptr) guideProcess->setEnabled(false); } else if (dev->getDriverInterface() & INDI::BaseDevice::FOCUSER_INTERFACE) { if (focusProcess.get() != nullptr) focusProcess->setEnabled(false); }*/ } void Manager::setTelescope(ISD::GDInterface * scopeDevice) { //mount = scopeDevice; managedDevices[KSTARS_TELESCOPE] = scopeDevice; appendLogText(i18n("%1 is online.", scopeDevice->getDeviceName())); // connect(scopeDevice, SIGNAL(numberUpdated(INumberVectorProperty *)), this, // SLOT(processNewNumber(INumberVectorProperty *)), Qt::UniqueConnection); initMount(); mountProcess->setTelescope(scopeDevice); double primaryScopeFL = 0, primaryScopeAperture = 0, guideScopeFL = 0, guideScopeAperture = 0; getCurrentProfileTelescopeInfo(primaryScopeFL, primaryScopeAperture, guideScopeFL, guideScopeAperture); // Save telescope info in mount driver mountProcess->setTelescopeInfo(QList() << primaryScopeFL << primaryScopeAperture << guideScopeFL << guideScopeAperture); if (guideProcess.get() != nullptr) { guideProcess->setTelescope(scopeDevice); guideProcess->setTelescopeInfo(primaryScopeFL, primaryScopeAperture, guideScopeFL, guideScopeAperture); } if (alignProcess.get() != nullptr) { alignProcess->setTelescope(scopeDevice); alignProcess->setTelescopeInfo(primaryScopeFL, primaryScopeAperture, guideScopeFL, guideScopeAperture); } // if (domeProcess.get() != nullptr) // domeProcess->setTelescope(scopeDevice); ekosLiveClient->message()->sendMounts(); ekosLiveClient->message()->sendScopes(); } void Manager::setCCD(ISD::GDInterface * ccdDevice) { // No duplicates for (auto oneCCD : findDevices(KSTARS_CCD)) if (oneCCD == ccdDevice) return; managedDevices.insertMulti(KSTARS_CCD, ccdDevice); initCapture(); captureProcess->setEnabled(true); captureProcess->addCCD(ccdDevice); QString primaryCCD, guiderCCD; // Only look for primary & guider CCDs if we can tell a difference between them // otherwise rely on saved options if (currentProfile->ccd() != currentProfile->guider()) { foreach (ISD::GDInterface * device, findDevices(KSTARS_CCD)) { if (QString(device->getDeviceName()).startsWith(currentProfile->ccd(), Qt::CaseInsensitive)) primaryCCD = QString(device->getDeviceName()); else if (QString(device->getDeviceName()).startsWith(currentProfile->guider(), Qt::CaseInsensitive)) guiderCCD = QString(device->getDeviceName()); } } bool rc = false; if (Options::defaultCaptureCCD().isEmpty() == false) rc = captureProcess->setCamera(Options::defaultCaptureCCD()); if (rc == false && primaryCCD.isEmpty() == false) captureProcess->setCamera(primaryCCD); initFocus(); focusProcess->addCCD(ccdDevice); rc = false; if (Options::defaultFocusCCD().isEmpty() == false) rc = focusProcess->setCamera(Options::defaultFocusCCD()); if (rc == false && primaryCCD.isEmpty() == false) focusProcess->setCamera(primaryCCD); initAlign(); alignProcess->addCCD(ccdDevice); rc = false; if (Options::defaultAlignCCD().isEmpty() == false) rc = alignProcess->setCamera(Options::defaultAlignCCD()); if (rc == false && primaryCCD.isEmpty() == false) alignProcess->setCamera(primaryCCD); initGuide(); guideProcess->addCamera(ccdDevice); rc = false; if (Options::defaultGuideCCD().isEmpty() == false) rc = guideProcess->setCamera(Options::defaultGuideCCD()); if (rc == false && guiderCCD.isEmpty() == false) guideProcess->setCamera(guiderCCD); appendLogText(i18n("%1 is online.", ccdDevice->getDeviceName())); // connect(ccdDevice, SIGNAL(numberUpdated(INumberVectorProperty *)), this, // SLOT(processNewNumber(INumberVectorProperty *)), Qt::UniqueConnection); if (managedDevices.contains(KSTARS_TELESCOPE)) { alignProcess->setTelescope(managedDevices[KSTARS_TELESCOPE]); captureProcess->setTelescope(managedDevices[KSTARS_TELESCOPE]); guideProcess->setTelescope(managedDevices[KSTARS_TELESCOPE]); } } void Manager::setFilter(ISD::GDInterface * filterDevice) { // No duplicates if (findDevices(KSTARS_FILTER).contains(filterDevice) == false) managedDevices.insertMulti(KSTARS_FILTER, filterDevice); appendLogText(i18n("%1 filter is online.", filterDevice->getDeviceName())); initCapture(); // connect(filterDevice, SIGNAL(numberUpdated(INumberVectorProperty *)), this, // SLOT(processNewNumber(INumberVectorProperty *)), Qt::UniqueConnection); // connect(filterDevice, SIGNAL(textUpdated(ITextVectorProperty *)), this, // SLOT(processNewText(ITextVectorProperty *)), Qt::UniqueConnection); captureProcess->addFilter(filterDevice); initFocus(); focusProcess->addFilter(filterDevice); initAlign(); alignProcess->addFilter(filterDevice); } void Manager::setFocuser(ISD::GDInterface * focuserDevice) { // No duplicates if (findDevices(KSTARS_FOCUSER).contains(focuserDevice) == false) managedDevices.insertMulti(KSTARS_FOCUSER, focuserDevice); initCapture(); initFocus(); focusProcess->addFocuser(focuserDevice); if (Options::defaultFocusFocuser().isEmpty() == false) focusProcess->setFocuser(Options::defaultFocusFocuser()); appendLogText(i18n("%1 focuser is online.", focuserDevice->getDeviceName())); } void Manager::setDome(ISD::GDInterface * domeDevice) { managedDevices[KSTARS_DOME] = domeDevice; initDome(); domeProcess->setDome(domeDevice); if (captureProcess.get() != nullptr) captureProcess->setDome(domeDevice); if (alignProcess.get() != nullptr) alignProcess->setDome(domeDevice); // if (managedDevices.contains(KSTARS_TELESCOPE)) // domeProcess->setTelescope(managedDevices[KSTARS_TELESCOPE]); appendLogText(i18n("%1 is online.", domeDevice->getDeviceName())); } void Manager::setWeather(ISD::GDInterface * weatherDevice) { managedDevices[KSTARS_WEATHER] = weatherDevice; initWeather(); weatherProcess->setWeather(weatherDevice); appendLogText(i18n("%1 is online.", weatherDevice->getDeviceName())); } void Manager::setDustCap(ISD::GDInterface * dustCapDevice) { // No duplicates if (findDevices(KSTARS_AUXILIARY).contains(dustCapDevice) == false) managedDevices.insertMulti(KSTARS_AUXILIARY, dustCapDevice); initDustCap(); dustCapProcess->setDustCap(dustCapDevice); appendLogText(i18n("%1 is online.", dustCapDevice->getDeviceName())); if (captureProcess.get() != nullptr) captureProcess->setDustCap(dustCapDevice); DarkLibrary::Instance()->setRemoteCap(dustCapDevice); } void Manager::setLightBox(ISD::GDInterface * lightBoxDevice) { // No duplicates if (findDevices(KSTARS_AUXILIARY).contains(lightBoxDevice) == false) managedDevices.insertMulti(KSTARS_AUXILIARY, lightBoxDevice); if (captureProcess.get() != nullptr) captureProcess->setLightBox(lightBoxDevice); } void Manager::removeDevice(ISD::GDInterface * devInterface) { // switch (devInterface->getType()) // { // case KSTARS_CCD: // removeTabs(); // break; // default: // break; // } if (alignProcess) alignProcess->removeDevice(devInterface); if (captureProcess) captureProcess->removeDevice(devInterface); if (focusProcess) focusProcess->removeDevice(devInterface); if (mountProcess) mountProcess->removeDevice(devInterface); if (guideProcess) guideProcess->removeDevice(devInterface); if (domeProcess) domeProcess->removeDevice(devInterface); if (weatherProcess) weatherProcess->removeDevice(devInterface); if (dustCapProcess) { dustCapProcess->removeDevice(devInterface); DarkLibrary::Instance()->removeDevice(devInterface); } appendLogText(i18n("%1 is offline.", devInterface->getDeviceName())); // #1 Remove from Generic Devices // Generic devices are ALL the devices we receive from INDI server // Whether Ekos cares about them (i.e. selected equipment) or extra devices we // do not care about for (auto &device : genericDevices) { if (device->getDeviceName() == devInterface->getDeviceName()) { genericDevices.removeOne(device); } } // #2 Remove from Ekos Managed Device // Managed devices are devices selected by the user in the device profile - for (auto &device : managedDevices.values()) + for (auto it = managedDevices.begin(); it != managedDevices.end();) { - if (device->getDeviceName() == devInterface->getDeviceName()) + if (it.value()->getDeviceName() == devInterface->getDeviceName()) { - managedDevices.remove(managedDevices.key(device)); - - if (managedDevices.count() == 0) - cleanDevices(); - - //break; + it = managedDevices.erase(it); } + else + ++it; } if (managedDevices.isEmpty()) + { + cleanDevices(); removeTabs(); + } } void Manager::processNewText(ITextVectorProperty * tvp) { ekosLiveClient.get()->message()->processNewText(tvp); if (!strcmp(tvp->name, "FILTER_NAME")) { ekosLiveClient.get()->message()->sendFilterWheels(); } if (!strcmp(tvp->name, "ACTIVE_DEVICES")) { syncActiveDevices(); } } void Manager::processNewSwitch(ISwitchVectorProperty * svp) { ekosLiveClient.get()->message()->processNewSwitch(svp); } void Manager::processNewLight(ILightVectorProperty * lvp) { ekosLiveClient.get()->message()->processNewLight(lvp); } void Manager::processNewBLOB(IBLOB * bp) { ekosLiveClient.get()->media()->processNewBLOB(bp); } void Manager::processNewNumber(INumberVectorProperty * nvp) { ekosLiveClient.get()->message()->processNewNumber(nvp); if (!strcmp(nvp->name, "TELESCOPE_INFO") && managedDevices.contains(KSTARS_TELESCOPE)) { if (guideProcess.get() != nullptr) { guideProcess->setTelescope(managedDevices[KSTARS_TELESCOPE]); //guideProcess->syncTelescopeInfo(); } if (alignProcess.get() != nullptr) { alignProcess->setTelescope(managedDevices[KSTARS_TELESCOPE]); //alignProcess->syncTelescopeInfo(); } if (mountProcess.get() != nullptr) { mountProcess->setTelescope(managedDevices[KSTARS_TELESCOPE]); //mountProcess->syncTelescopeInfo(); } return; } if (!strcmp(nvp->name, "CCD_INFO") || !strcmp(nvp->name, "GUIDER_INFO") || !strcmp(nvp->name, "CCD_FRAME") || !strcmp(nvp->name, "GUIDER_FRAME")) { if (focusProcess.get() != nullptr) focusProcess->syncCCDInfo(); if (guideProcess.get() != nullptr) guideProcess->syncCCDInfo(); if (alignProcess.get() != nullptr) alignProcess->syncCCDInfo(); return; } /* if (!strcmp(nvp->name, "FILTER_SLOT")) { if (captureProcess.get() != nullptr) captureProcess->checkFilter(); if (focusProcess.get() != nullptr) focusProcess->checkFilter(); if (alignProcess.get() != nullptr) alignProcess->checkFilter(); } */ } void Manager::processNewProperty(INDI::Property * prop) { ISD::GenericDevice * deviceInterface = qobject_cast(sender()); if (!strcmp(prop->getName(), "CONNECTION") && currentProfile->autoConnect) { // Check if we need to do any mappings const QString port = m_ProfileMapping.value(QString(deviceInterface->getDeviceName())).toString(); // If we don't have port mapping, then we connect immediately. if (port.isEmpty()) deviceInterface->Connect(); return; } if (!strcmp(prop->getName(), "DEVICE_PORT")) { // Check if we need to do any mappings const QString port = m_ProfileMapping.value(QString(deviceInterface->getDeviceName())).toString(); if (!port.isEmpty()) { ITextVectorProperty *tvp = prop->getText(); IUSaveText(&(tvp->tp[0]), port.toLatin1().data()); deviceInterface->getDriverInfo()->getClientManager()->sendNewText(tvp); // Now connect if we need to. if (currentProfile->autoConnect) deviceInterface->Connect(); return; } } // Check if we need to turn on DEBUG for logging purposes if (!strcmp(prop->getName(), "DEBUG")) { uint16_t interface = deviceInterface->getDriverInterface(); if ( opsLogs->getINDIDebugInterface() & interface ) { // Check if we need to enable debug logging for the INDI drivers. ISwitchVectorProperty * debugSP = prop->getSwitch(); debugSP->sp[0].s = ISS_ON; debugSP->sp[1].s = ISS_OFF; deviceInterface->getDriverInfo()->getClientManager()->sendNewSwitch(debugSP); } } // Handle debug levels for logging purposes if (!strcmp(prop->getName(), "DEBUG_LEVEL")) { uint16_t interface = deviceInterface->getDriverInterface(); // Check if the logging option for the specific device class is on and if the device interface matches it. if ( opsLogs->getINDIDebugInterface() & interface ) { // Turn on everything ISwitchVectorProperty * debugLevel = prop->getSwitch(); for (int i = 0; i < debugLevel->nsp; i++) debugLevel->sp[i].s = ISS_ON; deviceInterface->getDriverInfo()->getClientManager()->sendNewSwitch(debugLevel); } } if (!strcmp(prop->getName(), "ACTIVE_DEVICES")) { if (deviceInterface->getDriverInterface() > 0) syncActiveDevices(); } if (!strcmp(prop->getName(), "TELESCOPE_INFO") || !strcmp(prop->getName(), "TELESCOPE_SLEW_RATE") || !strcmp(prop->getName(), "TELESCOPE_PARK")) { ekosLiveClient.get()->message()->sendMounts(); ekosLiveClient.get()->message()->sendScopes(); } if (!strcmp(prop->getName(), "CCD_INFO") || !strcmp(prop->getName(), "CCD_TEMPERATURE") || !strcmp(prop->getName(), "CCD_ISO") || !strcmp(prop->getName(), "CCD_GAIN") || !strcmp(prop->getName(), "CCD_CONTROLS")) { ekosLiveClient.get()->message()->sendCameras(); ekosLiveClient.get()->media()->registerCameras(); } if (!strcmp(prop->getName(), "ABS_DOME_POSITION") || !strcmp(prop->getName(), "DOME_ABORT_MOTION") || !strcmp(prop->getName(), "DOME_PARK")) { ekosLiveClient.get()->message()->sendDomes(); } if (!strcmp(prop->getName(), "CAP_PARK") || !strcmp(prop->getName(), "FLAT_LIGHT_CONTROL")) { ekosLiveClient.get()->message()->sendCaps(); } if (!strcmp(prop->getName(), "FILTER_NAME")) ekosLiveClient.get()->message()->sendFilterWheels(); if (!strcmp(prop->getName(), "FILTER_NAME")) filterManager.data()->initFilterProperties(); if (!strcmp(prop->getName(), "CONFIRM_FILTER_SET")) filterManager.data()->initFilterProperties(); if (!strcmp(prop->getName(), "CCD_INFO") || !strcmp(prop->getName(), "GUIDER_INFO")) { if (focusProcess.get() != nullptr) focusProcess->syncCCDInfo(); if (guideProcess.get() != nullptr) guideProcess->syncCCDInfo(); if (alignProcess.get() != nullptr) alignProcess->syncCCDInfo(); return; } if (!strcmp(prop->getName(), "TELESCOPE_INFO") && managedDevices.contains(KSTARS_TELESCOPE)) { if (guideProcess.get() != nullptr) { guideProcess->setTelescope(managedDevices[KSTARS_TELESCOPE]); //guideProcess->syncTelescopeInfo(); } if (alignProcess.get() != nullptr) { alignProcess->setTelescope(managedDevices[KSTARS_TELESCOPE]); //alignProcess->syncTelescopeInfo(); } if (mountProcess.get() != nullptr) { mountProcess->setTelescope(managedDevices[KSTARS_TELESCOPE]); //mountProcess->syncTelescopeInfo(); } return; } if (!strcmp(prop->getName(), "GUIDER_EXPOSURE")) { foreach (ISD::GDInterface * device, findDevices(KSTARS_CCD)) { if (device->getDeviceName() == prop->getDeviceName()) { initCapture(); initGuide(); useGuideHead = true; captureProcess->addGuideHead(device); guideProcess->addGuideHead(device); bool rc = false; if (Options::defaultGuideCCD().isEmpty() == false) rc = guideProcess->setCamera(Options::defaultGuideCCD()); if (rc == false) guideProcess->setCamera(QString(device->getDeviceName()) + QString(" Guider")); return; } } return; } if (!strcmp(prop->getName(), "CCD_FRAME_TYPE")) { if (captureProcess.get() != nullptr) { foreach (ISD::GDInterface * device, findDevices(KSTARS_CCD)) { if (device->getDeviceName() == prop->getDeviceName()) { captureProcess->syncFrameType(device); return; } } } return; } if (!strcmp(prop->getName(), "CCD_ISO")) { if (captureProcess.get() != nullptr) captureProcess->checkCCD(); return; } if (!strcmp(prop->getName(), "TELESCOPE_PARK") && managedDevices.contains(KSTARS_TELESCOPE)) { if (captureProcess.get() != nullptr) captureProcess->setTelescope(managedDevices[KSTARS_TELESCOPE]); if (mountProcess.get() != nullptr) mountProcess->setTelescope(managedDevices[KSTARS_TELESCOPE]); return; } /* if (!strcmp(prop->getName(), "FILTER_NAME")) { if (captureProcess.get() != nullptr) captureProcess->checkFilter(); if (focusProcess.get() != nullptr) focusProcess->checkFilter(); if (alignProcess.get() != nullptr) alignProcess->checkFilter(); return; } */ if (!strcmp(prop->getName(), "ASTROMETRY_SOLVER")) { foreach (ISD::GDInterface * device, genericDevices) { if (device->getDeviceName() == prop->getDeviceName()) { initAlign(); alignProcess->setAstrometryDevice(device); break; } } } if (!strcmp(prop->getName(), "ABS_ROTATOR_ANGLE")) { managedDevices[KSTARS_ROTATOR] = deviceInterface; if (captureProcess.get() != nullptr) captureProcess->setRotator(deviceInterface); if (alignProcess.get() != nullptr) alignProcess->setRotator(deviceInterface); } if (!strcmp(prop->getName(), "GPS_REFRESH")) { managedDevices.insertMulti(KSTARS_AUXILIARY, deviceInterface); if (mountProcess.get() != nullptr) mountProcess->setGPS(deviceInterface); } if (focusProcess.get() != nullptr && strstr(prop->getName(), "FOCUS_")) { focusProcess->checkFocuser(); } } const QList &Manager::getAllDevices() const { return genericDevices; } QList Manager::findDevices(DeviceFamily type) { QList deviceList; QMapIterator i(managedDevices); while (i.hasNext()) { i.next(); if (i.key() == type) deviceList.append(i.value()); } return deviceList; } QList Manager::findDevicesByInterface(uint32_t interface) { QList deviceList; for (const auto dev : genericDevices) { uint32_t devInterface = dev->getDriverInterface(); if (devInterface & interface) deviceList.append(dev); } return deviceList; } void Manager::processTabChange() { QWidget * currentWidget = toolsWidget->currentWidget(); //if (focusProcess.get() != nullptr && currentWidget != focusProcess) //focusProcess->resetFrame(); if (alignProcess.get() && alignProcess.get() == currentWidget) { if (alignProcess->isEnabled() == false && captureProcess->isEnabled()) { if (managedDevices[KSTARS_CCD]->isConnected() && managedDevices.contains(KSTARS_TELESCOPE)) { if (alignProcess->isParserOK()) alignProcess->setEnabled(true); //#ifdef Q_OS_WIN else if (Options::solverBackend() == Ekos::Align::SOLVER_ASTROMETRYNET) { // If current setting is remote astrometry and profile doesn't contain // remote astrometry, then we switch to online solver. Otherwise, the whole align // module remains disabled and the user cannot change re-enable it since he cannot select online solver ProfileInfo * pi = getCurrentProfile(); if (Options::astrometrySolverType() == Ekos::Align::SOLVER_REMOTE && pi->aux1() != "Astrometry" && pi->aux2() != "Astrometry" && pi->aux3() != "Astrometry" && pi->aux4() != "Astrometry") { Options::setAstrometrySolverType(Ekos::Align::SOLVER_ONLINE); alignModule()->setAstrometrySolverType(Ekos::Align::SOLVER_ONLINE); alignProcess->setEnabled(true); } } //#endif } } alignProcess->checkCCD(); } else if (captureProcess.get() != nullptr && currentWidget == captureProcess.get()) { captureProcess->checkCCD(); } else if (focusProcess.get() != nullptr && currentWidget == focusProcess.get()) { focusProcess->checkCCD(); } else if (guideProcess.get() != nullptr && currentWidget == guideProcess.get()) { guideProcess->checkCCD(); } updateLog(); } void Manager::updateLog() { QWidget * currentWidget = toolsWidget->currentWidget(); if (currentWidget == setupTab) ekosLogOut->setPlainText(m_LogText.join("\n")); else if (currentWidget == alignProcess.get()) ekosLogOut->setPlainText(alignProcess->getLogText()); else if (currentWidget == captureProcess.get()) ekosLogOut->setPlainText(captureProcess->getLogText()); else if (currentWidget == focusProcess.get()) ekosLogOut->setPlainText(focusProcess->getLogText()); else if (currentWidget == guideProcess.get()) ekosLogOut->setPlainText(guideProcess->getLogText()); else if (currentWidget == mountProcess.get()) ekosLogOut->setPlainText(mountProcess->getLogText()); else if (currentWidget == schedulerProcess.get()) ekosLogOut->setPlainText(schedulerProcess->getLogText()); else if (currentWidget == observatoryProcess.get()) ekosLogOut->setPlainText(observatoryProcess->getLogText()); #ifdef Q_OS_OSX repaint(); //This is a band-aid for a bug in QT 5.10.0 #endif } void Manager::appendLogText(const QString &text) { m_LogText.insert(0, i18nc("log entry; %1 is the date, %2 is the text", "%1 %2", KStarsData::Instance()->lt().toString("yyyy-MM-ddThh:mm:ss"), text)); qCInfo(KSTARS_EKOS) << text; emit newLog(text); updateLog(); } void Manager::clearLog() { QWidget * currentWidget = toolsWidget->currentWidget(); if (currentWidget == setupTab) { m_LogText.clear(); updateLog(); } else if (currentWidget == alignProcess.get()) alignProcess->clearLog(); else if (currentWidget == captureProcess.get()) captureProcess->clearLog(); else if (currentWidget == focusProcess.get()) focusProcess->clearLog(); else if (currentWidget == guideProcess.get()) guideProcess->clearLog(); else if (currentWidget == mountProcess.get()) mountProcess->clearLog(); else if (currentWidget == schedulerProcess.get()) schedulerProcess->clearLog(); else if (currentWidget == observatoryProcess.get()) observatoryProcess->clearLog(); } void Manager::initCapture() { if (captureProcess.get() != nullptr) return; captureProcess.reset(new Ekos::Capture()); captureProcess->setEnabled(false); int index = toolsWidget->addTab(captureProcess.get(), QIcon(":/icons/ekos_ccd.png"), ""); toolsWidget->tabBar()->setTabToolTip(index, i18nc("Charge-Coupled Device", "CCD")); if (Options::ekosLeftIcons()) { QTransform trans; trans.rotate(90); QIcon icon = toolsWidget->tabIcon(index); QPixmap pix = icon.pixmap(QSize(48, 48)); icon = QIcon(pix.transformed(trans)); toolsWidget->setTabIcon(index, icon); } connect(captureProcess.get(), &Ekos::Capture::newLog, this, &Ekos::Manager::updateLog); connect(captureProcess.get(), &Ekos::Capture::newStatus, this, &Ekos::Manager::updateCaptureStatus); connect(captureProcess.get(), &Ekos::Capture::newImage, this, &Ekos::Manager::updateCaptureProgress); connect(captureProcess.get(), &Ekos::Capture::driverTimedout, this, &Ekos::Manager::restartDriver); connect(captureProcess.get(), &Ekos::Capture::newSequenceImage, [&](const QString & filename, const QString & previewFITS) { if (Options::useSummaryPreview() && QFile::exists(filename)) { if (Options::autoImageToFITS()) { if (previewFITS.isEmpty() == false) summaryPreview->loadFITS(previewFITS); } else summaryPreview->loadFITS(filename); } }); connect(captureProcess.get(), &Ekos::Capture::newDownloadProgress, this, &Ekos::Manager::updateDownloadProgress); connect(captureProcess.get(), &Ekos::Capture::newExposureProgress, this, &Ekos::Manager::updateExposureProgress); captureGroup->setEnabled(true); sequenceProgress->setEnabled(true); captureProgress->setEnabled(true); imageProgress->setEnabled(true); captureProcess->setFilterManager(filterManager); if (!capturePI) { capturePI = new QProgressIndicator(captureProcess.get()); captureStatusLayout->insertWidget(0, capturePI); } foreach (ISD::GDInterface * device, findDevices(KSTARS_AUXILIARY)) { if (device->getDriverInterface() & INDI::BaseDevice::DUSTCAP_INTERFACE) captureProcess->setDustCap(device); if (device->getDriverInterface() & INDI::BaseDevice::LIGHTBOX_INTERFACE) captureProcess->setLightBox(device); } if (managedDevices.contains(KSTARS_DOME)) { captureProcess->setDome(managedDevices[KSTARS_DOME]); } if (managedDevices.contains(KSTARS_ROTATOR)) { captureProcess->setRotator(managedDevices[KSTARS_ROTATOR]); } connectModules(); emit newModule("Capture"); } void Manager::initAlign() { if (alignProcess.get() != nullptr) return; alignProcess.reset(new Ekos::Align(currentProfile)); double primaryScopeFL = 0, primaryScopeAperture = 0, guideScopeFL = 0, guideScopeAperture = 0; getCurrentProfileTelescopeInfo(primaryScopeFL, primaryScopeAperture, guideScopeFL, guideScopeAperture); alignProcess->setTelescopeInfo(primaryScopeFL, primaryScopeAperture, guideScopeFL, guideScopeAperture); alignProcess->setEnabled(false); int index = toolsWidget->addTab(alignProcess.get(), QIcon(":/icons/ekos_align.png"), ""); toolsWidget->tabBar()->setTabToolTip(index, i18n("Align")); connect(alignProcess.get(), &Ekos::Align::newLog, this, &Ekos::Manager::updateLog); if (Options::ekosLeftIcons()) { QTransform trans; trans.rotate(90); QIcon icon = toolsWidget->tabIcon(index); QPixmap pix = icon.pixmap(QSize(48, 48)); icon = QIcon(pix.transformed(trans)); toolsWidget->setTabIcon(index, icon); } alignProcess->setFilterManager(filterManager); if (managedDevices.contains(KSTARS_DOME)) { alignProcess->setDome(managedDevices[KSTARS_DOME]); } if (managedDevices.contains(KSTARS_ROTATOR)) { alignProcess->setRotator(managedDevices[KSTARS_ROTATOR]); } connectModules(); emit newModule("Align"); } void Manager::initFocus() { if (focusProcess.get() != nullptr) return; focusProcess.reset(new Ekos::Focus()); int index = toolsWidget->addTab(focusProcess.get(), QIcon(":/icons/ekos_focus.png"), ""); toolsWidget->tabBar()->setTabToolTip(index, i18n("Focus")); // Focus <---> Manager connections connect(focusProcess.get(), &Ekos::Focus::newLog, this, &Ekos::Manager::updateLog); connect(focusProcess.get(), &Ekos::Focus::newStatus, this, &Ekos::Manager::setFocusStatus); connect(focusProcess.get(), &Ekos::Focus::newStarPixmap, this, &Ekos::Manager::updateFocusStarPixmap); connect(focusProcess.get(), &Ekos::Focus::newProfilePixmap, this, &Ekos::Manager::updateFocusProfilePixmap); connect(focusProcess.get(), &Ekos::Focus::newHFR, this, &Ekos::Manager::updateCurrentHFR); // Focus <---> Filter Manager connections focusProcess->setFilterManager(filterManager); connect(filterManager.data(), &Ekos::FilterManager::checkFocus, focusProcess.get(), &Ekos::Focus::checkFocus, Qt::UniqueConnection); connect(focusProcess.get(), &Ekos::Focus::newStatus, filterManager.data(), &Ekos::FilterManager::setFocusStatus, Qt::UniqueConnection); connect(filterManager.data(), &Ekos::FilterManager::newFocusOffset, focusProcess.get(), &Ekos::Focus::adjustFocusOffset, Qt::UniqueConnection); connect(focusProcess.get(), &Ekos::Focus::focusPositionAdjusted, filterManager.data(), &Ekos::FilterManager::setFocusOffsetComplete, Qt::UniqueConnection); connect(focusProcess.get(), &Ekos::Focus::absolutePositionChanged, filterManager.data(), &Ekos::FilterManager::setFocusAbsolutePosition, Qt::UniqueConnection); if (Options::ekosLeftIcons()) { QTransform trans; trans.rotate(90); QIcon icon = toolsWidget->tabIcon(index); QPixmap pix = icon.pixmap(QSize(48, 48)); icon = QIcon(pix.transformed(trans)); toolsWidget->setTabIcon(index, icon); } focusGroup->setEnabled(true); if (!focusPI) { focusPI = new QProgressIndicator(focusProcess.get()); focusStatusLayout->insertWidget(0, focusPI); } connectModules(); emit newModule("Focus"); } void Manager::updateCurrentHFR(double newHFR, int position) { currentHFR->setText(QString("%1").arg(newHFR, 0, 'f', 2) + " px"); QJsonObject cStatus = { {"hfr", newHFR}, {"pos", position} }; ekosLiveClient.get()->message()->updateFocusStatus(cStatus); } void Manager::updateSigmas(double ra, double de) { errRA->setText(QString::number(ra, 'f', 2) + "\""); errDEC->setText(QString::number(de, 'f', 2) + "\""); QJsonObject cStatus = { {"rarms", ra}, {"derms", de} }; ekosLiveClient.get()->message()->updateGuideStatus(cStatus); } void Manager::initMount() { if (mountProcess.get() != nullptr) return; mountProcess.reset(new Ekos::Mount()); int index = toolsWidget->addTab(mountProcess.get(), QIcon(":/icons/ekos_mount.png"), ""); toolsWidget->tabBar()->setTabToolTip(index, i18n("Mount")); connect(mountProcess.get(), &Ekos::Mount::newLog, this, &Ekos::Manager::updateLog); connect(mountProcess.get(), &Ekos::Mount::newCoords, this, &Ekos::Manager::updateMountCoords); connect(mountProcess.get(), &Ekos::Mount::newStatus, this, &Ekos::Manager::updateMountStatus); connect(mountProcess.get(), &Ekos::Mount::newTarget, [&](const QString & target) { mountTarget->setText(target); ekosLiveClient.get()->message()->updateMountStatus(QJsonObject({{"target", target}})); }); connect(mountProcess.get(), &Ekos::Mount::slewRateChanged, [&](int slewRate) { QJsonObject status = { { "slewRate", slewRate} }; ekosLiveClient.get()->message()->updateMountStatus(status); } ); foreach (ISD::GDInterface * device, findDevices(KSTARS_AUXILIARY)) { if (device->getDriverInterface() & INDI::BaseDevice::GPS_INTERFACE) mountProcess->setGPS(device); } if (Options::ekosLeftIcons()) { QTransform trans; trans.rotate(90); QIcon icon = toolsWidget->tabIcon(index); QPixmap pix = icon.pixmap(QSize(48, 48)); icon = QIcon(pix.transformed(trans)); toolsWidget->setTabIcon(index, icon); } if (!mountPI) { mountPI = new QProgressIndicator(mountProcess.get()); mountStatusLayout->insertWidget(0, mountPI); } mountGroup->setEnabled(true); connectModules(); emit newModule("Mount"); } void Manager::initGuide() { if (guideProcess.get() == nullptr) { guideProcess.reset(new Ekos::Guide()); double primaryScopeFL = 0, primaryScopeAperture = 0, guideScopeFL = 0, guideScopeAperture = 0; getCurrentProfileTelescopeInfo(primaryScopeFL, primaryScopeAperture, guideScopeFL, guideScopeAperture); // Save telescope info in mount driver guideProcess->setTelescopeInfo(primaryScopeFL, primaryScopeAperture, guideScopeFL, guideScopeAperture); } //if ( (haveGuider || ccdCount > 1 || useGuideHead) && useST4 && toolsWidget->indexOf(guideProcess) == -1) if ((findDevices(KSTARS_CCD).isEmpty() == false || useGuideHead) && useST4 && toolsWidget->indexOf(guideProcess.get()) == -1) { //if (mount && mount->isConnected()) if (managedDevices.contains(KSTARS_TELESCOPE) && managedDevices[KSTARS_TELESCOPE]->isConnected()) guideProcess->setTelescope(managedDevices[KSTARS_TELESCOPE]); int index = toolsWidget->addTab(guideProcess.get(), QIcon(":/icons/ekos_guide.png"), ""); toolsWidget->tabBar()->setTabToolTip(index, i18n("Guide")); connect(guideProcess.get(), &Ekos::Guide::newLog, this, &Ekos::Manager::updateLog); connect(guideProcess.get(), &Ekos::Guide::driverTimedout, this, &Ekos::Manager::restartDriver); guideGroup->setEnabled(true); if (!guidePI) { guidePI = new QProgressIndicator(guideProcess.get()); guideStatusLayout->insertWidget(0, guidePI); } connect(guideProcess.get(), &Ekos::Guide::newStatus, this, &Ekos::Manager::updateGuideStatus); connect(guideProcess.get(), &Ekos::Guide::newStarPixmap, this, &Ekos::Manager::updateGuideStarPixmap); connect(guideProcess.get(), &Ekos::Guide::newProfilePixmap, this, &Ekos::Manager::updateGuideProfilePixmap); connect(guideProcess.get(), &Ekos::Guide::newAxisSigma, this, &Ekos::Manager::updateSigmas); if (Options::ekosLeftIcons()) { QTransform trans; trans.rotate(90); QIcon icon = toolsWidget->tabIcon(index); QPixmap pix = icon.pixmap(QSize(48, 48)); icon = QIcon(pix.transformed(trans)); toolsWidget->setTabIcon(index, icon); } } connectModules(); emit newModule("Guide"); } void Manager::initDome() { if (domeProcess.get() != nullptr) return; domeProcess.reset(new Ekos::Dome()); connect(domeProcess.get(), &Ekos::Dome::newStatus, [&](ISD::Dome::Status newStatus) { // For roll-off domes // cw ---> unparking // ccw --> parking if (domeProcess->isRolloffRoof() && (newStatus == ISD::Dome::DOME_MOVING_CW || newStatus == ISD::Dome::DOME_MOVING_CCW)) { newStatus = (newStatus == ISD::Dome::DOME_MOVING_CW) ? ISD::Dome::DOME_UNPARKING : ISD::Dome::DOME_PARKING; } QJsonObject status = { { "status", ISD::Dome::getStatusString(newStatus)} }; ekosLiveClient.get()->message()->updateDomeStatus(status); }); connect(domeProcess.get(), &Ekos::Dome::azimuthPositionChanged, [&](double pos) { QJsonObject status = { { "az", pos} }; ekosLiveClient.get()->message()->updateDomeStatus(status); }); initObservatory(nullptr, domeProcess.get()); emit newModule("Dome"); ekosLiveClient->message()->sendDomes(); } void Manager::initWeather() { if (weatherProcess.get() != nullptr) return; weatherProcess.reset(new Ekos::Weather()); initObservatory(weatherProcess.get(), nullptr); emit newModule("Weather"); } void Manager::initObservatory(Weather *weather, Dome *dome) { if (observatoryProcess.get() == nullptr) { // Initialize the Observatory Module observatoryProcess.reset(new Ekos::Observatory()); int index = toolsWidget->addTab(observatoryProcess.get(), QIcon(":/icons/ekos_observatory.png"), ""); toolsWidget->tabBar()->setTabToolTip(index, i18n("Observatory")); connect(observatoryProcess.get(), &Ekos::Observatory::newLog, this, &Ekos::Manager::updateLog); } Observatory *obs = observatoryProcess.get(); if (weather != nullptr) obs->getWeatherModel()->initModel(weather); if (dome != nullptr) obs->getDomeModel()->initModel(dome); emit newModule("Observatory"); } void Manager::initDustCap() { if (dustCapProcess.get() != nullptr) return; dustCapProcess.reset(new Ekos::DustCap()); connect(dustCapProcess.get(), &Ekos::DustCap::newStatus, [&](ISD::DustCap::Status newStatus) { QJsonObject status = { { "status", ISD::DustCap::getStatusString(newStatus)} }; ekosLiveClient.get()->message()->updateCapStatus(status); }); connect(dustCapProcess.get(), &Ekos::DustCap::lightToggled, [&](bool enabled) { QJsonObject status = { { "lightS", enabled} }; ekosLiveClient.get()->message()->updateCapStatus(status); }); connect(dustCapProcess.get(), &Ekos::DustCap::lightIntensityChanged, [&](uint16_t value) { QJsonObject status = { { "lightB", value} }; ekosLiveClient.get()->message()->updateCapStatus(status); }); emit newModule("DustCap"); ekosLiveClient->message()->sendCaps(); } void Manager::setST4(ISD::ST4 * st4Driver) { appendLogText(i18n("Guider port from %1 is ready.", st4Driver->getDeviceName())); useST4 = true; initGuide(); guideProcess->addST4(st4Driver); if (Options::defaultST4Driver().isEmpty() == false) guideProcess->setST4(Options::defaultST4Driver()); } void Manager::removeTabs() { disconnect(toolsWidget, &QTabWidget::currentChanged, this, &Ekos::Manager::processTabChange); for (int i = 2; i < toolsWidget->count(); i++) toolsWidget->removeTab(i); alignProcess.reset(); captureProcess.reset(); focusProcess.reset(); guideProcess.reset(); mountProcess.reset(); domeProcess.reset(); weatherProcess.reset(); observatoryProcess.reset(); dustCapProcess.reset(); managedDevices.clear(); connect(toolsWidget, &QTabWidget::currentChanged, this, &Ekos::Manager::processTabChange, Qt::UniqueConnection); } bool Manager::isRunning(const QString &process) { QProcess ps; #ifdef Q_OS_OSX ps.start("pgrep", QStringList() << process); ps.waitForFinished(); QString output = ps.readAllStandardOutput(); return output.length() > 0; #else ps.start("ps", QStringList() << "-o" << "comm" << "--no-headers" << "-C" << process); ps.waitForFinished(); QString output = ps.readAllStandardOutput(); return output.contains(process); #endif } void Manager::addObjectToScheduler(SkyObject * object) { if (schedulerProcess.get() != nullptr) schedulerProcess->addObject(object); } QString Manager::getCurrentJobName() { return schedulerProcess->getCurrentJobName(); } bool Manager::setProfile(const QString &profileName) { int index = profileCombo->findText(profileName); if (index < 0) return false; profileCombo->setCurrentIndex(index); return true; } void Manager::editNamedProfile(const QJsonObject &profileInfo) { ProfileEditor editor(this); setProfile(profileInfo["name"].toString()); currentProfile = getCurrentProfile(); editor.setPi(currentProfile); editor.setSettings(profileInfo); editor.saveProfile(); } void Manager::addNamedProfile(const QJsonObject &profileInfo) { ProfileEditor editor(this); editor.setSettings(profileInfo); editor.saveProfile(); profiles.clear(); loadProfiles(); profileCombo->setCurrentIndex(profileCombo->count() - 1); currentProfile = getCurrentProfile(); } void Manager::deleteNamedProfile(const QString &name) { currentProfile = getCurrentProfile(); for (auto &pi : profiles) { // Do not delete an actively running profile // Do not delete simulator profile if (pi->name == "Simulators" || pi->name != name || (pi.get() == currentProfile && ekosStatus() != Idle)) continue; KStarsData::Instance()->userdb()->DeleteProfile(pi.get()); profiles.clear(); loadProfiles(); currentProfile = getCurrentProfile(); return; } } QJsonObject Manager::getNamedProfile(const QString &name) { QJsonObject profileInfo; // Get current profile for (auto &pi : profiles) { if (name == pi->name) return pi->toJson(); } return QJsonObject(); } QStringList Manager::getProfiles() { QStringList profiles; for (int i = 0; i < profileCombo->count(); i++) profiles << profileCombo->itemText(i); return profiles; } void Manager::addProfile() { ProfileEditor editor(this); if (editor.exec() == QDialog::Accepted) { profiles.clear(); loadProfiles(); profileCombo->setCurrentIndex(profileCombo->count() - 1); } currentProfile = getCurrentProfile(); } void Manager::editProfile() { ProfileEditor editor(this); currentProfile = getCurrentProfile(); editor.setPi(currentProfile); if (editor.exec() == QDialog::Accepted) { int currentIndex = profileCombo->currentIndex(); profiles.clear(); loadProfiles(); profileCombo->setCurrentIndex(currentIndex); } currentProfile = getCurrentProfile(); } void Manager::deleteProfile() { currentProfile = getCurrentProfile(); if (currentProfile->name == "Simulators") return; auto executeDeleteProfile = [&]() { KStarsData::Instance()->userdb()->DeleteProfile(currentProfile); profiles.clear(); loadProfiles(); currentProfile = getCurrentProfile(); }; connect(KSMessageBox::Instance(), &KSMessageBox::accepted, this, [this, executeDeleteProfile]() { //QObject::disconnect(KSMessageBox::Instance(), &KSMessageBox::accepted, this, nullptr); KSMessageBox::Instance()->disconnect(this); executeDeleteProfile(); }); KSMessageBox::Instance()->questionYesNo(i18n("Are you sure you want to delete the profile?"), i18n("Confirm Delete")); } void Manager::wizardProfile() { ProfileWizard wz; if (wz.exec() != QDialog::Accepted) return; ProfileEditor editor(this); editor.setProfileName(wz.profileName); editor.setAuxDrivers(wz.selectedAuxDrivers()); if (wz.useInternalServer == false) editor.setHostPort(wz.host, wz.port); editor.setWebManager(wz.useWebManager); editor.setGuiderType(wz.selectedExternalGuider()); // Disable connection options editor.setConnectionOptionsEnabled(false); if (editor.exec() == QDialog::Accepted) { profiles.clear(); loadProfiles(); profileCombo->setCurrentIndex(profileCombo->count() - 1); } currentProfile = getCurrentProfile(); } ProfileInfo * Manager::getCurrentProfile() { ProfileInfo * currProfile = nullptr; // Get current profile for (auto &pi : profiles) { if (profileCombo->currentText() == pi->name) { currProfile = pi.get(); break; } } return currProfile; } void Manager::updateProfileLocation(ProfileInfo * pi) { if (pi->city.isEmpty() == false) { bool cityFound = KStars::Instance()->setGeoLocation(pi->city, pi->province, pi->country); if (cityFound) appendLogText(i18n("Site location updated to %1.", KStarsData::Instance()->geo()->fullName())); else appendLogText(i18n("Failed to update site location to %1. City not found.", KStarsData::Instance()->geo()->fullName())); } } void Manager::updateMountStatus(ISD::Telescope::Status status) { static ISD::Telescope::Status lastStatus = ISD::Telescope::MOUNT_IDLE; if (status == lastStatus) return; lastStatus = status; mountStatus->setText(dynamic_cast(managedDevices[KSTARS_TELESCOPE])->getStatusString(status)); mountStatus->setStyleSheet(QString()); switch (status) { case ISD::Telescope::MOUNT_PARKING: case ISD::Telescope::MOUNT_SLEWING: case ISD::Telescope::MOUNT_MOVING: mountPI->setColor(QColor(KStarsData::Instance()->colorScheme()->colorNamed("TargetColor"))); if (mountPI->isAnimated() == false) mountPI->startAnimation(); break; case ISD::Telescope::MOUNT_TRACKING: mountPI->setColor(Qt::darkGreen); if (mountPI->isAnimated() == false) mountPI->startAnimation(); break; case ISD::Telescope::MOUNT_PARKED: mountStatus->setStyleSheet("font-weight:bold;background-color:red;border:2px solid black;"); if (mountPI->isAnimated()) mountPI->stopAnimation(); break; default: if (mountPI->isAnimated()) mountPI->stopAnimation(); } QJsonObject cStatus = { {"status", mountStatus->text()} }; ekosLiveClient.get()->message()->updateMountStatus(cStatus); } void Manager::updateMountCoords(const QString &ra, const QString &dec, const QString &az, const QString &alt) { raOUT->setText(ra); decOUT->setText(dec); azOUT->setText(az); altOUT->setText(alt); QJsonObject cStatus = { {"ra", dms::fromString(ra, false).Degrees()}, {"de", dms::fromString(dec, true).Degrees()}, {"az", dms::fromString(az, true).Degrees()}, {"at", dms::fromString(alt, true).Degrees()}, }; ekosLiveClient.get()->message()->updateMountStatus(cStatus); } void Manager::updateCaptureStatus(Ekos::CaptureState status) { captureStatus->setText(Ekos::getCaptureStatusString(status)); captureProgress->setValue(captureProcess->getProgressPercentage()); overallCountDown.setHMS(0, 0, 0); overallCountDown = overallCountDown.addSecs(captureProcess->getOverallRemainingTime()); sequenceCountDown.setHMS(0, 0, 0); sequenceCountDown = sequenceCountDown.addSecs(captureProcess->getActiveJobRemainingTime()); if (status != Ekos::CAPTURE_ABORTED && status != Ekos::CAPTURE_COMPLETE && status != Ekos::CAPTURE_IDLE) { if (status == Ekos::CAPTURE_CAPTURING) capturePI->setColor(Qt::darkGreen); else capturePI->setColor(QColor(KStarsData::Instance()->colorScheme()->colorNamed("TargetColor"))); if (capturePI->isAnimated() == false) { capturePI->startAnimation(); countdownTimer.start(); } } else { if (capturePI->isAnimated()) { capturePI->stopAnimation(); countdownTimer.stop(); if (focusStatus->text() == "Complete") { if (focusPI->isAnimated()) focusPI->stopAnimation(); } imageProgress->setValue(0); sequenceLabel->setText(i18n("Sequence")); imageRemainingTime->setText("--:--:--"); overallRemainingTime->setText("--:--:--"); sequenceRemainingTime->setText("--:--:--"); } } QJsonObject cStatus = { {"status", captureStatus->text()}, {"seqt", sequenceRemainingTime->text()}, {"ovt", overallRemainingTime->text()} }; ekosLiveClient.get()->message()->updateCaptureStatus(cStatus); } void Manager::updateCaptureProgress(Ekos::SequenceJob * job) { // Image is set to nullptr only on initial capture start up int completed = job->getCompleted(); // if (job->getUploadMode() == ISD::CCD::UPLOAD_LOCAL) // completed = job->getCompleted() + 1; // else // completed = job->isPreview() ? job->getCompleted() : job->getCompleted() + 1; if (job->isPreview() == false) { sequenceLabel->setText(QString("Job # %1/%2 %3 (%4/%5)") .arg(captureProcess->getActiveJobID() + 1) .arg(captureProcess->getJobCount()) .arg(job->getFullPrefix()) .arg(completed) .arg(job->getCount())); } else sequenceLabel->setText(i18n("Preview")); sequenceProgress->setRange(0, job->getCount()); sequenceProgress->setValue(completed); QJsonObject status = { {"seqv", completed}, {"seqr", job->getCount()}, {"seql", sequenceLabel->text()} }; ekosLiveClient.get()->message()->updateCaptureStatus(status); if (job->getStatus() == SequenceJob::JOB_BUSY) { QString uuid = QUuid::createUuid().toString(); uuid = uuid.remove(QRegularExpression("[-{}]")); // FITSView *image = job->getActiveChip()->getImageView(FITS_NORMAL); // ekosLiveClient.get()->media()->sendPreviewImage(image, uuid); // ekosLiveClient.get()->cloud()->sendPreviewImage(image, uuid); QString filename = job->property("filename").toString(); ekosLiveClient.get()->media()->sendPreviewImage(filename, uuid); if (job->isPreview() == false) ekosLiveClient.get()->cloud()->sendPreviewImage(filename, uuid); } } void Manager::updateDownloadProgress(double timeLeft) { imageCountDown.setHMS(0, 0, 0); imageCountDown = imageCountDown.addSecs(timeLeft); imageRemainingTime->setText(imageCountDown.toString("hh:mm:ss")); } void Manager::updateExposureProgress(Ekos::SequenceJob * job) { imageCountDown.setHMS(0, 0, 0); imageCountDown = imageCountDown.addSecs(job->getExposeLeft() + captureProcess->getEstimatedDownloadTime()); if (imageCountDown.hour() == 23) imageCountDown.setHMS(0, 0, 0); imageProgress->setRange(0, job->getExposure()); imageProgress->setValue(job->getExposeLeft()); imageRemainingTime->setText(imageCountDown.toString("hh:mm:ss")); QJsonObject status { {"expv", job->getExposeLeft()}, {"expr", job->getExposure()} }; ekosLiveClient.get()->message()->updateCaptureStatus(status); } void Manager::updateCaptureCountDown() { overallCountDown = overallCountDown.addSecs(-1); if (overallCountDown.hour() == 23) overallCountDown.setHMS(0, 0, 0); sequenceCountDown = sequenceCountDown.addSecs(-1); if (sequenceCountDown.hour() == 23) sequenceCountDown.setHMS(0, 0, 0); overallRemainingTime->setText(overallCountDown.toString("hh:mm:ss")); sequenceRemainingTime->setText(sequenceCountDown.toString("hh:mm:ss")); QJsonObject status = { {"seqt", sequenceRemainingTime->text()}, {"ovt", overallRemainingTime->text()} }; ekosLiveClient.get()->message()->updateCaptureStatus(status); } void Manager::updateFocusStarPixmap(QPixmap &starPixmap) { if (starPixmap.isNull()) return; focusStarPixmap.reset(new QPixmap(starPixmap)); focusStarImage->setPixmap(focusStarPixmap->scaled(focusStarImage->width(), focusStarImage->height(), Qt::KeepAspectRatio, Qt::SmoothTransformation)); } void Manager::updateFocusProfilePixmap(QPixmap &profilePixmap) { if (profilePixmap.isNull()) return; focusProfileImage->setPixmap(profilePixmap); } void Manager::setFocusStatus(Ekos::FocusState status) { focusStatus->setText(Ekos::getFocusStatusString(status)); if (status >= Ekos::FOCUS_PROGRESS) { focusPI->setColor(QColor(KStarsData::Instance()->colorScheme()->colorNamed("TargetColor"))); if (focusPI->isAnimated() == false) focusPI->startAnimation(); } else if (status == Ekos::FOCUS_COMPLETE && Options::enforceAutofocus() && captureProcess->getActiveJobID() != -1) { focusPI->setColor(Qt::darkGreen); if (focusPI->isAnimated() == false) focusPI->startAnimation(); } else { if (focusPI->isAnimated()) focusPI->stopAnimation(); } QJsonObject cStatus = { {"status", focusStatus->text()} }; ekosLiveClient.get()->message()->updateFocusStatus(cStatus); } void Manager::updateGuideStatus(Ekos::GuideState status) { guideStatus->setText(Ekos::getGuideStatusString(status)); switch (status) { case Ekos::GUIDE_IDLE: case Ekos::GUIDE_CALIBRATION_ERROR: case Ekos::GUIDE_ABORTED: case Ekos::GUIDE_SUSPENDED: case Ekos::GUIDE_DITHERING_ERROR: case Ekos::GUIDE_CALIBRATION_SUCESS: if (guidePI->isAnimated()) guidePI->stopAnimation(); break; case Ekos::GUIDE_CALIBRATING: guidePI->setColor(QColor(KStarsData::Instance()->colorScheme()->colorNamed("TargetColor"))); if (guidePI->isAnimated() == false) guidePI->startAnimation(); break; case Ekos::GUIDE_GUIDING: guidePI->setColor(Qt::darkGreen); if (guidePI->isAnimated() == false) guidePI->startAnimation(); break; case Ekos::GUIDE_DITHERING: guidePI->setColor(QColor(KStarsData::Instance()->colorScheme()->colorNamed("TargetColor"))); if (guidePI->isAnimated() == false) guidePI->startAnimation(); break; case Ekos::GUIDE_DITHERING_SUCCESS: guidePI->setColor(Qt::darkGreen); if (guidePI->isAnimated() == false) guidePI->startAnimation(); break; default: if (guidePI->isAnimated()) guidePI->stopAnimation(); break; } QJsonObject cStatus = { {"status", guideStatus->text()} }; ekosLiveClient.get()->message()->updateGuideStatus(cStatus); } void Manager::updateGuideStarPixmap(QPixmap &starPix) { if (starPix.isNull()) return; guideStarPixmap.reset(new QPixmap(starPix)); guideStarImage->setPixmap(guideStarPixmap->scaled(guideStarImage->width(), guideStarImage->height(), Qt::KeepAspectRatio, Qt::SmoothTransformation)); } void Manager::updateGuideProfilePixmap(QPixmap &profilePix) { if (profilePix.isNull()) return; guideProfileImage->setPixmap(profilePix); } void Manager::setTarget(SkyObject * o) { mountTarget->setText(o->name()); ekosLiveClient.get()->message()->updateMountStatus(QJsonObject({{"target", o->name()}})); } void Manager::showEkosOptions() { QWidget * currentWidget = toolsWidget->currentWidget(); if (alignProcess.get() && alignProcess.get() == currentWidget) { KConfigDialog * alignSettings = KConfigDialog::exists("alignsettings"); if (alignSettings) { alignSettings->setEnabled(true); alignSettings->show(); } return; } if (guideProcess.get() && guideProcess.get() == currentWidget) { KConfigDialog::showDialog("guidesettings"); return; } if (ekosOptionsWidget == nullptr) { optionsB->click(); } else if (KConfigDialog::showDialog("settings")) { KConfigDialog * cDialog = KConfigDialog::exists("settings"); cDialog->setCurrentPage(ekosOptionsWidget); } } void Manager::getCurrentProfileTelescopeInfo(double &primaryFocalLength, double &primaryAperture, double &guideFocalLength, double &guideAperture) { ProfileInfo * pi = getCurrentProfile(); if (pi) { int primaryScopeID = 0, guideScopeID = 0; primaryScopeID = pi->primaryscope; guideScopeID = pi->guidescope; if (primaryScopeID > 0 || guideScopeID > 0) { // Get all OAL equipment filter list QList m_scopeList; KStarsData::Instance()->userdb()->GetAllScopes(m_scopeList); for(auto oneScope : m_scopeList) { if (oneScope->id().toInt() == primaryScopeID) { primaryFocalLength = oneScope->focalLength(); primaryAperture = oneScope->aperture(); } if (oneScope->id().toInt() == guideScopeID) { guideFocalLength = oneScope->focalLength(); guideAperture = oneScope->aperture(); } } qDeleteAll(m_scopeList); } } } void Manager::updateDebugInterfaces() { KSUtils::Logging::SyncFilterRules(); for (ISD::GDInterface * device : genericDevices) { INDI::Property * debugProp = device->getProperty("DEBUG"); ISwitchVectorProperty * debugSP = nullptr; if (debugProp) debugSP = debugProp->getSwitch(); else continue; // Check if the debug interface matches the driver device class if ( ( opsLogs->getINDIDebugInterface() & device->getDriverInterface() ) && debugSP->sp[0].s != ISS_ON) { debugSP->sp[0].s = ISS_ON; debugSP->sp[1].s = ISS_OFF; device->getDriverInfo()->getClientManager()->sendNewSwitch(debugSP); appendLogText(i18n("Enabling debug logging for %1...", device->getDeviceName())); } else if ( !( opsLogs->getINDIDebugInterface() & device->getDriverInterface() ) && debugSP->sp[0].s != ISS_OFF) { debugSP->sp[0].s = ISS_OFF; debugSP->sp[1].s = ISS_ON; device->getDriverInfo()->getClientManager()->sendNewSwitch(debugSP); appendLogText(i18n("Disabling debug logging for %1...", device->getDeviceName())); } if (opsLogs->isINDISettingsChanged()) device->setConfig(SAVE_CONFIG); } } void Manager::watchDebugProperty(ISwitchVectorProperty * svp) { if (!strcmp(svp->name, "DEBUG")) { ISD::GenericDevice * deviceInterface = qobject_cast(sender()); // We don't process pure general interfaces if (deviceInterface->getDriverInterface() == INDI::BaseDevice::GENERAL_INTERFACE) return; // If debug was turned off, but our logging policy requires it then turn it back on. // We turn on debug logging if AT LEAST one driver interface is selected by the logging settings if (svp->s == IPS_OK && svp->sp[0].s == ISS_OFF && (opsLogs->getINDIDebugInterface() & deviceInterface->getDriverInterface())) { svp->sp[0].s = ISS_ON; svp->sp[1].s = ISS_OFF; deviceInterface->getDriverInfo()->getClientManager()->sendNewSwitch(svp); appendLogText(i18n("Re-enabling debug logging for %1...", deviceInterface->getDeviceName())); } // To turn off debug logging, NONE of the driver interfaces should be enabled in logging settings. // For example, if we have CCD+FilterWheel device and CCD + Filter Wheel logging was turned on in // the log settings, then if the user turns off only CCD logging, the debug logging is NOT // turned off until he turns off Filter Wheel logging as well. else if (svp->s == IPS_OK && svp->sp[0].s == ISS_ON && !(opsLogs->getINDIDebugInterface() & deviceInterface->getDriverInterface())) { svp->sp[0].s = ISS_OFF; svp->sp[1].s = ISS_ON; deviceInterface->getDriverInfo()->getClientManager()->sendNewSwitch(svp); appendLogText(i18n("Re-disabling debug logging for %1...", deviceInterface->getDeviceName())); } } } void Manager::announceEvent(const QString &message, KSNotification::EventType event) { ekosLiveClient.get()->message()->sendEvent(message, event); } void Manager::connectModules() { // Guide <---> Capture connections if (captureProcess.get() && guideProcess.get()) { captureProcess.get()->disconnect(guideProcess.get()); guideProcess.get()->disconnect(captureProcess.get()); // Guide Limits connect(guideProcess.get(), &Ekos::Guide::newStatus, captureProcess.get(), &Ekos::Capture::setGuideStatus, Qt::UniqueConnection); connect(guideProcess.get(), &Ekos::Guide::newAxisDelta, captureProcess.get(), &Ekos::Capture::setGuideDeviation); // Dithering connect(captureProcess.get(), &Ekos::Capture::newStatus, guideProcess.get(), &Ekos::Guide::setCaptureStatus, Qt::UniqueConnection); // Guide Head connect(captureProcess.get(), &Ekos::Capture::suspendGuiding, guideProcess.get(), &Ekos::Guide::suspend, Qt::UniqueConnection); connect(captureProcess.get(), &Ekos::Capture::resumeGuiding, guideProcess.get(), &Ekos::Guide::resume, Qt::UniqueConnection); connect(guideProcess.get(), &Ekos::Guide::guideChipUpdated, captureProcess.get(), &Ekos::Capture::setGuideChip, Qt::UniqueConnection); // Meridian Flip connect(captureProcess.get(), &Ekos::Capture::meridianFlipStarted, guideProcess.get(), &Ekos::Guide::abort, Qt::UniqueConnection); connect(captureProcess.get(), &Ekos::Capture::meridianFlipCompleted, guideProcess.get(), &Ekos::Guide::guideAfterMeridianFlip, Qt::UniqueConnection); } // Guide <---> Mount connections if (guideProcess.get() && mountProcess.get()) { // Parking connect(mountProcess.get(), &Ekos::Mount::newStatus, guideProcess.get(), &Ekos::Guide::setMountStatus, Qt::UniqueConnection); } // Focus <---> Guide connections if (guideProcess.get() && focusProcess.get()) { // Suspend connect(focusProcess.get(), &Ekos::Focus::suspendGuiding, guideProcess.get(), &Ekos::Guide::suspend, Qt::UniqueConnection); connect(focusProcess.get(), &Ekos::Focus::resumeGuiding, guideProcess.get(), &Ekos::Guide::resume, Qt::UniqueConnection); } // Capture <---> Focus connections if (captureProcess.get() && focusProcess.get()) { // Check focus HFR value connect(captureProcess.get(), &Ekos::Capture::checkFocus, focusProcess.get(), &Ekos::Focus::checkFocus, Qt::UniqueConnection); // Reset Focus connect(captureProcess.get(), &Ekos::Capture::resetFocus, focusProcess.get(), &Ekos::Focus::resetFrame, Qt::UniqueConnection); // New Focus Status connect(focusProcess.get(), &Ekos::Focus::newStatus, captureProcess.get(), &Ekos::Capture::setFocusStatus, Qt::UniqueConnection); // New Focus HFR connect(focusProcess.get(), &Ekos::Focus::newHFR, captureProcess.get(), &Ekos::Capture::setHFR, Qt::UniqueConnection); } // Capture <---> Align connections if (captureProcess.get() && alignProcess.get()) { // Alignment flag connect(alignProcess.get(), &Ekos::Align::newStatus, captureProcess.get(), &Ekos::Capture::setAlignStatus, Qt::UniqueConnection); // Solver data connect(alignProcess.get(), &Ekos::Align::newSolverResults, captureProcess.get(), &Ekos::Capture::setAlignResults, Qt::UniqueConnection); // Capture Status connect(captureProcess.get(), &Ekos::Capture::newStatus, alignProcess.get(), &Ekos::Align::setCaptureStatus, Qt::UniqueConnection); } // Capture <---> Mount connections if (captureProcess.get() && mountProcess.get()) { // Meridian Flip states connect(captureProcess.get(), &Ekos::Capture::meridianFlipStarted, mountProcess.get(), &Ekos::Mount::disableAltLimits, Qt::UniqueConnection); connect(captureProcess.get(), &Ekos::Capture::meridianFlipCompleted, mountProcess.get(), &Ekos::Mount::enableAltLimits, Qt::UniqueConnection); connect(captureProcess.get(), &Ekos::Capture::newMeridianFlipStatus, mountProcess.get(), &Ekos::Mount::meridianFlipStatusChanged, Qt::UniqueConnection); connect(mountProcess.get(), &Ekos::Mount::newMeridianFlipStatus, captureProcess.get(), &Ekos::Capture::meridianFlipStatusChanged, Qt::UniqueConnection); // Mount Status connect(mountProcess.get(), &Ekos::Mount::newStatus, captureProcess.get(), &Ekos::Capture::setMountStatus, Qt::UniqueConnection); } // Capture <---> EkosLive connections if (captureProcess.get() && ekosLiveClient.get()) { captureProcess.get()->disconnect(ekosLiveClient.get()->message()); connect(captureProcess.get(), &Ekos::Capture::dslrInfoRequested, ekosLiveClient.get()->message(), &EkosLive::Message::requestDSLRInfo); connect(captureProcess.get(), &Ekos::Capture::sequenceChanged, ekosLiveClient.get()->message(), &EkosLive::Message::sendCaptureSequence); connect(captureProcess.get(), &Ekos::Capture::settingsUpdated, ekosLiveClient.get()->message(), &EkosLive::Message::sendCaptureSettings); } // Focus <---> Align connections if (focusProcess.get() && alignProcess.get()) { connect(focusProcess.get(), &Ekos::Focus::newStatus, alignProcess.get(), &Ekos::Align::setFocusStatus, Qt::UniqueConnection); } // Focus <---> Mount connections if (focusProcess.get() && mountProcess.get()) { connect(mountProcess.get(), &Ekos::Mount::newStatus, focusProcess.get(), &Ekos::Focus::setMountStatus, Qt::UniqueConnection); } // Mount <---> Align connections if (mountProcess.get() && alignProcess.get()) { connect(mountProcess.get(), &Ekos::Mount::newStatus, alignProcess.get(), &Ekos::Align::setMountStatus, Qt::UniqueConnection); } // Mount <---> Guide connections if (mountProcess.get() && guideProcess.get()) { connect(mountProcess.get(), &Ekos::Mount::pierSideChanged, guideProcess.get(), &Ekos::Guide::setPierSide, Qt::UniqueConnection); } // Focus <---> Align connections if (focusProcess.get() && alignProcess.get()) { connect(focusProcess.get(), &Ekos::Focus::newStatus, alignProcess.get(), &Ekos::Align::setFocusStatus, Qt::UniqueConnection); } // Align <--> EkosLive connections if (alignProcess.get() && ekosLiveClient.get()) { alignProcess.get()->disconnect(ekosLiveClient.get()->message()); alignProcess.get()->disconnect(ekosLiveClient.get()->media()); connect(alignProcess.get(), &Ekos::Align::newStatus, ekosLiveClient.get()->message(), &EkosLive::Message::setAlignStatus); connect(alignProcess.get(), &Ekos::Align::newSolution, ekosLiveClient.get()->message(), &EkosLive::Message::setAlignSolution); connect(alignProcess.get(), &Ekos::Align::newPAHStage, ekosLiveClient.get()->message(), &EkosLive::Message::setPAHStage); connect(alignProcess.get(), &Ekos::Align::newPAHMessage, ekosLiveClient.get()->message(), &EkosLive::Message::setPAHMessage); connect(alignProcess.get(), &Ekos::Align::PAHEnabled, ekosLiveClient.get()->message(), &EkosLive::Message::setPAHEnabled); connect(alignProcess.get(), &Ekos::Align::newImage, [&](FITSView * view) { ekosLiveClient.get()->media()->sendPreviewImage(view, QString()); }); connect(alignProcess.get(), &Ekos::Align::newFrame, ekosLiveClient.get()->media(), &EkosLive::Media::sendUpdatedFrame); connect(alignProcess.get(), &Ekos::Align::polarResultUpdated, ekosLiveClient.get()->message(), &EkosLive::Message::setPolarResults); connect(alignProcess.get(), &Ekos::Align::settingsUpdated, ekosLiveClient.get()->message(), &EkosLive::Message::sendAlignSettings); connect(alignProcess.get(), &Ekos::Align::newCorrectionVector, ekosLiveClient.get()->media(), &EkosLive::Media::setCorrectionVector); } } void Manager::setEkosLiveConnected(bool enabled) { ekosLiveClient.get()->setConnected(enabled); } void Manager::setEkosLiveConfig(bool onlineService, bool rememberCredentials, bool autoConnect) { ekosLiveClient.get()->setConfig(onlineService, rememberCredentials, autoConnect); } void Manager::setEkosLiveUser(const QString &username, const QString &password) { ekosLiveClient.get()->setUser(username, password); } bool Manager::ekosLiveStatus() { return ekosLiveClient.get()->isConnected(); } void Manager::syncActiveDevices() { for (auto oneDevice : genericDevices) { // Find out what ACTIVE_DEVICES properties this driver needs // and update it from the existing drivers. ITextVectorProperty *tvp = oneDevice->getBaseDevice()->getText("ACTIVE_DEVICES"); if (tvp) { //bool propertyUpdated = false; for (int i = 0; i < tvp->ntp; i++) { QList devs; if (!strcmp(tvp->tp[i].name, "ACTIVE_TELESCOPE")) { devs = findDevicesByInterface(INDI::BaseDevice::TELESCOPE_INTERFACE); } else if (!strcmp(tvp->tp[i].name, "ACTIVE_DOME")) { devs = findDevicesByInterface(INDI::BaseDevice::DOME_INTERFACE); } else if (!strcmp(tvp->tp[i].name, "ACTIVE_GPS")) { devs = findDevicesByInterface(INDI::BaseDevice::GPS_INTERFACE); } else if (!strcmp(tvp->tp[i].name, "ACTIVE_FILTER")) { if (tvp->tp[i].aux0 != nullptr) { bool *override = static_cast(tvp->tp[i].aux0); if (override && *override) continue; } devs = findDevicesByInterface(INDI::BaseDevice::FILTER_INTERFACE); } else if (!strcmp(tvp->tp[i].name, "ACTIVE_WEATHER")) { devs = findDevicesByInterface(INDI::BaseDevice::WEATHER_INTERFACE); } if (!devs.empty()) { if (tvp->tp[i].text != devs.first()->getDeviceName()) { //propertyUpdated = true; IUSaveText(&tvp->tp[i], devs.first()->getDeviceName().toLatin1().constData()); oneDevice->getDriverInfo()->getClientManager()->sendNewText(tvp); } } } // Save configuration // if (propertyUpdated) // oneDevice->setConfig(SAVE_CONFIG); } } } bool Manager::checkUniqueBinaryDriver(DriverInfo *primaryDriver, DriverInfo *secondaryDriver) { if (!primaryDriver || !secondaryDriver) return false; return (primaryDriver->getExecutable() == secondaryDriver->getExecutable() && primaryDriver->getAuxInfo().value("mdpd", false).toBool() == true); } void Manager::restartDriver(const QString &deviceName) { if (m_LocalMode) { for (auto &device : managedDevices) { if (QString(device->getDeviceName()) == deviceName) { DriverManager::Instance()->restartDriver(device->getDriverInfo()); break; } } } else INDI::WebManager::restartDriver(currentProfile, deviceName); } } diff --git a/kstars/indi/guimanager.cpp b/kstars/indi/guimanager.cpp index 8ad2424ae..b85ad286f 100644 --- a/kstars/indi/guimanager.cpp +++ b/kstars/indi/guimanager.cpp @@ -1,289 +1,282 @@ /* INDI frontend for KStars 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 "guimanager.h" #include "clientmanager.h" #include "deviceinfo.h" #include "indidevice.h" #include "kstars.h" #include "Options.h" #include "fitsviewer/fitsviewer.h" #include "ksnotification.h" #include #include #include #include #include #include #include #include extern const char *libindi_strings_context; GUIManager *GUIManager::_GUIManager = nullptr; GUIManager *GUIManager::Instance() { if (_GUIManager == nullptr) _GUIManager = new GUIManager(Options::independentWindowINDI() ? nullptr : KStars::Instance()); return _GUIManager; } void GUIManager::release() { delete _GUIManager; } GUIManager::GUIManager(QWidget *parent) : QWidget(parent, Qt::Window) { #ifdef Q_OS_OSX if (Options::independentWindowINDI()) setWindowFlags(Qt::Window); else { setWindowFlags(Qt::Window | Qt::WindowStaysOnTopHint); connect(QApplication::instance(), SIGNAL(applicationStateChanged(Qt::ApplicationState)), this, SLOT(changeAlwaysOnTop(Qt::ApplicationState))); } #endif mainLayout = new QVBoxLayout(this); mainLayout->setContentsMargins(10, 10, 10, 10); mainLayout->setSpacing(10); mainTabWidget = new QTabWidget(this); mainLayout->addWidget(mainTabWidget); setWindowIcon(QIcon::fromTheme("kstars_indi")); setWindowTitle(i18n("INDI Control Panel")); setAttribute(Qt::WA_ShowModal, false); clearB = new QPushButton(i18n("Clear")); closeB = new QPushButton(i18n("Close")); QHBoxLayout *buttonLayout = new QHBoxLayout; buttonLayout->insertStretch(0); buttonLayout->addWidget(clearB, 0, Qt::AlignRight); buttonLayout->addWidget(closeB, 0, Qt::AlignRight); mainLayout->addLayout(buttonLayout); connect(closeB, SIGNAL(clicked()), this, SLOT(close())); connect(clearB, SIGNAL(clicked()), this, SLOT(clearLog())); resize(Options::iNDIWindowWidth(), Options::iNDIWindowHeight()); } void GUIManager::changeAlwaysOnTop(Qt::ApplicationState state) { if (isVisible()) { if (state == Qt::ApplicationActive) setWindowFlags(Qt::Window | Qt::WindowStaysOnTopHint); else setWindowFlags(windowFlags() & ~Qt::WindowStaysOnTopHint); show(); } } void GUIManager::closeEvent(QCloseEvent * /*event*/) { KStars *ks = KStars::Instance(); if (ks) { QAction *showINDIPanel = KStars::Instance()->actionCollection()->action("show_control_panel"); showINDIPanel->setChecked(false); } Options::setINDIWindowWidth(width()); Options::setINDIWindowHeight(height()); } void GUIManager::hideEvent(QHideEvent * /*event*/) { KStars *ks = KStars::Instance(); if (ks) { QAction *a = KStars::Instance()->actionCollection()->action("show_control_panel"); a->setChecked(false); } } void GUIManager::showEvent(QShowEvent * /*event*/) { QAction *a = KStars::Instance()->actionCollection()->action("show_control_panel"); a->setEnabled(true); a->setChecked(true); } /********************************************************************* ** Traverse the drivers list, check for updated drivers and take ** appropriate action *********************************************************************/ void GUIManager::updateStatus(bool toggle_behavior) { QAction *showINDIPanel = KStars::Instance()->actionCollection()->action("show_control_panel"); if (guidevices.count() == 0) { KSNotification::error(i18n("No INDI devices currently running. To run devices, please select devices from the " "Device Manager in the devices menu.")); showINDIPanel->setChecked(false); showINDIPanel->setEnabled(false); return; } showINDIPanel->setChecked(true); if (isVisible() && isActiveWindow() && toggle_behavior) { hide(); } else { raise(); activateWindow(); showNormal(); } } INDI_D *GUIManager::findGUIDevice(const QString &deviceName) { for (auto oneGUIDevice : guidevices) { if (oneGUIDevice->name() == deviceName) return oneGUIDevice; } return nullptr; } void GUIManager::clearLog() { INDI_D *dev = findGUIDevice(mainTabWidget->tabText(mainTabWidget->currentIndex()).remove(QChar('&'))); if (dev) dev->clearMessageLog(); } void GUIManager::addClient(ClientManager *cm) { clients.append(cm); - - Qt::ConnectionType type = Qt::QueuedConnection; - -#ifdef USE_QT5_INDI - type = Qt::DirectConnection; -#endif - - connect(cm, SIGNAL(newINDIDevice(DeviceInfo*)), this, SLOT(buildDevice(DeviceInfo*)), type); + connect(cm, &ClientManager::newINDIDevice, this, &GUIManager::buildDevice, Qt::BlockingQueuedConnection); connect(cm, &ClientManager::removeINDIDevice, this, &GUIManager::removeDevice); } void GUIManager::removeClient(ClientManager *cm) { clients.removeOne(cm); foreach (INDI_D *gdv, guidevices) { if (gdv->getClientManager() == cm) { for (int i = 0; i < mainTabWidget->count(); i++) { if (mainTabWidget->tabText(i).remove('&') == QString(gdv->getBaseDevice()->getDeviceName())) { mainTabWidget->removeTab(i); break; } } guidevices.removeOne(gdv); gdv->deleteLater(); //break; } } if (clients.size() == 0) hide(); } void GUIManager::removeDevice(const QString &name) { INDI_D *dp = findGUIDevice(name); if (dp == nullptr) return; // Hack to give mainTabWidget sometime to remove its item as these calls are coming from a different thread // the clientmanager thread. Sometimes removeTab() requires sometime to be removed properly and hence the wait. if (mainTabWidget->count() != guidevices.count()) QThread::msleep(100); for (int i = 0; i < mainTabWidget->count(); i++) { if (mainTabWidget->tabText(i).remove('&') == name) { mainTabWidget->removeTab(i); break; } } guidevices.removeOne(dp); delete (dp); if (guidevices.isEmpty()) { QAction *showINDIPanel = KStars::Instance()->actionCollection()->action("show_control_panel"); showINDIPanel->setEnabled(false); } } void GUIManager::buildDevice(DeviceInfo *di) { //qDebug() << "In build Device for device with tree label " << di->getTreeLabel() << endl; ClientManager *cm = di->getDriverInfo()->getClientManager(); if (cm == nullptr) { qCritical() << "ClientManager is null in build device!" << endl; return; } INDI_D *gdm = new INDI_D(di->getBaseDevice(), cm); connect(cm, &ClientManager::newINDIProperty, gdm, &INDI_D::buildProperty); connect(cm, &ClientManager::removeINDIProperty, gdm, &INDI_D::removeProperty, Qt::QueuedConnection); // connect(cm, &ClientManager::removeINDIProperty, [gdm](const QString & device, const QString & name) // { // if (device == gdm->name()) // QMetaObject::invokeMethod(gdm, "removeProperty", Qt::QueuedConnection, Q_ARG(QString, name)); // }); connect(cm, &ClientManager::newINDISwitch, gdm, &INDI_D::updateSwitchGUI); connect(cm, &ClientManager::newINDIText, gdm, &INDI_D::updateTextGUI); connect(cm, &ClientManager::newINDINumber, gdm, &INDI_D::updateNumberGUI); connect(cm, &ClientManager::newINDILight, gdm, &INDI_D::updateLightGUI); connect(cm, &ClientManager::newINDIBLOB, gdm, &INDI_D::updateBLOBGUI); connect(cm, &ClientManager::newINDIMessage, gdm, &INDI_D::updateMessageLog); mainTabWidget->addTab(gdm->getDeviceBox(), di->getBaseDevice()->getDeviceName()); guidevices.append(gdm); updateStatus(false); } diff --git a/kstars/indi/indilistener.cpp b/kstars/indi/indilistener.cpp index b52b6fe46..bf9965d92 100644 --- a/kstars/indi/indilistener.cpp +++ b/kstars/indi/indilistener.cpp @@ -1,472 +1,472 @@ /* INDI Listener Copyright (C) 2012 Jasem Mutlaq (mutlaqja@ikarustech.com) This application is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version. Handle INDI Standard properties. */ #include "indilistener.h" #include "clientmanager.h" #include "deviceinfo.h" #include "indicap.h" #include "indiccd.h" #include "indidome.h" #include "indifilter.h" #include "indifocuser.h" #include "indilightbox.h" #include "inditelescope.h" #include "indiweather.h" #include "kstars.h" #include "Options.h" #include "auxiliary/ksnotification.h" #include #include #include #define NINDI_STD 35 /* INDI standard property used across all clients to enable interoperability. */ static const char *indi_std[NINDI_STD] = { "CONNECTION", "DEVICE_PORT", "TIME_UTC", "TIME_LST", "GEOGRAPHIC_COORD", "EQUATORIAL_COORD", "EQUATORIAL_EOD_COORD", "EQUATORIAL_EOD_COORD_REQUEST", "HORIZONTAL_COORD", "TELESCOPE_ABORT_MOTION", "ON_COORD_SET", "SOLAR_SYSTEM", "TELESCOPE_MOTION_NS", "TELESCOPE_MOTION_WE", "TELESCOPE_PARK", "DOME_PARK", "GPS_REFRESH", "WEATHER_STATUS", "CCD_EXPOSURE", "CCD_TEMPERATURE", "CCD_FRAME", "CCD_FRAME_TYPE", "CCD_BINNING", "CCD_INFO", "CCD_VIDEO_STREAM", "RAW_STREAM", "IMAGE_STREAM", "FOCUS_SPEED", "FOCUS_MOTION", "FOCUS_TIMER", "FILTER_SLOT", "WATCHDOG_HEARTBEAT", "CAP_PARK", "FLAT_LIGHT_CONTROL", "FLAT_LIGHT_INTENSITY" }; INDIListener *INDIListener::_INDIListener = nullptr; INDIListener *INDIListener::Instance() { if (_INDIListener == nullptr) { _INDIListener = new INDIListener(KStars::Instance()); connect(_INDIListener, &INDIListener::newTelescope, [&]() { KStars::Instance()->slotSetTelescopeEnabled(true); }); connect(_INDIListener, &INDIListener::newDome, [&]() { KStars::Instance()->slotSetDomeEnabled(true); }); } return _INDIListener; } INDIListener::INDIListener(QObject *parent) : QObject(parent) { } INDIListener::~INDIListener() { qDeleteAll(devices); qDeleteAll(st4Devices); } bool INDIListener::isStandardProperty(const QString &name) { for (auto &item : indi_std) { if (!strcmp(name.toLatin1().constData(), item)) return true; } return false; } ISD::GDInterface *INDIListener::getDevice(const QString &name) { for (auto &oneDevice : devices) { if (oneDevice->getDeviceName() == name) return oneDevice; } return nullptr; } void INDIListener::addClient(ClientManager *cm) { qCDebug(KSTARS_INDI) << "INDIListener: Adding a new client manager to INDI listener.."; clients.append(cm); connect(cm, &ClientManager::newINDIDevice, this, &INDIListener::processDevice, Qt::BlockingQueuedConnection); connect(cm, &ClientManager::newINDIProperty, this, &INDIListener::registerProperty); - connect(cm, &ClientManager::removeINDIDevice, this, &INDIListener::removeDevice, Qt::DirectConnection); - connect(cm, &ClientManager::removeINDIProperty, this, &INDIListener::removeProperty, Qt::DirectConnection); + connect(cm, &ClientManager::removeINDIDevice, this, &INDIListener::removeDevice); + connect(cm, &ClientManager::removeINDIProperty, this, &INDIListener::removeProperty); connect(cm, &ClientManager::newINDISwitch, this, &INDIListener::processSwitch); connect(cm, &ClientManager::newINDIText, this, &INDIListener::processText); connect(cm, &ClientManager::newINDINumber, this, &INDIListener::processNumber); connect(cm, &ClientManager::newINDILight, this, &INDIListener::processLight); connect(cm, &ClientManager::newINDIBLOB, this, &INDIListener::processBLOB); #if INDI_VERSION_MAJOR >= 1 && INDI_VERSION_MINOR >= 5 connect(cm, &ClientManager::newINDIUniversalMessage, this, &INDIListener::processUniversalMessage); #endif } void INDIListener::removeClient(ClientManager *cm) { qCDebug(KSTARS_INDI) << "INDIListener: Removing client manager for server" << cm->getHost() << "@" << cm->getPort(); QList::iterator it = devices.begin(); clients.removeOne(cm); while (it != devices.end()) { DriverInfo *dv = (*it)->getDriverInfo(); bool hostSource = (dv->getDriverSource() == HOST_SOURCE) || (dv->getDriverSource() == GENERATED_SOURCE); if (cm->isDriverManaged(dv)) { // If we have multiple devices per driver, we need to remove them all if (dv->getAuxInfo().value("mdpd", false).toBool() == true) { while (it != devices.end()) { if (dv->getDevice((*it)->getDeviceName()) != nullptr) { it = devices.erase(it); } else break; } } else it = devices.erase(it); cm->removeManagedDriver(dv); cm->disconnect(this); if (hostSource) return; } else ++it; } } void INDIListener::processDevice(DeviceInfo *dv) { qCDebug(KSTARS_INDI) << "INDIListener: New device" << dv->getBaseDevice()->getDeviceName(); ISD::GDInterface *gd = new ISD::GenericDevice(*dv); devices.append(gd); emit newDevice(gd); } //void INDIListener::removeDevice(DeviceInfo *dv) //{ // qCDebug(KSTARS_INDI) << "INDIListener: Removing device" << dv->getBaseDevice()->getDeviceName() << "with unique label " // << dv->getDriverInfo()->getUniqueLabel(); // foreach (ISD::GDInterface *gd, devices) // { // if (gd->getDeviceInfo() == dv) // { // emit deviceRemoved(gd); // devices.removeOne(gd); // delete (gd); // } // } //} void INDIListener::removeDevice(const QString &deviceName) { qCDebug(KSTARS_INDI) << "INDIListener: Removing device" << deviceName; for (ISD::GDInterface *oneDevice : devices) { if (oneDevice->getDeviceName() == deviceName) { emit deviceRemoved(oneDevice); devices.removeOne(oneDevice); delete (oneDevice); break; } } } void INDIListener::registerProperty(INDI::Property *prop) { if (!prop->getRegistered()) return; qCDebug(KSTARS_INDI) << "<" << prop->getDeviceName() << ">: <" << prop->getName() << ">"; for (auto oneDevice : devices) { if (oneDevice->getDeviceName() == prop->getDeviceName()) { if (!strcmp(prop->getName(), "ON_COORD_SET") || !strcmp(prop->getName(), "EQUATORIAL_EOD_COORD") || !strcmp(prop->getName(), "EQUATORIAL_COORD") || !strcmp(prop->getName(), "HORIZONTAL_COORD")) { if (oneDevice->getType() == KSTARS_UNKNOWN) { devices.removeOne(oneDevice); oneDevice = new ISD::Telescope(oneDevice); devices.append(oneDevice); } emit newTelescope(oneDevice); } else if (!strcmp(prop->getName(), "CCD_EXPOSURE")) { //if (gd->getType() != KSTARS_CCD) if (oneDevice->getDriverInterface() & INDI::BaseDevice::CCD_INTERFACE) { devices.removeOne(oneDevice); oneDevice = new ISD::CCD(oneDevice); devices.append(oneDevice); } emit newCCD(oneDevice); } else if (!strcmp(prop->getName(), "FILTER_NAME")) { if (oneDevice->getType() == KSTARS_UNKNOWN) { devices.removeOne(oneDevice); oneDevice = new ISD::Filter(oneDevice); devices.append(oneDevice); } emit newFilter(oneDevice); } else if (!strcmp(prop->getName(), "FOCUS_MOTION")) { if (oneDevice->getType() == KSTARS_UNKNOWN) { devices.removeOne(oneDevice); oneDevice = new ISD::Focuser(oneDevice); devices.append(oneDevice); } emit newFocuser(oneDevice); } else if (!strcmp(prop->getName(), "DOME_SHUTTER") || !strcmp(prop->getName(), "DOME_MOTION")) { if (oneDevice->getType() == KSTARS_UNKNOWN) { devices.removeOne(oneDevice); oneDevice = new ISD::Dome(oneDevice); devices.append(oneDevice); } emit newDome(oneDevice); } else if (!strcmp(prop->getName(), "WEATHER_STATUS")) { if (oneDevice->getType() == KSTARS_UNKNOWN) { devices.removeOne(oneDevice); oneDevice = new ISD::Weather(oneDevice); devices.append(oneDevice); } emit newWeather(oneDevice); } else if (!strcmp(prop->getName(), "CAP_PARK")) { if (oneDevice->getType() == KSTARS_UNKNOWN) { devices.removeOne(oneDevice); oneDevice = new ISD::DustCap(oneDevice); devices.append(oneDevice); } emit newDustCap(oneDevice); } else if (!strcmp(prop->getName(), "FLAT_LIGHT_CONTROL")) { // If light box part of dust cap if (oneDevice->getType() == KSTARS_UNKNOWN) { if (oneDevice->getBaseDevice()->getDriverInterface() & INDI::BaseDevice::DUSTCAP_INTERFACE) { devices.removeOne(oneDevice); oneDevice = new ISD::DustCap(oneDevice); devices.append(oneDevice); emit newDustCap(oneDevice); } // If stand-alone light box else { devices.removeOne(oneDevice); oneDevice = new ISD::LightBox(oneDevice); devices.append(oneDevice); emit newLightBox(oneDevice); } } } if (!strcmp(prop->getName(), "TELESCOPE_TIMED_GUIDE_WE")) { ISD::ST4 *st4Driver = new ISD::ST4(oneDevice->getBaseDevice(), oneDevice->getDriverInfo()->getClientManager()); st4Devices.append(st4Driver); emit newST4(st4Driver); } oneDevice->registerProperty(prop); break; } } } void INDIListener::removeProperty(const QString &device, const QString &name) { for (auto &oneDevice : devices) { if (oneDevice->getDeviceName() == device) { oneDevice->removeProperty(name); return; } } } void INDIListener::processSwitch(ISwitchVectorProperty *svp) { for (auto &oneDevice : devices) { if (oneDevice->getDeviceName() == svp->device) { oneDevice->processSwitch(svp); break; } } } void INDIListener::processNumber(INumberVectorProperty *nvp) { for (auto &oneDevice : devices) { if (oneDevice->getDeviceName() == nvp->device) { oneDevice->processNumber(nvp); break; } } } void INDIListener::processText(ITextVectorProperty *tvp) { for (auto &oneDevice : devices) { if (oneDevice->getDeviceName() == tvp->device) { oneDevice->processText(tvp); break; } } } void INDIListener::processLight(ILightVectorProperty *lvp) { for (auto &oneDevice : devices) { if (oneDevice->getDeviceName() == lvp->device) { oneDevice->processLight(lvp); break; } } } void INDIListener::processBLOB(IBLOB *bp) { for (auto &oneDevice : devices) { if (oneDevice->getDeviceName() == bp->bvp->device) { oneDevice->processBLOB(bp); break; } } } void INDIListener::processMessage(INDI::BaseDevice *dp, int messageID) { for (auto &oneDevice : devices) { if (oneDevice->getDeviceName() == dp->getDeviceName()) { oneDevice->processMessage(messageID); break; } } } void INDIListener::processUniversalMessage(const QString &message) { QString displayMessage = message; // Remove timestamp info as it is not suitable for message box int colonIndex = displayMessage.indexOf(": "); if (colonIndex > 0) displayMessage = displayMessage.mid(colonIndex + 2); // Special case for Alignment since it is not tied to a device if (displayMessage.startsWith("[ALIGNMENT]")) { qCDebug(KSTARS_INDI) << "AlignmentSubSystem:" << displayMessage; return; } if (Options::messageNotificationINDI()) { KNotification::event(QLatin1String("IndiServerMessage"), displayMessage + " (INDI)"); } else { KSNotification::transient(displayMessage, i18n("INDI Server Message")); } } diff --git a/kstars/indi/indistd.cpp b/kstars/indi/indistd.cpp index da9a2eb9b..2569946c6 100644 --- a/kstars/indi/indistd.cpp +++ b/kstars/indi/indistd.cpp @@ -1,1363 +1,1357 @@ /* INDI STD Copyright (C) 2012 Jasem Mutlaq (mutlaqja@ikarustech.com) This application is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version. Handle INDI Standard properties. */ #include "indistd.h" #include "clientmanager.h" #include "driverinfo.h" #include "deviceinfo.h" #include "imageviewer.h" #include "indi_debug.h" #include "kstars.h" #include "kstarsdata.h" #include "Options.h" #include "skymap.h" #include #include namespace ISD { GDSetCommand::GDSetCommand(INDI_PROPERTY_TYPE inPropertyType, const QString &inProperty, const QString &inElement, QVariant qValue, QObject *parent) : QObject(parent) { propType = inPropertyType; indiProperty = inProperty; indiElement = inElement; elementValue = qValue; } GenericDevice::GenericDevice(DeviceInfo &idv) { deviceInfo = &idv; driverInfo = idv.getDriverInfo(); baseDevice = idv.getBaseDevice(); m_Name = baseDevice->getDeviceName(); clientManager = driverInfo->getClientManager(); dType = KSTARS_UNKNOWN; registerDBusType(); } void GenericDevice::registerDBusType() { #ifndef KSTARS_LITE static bool isRegistered = false; if (isRegistered == false) { qRegisterMetaType("ISD::ParkStatus"); qDBusRegisterMetaType(); isRegistered = true; } #endif } const QString &GenericDevice::getDeviceName() const { return m_Name; } void GenericDevice::registerProperty(INDI::Property *prop) { if (!prop->getRegistered()) return; const QString name = prop->getName(); if (properties.contains(name)) return; properties[name] = prop; emit propertyDefined(prop); // In case driver already started if (name == "CONNECTION") { ISwitchVectorProperty *svp = prop->getSwitch(); if (svp == nullptr) return; // Still connecting/disconnecting... if (svp->s == IPS_BUSY) return; ISwitch *conSP = IUFindSwitch(svp, "CONNECT"); if (conSP == nullptr) return; if (svp->s == IPS_OK && conSP->s == ISS_ON) { connected = true; emit Connected(); createDeviceInit(); } } else if (name == "DRIVER_INFO") { ITextVectorProperty *tvp = prop->getText(); if (tvp) { IText *tp = IUFindText(tvp, "DRIVER_INTERFACE"); if (tp) { driverInterface = static_cast(atoi(tp->text)); emit interfaceDefined(); } tp = IUFindText(tvp, "DRIVER_VERSION"); if (tp) { driverVersion = QString(tp->text); } } } else if (name == "SYSTEM_PORTS") { // Check if our current port is set to one of the system ports. This indicates that the port // is not mapped yet to a permenant designation ISwitchVectorProperty *svp = prop->getSwitch(); ITextVectorProperty *port = baseDevice->getText("DEVICE_PORT"); if (svp && port) { for (int i = 0; i < svp->nsp; i++) { if (!strcmp(port->tp[0].text, svp->sp[i].name)) { emit systemPortDetected(); break; } } } } else if (name == "TIME_UTC" && Options::useTimeUpdate() && Options::useKStarsSource()) { ITextVectorProperty *tvp = prop->getText(); if (tvp && tvp->p != IP_RO) updateTime(); } else if (name == "GEOGRAPHIC_COORD" && Options::useGeographicUpdate() && Options::useKStarsSource()) { INumberVectorProperty *nvp = prop->getNumber(); if (nvp && nvp->p != IP_RO) updateLocation(); } else if (name == "WATCHDOG_HEARTBEAT") { INumberVectorProperty *nvp = prop->getNumber(); if (nvp) { if (watchDogTimer == nullptr) { watchDogTimer = new QTimer(this); connect(watchDogTimer, SIGNAL(timeout()), this, SLOT(resetWatchdog())); } if (connected && nvp->np[0].value > 0) { // Send immediately a heart beat clientManager->sendNewNumber(nvp); //watchDogTimer->start(0); } } } } void GenericDevice::removeProperty(const QString &name) { properties.remove(name); emit propertyDeleted(name); } void GenericDevice::processSwitch(ISwitchVectorProperty *svp) { if (!strcmp(svp->name, "CONNECTION")) { ISwitch *conSP = IUFindSwitch(svp, "CONNECT"); if (conSP == nullptr) return; // Still connecting/disconnecting... if (svp->s == IPS_BUSY) return; if (svp->s == IPS_OK && conSP->s == ISS_ON) { connected = true; emit Connected(); createDeviceInit(); if (watchDogTimer != nullptr) { INumberVectorProperty *nvp = baseDevice->getNumber("WATCHDOG_HEARTBEAT"); if (nvp && nvp->np[0].value > 0) { // Send immediately clientManager->sendNewNumber(nvp); //watchDogTimer->start(0); } } } else { connected = false; emit Disconnected(); } } emit switchUpdated(svp); } void GenericDevice::processNumber(INumberVectorProperty *nvp) { QString deviceName = getDeviceName(); // uint32_t interface = getDriverInterface(); // Q_UNUSED(interface); if (!strcmp(nvp->name, "GEOGRAPHIC_COORD") && nvp->s == IPS_OK && ( (Options::useMountSource() && (getDriverInterface() & INDI::BaseDevice::TELESCOPE_INTERFACE)) || (Options::useGPSSource() && (getDriverInterface() & INDI::BaseDevice::GPS_INTERFACE)))) { // Update KStars Location once we receive update from INDI, if the source is set to DEVICE dms lng, lat; double elev = 0; INumber *np = IUFindNumber(nvp, "LONG"); if (!np) return; // INDI Longitude convention is 0 to 360. We need to turn it back into 0 to 180 EAST, 0 to -180 WEST if (np->value < 180) lng.setD(np->value); else lng.setD(np->value - 360.0); np = IUFindNumber(nvp, "LAT"); if (!np) return; lat.setD(np->value); // Double check we have valid values if (lng.Degrees() == 0 && lat.Degrees() == 0) { qCWarning(KSTARS_INDI) << "Ignoring invalid device coordinates."; return; } np = IUFindNumber(nvp, "ELEV"); if (np) elev = np->value; GeoLocation *geo = KStars::Instance()->data()->geo(); std::unique_ptr tempGeo; QString newLocationName; if (getDriverInterface() & INDI::BaseDevice::GPS_INTERFACE) newLocationName = i18n("GPS Location"); else newLocationName = i18n("Mount Location"); if (geo->name() != newLocationName) { double TZ0 = geo->TZ0(); TimeZoneRule *rule = geo->tzrule(); tempGeo.reset(new GeoLocation(lng, lat, newLocationName, "", "", TZ0, rule, elev)); geo = tempGeo.get(); } else { geo->setLong(lng); geo->setLat(lat); } qCInfo(KSTARS_INDI) << "Setting location from device:" << deviceName << "Longitude:" << lng.toDMSString() << "Latitude:" << lat.toDMSString(); KStars::Instance()->data()->setLocation(*geo); } else if (!strcmp(nvp->name, "WATCHDOG_HEARTBEAT")) { if (watchDogTimer == nullptr) { watchDogTimer = new QTimer(this); connect(watchDogTimer, SIGNAL(timeout()), this, SLOT(resetWatchdog())); } if (connected && nvp->np[0].value > 0) { // Reset timer 15 seconds before it is due watchDogTimer->start(nvp->np[0].value * 60 * 1000 - 15 * 1000); } else if (nvp->np[0].value == 0) watchDogTimer->stop(); } emit numberUpdated(nvp); } void GenericDevice::processText(ITextVectorProperty *tvp) { // Update KStars time once we receive update from INDI, if the source is set to DEVICE if (!strcmp(tvp->name, "TIME_UTC") && tvp->s == IPS_OK && ( (Options::useMountSource() && (getDriverInterface() & INDI::BaseDevice::TELESCOPE_INTERFACE)) || (Options::useGPSSource() && (getDriverInterface() & INDI::BaseDevice::GPS_INTERFACE)))) { int d, m, y, min, sec, hour; float utcOffset; QDate indiDate; QTime indiTime; IText *tp = IUFindText(tvp, "UTC"); if (!tp) { qCWarning(KSTARS_INDI) << "UTC property missing from TIME_UTC"; return; } sscanf(tp->text, "%d%*[^0-9]%d%*[^0-9]%dT%d%*[^0-9]%d%*[^0-9]%d", &y, &m, &d, &hour, &min, &sec); indiDate.setDate(y, m, d); indiTime.setHMS(hour, min, sec); KStarsDateTime indiDateTime(QDateTime(indiDate, indiTime, Qt::UTC)); tp = IUFindText(tvp, "OFFSET"); if (!tp) { qCWarning(KSTARS_INDI) << "Offset property missing from TIME_UTC"; return; } sscanf(tp->text, "%f", &utcOffset); qCInfo(KSTARS_INDI) << "Setting UTC time from device:" << getDeviceName() << indiDateTime.toString(); KStars::Instance()->data()->changeDateTime(indiDateTime); KStars::Instance()->data()->syncLST(); GeoLocation *geo = KStars::Instance()->data()->geo(); if (geo->tzrule()) utcOffset -= geo->tzrule()->deltaTZ(); // TZ0 is the timezone WTIHOUT any DST offsets. Above, we take INDI UTC Offset (with DST already included) // and subtract from it the deltaTZ from the current TZ rule. geo->setTZ0(utcOffset); } emit textUpdated(tvp); } void GenericDevice::processLight(ILightVectorProperty *lvp) { emit lightUpdated(lvp); } void GenericDevice::processMessage(int messageID) { emit messageUpdated(messageID); } void GenericDevice::processBLOB(IBLOB *bp) { // Ignore write-only BLOBs since we only receive it for state-change if (bp->bvp->p == IP_WO) return; QFile *data_file = nullptr; INDIDataTypes dataType; if (!strcmp(bp->format, ".ascii")) dataType = DATA_ASCII; else dataType = DATA_OTHER; QString currentDir = Options::fitsDir(); int nr, n = 0; if (currentDir.endsWith('/')) currentDir.truncate(sizeof(currentDir) - 1); QString filename(currentDir + '/'); QString ts = QDateTime::currentDateTime().toString("yyyy-MM-ddThh-mm-ss"); filename += QString("%1_").arg(bp->label) + ts + QString(bp->format).trimmed(); strncpy(BLOBFilename, filename.toLatin1(), MAXINDIFILENAME); bp->aux2 = BLOBFilename; if (dataType == DATA_ASCII) { if (bp->aux0 == nullptr) { bp->aux0 = new int(); QFile *ascii_data_file = new QFile(); ascii_data_file->setFileName(filename); if (!ascii_data_file->open(QIODevice::WriteOnly)) { qCCritical(KSTARS_INDI) << "GenericDevice Error: Unable to open " << ascii_data_file->fileName() << endl; return; } bp->aux1 = ascii_data_file; } data_file = (QFile *)bp->aux1; QDataStream out(data_file); for (nr = 0; nr < (int)bp->size; nr += n) n = out.writeRawData(static_cast(bp->blob) + nr, bp->size - nr); out.writeRawData((const char *)"\n", 1); data_file->flush(); } else { QFile fits_temp_file(filename); if (!fits_temp_file.open(QIODevice::WriteOnly)) { qCCritical(KSTARS_INDI) << "GenericDevice Error: Unable to open " << fits_temp_file.fileName() << endl; return; } QDataStream out(&fits_temp_file); for (nr = 0; nr < (int)bp->size; nr += n) n = out.writeRawData(static_cast(bp->blob) + nr, bp->size - nr); fits_temp_file.flush(); fits_temp_file.close(); QByteArray fmt = QString(bp->format).toLower().remove('.').toUtf8(); if (QImageReader::supportedImageFormats().contains(fmt)) { QUrl url(filename); url.setScheme("file"); ImageViewer *iv = new ImageViewer(url, QString(), KStars::Instance()); if (iv) iv->show(); } } if (dataType == DATA_OTHER) KStars::Instance()->statusBar()->showMessage(i18n("Data file saved to %1", filename), 0); emit BLOBUpdated(bp); } bool GenericDevice::setConfig(INDIConfig tConfig) { ISwitchVectorProperty *svp = baseDevice->getSwitch("CONFIG_PROCESS"); if (svp == nullptr) return false; ISwitch *sp = nullptr; IUResetSwitch(svp); switch (tConfig) { case LOAD_LAST_CONFIG: sp = IUFindSwitch(svp, "CONFIG_LOAD"); if (sp == nullptr) return false; IUResetSwitch(svp); sp->s = ISS_ON; break; case SAVE_CONFIG: sp = IUFindSwitch(svp, "CONFIG_SAVE"); if (sp == nullptr) return false; IUResetSwitch(svp); sp->s = ISS_ON; break; case LOAD_DEFAULT_CONFIG: sp = IUFindSwitch(svp, "CONFIG_DEFAULT"); if (sp == nullptr) return false; IUResetSwitch(svp); sp->s = ISS_ON; break; case PURGE_CONFIG: sp = IUFindSwitch(svp, "CONFIG_PURGE"); if (sp == nullptr) return false; IUResetSwitch(svp); sp->s = ISS_ON; break; } clientManager->sendNewSwitch(svp); return true; } void GenericDevice::createDeviceInit() { if (Options::showINDIMessages()) - KStars::Instance()->statusBar()->showMessage(i18n("%1 is online.", baseDevice->getDeviceName()), 0); + KStars::Instance()->statusBar()->showMessage(i18n("%1 is online.", m_Name), 0); KStars::Instance()->map()->forceUpdateNow(); } /*********************************************************************************/ /* Update the Driver's Time */ /*********************************************************************************/ void GenericDevice::updateTime() { QString offset, isoTS; offset = QString().setNum(KStars::Instance()->data()->geo()->TZ(), 'g', 2); //QTime newTime( KStars::Instance()->data()->ut().time()); //QDate newDate( KStars::Instance()->data()->ut().date()); //isoTS = QString("%1-%2-%3T%4:%5:%6").arg(newDate.year()).arg(newDate.month()).arg(newDate.day()).arg(newTime.hour()).arg(newTime.minute()).arg(newTime.second()); isoTS = KStars::Instance()->data()->ut().toString(Qt::ISODate).remove('Z'); /* Update Date/Time */ ITextVectorProperty *timeUTC = baseDevice->getText("TIME_UTC"); if (timeUTC) { IText *timeEle = IUFindText(timeUTC, "UTC"); if (timeEle) IUSaveText(timeEle, isoTS.toLatin1().constData()); IText *offsetEle = IUFindText(timeUTC, "OFFSET"); if (offsetEle) IUSaveText(offsetEle, offset.toLatin1().constData()); if (timeEle && offsetEle) clientManager->sendNewText(timeUTC); } } /*********************************************************************************/ /* Update the Driver's Geographical Location */ /*********************************************************************************/ void GenericDevice::updateLocation() { GeoLocation *geo = KStars::Instance()->data()->geo(); double longNP; if (geo->lng()->Degrees() >= 0) longNP = geo->lng()->Degrees(); else longNP = dms(geo->lng()->Degrees() + 360.0).Degrees(); INumberVectorProperty *nvp = baseDevice->getNumber("GEOGRAPHIC_COORD"); if (nvp == nullptr) return; INumber *np = IUFindNumber(nvp, "LONG"); if (np == nullptr) return; np->value = longNP; np = IUFindNumber(nvp, "LAT"); if (np == nullptr) return; np->value = geo->lat()->Degrees(); np = IUFindNumber(nvp, "ELEV"); if (np == nullptr) return; np->value = geo->elevation(); clientManager->sendNewNumber(nvp); } bool GenericDevice::Connect() { return runCommand(INDI_CONNECT, nullptr); } bool GenericDevice::Disconnect() { return runCommand(INDI_DISCONNECT, nullptr); } bool GenericDevice::runCommand(int command, void *ptr) { switch (command) { case INDI_CONNECT: - if (baseDevice->getDeviceName()) - clientManager->connectDevice(baseDevice->getDeviceName()); + clientManager->connectDevice(m_Name.toLatin1().constData()); break; case INDI_DISCONNECT: - if (baseDevice->getDeviceName()) - clientManager->disconnectDevice(baseDevice->getDeviceName()); + clientManager->disconnectDevice(m_Name.toLatin1().constData()); break; case INDI_SET_PORT: { if (ptr == nullptr) return false; ITextVectorProperty *tvp = baseDevice->getText("DEVICE_PORT"); if (tvp == nullptr) return false; IText *tp = IUFindText(tvp, "PORT"); IUSaveText(tp, (static_cast(ptr))->toLatin1().constData()); clientManager->sendNewText(tvp); } break; // We do it here because it could be either CCD or FILTER interfaces, so no need to duplicate code case INDI_SET_FILTER: { if (ptr == nullptr) return false; INumberVectorProperty *nvp = baseDevice->getNumber("FILTER_SLOT"); if (nvp == nullptr) return false; - int requestedFilter = *((int *)ptr); + int requestedFilter = *(static_cast(ptr)); if (requestedFilter == nvp->np[0].value) break; nvp->np[0].value = requestedFilter; clientManager->sendNewNumber(nvp); } break; case INDI_CONFIRM_FILTER: { ISwitchVectorProperty *svp = baseDevice->getSwitch("CONFIRM_FILTER_SET"); if (svp == nullptr) return false; svp->sp[0].s = ISS_ON; clientManager->sendNewSwitch(svp); } break; // We do it here because it could be either FOCUSER or ROTATOR interfaces, so no need to duplicate code case INDI_SET_ROTATOR_ANGLE: { if (ptr == nullptr) return false; INumberVectorProperty *nvp = baseDevice->getNumber("ABS_ROTATOR_ANGLE"); if (nvp == nullptr) return false; - double requestedAngle = *((double *)ptr); + double requestedAngle = *(static_cast(ptr)); if (requestedAngle == nvp->np[0].value) break; nvp->np[0].value = requestedAngle; clientManager->sendNewNumber(nvp); } break; // We do it here because it could be either FOCUSER or ROTATOR interfaces, so no need to duplicate code case INDI_SET_ROTATOR_TICKS: { if (ptr == nullptr) return false; INumberVectorProperty *nvp = baseDevice->getNumber("ABS_ROTATOR_POSITION"); if (nvp == nullptr) return false; - int32_t requestedTicks = *((int32_t *)ptr); + int32_t requestedTicks = *(static_cast(ptr)); if (requestedTicks == nvp->np[0].value) break; nvp->np[0].value = requestedTicks; clientManager->sendNewNumber(nvp); } break; } return true; } bool GenericDevice::setProperty(QObject *setPropCommand) { GDSetCommand *indiCommand = static_cast(setPropCommand); //qDebug() << "We are trying to set value for property " << indiCommand->indiProperty << " and element" << indiCommand->indiElement << " and value " << indiCommand->elementValue << endl; INDI::Property *pp = baseDevice->getProperty(indiCommand->indiProperty.toLatin1().constData()); if (pp == nullptr) return false; switch (indiCommand->propType) { case INDI_SWITCH: { ISwitchVectorProperty *svp = pp->getSwitch(); if (svp == nullptr) return false; ISwitch *sp = IUFindSwitch(svp, indiCommand->indiElement.toLatin1().constData()); if (sp == nullptr) return false; if (svp->r == ISR_1OFMANY || svp->r == ISR_ATMOST1) IUResetSwitch(svp); sp->s = indiCommand->elementValue.toInt() == 0 ? ISS_OFF : ISS_ON; //qDebug() << "Sending switch " << sp->name << " with status " << ((sp->s == ISS_ON) ? "On" : "Off") << endl; clientManager->sendNewSwitch(svp); return true; } case INDI_NUMBER: { INumberVectorProperty *nvp = pp->getNumber(); if (nvp == nullptr) return false; INumber *np = IUFindNumber(nvp, indiCommand->indiElement.toLatin1().constData()); if (np == nullptr) return false; double value = indiCommand->elementValue.toDouble(); if (value == np->value) return true; np->value = value; //qDebug() << "Sending switch " << sp->name << " with status " << ((sp->s == ISS_ON) ? "On" : "Off") << endl; clientManager->sendNewNumber(nvp); } break; // TODO: Add set property for other types of properties default: break; } return true; } bool GenericDevice::getMinMaxStep(const QString &propName, const QString &elementName, double *min, double *max, double *step) { INumberVectorProperty *nvp = baseDevice->getNumber(propName.toLatin1()); if (nvp == nullptr) return false; INumber *np = IUFindNumber(nvp, elementName.toLatin1()); if (np == nullptr) return false; *min = np->min; *max = np->max; *step = np->step; return true; } IPState GenericDevice::getState(const QString &propName) { return baseDevice->getPropertyState(propName.toLatin1().constData()); } IPerm GenericDevice::getPermission(const QString &propName) { return baseDevice->getPropertyPermission(propName.toLatin1().constData()); } INDI::Property *GenericDevice::getProperty(const QString &propName) { for (auto &oneProp : properties) { if (propName == QString(oneProp->getName())) return oneProp; } return nullptr; } bool GenericDevice::setJSONProperty(const QString &propName, const QJsonArray &propValue) { for (auto &oneProp : properties) { if (propName == QString(oneProp->getName())) { switch (oneProp->getType()) { case INDI_SWITCH: { ISwitchVectorProperty *svp = oneProp->getSwitch(); if (svp->r == ISR_1OFMANY || svp->r == ISR_ATMOST1) IUResetSwitch(svp); for (auto oneElement : propValue) { QJsonObject oneElementObject = oneElement.toObject(); ISwitch *sp = IUFindSwitch(svp, oneElementObject["name"].toString().toLatin1().constData()); if (sp) { sp->s = static_cast(oneElementObject["state"].toInt()); } } clientManager->sendNewSwitch(svp); return true; } case INDI_NUMBER: { INumberVectorProperty *nvp = oneProp->getNumber(); for (auto oneElement : propValue) { QJsonObject oneElementObject = oneElement.toObject(); INumber *np = IUFindNumber(nvp, oneElementObject["name"].toString().toLatin1().constData()); if (np) np->value = oneElementObject["value"].toDouble(); } clientManager->sendNewNumber(nvp); return true; } case INDI_TEXT: { ITextVectorProperty *tvp = oneProp->getText(); for (auto oneElement : propValue) { QJsonObject oneElementObject = oneElement.toObject(); IText *tp = IUFindText(tvp, oneElementObject["name"].toString().toLatin1().constData()); if (tp) IUSaveText(tp, oneElementObject["text"].toString().toLatin1().constData()); } clientManager->sendNewText(tvp); return true; } case INDI_BLOB: // TODO break; default: break; } } } return false; } bool GenericDevice::getJSONProperty(const QString &propName, QJsonObject &propObject, bool compact) { for (auto &oneProp : properties) { if (propName == QString(oneProp->getName())) { switch (oneProp->getType()) { case INDI_SWITCH: { ISwitchVectorProperty *svp = oneProp->getSwitch(); propertyToJson(svp, propObject, compact); return true; } case INDI_NUMBER: { INumberVectorProperty *nvp = oneProp->getNumber(); propertyToJson(nvp, propObject, compact); return true; } case INDI_TEXT: { ITextVectorProperty *tvp = oneProp->getText(); propertyToJson(tvp, propObject, compact); return true; } case INDI_LIGHT: { ILightVectorProperty *lvp = oneProp->getLight(); propertyToJson(lvp, propObject, compact); return true; } case INDI_BLOB: // TODO break; default: break; } } } return false; } bool GenericDevice::getJSONBLOB(const QString &propName, const QString &elementName, QJsonObject &blobObject) { auto prop = std::find_if(properties.begin(), properties.end(), [propName](INDI::Property * oneProperty) { return (QString(oneProperty->getName()) == propName); }); if (prop == properties.end()) return false; auto blobProperty = (*prop)->getBLOB(); IBLOB *oneBLOB = IUFindBLOB(blobProperty, elementName.toLatin1().constData()); if (!oneBLOB) return false; // Now convert to base64 and send back. QByteArray data = QByteArray::fromRawData(static_cast(oneBLOB->blob), oneBLOB->bloblen); QString encoded = data.toBase64(QByteArray::Base64UrlEncoding); blobObject.insert("property", propName); blobObject.insert("element", elementName); blobObject.insert("size", encoded.size()); blobObject.insert("data", encoded); return true; } void GenericDevice::resetWatchdog() { INumberVectorProperty *nvp = baseDevice->getNumber("WATCHDOG_HEARTBEAT"); if (nvp) // Send heartbeat to driver clientManager->sendNewNumber(nvp); } DeviceDecorator::DeviceDecorator(GDInterface * iPtr) { interfacePtr = iPtr; baseDevice = interfacePtr->getBaseDevice(); clientManager = interfacePtr->getDriverInfo()->getClientManager(); connect(iPtr, &GDInterface::Connected, this, &DeviceDecorator::Connected); connect(iPtr, &GDInterface::Disconnected, this, &DeviceDecorator::Disconnected); connect(iPtr, &GDInterface::propertyDefined, this, &DeviceDecorator::propertyDefined); connect(iPtr, &GDInterface::propertyDeleted, this, &DeviceDecorator::propertyDeleted); connect(iPtr, &GDInterface::messageUpdated, this, &DeviceDecorator::messageUpdated); connect(iPtr, &GDInterface::switchUpdated, this, &DeviceDecorator::switchUpdated); connect(iPtr, &GDInterface::numberUpdated, this, &DeviceDecorator::numberUpdated); connect(iPtr, &GDInterface::textUpdated, this, &DeviceDecorator::textUpdated); connect(iPtr, &GDInterface::BLOBUpdated, this, &DeviceDecorator::BLOBUpdated); connect(iPtr, &GDInterface::lightUpdated, this, &DeviceDecorator::lightUpdated); } DeviceDecorator::~DeviceDecorator() { delete (interfacePtr); } bool DeviceDecorator::runCommand(int command, void *ptr) { return interfacePtr->runCommand(command, ptr); } bool DeviceDecorator::setProperty(QObject * setPropCommand) { return interfacePtr->setProperty(setPropCommand); } void DeviceDecorator::processBLOB(IBLOB * bp) { interfacePtr->processBLOB(bp); } void DeviceDecorator::processLight(ILightVectorProperty * lvp) { interfacePtr->processLight(lvp); } void DeviceDecorator::processNumber(INumberVectorProperty * nvp) { interfacePtr->processNumber(nvp); } void DeviceDecorator::processSwitch(ISwitchVectorProperty * svp) { interfacePtr->processSwitch(svp); } void DeviceDecorator::processText(ITextVectorProperty * tvp) { interfacePtr->processText(tvp); } void DeviceDecorator::processMessage(int messageID) { interfacePtr->processMessage(messageID); } void DeviceDecorator::registerProperty(INDI::Property * prop) { interfacePtr->registerProperty(prop); } void DeviceDecorator::removeProperty(const QString &name) { interfacePtr->removeProperty(name); } bool DeviceDecorator::setConfig(INDIConfig tConfig) { return interfacePtr->setConfig(tConfig); } DeviceFamily DeviceDecorator::getType() { return interfacePtr->getType(); } DriverInfo *DeviceDecorator::getDriverInfo() { return interfacePtr->getDriverInfo(); } DeviceInfo *DeviceDecorator::getDeviceInfo() { return interfacePtr->getDeviceInfo(); } const QString &DeviceDecorator::getDeviceName() const { return interfacePtr->getDeviceName(); } INDI::BaseDevice *DeviceDecorator::getBaseDevice() { return interfacePtr->getBaseDevice(); } uint32_t DeviceDecorator::getDriverInterface() { return interfacePtr->getDriverInterface(); } QString DeviceDecorator::getDriverVersion() { return interfacePtr->getDriverVersion(); } const QHash &DeviceDecorator::getProperties() { return interfacePtr->getProperties(); } INDI::Property *DeviceDecorator::getProperty(const QString &propName) { return interfacePtr->getProperty(propName); } bool DeviceDecorator::getJSONProperty(const QString &propName, QJsonObject &propObject, bool compact) { return interfacePtr->getJSONProperty(propName, propObject, compact); } bool DeviceDecorator::getJSONBLOB(const QString &propName, const QString &elementName, QJsonObject &blobObject) { return interfacePtr->getJSONBLOB(propName, elementName, blobObject); } bool DeviceDecorator::setJSONProperty(const QString &propName, const QJsonArray &propValue) { return interfacePtr->setJSONProperty(propName, propValue); } bool DeviceDecorator::isConnected() { return interfacePtr->isConnected(); } bool DeviceDecorator::Connect() { return interfacePtr->Connect(); } bool DeviceDecorator::Disconnect() { return interfacePtr->Disconnect(); } bool DeviceDecorator::getMinMaxStep(const QString &propName, const QString &elementName, double * min, double * max, double * step) { return interfacePtr->getMinMaxStep(propName, elementName, min, max, step); } IPState DeviceDecorator::getState(const QString &propName) { return interfacePtr->getState(propName); } IPerm DeviceDecorator::getPermission(const QString &propName) { return interfacePtr->getPermission(propName); } ST4::ST4(INDI::BaseDevice * bdv, ClientManager * cm) { baseDevice = bdv; clientManager = cm; + m_Name = baseDevice->getDeviceName(); } -const char *ST4::getDeviceName() +const QString &ST4::getDeviceName() { - return baseDevice->getDeviceName(); + return m_Name; } void ST4::setDECSwap(bool enable) { swapDEC = enable; } bool ST4::doPulse(GuideDirection ra_dir, int ra_msecs, GuideDirection dec_dir, int dec_msecs) { - bool raOK = false, decOK = false; - raOK = doPulse(ra_dir, ra_msecs); - decOK = doPulse(dec_dir, dec_msecs); - - if (raOK && decOK) - return true; - else - return false; + bool raOK = doPulse(ra_dir, ra_msecs); + bool decOK = doPulse(dec_dir, dec_msecs); + return (raOK && decOK); } bool ST4::doPulse(GuideDirection dir, int msecs) { INumberVectorProperty *raPulse = baseDevice->getNumber("TELESCOPE_TIMED_GUIDE_WE"); INumberVectorProperty *decPulse = baseDevice->getNumber("TELESCOPE_TIMED_GUIDE_NS"); INumberVectorProperty *npulse = nullptr; INumber *dirPulse = nullptr; if (raPulse == nullptr || decPulse == nullptr) return false; if (dir == RA_INC_DIR || dir == RA_DEC_DIR) raPulse->np[0].value = raPulse->np[1].value = 0; else decPulse->np[0].value = decPulse->np[1].value = 0; switch (dir) { case RA_INC_DIR: dirPulse = IUFindNumber(raPulse, "TIMED_GUIDE_W"); if (dirPulse == nullptr) return false; npulse = raPulse; break; case RA_DEC_DIR: dirPulse = IUFindNumber(raPulse, "TIMED_GUIDE_E"); if (dirPulse == nullptr) return false; npulse = raPulse; break; case DEC_INC_DIR: if (swapDEC) dirPulse = IUFindNumber(decPulse, "TIMED_GUIDE_S"); else dirPulse = IUFindNumber(decPulse, "TIMED_GUIDE_N"); if (dirPulse == nullptr) return false; npulse = decPulse; break; case DEC_DEC_DIR: if (swapDEC) dirPulse = IUFindNumber(decPulse, "TIMED_GUIDE_N"); else dirPulse = IUFindNumber(decPulse, "TIMED_GUIDE_S"); if (dirPulse == nullptr) return false; npulse = decPulse; break; default: return false; } dirPulse->value = msecs; clientManager->sendNewNumber(npulse); //qDebug() << "Sending pulse for " << npulse->name << " in direction " << dirPulse->name << " for " << msecs << " ms " << endl; return true; } void propertyToJson(ISwitchVectorProperty *svp, QJsonObject &propObject, bool compact) { QJsonArray switches; for (int i = 0; i < svp->nsp; i++) { QJsonObject oneSwitch = {{"name", svp->sp[i].name}, {"state", svp->sp[i].s}}; if (!compact) oneSwitch.insert("label", svp->sp[i].label); switches.append(oneSwitch); } propObject = {{"name", svp->name}, {"state", svp->s}, {"switches", switches}}; if (!compact) { propObject.insert("label", svp->label); propObject.insert("group", svp->group); propObject.insert("perm", svp->p); propObject.insert("rule", svp->r); } } void propertyToJson(INumberVectorProperty *nvp, QJsonObject &propObject, bool compact) { QJsonArray numbers; for (int i = 0; i < nvp->nnp; i++) { QJsonObject oneNumber = {{"name", nvp->np[i].name}, {"value", nvp->np[i].value}}; if (!compact) { oneNumber.insert("label", nvp->np[i].label); oneNumber.insert("min", nvp->np[i].min); oneNumber.insert("mix", nvp->np[i].max); oneNumber.insert("step", nvp->np[i].step); } numbers.append(oneNumber); } propObject = {{"name", nvp->name}, {"state", nvp->s}, {"numbers", numbers}}; if (!compact) { propObject.insert("label", nvp->label); propObject.insert("group", nvp->group); propObject.insert("perm", nvp->p); } } void propertyToJson(ITextVectorProperty *tvp, QJsonObject &propObject, bool compact) { QJsonArray Texts; for (int i = 0; i < tvp->ntp; i++) { QJsonObject oneText = {{"name", tvp->tp[i].name}, {"value", tvp->tp[i].text}}; if (!compact) { oneText.insert("label", tvp->tp[i].label); } Texts.append(oneText); } propObject = {{"name", tvp->name}, {"state", tvp->s}, {"texts", Texts}}; if (!compact) { propObject.insert("label", tvp->label); propObject.insert("group", tvp->group); propObject.insert("perm", tvp->p); } } void propertyToJson(ILightVectorProperty *lvp, QJsonObject &propObject, bool compact) { QJsonArray Lights; for (int i = 0; i < lvp->nlp; i++) { QJsonObject oneLight = {{"name", lvp->lp[i].name}, {"state", lvp->lp[i].s}}; if (!compact) { oneLight.insert("label", lvp->lp[i].label); } Lights.append(oneLight); } propObject = {{"name", lvp->name}, {"state", lvp->s}, {"lights", Lights}}; if (!compact) { propObject.insert("label", lvp->label); propObject.insert("group", lvp->group); } } } #ifndef KSTARS_LITE QDBusArgument &operator<<(QDBusArgument &argument, const ISD::ParkStatus &source) { argument.beginStructure(); argument << static_cast(source); argument.endStructure(); return argument; } const QDBusArgument &operator>>(const QDBusArgument &argument, ISD::ParkStatus &dest) { int a; argument.beginStructure(); argument >> a; argument.endStructure(); dest = static_cast(a); return argument; } #endif diff --git a/kstars/indi/indistd.h b/kstars/indi/indistd.h index 35b699b82..0e6188b06 100644 --- a/kstars/indi/indistd.h +++ b/kstars/indi/indistd.h @@ -1,321 +1,322 @@ /* INDI STD Copyright (C) 2012 Jasem Mutlaq (mutlaqja@ikarustech.com) This application is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version. Handle INDI Standard properties. */ #pragma once #include "indicommon.h" #include #include #include #include #ifndef KSTARS_LITE #include #endif #define MAXINDIFILENAME 512 class ClientManager; class DriverInfo; class DeviceInfo; class QTimer; // INDI Standard Device Namespace namespace ISD { typedef enum { PARK_UNKNOWN, PARK_PARKED, PARK_PARKING, PARK_UNPARKING, PARK_UNPARKED, PARK_ERROR } ParkStatus; class GDSetCommand : public QObject { Q_OBJECT public: GDSetCommand(INDI_PROPERTY_TYPE inPropertyType, const QString &inProperty, const QString &inElement, QVariant qValue, QObject *parent); INDI_PROPERTY_TYPE propType; QString indiProperty; QString indiElement; QVariant elementValue; }; /** * @class GDInterface * GDInterface is the Generic Device Interface for INDI devices. It is used as part of the Decorator Pattern when initially a new INDI device is created as a * Generic Device in INDIListener. If the device registers an INDI Standard Property belonging to one specific device type (e.g. Telescope), then the device functionality * is extended to the particular device type. * * DeviceDecorator subclasses GDInterface and calls concrete decorators methods. * * @author Jasem Mutlaq */ class GDInterface : public QObject { Q_OBJECT public: // Property handling virtual void registerProperty(INDI::Property *prop) = 0; virtual void removeProperty(const QString &name) = 0; virtual void processSwitch(ISwitchVectorProperty *svp) = 0; virtual void processText(ITextVectorProperty *tvp) = 0; virtual void processNumber(INumberVectorProperty *nvp) = 0; virtual void processLight(ILightVectorProperty *lvp) = 0; virtual void processBLOB(IBLOB *bp) = 0; virtual void processMessage(int messageID) = 0; // Accessors virtual const QHash &getProperties() = 0; virtual DeviceFamily getType() = 0; virtual DriverInfo *getDriverInfo() = 0; virtual DeviceInfo *getDeviceInfo() = 0; virtual INDI::BaseDevice *getBaseDevice() = 0; virtual uint32_t getDriverInterface() = 0; virtual QString getDriverVersion() = 0; // Convenience functions virtual bool setConfig(INDIConfig tConfig) = 0; virtual const QString &getDeviceName() const = 0; virtual bool isConnected() = 0; virtual bool getMinMaxStep(const QString &propName, const QString &elementName, double *min, double *max, double *step) = 0; virtual IPState getState(const QString &propName) = 0; virtual IPerm getPermission(const QString &propName) = 0; virtual INDI::Property *getProperty(const QString &propName) = 0; virtual bool getJSONProperty(const QString &propName, QJsonObject &propObject, bool compact) = 0; virtual bool getJSONBLOB(const QString &propName, const QString &elementName, QJsonObject &blobObject) = 0; virtual bool setJSONProperty(const QString &propName, const QJsonArray &propValue) = 0; virtual ~GDInterface() = default; public slots: virtual bool Connect() = 0; virtual bool Disconnect() = 0; virtual bool runCommand(int command, void *ptr = nullptr) = 0; virtual bool setProperty(QObject *) = 0; protected: DeviceFamily dType { KSTARS_CCD }; uint32_t driverInterface { 0 }; QString driverVersion; QHash properties; signals: void Connected(); void Disconnected(); void switchUpdated(ISwitchVectorProperty *svp); void textUpdated(ITextVectorProperty *tvp); void numberUpdated(INumberVectorProperty *nvp); void lightUpdated(ILightVectorProperty *lvp); void BLOBUpdated(IBLOB *bp); void messageUpdated(int messageID); void interfaceDefined(); void systemPortDetected(); void propertyDefined(INDI::Property *prop); void propertyDeleted(const QString &name); }; /** * @class GenericDevice * GenericDevice is the Generic Device for INDI devices. When a new INDI device is created in INDIListener, it gets created as a GenericDevice initially. If the device * registers a standard property that is a key property to a device type family (e.g. Number property EQUATORIAL_EOD_COORD signifies a Telescope device), then the specialized version of * the device is extended via the Decorator Pattern. * * GenericDevice handles common functions shared across many devices such as time and location handling, configuration processing, retrieving information about properties, driver info..etc. * * @author Jasem Mutlaq */ class GenericDevice : public GDInterface { Q_OBJECT public: explicit GenericDevice(DeviceInfo &idv); virtual ~GenericDevice() override = default; virtual void registerProperty(INDI::Property *prop) override; virtual void removeProperty(const QString &name) override; virtual void processSwitch(ISwitchVectorProperty *svp) override; virtual void processText(ITextVectorProperty *tvp) override; virtual void processNumber(INumberVectorProperty *nvp) override; virtual void processLight(ILightVectorProperty *lvp) override; virtual void processBLOB(IBLOB *bp) override; virtual void processMessage(int messageID) override; virtual DeviceFamily getType() override { return dType; } virtual const QString &getDeviceName() const override; virtual DriverInfo *getDriverInfo() override { return driverInfo; } virtual DeviceInfo *getDeviceInfo() override { return deviceInfo; } virtual const QHash &getProperties() override { return properties; } virtual uint32_t getDriverInterface() override { return driverInterface; } virtual QString getDriverVersion() override { return driverVersion; } virtual bool setConfig(INDIConfig tConfig) override; virtual bool isConnected() override { return connected; } virtual INDI::BaseDevice *getBaseDevice() override { return baseDevice; } virtual bool getMinMaxStep(const QString &propName, const QString &elementName, double *min, double *max, double *step) override; virtual IPState getState(const QString &propName) override; virtual IPerm getPermission(const QString &propName) override; virtual INDI::Property *getProperty(const QString &propName) override; virtual bool getJSONProperty(const QString &propName, QJsonObject &propObject, bool compact) override; virtual bool getJSONBLOB(const QString &propName, const QString &elementName, QJsonObject &blobObject) override; virtual bool setJSONProperty(const QString &propName, const QJsonArray &propValue) override; public slots: virtual bool Connect() override; virtual bool Disconnect() override; virtual bool runCommand(int command, void *ptr = nullptr) override; virtual bool setProperty(QObject *) override; protected slots: virtual void resetWatchdog(); protected: void createDeviceInit(); void updateTime(); void updateLocation(); private: static void registerDBusType(); bool connected { false }; QString m_Name; DriverInfo *driverInfo { nullptr }; DeviceInfo *deviceInfo { nullptr }; INDI::BaseDevice *baseDevice { nullptr }; ClientManager *clientManager { nullptr }; QTimer *watchDogTimer { nullptr }; char BLOBFilename[MAXINDIFILENAME + 1]; }; /** * @class DeviceDecorator * DeviceDecorator is the base decorator for all specialized devices. It extends the functionality of GenericDevice. * * @author Jasem Mutlaq */ class DeviceDecorator : public GDInterface { Q_OBJECT public: explicit DeviceDecorator(GDInterface *iPtr); virtual ~DeviceDecorator() override; virtual void registerProperty(INDI::Property *prop) override; virtual void removeProperty(const QString &name) override; virtual void processSwitch(ISwitchVectorProperty *svp) override; virtual void processText(ITextVectorProperty *tvp) override; virtual void processNumber(INumberVectorProperty *nvp) override; virtual void processLight(ILightVectorProperty *lvp) override; virtual void processBLOB(IBLOB *bp) override; virtual void processMessage(int messageID) override; virtual DeviceFamily getType() override; virtual bool setConfig(INDIConfig tConfig) override; virtual bool isConnected() override; virtual const QString &getDeviceName() const override; DriverInfo *getDriverInfo() override; DeviceInfo *getDeviceInfo() override; virtual const QHash &getProperties() override; uint32_t getDriverInterface() override; QString getDriverVersion() override; virtual INDI::BaseDevice *getBaseDevice() override; bool getMinMaxStep(const QString &propName, const QString &elementName, double *min, double *max, double *step) override; IPState getState(const QString &propName) override; IPerm getPermission(const QString &propName) override; INDI::Property *getProperty(const QString &propName) override; bool getJSONProperty(const QString &propName, QJsonObject &propObject, bool compact) override; bool getJSONBLOB(const QString &propName, const QString &elementName, QJsonObject &blobObject) override; bool setJSONProperty(const QString &propName, const QJsonArray &propValue) override; public slots: virtual bool Connect() override; virtual bool Disconnect() override; virtual bool runCommand(int command, void *ptr = nullptr) override; virtual bool setProperty(QObject *) override; protected: INDI::BaseDevice *baseDevice { nullptr }; ClientManager *clientManager { nullptr }; GDInterface *interfacePtr { nullptr }; }; /** * @class ST4 * ST4 is a special class that handles ST4 commands. Since ST4 functionality can be part of a stand alone ST4 device, * or as part of a larger device as CCD or Telescope, it is handled separately to enable one ST4 device regardless of the parent device type. * * ST4 is a hardware port dedicated to sending guiding correction pulses to the mount. * * @author Jasem Mutlaq */ class ST4 { public: ST4(INDI::BaseDevice *bdv, ClientManager *cm); ~ST4() = default; bool doPulse(GuideDirection ra_dir, int ra_msecs, GuideDirection dec_dir, int dec_msecs); bool doPulse(GuideDirection dir, int msecs); void setDECSwap(bool enable); - const char *getDeviceName(); + const QString &getDeviceName(); private: INDI::BaseDevice *baseDevice { nullptr }; ClientManager *clientManager { nullptr }; bool swapDEC { false }; + QString m_Name; }; void propertyToJson(ISwitchVectorProperty *svp, QJsonObject &propObject, bool compact = true); void propertyToJson(ITextVectorProperty *tvp, QJsonObject &propObject, bool compact = true); void propertyToJson(INumberVectorProperty *nvp, QJsonObject &propObject, bool compact = true); void propertyToJson(ILightVectorProperty *lvp, QJsonObject &propObject, bool compact = true); } #ifndef KSTARS_LITE Q_DECLARE_METATYPE(ISD::ParkStatus) QDBusArgument &operator<<(QDBusArgument &argument, const ISD::ParkStatus &source); const QDBusArgument &operator>>(const QDBusArgument &argument, ISD::ParkStatus &dest); #endif