File indexing completed on 2024-04-28 15:09:52

0001 /*
0002     SPDX-FileCopyrightText: 2012 Jasem Mutlaq <mutlaqja@ikarustech.com>
0003 
0004     SPDX-License-Identifier: GPL-2.0-or-later
0005 */
0006 
0007 #include "guide.h"
0008 
0009 #include "guideadaptor.h"
0010 #include "kstars.h"
0011 #include "ksmessagebox.h"
0012 #include "kstarsdata.h"
0013 #include "opscalibration.h"
0014 #include "opsguide.h"
0015 #include "opsdither.h"
0016 #include "opsgpg.h"
0017 #include "Options.h"
0018 #include "indi/indiguider.h"
0019 #include "indi/indiadaptiveoptics.h"
0020 #include "auxiliary/QProgressIndicator.h"
0021 #include "ekos/auxiliary/opticaltrainmanager.h"
0022 #include "ekos/auxiliary/profilesettings.h"
0023 #include "ekos/auxiliary/opticaltrainsettings.h"
0024 #include "ekos/auxiliary/darklibrary.h"
0025 #include "externalguide/linguider.h"
0026 #include "externalguide/phd2.h"
0027 #include "fitsviewer/fitsdata.h"
0028 #include "fitsviewer/fitsview.h"
0029 #include "fitsviewer/fitsviewer.h"
0030 #include "internalguide/internalguider.h"
0031 #include "guideview.h"
0032 #include "guidegraph.h"
0033 #include "guidestatewidget.h"
0034 #include "manualpulse.h"
0035 #include "ekos/auxiliary/darkprocessor.h"
0036 
0037 #include <KConfigDialog>
0038 
0039 #include <basedevice.h>
0040 #include <ekos_guide_debug.h>
0041 
0042 #include "ui_manualdither.h"
0043 
0044 #include <random>
0045 
0046 #define CAPTURE_TIMEOUT_THRESHOLD 30000
0047 
0048 namespace Ekos
0049 {
0050 Guide::Guide() : QWidget()
0051 {
0052     // #0 Prelude
0053     internalGuider = new InternalGuider(); // Init Internal Guider always
0054 
0055     KConfigDialog *dialog = new KConfigDialog(this, "guidesettings", Options::self());
0056 
0057     opsGuide = new OpsGuide();  // Initialize sub dialog AFTER main dialog
0058     KPageWidgetItem *page = dialog->addPage(opsGuide, i18n("Guide"));
0059     page->setIcon(QIcon::fromTheme("kstars_guides"));
0060     connect(opsGuide, &OpsGuide::settingsUpdated, this, [this]()
0061     {
0062         onThresholdChanged(Options::guideAlgorithm());
0063         configurePHD2Camera();
0064         configSEPMultistarOptions(); // due to changes in 'Guide Setting: Algorithm'
0065         checkUseGuideHead();
0066     });
0067 
0068     opsCalibration = new OpsCalibration(internalGuider);
0069     page = dialog->addPage(opsCalibration, i18n("Calibration"));
0070     page->setIcon(QIcon::fromTheme("tool-measure"));
0071 
0072     opsDither = new OpsDither();
0073     page = dialog->addPage(opsDither, i18n("Dither"));
0074     page->setIcon(QIcon::fromTheme("transform-move"));
0075 
0076     opsGPG = new OpsGPG(internalGuider);
0077     page = dialog->addPage(opsGPG, i18n("GPG RA Guider"));
0078     page->setIcon(QIcon::fromTheme("pathshape"));
0079 
0080     // #1 Setup UI
0081     setupUi(this);
0082 
0083     // #2 Register DBus
0084     qRegisterMetaType<Ekos::GuideState>("Ekos::GuideState");
0085     qDBusRegisterMetaType<Ekos::GuideState>();
0086     new GuideAdaptor(this);
0087     QDBusConnection::sessionBus().registerObject("/KStars/Ekos/Guide", this);
0088 
0089     // #3 Init Plots
0090     initPlots();
0091 
0092     // #4 Init View
0093     initView();
0094     internalGuider->setGuideView(m_GuideView);
0095 
0096     // #5 Init Connections
0097     initConnections();
0098 
0099     // Progress Indicator
0100     pi = new QProgressIndicator(this);
0101     controlLayout->addWidget(pi, 1, 2, 1, 1);
0102 
0103     showFITSViewerB->setIcon(
0104         QIcon::fromTheme("kstars_fitsviewer"));
0105     connect(showFITSViewerB, &QPushButton::clicked, this, &Ekos::Guide::showFITSViewer);
0106     showFITSViewerB->setAttribute(Qt::WA_LayoutUsesWidgetRect);
0107 
0108     guideAutoScaleGraphB->setIcon(
0109         QIcon::fromTheme("zoom-fit-best"));
0110     connect(guideAutoScaleGraphB, &QPushButton::clicked, this, &Ekos::Guide::slotAutoScaleGraphs);
0111     guideAutoScaleGraphB->setAttribute(Qt::WA_LayoutUsesWidgetRect);
0112 
0113     guideSaveDataB->setIcon(
0114         QIcon::fromTheme("document-save"));
0115     connect(guideSaveDataB, &QPushButton::clicked, driftGraph, &GuideDriftGraph::exportGuideData);
0116     guideSaveDataB->setAttribute(Qt::WA_LayoutUsesWidgetRect);
0117 
0118     guideDataClearB->setIcon(
0119         QIcon::fromTheme("application-exit"));
0120     connect(guideDataClearB, &QPushButton::clicked, this, &Ekos::Guide::clearGuideGraphs);
0121     guideDataClearB->setAttribute(Qt::WA_LayoutUsesWidgetRect);
0122 
0123     // These icons seem very hard to read for this button. Just went with +.
0124     // guideZoomInXB->setIcon(QIcon::fromTheme("zoom-in"));
0125     guideZoomInXB->setText("+");
0126     connect(guideZoomInXB, &QPushButton::clicked, driftGraph, &GuideDriftGraph::zoomInX);
0127     guideZoomInXB->setAttribute(Qt::WA_LayoutUsesWidgetRect);
0128 
0129     // These icons seem very hard to read for this button. Just went with -.
0130     // guideZoomOutXB->setIcon(QIcon::fromTheme("zoom-out"));
0131     guideZoomOutXB->setText("-");
0132     connect(guideZoomOutXB, &QPushButton::clicked, driftGraph, &GuideDriftGraph::zoomOutX);
0133     guideZoomOutXB->setAttribute(Qt::WA_LayoutUsesWidgetRect);
0134 
0135     // Icons
0136     focalLengthIcon->setPixmap(QIcon::fromTheme("gnumeric-formulaguru").pixmap(32, 32));
0137     apertureIcon->setPixmap(QIcon::fromTheme("lensautofix").pixmap(32, 32));
0138     reducerIcon->setPixmap(QIcon::fromTheme("format-align-vertical-bottom").pixmap(32, 32));
0139     FOVIcon->setPixmap(QIcon::fromTheme("timeline-use-zone-on").pixmap(32, 32));
0140     focalRatioIcon->setPixmap(QIcon::fromTheme("node-type-symmetric").pixmap(32, 32));
0141 
0142     // Exposure
0143     //Should we set the range for the spin box here?
0144     QList<double> exposureValues;
0145     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
0146                    << 10 << 15 << 30;
0147     guideExposure->setRecommendedValues(exposureValues);
0148     connect(guideExposure, &NonLinearDoubleSpinBox::editingFinished, this, &Ekos::Guide::saveDefaultGuideExposure);
0149 
0150     // Set current guide type
0151     setGuiderType(-1);
0152 
0153     //This allows the current guideSubframe option to be loaded.
0154     if(guiderType == GUIDE_PHD2)
0155     {
0156         setExternalGuiderBLOBEnabled(!Options::guideSubframe());
0157     }
0158     else
0159     {
0160         // These only apply to PHD2, so disabling them when using the internal guider.
0161         opsDither->kcfg_DitherTimeout->setEnabled(false);
0162         opsDither->kcfg_DitherThreshold->setEnabled(false);
0163         opsDither->kcfg_DitherMaxIterations->setEnabled(!Options::ditherWithOnePulse());
0164     }
0165 
0166     // Initialize non guided dithering random generator.
0167     resetNonGuidedDither();
0168 
0169     //Note:  This is to prevent a button from being called the default button
0170     //and then executing when the user hits the enter key such as when on a Text Box
0171     QList<QPushButton *> qButtons = findChildren<QPushButton *>();
0172     for (auto &button : qButtons)
0173         button->setAutoDefault(false);
0174 
0175     connect(KStars::Instance(), &KStars::colorSchemeChanged, driftGraph, &GuideDriftGraph::refreshColorScheme);
0176 
0177     m_DarkProcessor = new DarkProcessor(this);
0178     connect(m_DarkProcessor, &DarkProcessor::newLog, this, &Ekos::Guide::appendLogText);
0179     connect(m_DarkProcessor, &DarkProcessor::darkFrameCompleted, this, [this](bool completed)
0180     {
0181         if (completed != guideDarkFrame->isChecked())
0182             setDarkFrameEnabled(completed);
0183         m_GuideView->setProperty("suspended", false);
0184         if (completed)
0185         {
0186             m_GuideView->rescale(ZOOM_KEEP_LEVEL);
0187             m_GuideView->updateFrame();
0188         }
0189         m_GuideView->updateFrame();
0190         setCaptureComplete();
0191     });
0192 
0193     m_ManaulPulse = new ManualPulse(this);
0194     connect(m_ManaulPulse, &ManualPulse::newSinglePulse, this, &Guide::sendSinglePulse);
0195     connect(manualPulseB, &QPushButton::clicked, this, [this]()
0196     {
0197         m_ManaulPulse->reset();
0198         m_ManaulPulse->show();
0199     });
0200 
0201     loadGlobalSettings();
0202     connectSettings();
0203 
0204     setupOpticalTrainManager();
0205 }
0206 
0207 Guide::~Guide()
0208 {
0209     delete m_GuiderInstance;
0210 }
0211 
0212 void Guide::handleHorizontalPlotSizeChange()
0213 {
0214     targetPlot->handleHorizontalPlotSizeChange();
0215     calibrationPlot->xAxis->setScaleRatio(calibrationPlot->yAxis, 1.0);
0216     calibrationPlot->replot();
0217 }
0218 
0219 void Guide::handleVerticalPlotSizeChange()
0220 {
0221     targetPlot->handleVerticalPlotSizeChange();
0222     calibrationPlot->yAxis->setScaleRatio(calibrationPlot->xAxis, 1.0);
0223     calibrationPlot->replot();
0224 }
0225 
0226 void Guide::guideAfterMeridianFlip()
0227 {
0228     //This will clear the tracking box selection
0229     //The selected guide star is no longer valid due to the flip
0230     m_GuideView->setTrackingBoxEnabled(false);
0231     starCenter = QVector3D();
0232 
0233     if (Options::resetGuideCalibration())
0234         clearCalibration();
0235 
0236     // GPG guide algorithm should be reset on any slew.
0237     if (Options::gPGEnabled())
0238         m_GuiderInstance->resetGPG();
0239 
0240     guide();
0241 }
0242 
0243 void Guide::resizeEvent(QResizeEvent * event)
0244 {
0245     if (event->oldSize().width() != -1)
0246     {
0247         if (event->oldSize().width() != size().width())
0248             handleHorizontalPlotSizeChange();
0249         else if (event->oldSize().height() != size().height())
0250             handleVerticalPlotSizeChange();
0251     }
0252     else
0253     {
0254         QTimer::singleShot(10, this, &Ekos::Guide::handleHorizontalPlotSizeChange);
0255     }
0256 }
0257 
0258 void Guide::buildTarget()
0259 {
0260     targetPlot->buildTarget(guiderAccuracyThreshold->value());
0261 }
0262 
0263 void Guide::clearGuideGraphs()
0264 {
0265     driftGraph->clear();
0266     targetPlot->clear();
0267 }
0268 
0269 void Guide::clearCalibrationGraphs()
0270 {
0271     calibrationPlot->graph(GuideGraph::G_RA)->data()->clear(); //RA out
0272     calibrationPlot->graph(GuideGraph::G_DEC)->data()->clear(); //RA back
0273     calibrationPlot->graph(GuideGraph::G_RA_HIGHLIGHT)->data()->clear(); //Backlash
0274     calibrationPlot->graph(GuideGraph::G_DEC_HIGHLIGHT)->data()->clear(); //DEC out
0275     calibrationPlot->graph(GuideGraph::G_RA_PULSE)->data()->clear(); //DEC back
0276     calibrationPlot->replot();
0277 }
0278 
0279 void Guide::slotAutoScaleGraphs()
0280 {
0281     driftGraph->zoomX(defaultXZoomLevel);
0282 
0283     driftGraph->autoScaleGraphs();
0284     targetPlot->autoScaleGraphs(guiderAccuracyThreshold->value());
0285 
0286     calibrationPlot->rescaleAxes();
0287     calibrationPlot->yAxis->setScaleRatio(calibrationPlot->xAxis, 1.0);
0288     calibrationPlot->xAxis->setScaleRatio(calibrationPlot->yAxis, 1.0);
0289     calibrationPlot->replot();
0290 }
0291 
0292 void Guide::guideHistory()
0293 {
0294     int sliderValue = guideSlider->value();
0295     latestCheck->setChecked(sliderValue == guideSlider->maximum() - 1 || sliderValue == guideSlider->maximum());
0296     double ra = driftGraph->graph(GuideGraph::G_RA)->dataMainValue(sliderValue); //Get RA from RA data
0297     double de = driftGraph->graph(GuideGraph::G_DEC)->dataMainValue(sliderValue); //Get DEC from DEC data
0298     driftGraph->guideHistory(sliderValue, graphOnLatestPt);
0299 
0300     targetPlot->showPoint(ra, de);
0301 }
0302 
0303 void Guide::setLatestGuidePoint(bool isChecked)
0304 {
0305     graphOnLatestPt = isChecked;
0306     driftGraph->setLatestGuidePoint(isChecked);
0307     targetPlot->setLatestGuidePoint(isChecked);
0308 
0309     if(isChecked)
0310         guideSlider->setValue(guideSlider->maximum());
0311 }
0312 
0313 QString Guide::setRecommendedExposureValues(QList<double> values)
0314 {
0315     guideExposure->setRecommendedValues(values);
0316     return guideExposure->getRecommendedValuesString();
0317 }
0318 
0319 bool Guide::setCamera(ISD::Camera * device)
0320 {
0321     if (m_Camera && device == m_Camera)
0322     {
0323         checkCamera();
0324         return false;
0325     }
0326 
0327     if (m_Camera)
0328         m_Camera->disconnect(this);
0329 
0330     m_Camera = device;
0331 
0332     if (m_Camera)
0333     {
0334         connect(m_Camera, &ISD::Camera::Connected, this, [this]()
0335         {
0336             controlGroupBox->setEnabled(true);
0337         });
0338         connect(m_Camera, &ISD::ConcreteDevice::Disconnected, this, [this]()
0339         {
0340             controlGroupBox->setEnabled(false);
0341         });
0342     }
0343 
0344     controlGroupBox->setEnabled(m_Camera && m_Camera->isConnected());
0345 
0346     // If camera was reset, return now.
0347     if (!m_Camera)
0348         return false;
0349 
0350     if(guiderType != GUIDE_INTERNAL)
0351         m_Camera->setBLOBEnabled(false);
0352 
0353     checkCamera();
0354     configurePHD2Camera();
0355 
0356     // In case we are recovering from a crash and capture is pending, process it immediately.
0357     if (captureTimeout.isActive() && m_State >= Ekos::GUIDE_CAPTURE)
0358         QTimer::singleShot(100, this, &Guide::processCaptureTimeout);
0359 
0360     return true;
0361 }
0362 
0363 void Guide::configurePHD2Camera()
0364 {
0365     //Maybe something like this can be done for Linguider?
0366     //But for now, Linguider doesn't support INDI Cameras
0367     if(guiderType != GUIDE_PHD2)
0368         return;
0369     //This prevents a crash if phd2guider is null
0370     if(!phd2Guider)
0371         return;
0372     //This way it doesn't check if the equipment isn't connected yet.
0373     //It will check again when the equipment is connected.
0374     if(!phd2Guider->isConnected())
0375         return;
0376     //This way it doesn't check if the equipment List has not been received yet.
0377     //It will ask for the list.  When the list is received it will check again.
0378     if(phd2Guider->getCurrentCamera().isEmpty())
0379     {
0380         phd2Guider->requestCurrentEquipmentUpdate();
0381         return;
0382     }
0383 
0384     //this checks to see if a CCD in the list matches the name of PHD2's camera
0385     ISD::Camera *ccdMatch = nullptr;
0386     QString currentPHD2CameraName = "None";
0387     if(m_Camera && phd2Guider->getCurrentCamera().contains(m_Camera->getDeviceName()))
0388     {
0389         ccdMatch = m_Camera;
0390         currentPHD2CameraName = (phd2Guider->getCurrentCamera());
0391     }
0392 
0393     //If this method gives the same result as last time, no need to update the Camera info again.
0394     //That way the user doesn't see a ton of messages printing about the PHD2 external camera.
0395     //But lets make sure the blob is set correctly every time.
0396     if(m_LastPHD2CameraName == currentPHD2CameraName)
0397     {
0398         setExternalGuiderBLOBEnabled(!guideSubframe->isChecked());
0399         return;
0400     }
0401 
0402     //This means that a Guide Camera was connected before but it changed.
0403     if(m_Camera)
0404         setExternalGuiderBLOBEnabled(false);
0405 
0406     //Updating the currentCCD
0407     m_Camera = ccdMatch;
0408 
0409     //This updates the last camera name for the next time it is checked.
0410     m_LastPHD2CameraName = currentPHD2CameraName;
0411 
0412     m_LastPHD2MountName = phd2Guider->getCurrentMount();
0413 
0414     //This sets a boolean that allows you to tell if the PHD2 camera is in Ekos
0415     phd2Guider->setCurrentCameraIsNotInEkos(m_Camera == nullptr);
0416 
0417     if(phd2Guider->isCurrentCameraNotInEkos())
0418     {
0419         appendLogText(
0420             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.",
0421                  phd2Guider->getCurrentCamera()));
0422         guideSubframe->setEnabled(false);
0423         //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.
0424         disconnect(guideSubframe, &QCheckBox::toggled, this, &Ekos::Guide::setSubFrameEnabled);
0425         guideSubframe->setChecked(true);
0426         return;
0427     }
0428 
0429     appendLogText(
0430         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.",
0431              phd2Guider->getCurrentCamera()));
0432     guideSubframe->setEnabled(true);
0433     connect(guideSubframe, &QCheckBox::toggled, this, &Ekos::Guide::setSubFrameEnabled);
0434     guideSubframe->setChecked(guideSubframe->isChecked());
0435 }
0436 
0437 bool Guide::setMount(ISD::Mount * device)
0438 {
0439     if (m_Mount && m_Mount == device)
0440     {
0441         syncTelescopeInfo();
0442         return false;
0443     }
0444 
0445     if (m_Mount)
0446         m_Mount->disconnect(this);
0447 
0448     m_Mount = device;
0449     syncTelescopeInfo();
0450     return true;
0451 }
0452 
0453 QString Guide::camera()
0454 {
0455     if (m_Camera)
0456         return m_Camera->getDeviceName();
0457 
0458     return QString();
0459 }
0460 
0461 void Guide::checkCamera()
0462 {
0463     // Do NOT perform checks when the camera is capturing as this may result
0464     // in signals/slots getting disconnected.
0465     if (!m_Camera || guiderType != GUIDE_INTERNAL)
0466         return;
0467 
0468     switch (m_State)
0469     {
0470         // Not busy, camera change is OK
0471         case GUIDE_IDLE:
0472         case GUIDE_ABORTED:
0473         case GUIDE_CONNECTED:
0474         case GUIDE_DISCONNECTED:
0475         case GUIDE_CALIBRATION_ERROR:
0476             break;
0477 
0478         // Busy, camera change is not OK
0479         case GUIDE_CAPTURE:
0480         case GUIDE_LOOPING:
0481         case GUIDE_DARK:
0482         case GUIDE_SUBFRAME:
0483         case GUIDE_STAR_SELECT:
0484         case GUIDE_CALIBRATING:
0485         case GUIDE_CALIBRATION_SUCCESS:
0486         case GUIDE_GUIDING:
0487         case GUIDE_SUSPENDED:
0488         case GUIDE_REACQUIRE:
0489         case GUIDE_DITHERING:
0490         case GUIDE_MANUAL_DITHERING:
0491         case GUIDE_DITHERING_ERROR:
0492         case GUIDE_DITHERING_SUCCESS:
0493         case GUIDE_DITHERING_SETTLE:
0494             return;
0495     }
0496 
0497     checkUseGuideHead();
0498 
0499     auto targetChip = m_Camera->getChip(useGuideHead ? ISD::CameraChip::GUIDE_CCD : ISD::CameraChip::PRIMARY_CCD);
0500     if (!targetChip)
0501     {
0502         qCCritical(KSTARS_EKOS_GUIDE) << "Failed to retrieve active guide chip in camera";
0503         return;
0504     }
0505 
0506     if (targetChip->isCapturing())
0507         return;
0508 
0509     if (guiderType != GUIDE_INTERNAL)
0510     {
0511         syncCameraInfo();
0512         return;
0513     }
0514 
0515     connect(m_Camera, &ISD::Camera::propertyUpdated, this, &Ekos::Guide::updateProperty, Qt::UniqueConnection);
0516     connect(m_Camera, &ISD::Camera::newExposureValue, this, &Ekos::Guide::checkExposureValue, Qt::UniqueConnection);
0517 
0518     syncCameraInfo();
0519 }
0520 
0521 void Ekos::Guide::checkUseGuideHead()
0522 {
0523     if (m_Camera == nullptr)
0524         return;
0525 
0526     if (m_Camera->hasGuideHead() && Options::useGuideHead())
0527         useGuideHead = true;
0528     else
0529         useGuideHead = false;
0530     // guiding option only enabled if camera has a dedicated guiding chip
0531     opsGuide->kcfg_UseGuideHead->setEnabled(m_Camera->hasGuideHead());
0532 }
0533 
0534 void Guide::syncCameraInfo()
0535 {
0536     if (!m_Camera)
0537         return;
0538 
0539     auto nvp = m_Camera->getNumber(useGuideHead ? "GUIDER_INFO" : "CCD_INFO");
0540 
0541     if (nvp)
0542     {
0543         auto np = nvp->findWidgetByName("CCD_PIXEL_SIZE_X");
0544         if (np)
0545             ccdPixelSizeX = np->getValue();
0546 
0547         np = nvp->findWidgetByName( "CCD_PIXEL_SIZE_Y");
0548         if (np)
0549             ccdPixelSizeY = np->getValue();
0550 
0551         np = nvp->findWidgetByName("CCD_PIXEL_SIZE_Y");
0552         if (np)
0553             ccdPixelSizeY = np->getValue();
0554     }
0555 
0556     updateGuideParams();
0557 }
0558 
0559 void Guide::syncTelescopeInfo()
0560 {
0561     if (m_Mount == nullptr || m_Mount->isConnected() == false)
0562         return;
0563 
0564     updateGuideParams();
0565 }
0566 
0567 void Guide::updateGuideParams()
0568 {
0569     if (m_Camera == nullptr)
0570         return;
0571 
0572     checkUseGuideHead();
0573 
0574     ISD::CameraChip *targetChip = m_Camera->getChip(useGuideHead ? ISD::CameraChip::GUIDE_CCD : ISD::CameraChip::PRIMARY_CCD);
0575 
0576     if (targetChip == nullptr)
0577     {
0578         appendLogText(i18n("Connection to the guide CCD is lost."));
0579         return;
0580     }
0581 
0582     if (targetChip->getFrameType() != FRAME_LIGHT)
0583         return;
0584 
0585     if(guiderType == GUIDE_INTERNAL)
0586         guideBinning->setEnabled(targetChip->canBin());
0587 
0588     int subBinX = 1, subBinY = 1;
0589     if (targetChip->canBin())
0590     {
0591         int maxBinX, maxBinY;
0592 
0593         targetChip->getBinning(&subBinX, &subBinY);
0594         targetChip->getMaxBin(&maxBinX, &maxBinY);
0595 
0596         //override with stored guide bin index, if within the range of possible bin modes
0597         if( guideBinIndex >= 0 && guideBinIndex < maxBinX && guideBinIndex < maxBinY )
0598         {
0599             subBinX = guideBinIndex + 1;
0600             subBinY = guideBinIndex + 1;
0601         }
0602 
0603         guideBinIndex = subBinX - 1;
0604 
0605         guideBinning->blockSignals(true);
0606 
0607         guideBinning->clear();
0608         for (int i = 1; i <= maxBinX; i++)
0609             guideBinning->addItem(QString("%1x%2").arg(i).arg(i));
0610 
0611         guideBinning->setCurrentIndex( guideBinIndex );
0612 
0613         guideBinning->blockSignals(false);
0614     }
0615 
0616     // If frame setting does not exist, create a new one.
0617     if (frameSettings.contains(targetChip) == false)
0618     {
0619         int x, y, w, h;
0620         if (targetChip->getFrame(&x, &y, &w, &h))
0621         {
0622             if (w > 0 && h > 0)
0623             {
0624                 int minX, maxX, minY, maxY, minW, maxW, minH, maxH;
0625                 targetChip->getFrameMinMax(&minX, &maxX, &minY, &maxY, &minW, &maxW, &minH, &maxH);
0626                 auto subframed = guideSubframe->isChecked();
0627 
0628                 QVariantMap settings;
0629 
0630                 settings["x"]    = subframed ? x : minX;
0631                 settings["y"]    = subframed ? y : minY;
0632                 settings["w"]    = subframed ? w : maxW;
0633                 settings["h"]    = subframed ? h : maxH;
0634                 settings["binx"] = subBinX;
0635                 settings["biny"] = subBinY;
0636 
0637                 frameSettings[targetChip] = settings;
0638             }
0639         }
0640     }
0641     // Otherwise update existing map
0642     else
0643     {
0644         QVariantMap settings = frameSettings[targetChip];
0645         settings["binx"] = subBinX;
0646         settings["biny"] = subBinY;
0647         frameSettings[targetChip] = settings;
0648     }
0649 
0650     if (ccdPixelSizeX != -1 && ccdPixelSizeY != -1 && m_FocalLength > 0)
0651     {
0652         auto effectiveFocaLength = m_Reducer * m_FocalLength;
0653         m_GuiderInstance->setGuiderParams(ccdPixelSizeX, ccdPixelSizeY, m_Aperture, effectiveFocaLength);
0654         emit guideChipUpdated(targetChip);
0655 
0656         int x, y, w, h;
0657         if (targetChip->getFrame(&x, &y, &w, &h))
0658         {
0659             m_GuiderInstance->setFrameParams(x, y, w, h, subBinX, subBinY);
0660         }
0661 
0662         l_Focal->setText(QString("%1mm").arg(m_FocalLength, 0, 'f', 0));
0663         if (m_Aperture > 0)
0664             l_Aperture->setText(QString("%1mm").arg(m_Aperture, 0, 'f', 0));
0665         // FIXME is there a way to know aperture?
0666         else
0667             l_Aperture->setText("DSLR");
0668         l_Reducer->setText(QString("%1x").arg(QString::number(m_Reducer, 'f', 2)));
0669 
0670         if (m_FocalRatio > 0)
0671             l_FbyD->setText(QString("F/%1").arg(m_FocalRatio, 0, 'f', 1));
0672         else if (m_Aperture > 0)
0673             l_FbyD->setText(QString("F/%1").arg(m_FocalLength / m_Aperture, 0, 'f', 1));
0674 
0675         // Pixel scale in arcsec/pixel
0676         pixScaleX = 206264.8062470963552 * ccdPixelSizeX / 1000.0 / effectiveFocaLength;
0677         pixScaleY = 206264.8062470963552 * ccdPixelSizeY / 1000.0 / effectiveFocaLength;
0678 
0679         // FOV in arcmin
0680         double fov_w = (w * pixScaleX) / 60.0;
0681         double fov_h = (h * pixScaleY) / 60.0;
0682 
0683         l_FOV->setText(QString("%1' x %2'").arg(QString::number(fov_w, 'f', 1), QString::number(fov_h, 'f', 1)));
0684     }
0685 }
0686 
0687 bool Guide::setGuider(ISD::Guider * device)
0688 {
0689     if (guiderType != GUIDE_INTERNAL || (m_Guider && device == m_Guider))
0690         return false;
0691 
0692     if (m_Guider)
0693         m_Guider->disconnect(this);
0694 
0695     m_Guider = device;
0696 
0697     if (m_Guider)
0698     {
0699         connect(m_Guider, &ISD::ConcreteDevice::Connected, this, [this]()
0700         {
0701             guideB->setEnabled(true);
0702         });
0703         connect(m_Guider, &ISD::ConcreteDevice::Disconnected, this, [this]()
0704         {
0705             guideB->setEnabled(false);
0706         });
0707     }
0708 
0709     guideB->setEnabled(m_Guider && m_Guider->isConnected());
0710     return true;
0711 }
0712 
0713 bool Guide::setAdaptiveOptics(ISD::AdaptiveOptics * device)
0714 {
0715     if (guiderType != GUIDE_INTERNAL || (m_AO && device == m_AO))
0716         return false;
0717 
0718     if (m_AO)
0719         m_AO->disconnect(this);
0720 
0721     // FIXME AO are not yet utilized property in Guide module
0722     m_AO = device;
0723     return true;
0724 }
0725 
0726 QString Guide::guider()
0727 {
0728     if (guiderType != GUIDE_INTERNAL || m_Guider == nullptr)
0729         return QString();
0730 
0731     return m_Guider->getDeviceName();
0732 }
0733 
0734 bool Guide::capture()
0735 {
0736     buildOperationStack(GUIDE_CAPTURE);
0737 
0738     return executeOperationStack();
0739 }
0740 
0741 bool Guide::captureOneFrame()
0742 {
0743     captureTimeout.stop();
0744 
0745     if (m_Camera == nullptr)
0746         return false;
0747 
0748     if (m_Camera->isConnected() == false)
0749     {
0750         appendLogText(i18n("Error: lost connection to CCD."));
0751         return false;
0752     }
0753 
0754     double seqExpose = guideExposure->value();
0755 
0756     ISD::CameraChip *targetChip = m_Camera->getChip(useGuideHead ? ISD::CameraChip::GUIDE_CCD : ISD::CameraChip::PRIMARY_CCD);
0757 
0758     prepareCapture(targetChip);
0759 
0760     m_GuideView->setBaseSize(guideWidget->size());
0761     setBusy(true);
0762 
0763     // Check if we have a valid frame setting
0764     if (frameSettings.contains(targetChip))
0765     {
0766         QVariantMap settings = frameSettings[targetChip];
0767         targetChip->setFrame(settings["x"].toInt(), settings["y"].toInt(), settings["w"].toInt(),
0768                              settings["h"].toInt());
0769         targetChip->setBinning(settings["binx"].toInt(), settings["biny"].toInt());
0770     }
0771 
0772     connect(m_Camera, &ISD::Camera::newImage, this, &Ekos::Guide::processData, Qt::UniqueConnection);
0773     qCDebug(KSTARS_EKOS_GUIDE) << "Capturing frame...";
0774 
0775     double finalExposure = seqExpose;
0776 
0777     // Increase exposure for calibration frame if we need auto-select a star
0778     // To increase chances we detect one.
0779     if (operationStack.contains(GUIDE_STAR_SELECT) && guideAutoStar->isChecked() &&
0780             !((guiderType == GUIDE_INTERNAL) && internalGuider->SEPMultiStarEnabled()))
0781         finalExposure *= 3;
0782 
0783     // Prevent flicker when processing dark frame by suspending updates
0784     m_GuideView->setProperty("suspended", operationStack.contains(GUIDE_DARK));
0785 
0786     // Timeout is exposure duration + timeout threshold in seconds
0787     captureTimeout.start(finalExposure * 1000 + CAPTURE_TIMEOUT_THRESHOLD);
0788 
0789     targetChip->capture(finalExposure);
0790 
0791     return true;
0792 }
0793 
0794 void Guide::prepareCapture(ISD::CameraChip * targetChip)
0795 {
0796     targetChip->setBatchMode(false);
0797     targetChip->setCaptureMode(FITS_GUIDE);
0798     targetChip->setFrameType(FRAME_LIGHT);
0799     targetChip->setCaptureFilter(FITS_NONE);
0800     m_Camera->setEncodingFormat("FITS");
0801 }
0802 
0803 void Guide::abortExposure()
0804 {
0805     if (m_Camera && guiderType == GUIDE_INTERNAL)
0806     {
0807         captureTimeout.stop();
0808         m_PulseTimer.stop();
0809         ISD::CameraChip *targetChip =
0810             m_Camera->getChip(useGuideHead ? ISD::CameraChip::GUIDE_CCD : ISD::CameraChip::PRIMARY_CCD);
0811         if (targetChip->isCapturing())
0812         {
0813             qCDebug(KSTARS_EKOS_GUIDE) << "Aborting guide capture";
0814             targetChip->abortExposure();
0815         }
0816     }
0817 }
0818 
0819 bool Guide::abort()
0820 {
0821     if (m_Camera && guiderType == GUIDE_INTERNAL)
0822     {
0823         captureTimeout.stop();
0824         m_PulseTimer.stop();
0825         ISD::CameraChip *targetChip =
0826             m_Camera->getChip(useGuideHead ? ISD::CameraChip::GUIDE_CCD : ISD::CameraChip::PRIMARY_CCD);
0827         if (targetChip->isCapturing())
0828             targetChip->abortExposure();
0829     }
0830 
0831     manualDitherB->setEnabled(false);
0832 
0833     setBusy(false);
0834 
0835     switch (m_State)
0836     {
0837         case GUIDE_IDLE:
0838         case GUIDE_CONNECTED:
0839         case GUIDE_DISCONNECTED:
0840             break;
0841 
0842         case GUIDE_CALIBRATING:
0843         case GUIDE_DITHERING:
0844         case GUIDE_STAR_SELECT:
0845         case GUIDE_CAPTURE:
0846         case GUIDE_GUIDING:
0847         case GUIDE_LOOPING:
0848             m_GuiderInstance->abort();
0849             break;
0850 
0851         default:
0852             break;
0853     }
0854 
0855     return true;
0856 }
0857 
0858 void Guide::setBusy(bool enable)
0859 {
0860     if (enable && pi->isAnimated())
0861         return;
0862     else if (enable == false && pi->isAnimated() == false)
0863         return;
0864 
0865     if (enable)
0866     {
0867         clearCalibrationB->setEnabled(false);
0868         guideB->setEnabled(false);
0869         captureB->setEnabled(false);
0870         loopB->setEnabled(false);
0871         guideDarkFrame->setEnabled(false);
0872         guideSubframe->setEnabled(false);
0873         guideAutoStar->setEnabled(false);
0874         stopB->setEnabled(true);
0875         // Optical Train
0876         opticalTrainCombo->setEnabled(false);
0877         trainB->setEnabled(false);
0878 
0879         pi->startAnimation();
0880     }
0881     else
0882     {
0883         if(guiderType != GUIDE_LINGUIDER)
0884         {
0885             captureB->setEnabled(true);
0886             loopB->setEnabled(true);
0887             guideAutoStar->setEnabled(!internalGuider->SEPMultiStarEnabled()); // cf. configSEPMultistarOptions()
0888             if(m_Camera)
0889                 guideSubframe->setEnabled(!internalGuider->SEPMultiStarEnabled()); // cf. configSEPMultistarOptions()
0890         }
0891         if (guiderType == GUIDE_INTERNAL)
0892             guideDarkFrame->setEnabled(true);
0893 
0894         if (calibrationComplete ||
0895                 ((guiderType == GUIDE_INTERNAL) &&
0896                  Options::reuseGuideCalibration() &&
0897                  !Options::serializedCalibration().isEmpty()))
0898             clearCalibrationB->setEnabled(true);
0899         guideB->setEnabled(true);
0900         stopB->setEnabled(false);
0901         pi->stopAnimation();
0902 
0903         // Optical Train
0904         opticalTrainCombo->setEnabled(true);
0905         trainB->setEnabled(true);
0906 
0907         connect(m_GuideView.get(), &FITSView::trackingStarSelected, this, &Ekos::Guide::setTrackingStar, Qt::UniqueConnection);
0908     }
0909 }
0910 
0911 void Guide::processCaptureTimeout()
0912 {
0913     auto restartExposure = [&]()
0914     {
0915         appendLogText(i18n("Exposure timeout. Restarting exposure..."));
0916         m_Camera->setEncodingFormat("FITS");
0917         ISD::CameraChip *targetChip = m_Camera->getChip(useGuideHead ? ISD::CameraChip::GUIDE_CCD : ISD::CameraChip::PRIMARY_CCD);
0918         targetChip->abortExposure();
0919         prepareCapture(targetChip);
0920         connect(m_Camera, &ISD::Camera::newImage, this, &Ekos::Guide::processData, Qt::UniqueConnection);
0921         connect(m_Camera, &ISD::Camera::propertyUpdated, this, &Ekos::Guide::updateProperty, Qt::UniqueConnection);
0922         connect(m_Camera, &ISD::Camera::newExposureValue, this, &Ekos::Guide::checkExposureValue, Qt::UniqueConnection);
0923         targetChip->capture(guideExposure->value());
0924         captureTimeout.start(guideExposure->value() * 1000 + CAPTURE_TIMEOUT_THRESHOLD);
0925     };
0926 
0927     m_CaptureTimeoutCounter++;
0928 
0929     if (m_Camera == nullptr)
0930         return;
0931 
0932     if (m_DeviceRestartCounter >= 3)
0933     {
0934         m_CaptureTimeoutCounter = 0;
0935         m_DeviceRestartCounter = 0;
0936         if (m_State == GUIDE_GUIDING)
0937             appendLogText(i18n("Exposure timeout. Aborting Autoguide."));
0938         else if (m_State == GUIDE_DITHERING)
0939             appendLogText(i18n("Exposure timeout. Aborting Dithering."));
0940         else if (m_State == GUIDE_CALIBRATING)
0941             appendLogText(i18n("Exposure timeout. Aborting Calibration."));
0942 
0943         captureTimeout.stop();
0944         abort();
0945         return;
0946     }
0947 
0948     if (m_CaptureTimeoutCounter > 1)
0949     {
0950         QString camera = m_Camera->getDeviceName();
0951         QString via = m_Guider ? m_Guider->getDeviceName() : "";
0952         ISD::CameraChip *targetChip = m_Camera->getChip(useGuideHead ? ISD::CameraChip::GUIDE_CCD : ISD::CameraChip::PRIMARY_CCD);
0953         QVariantMap settings = frameSettings[targetChip];
0954         emit driverTimedout(camera);
0955         QTimer::singleShot(5000, [ &, camera, settings]()
0956         {
0957             m_DeviceRestartCounter++;
0958             reconnectDriver(camera, settings);
0959         });
0960         return;
0961     }
0962     else
0963         restartExposure();
0964 }
0965 
0966 void Guide::reconnectDriver(const QString &camera, QVariantMap settings)
0967 {
0968     if (m_Camera && m_Camera->getDeviceName() == camera)
0969     {
0970         // Set state to IDLE so that checkCamera is processed since it will not process GUIDE_GUIDING state.
0971         Ekos::GuideState currentState = m_State;
0972         m_State = GUIDE_IDLE;
0973         checkCamera();
0974         // Restore state to last state.
0975         m_State = currentState;
0976 
0977         if (guiderType == GUIDE_INTERNAL)
0978         {
0979             // Reset the frame settings to the restarted camera once again before capture.
0980             ISD::CameraChip *targetChip = m_Camera->getChip(useGuideHead ? ISD::CameraChip::GUIDE_CCD : ISD::CameraChip::PRIMARY_CCD);
0981             frameSettings[targetChip] = settings;
0982             // restart capture
0983             m_CaptureTimeoutCounter = 0;
0984             captureOneFrame();
0985         }
0986 
0987         return;
0988     }
0989 
0990     QTimer::singleShot(5000, this, [ &, camera, settings]()
0991     {
0992         reconnectDriver(camera, settings);
0993     });
0994 }
0995 
0996 void Guide::processData(const QSharedPointer<FITSData> &data)
0997 {
0998     ISD::CameraChip *targetChip = m_Camera->getChip(useGuideHead ? ISD::CameraChip::GUIDE_CCD : ISD::CameraChip::PRIMARY_CCD);
0999     if (targetChip->getCaptureMode() != FITS_GUIDE)
1000     {
1001         if (data)
1002         {
1003             QString blobInfo = QString("{Device: %1 Property: %2 Element: %3 Chip: %4}").arg(data->property("device").toString())
1004                                .arg(data->property("blobVector").toString())
1005                                .arg(data->property("blobElement").toString())
1006                                .arg(data->property("chip").toInt());
1007 
1008             qCWarning(KSTARS_EKOS_GUIDE) << blobInfo << "Ignoring Received FITS as it has the wrong capture mode" <<
1009                                          targetChip->getCaptureMode();
1010         }
1011 
1012         return;
1013     }
1014 
1015     if (data)
1016     {
1017         m_GuideView->loadData(data);
1018         m_ImageData = data;
1019     }
1020     else
1021         m_ImageData.reset();
1022 
1023     if (guiderType == GUIDE_INTERNAL)
1024         internalGuider->setImageData(m_ImageData);
1025 
1026     captureTimeout.stop();
1027     m_CaptureTimeoutCounter = 0;
1028 
1029     disconnect(m_Camera, &ISD::Camera::newImage, this, &Ekos::Guide::processData);
1030 
1031     // qCDebug(KSTARS_EKOS_GUIDE) << "Received guide frame.";
1032 
1033     int subBinX = 1, subBinY = 1;
1034     targetChip->getBinning(&subBinX, &subBinY);
1035 
1036     if (starCenter.x() == 0 && starCenter.y() == 0)
1037     {
1038         int x = 0, y = 0, w = 0, h = 0;
1039 
1040         if (frameSettings.contains(targetChip))
1041         {
1042             QVariantMap settings = frameSettings[targetChip];
1043             x                    = settings["x"].toInt();
1044             y                    = settings["y"].toInt();
1045             w                    = settings["w"].toInt();
1046             h                    = settings["h"].toInt();
1047         }
1048         else
1049             targetChip->getFrame(&x, &y, &w, &h);
1050 
1051         starCenter.setX(w / (2 * subBinX));
1052         starCenter.setY(h / (2 * subBinY));
1053         starCenter.setZ(subBinX);
1054     }
1055 
1056     syncTrackingBoxPosition();
1057     // qCDebug(KSTARS_EKOS_GUIDE) << "Tracking box position synched.";
1058 
1059     setCaptureComplete();
1060     // qCDebug(KSTARS_EKOS_GUIDE) << "Capture complete.";
1061 
1062 }
1063 
1064 void Guide::setCaptureComplete()
1065 {
1066     if (!m_GuideView.isNull())
1067         m_GuideView->clearNeighbors();
1068 
1069     DarkLibrary::Instance()->disconnect(this);
1070 
1071     if (operationStack.isEmpty() == false)
1072     {
1073         executeOperationStack();
1074         return;
1075     }
1076 
1077     qCDebug(KSTARS_EKOS_GUIDE) << "Capture complete, state=" << getGuideStatusString(m_State);
1078     switch (m_State)
1079     {
1080         case GUIDE_IDLE:
1081         case GUIDE_ABORTED:
1082         case GUIDE_CONNECTED:
1083         case GUIDE_DISCONNECTED:
1084         case GUIDE_CALIBRATION_SUCCESS:
1085         case GUIDE_CALIBRATION_ERROR:
1086         case GUIDE_DITHERING_ERROR:
1087             setBusy(false);
1088             break;
1089 
1090         case GUIDE_CAPTURE:
1091             qCDebug(KSTARS_EKOS_GUIDE) << "Guiding capture complete.";
1092             m_State = GUIDE_IDLE;
1093             emit newStatus(m_State);
1094             setBusy(false);
1095             break;
1096 
1097         case GUIDE_LOOPING:
1098             capture();
1099             break;
1100 
1101         case GUIDE_CALIBRATING:
1102             m_GuiderInstance->calibrate();
1103             break;
1104 
1105         case GUIDE_GUIDING:
1106             m_GuiderInstance->guide();
1107             break;
1108 
1109         case GUIDE_DITHERING:
1110             m_GuiderInstance->dither(Options::ditherPixels());
1111             break;
1112 
1113         // Feature only of internal guider
1114         case GUIDE_MANUAL_DITHERING:
1115             dynamic_cast<InternalGuider*>(m_GuiderInstance)->processManualDithering();
1116             break;
1117 
1118         case GUIDE_REACQUIRE:
1119             m_GuiderInstance->reacquire();
1120             break;
1121 
1122         case GUIDE_DITHERING_SETTLE:
1123             if (Options::ditherNoGuiding())
1124                 return;
1125             capture();
1126             break;
1127 
1128         case GUIDE_SUSPENDED:
1129             if (Options::gPGEnabled())
1130                 m_GuiderInstance->guide();
1131             break;
1132 
1133         default:
1134             break;
1135     }
1136 
1137     emit newImage(m_GuideView);
1138     emit newStarPixmap(m_GuideView->getTrackingBoxPixmap(10));
1139 }
1140 
1141 void Guide::appendLogText(const QString &text)
1142 {
1143     m_LogText.insert(0, i18nc("log entry; %1 is the date, %2 is the text", "%1 %2",
1144                               KStarsData::Instance()->lt().toString("yyyy-MM-ddThh:mm:ss"), text));
1145 
1146     qCInfo(KSTARS_EKOS_GUIDE) << text;
1147 
1148     emit newLog(text);
1149 }
1150 
1151 void Guide::clearLog()
1152 {
1153     m_LogText.clear();
1154     emit newLog(QString());
1155 }
1156 
1157 void Guide::setDECSwap(bool enable)
1158 {
1159     if (m_Guider == nullptr || m_GuiderInstance == nullptr)
1160         return;
1161 
1162     if (guiderType == GUIDE_INTERNAL)
1163     {
1164         dynamic_cast<InternalGuider *>(m_GuiderInstance)->setDECSwap(enable);
1165         m_Guider->setDECSwap(enable);
1166     }
1167 }
1168 
1169 bool Guide::sendMultiPulse(GuideDirection ra_dir, int ra_msecs, GuideDirection dec_dir, int dec_msecs,
1170                            CaptureAfterPulses followWithCapture)
1171 {
1172     if (m_Guider == nullptr || (ra_dir == NO_DIR && dec_dir == NO_DIR))
1173         return false;
1174 
1175     if (followWithCapture == StartCaptureAfterPulses)
1176     {
1177         // Delay next capture by user-configurable delay.
1178         // If user delay is zero, delay by the pulse length plus 100 milliseconds before next capture.
1179         auto ms = std::max(ra_msecs, dec_msecs) + 100;
1180         auto delay = std::max(static_cast<int>(guideDelay->value() * 1000), ms);
1181 
1182         m_PulseTimer.start(delay);
1183     }
1184     return m_Guider->doPulse(ra_dir, ra_msecs, dec_dir, dec_msecs);
1185 }
1186 
1187 bool Guide::sendSinglePulse(GuideDirection dir, int msecs, CaptureAfterPulses followWithCapture)
1188 {
1189     if (m_Guider == nullptr || dir == NO_DIR)
1190         return false;
1191 
1192     if (followWithCapture == StartCaptureAfterPulses)
1193     {
1194         // Delay next capture by user-configurable delay.
1195         // If user delay is zero, delay by the pulse length plus 100 milliseconds before next capture.
1196         auto ms = msecs + 100;
1197         auto delay = std::max(static_cast<int>(guideDelay->value() * 1000), ms);
1198 
1199         m_PulseTimer.start(delay);
1200     }
1201 
1202     return m_Guider->doPulse(dir, msecs);
1203 }
1204 
1205 bool Guide::calibrate()
1206 {
1207     // Set status to idle and let the operations change it as they get executed
1208     m_State = GUIDE_IDLE;
1209     qCDebug(KSTARS_EKOS_GUIDE) << "Calibrating...";
1210     emit newStatus(m_State);
1211 
1212     if (guiderType == GUIDE_INTERNAL)
1213     {
1214         if (!m_Camera)
1215         {
1216             qCCritical(KSTARS_EKOS_GUIDE) << "No camera detected. Check optical trains.";
1217             return false;
1218         }
1219 
1220         auto targetChip = m_Camera->getChip(useGuideHead ? ISD::CameraChip::GUIDE_CCD : ISD::CameraChip::PRIMARY_CCD);
1221 
1222         if (frameSettings.contains(targetChip))
1223         {
1224             targetChip->resetFrame();
1225             int x, y, w, h;
1226             targetChip->getFrame(&x, &y, &w, &h);
1227             QVariantMap settings      = frameSettings[targetChip];
1228             settings["x"]             = x;
1229             settings["y"]             = y;
1230             settings["w"]             = w;
1231             settings["h"]             = h;
1232             frameSettings[targetChip] = settings;
1233 
1234             subFramed = false;
1235         }
1236     }
1237 
1238     buildOperationStack(GUIDE_CALIBRATING);
1239 
1240     executeOperationStack();
1241 
1242     if (m_Camera && m_Guider)
1243     {
1244         qCDebug(KSTARS_EKOS_GUIDE) << "Starting calibration using camera:" << m_Camera->getDeviceName() << "via" <<
1245                                    m_Guider->getDeviceName();
1246     }
1247 
1248     return true;
1249 }
1250 
1251 bool Guide::guide()
1252 {
1253     auto executeGuide = [this]()
1254     {
1255         if(guiderType != GUIDE_PHD2)
1256         {
1257             if (calibrationComplete == false)
1258             {
1259                 calibrate();
1260                 return;
1261             }
1262         }
1263 
1264         m_GuiderInstance->guide();
1265 
1266         //If PHD2 gets a Guide command and it is looping, it will accept a lock position
1267         //but if it was not looping it will ignore the lock position and do an auto star automatically
1268         //This is not the default behavior in Ekos if auto star is not selected.
1269         //This gets around that by noting the position of the tracking box, and enforcing it after the state switches to guide.
1270         if(!guideAutoStar->isChecked())
1271         {
1272             if(guiderType == GUIDE_PHD2 && m_GuideView->isTrackingBoxEnabled())
1273             {
1274                 double x = starCenter.x();
1275                 double y = starCenter.y();
1276 
1277                 if(!m_ImageData.isNull())
1278                 {
1279                     if(m_ImageData->width() > 50)
1280                     {
1281                         guideConnect = connect(this, &Guide::newStatus, this, [this, x, y](Ekos::GuideState newState)
1282                         {
1283                             if(newState == GUIDE_GUIDING)
1284                             {
1285                                 phd2Guider->setLockPosition(x, y);
1286                                 disconnect(guideConnect);
1287                             }
1288                         });
1289                     }
1290                 }
1291             }
1292         }
1293     };
1294 
1295     if (m_MountStatus == ISD::Mount::MOUNT_PARKED)
1296     {
1297         KSMessageBox::Instance()->sorry(i18n("The mount is parked. Unpark to start guiding."));
1298         return false;
1299     }
1300 
1301     executeGuide();
1302     return true;
1303 }
1304 
1305 bool Guide::dither()
1306 {
1307     if (Options::ditherNoGuiding() && m_State == GUIDE_IDLE)
1308     {
1309         nonGuidedDither();
1310         return true;
1311     }
1312 
1313     if (m_State == GUIDE_DITHERING || m_State == GUIDE_DITHERING_SETTLE)
1314         return true;
1315 
1316     //This adds a dither text item to the graph where dithering occurred.
1317     double time = guideTimer.elapsed() / 1000.0;
1318     QCPItemText *ditherLabel = new QCPItemText(driftGraph);
1319     ditherLabel->setPositionAlignment(Qt::AlignVCenter | Qt::AlignLeft);
1320     ditherLabel->position->setType(QCPItemPosition::ptPlotCoords);
1321     ditherLabel->position->setCoords(time, 1.5);
1322     ditherLabel->setColor(Qt::white);
1323     ditherLabel->setBrush(Qt::NoBrush);
1324     ditherLabel->setPen(Qt::NoPen);
1325     ditherLabel->setText("Dither");
1326     ditherLabel->setFont(QFont(font().family(), 10));
1327 
1328     if (guiderType == GUIDE_INTERNAL && !Options::ditherWithOnePulse())
1329     {
1330         if (m_State != GUIDE_GUIDING)
1331             capture();
1332 
1333         setStatus(GUIDE_DITHERING);
1334 
1335         return true;
1336     }
1337     else
1338         return m_GuiderInstance->dither(Options::ditherPixels());
1339 }
1340 
1341 bool Guide::suspend()
1342 {
1343     if (m_State == GUIDE_SUSPENDED)
1344         return true;
1345     else if (m_State >= GUIDE_CAPTURE)
1346         return m_GuiderInstance->suspend();
1347     else
1348         return false;
1349 }
1350 
1351 bool Guide::resume()
1352 {
1353     if (m_State == GUIDE_GUIDING)
1354         return true;
1355     else if (m_State == GUIDE_SUSPENDED)
1356         return m_GuiderInstance->resume();
1357     else
1358         return false;
1359 }
1360 
1361 void Guide::setCaptureStatus(CaptureState newState)
1362 {
1363     switch (newState)
1364     {
1365         case CAPTURE_DITHERING:
1366             dither();
1367             break;
1368         case CAPTURE_IDLE:
1369         case CAPTURE_ABORTED:
1370             // We need to reset the non guided dithering status every time a new capture task is started (and not for every single capture).
1371             // The non dithering logic is a bit convoluted and controlled by the Capture module,
1372             // which calls Guide::setCaptureStatus(CAPTURE_DITHERING) when it wants guide to dither.
1373             // It actually calls newStatus(CAPTURE_DITHERING) in Capture::checkDithering(), but manager.cpp in Manager::connectModules() connects that to Guide::setCaptureStatus()).
1374             // So the only way to reset the non guided dithering prior to a new capture task is to put it here, when the Capture status moves to IDLE or ABORTED state.
1375             resetNonGuidedDither();
1376             break;
1377         default:
1378             break;
1379     }
1380 }
1381 
1382 void Guide::setPierSide(ISD::Mount::PierSide newSide)
1383 {
1384     m_GuiderInstance->setPierSide(newSide);
1385 
1386     // If pier side changes in internal guider
1387     // and calibration was already done
1388     // then let's swap
1389     if (guiderType == GUIDE_INTERNAL &&
1390             m_State != GUIDE_GUIDING &&
1391             m_State != GUIDE_CALIBRATING &&
1392             calibrationComplete)
1393     {
1394         // Couldn't restore an old calibration if we call clearCalibration().
1395         if (Options::reuseGuideCalibration())
1396             calibrationComplete = false;
1397         else
1398         {
1399             clearCalibration();
1400             appendLogText(i18n("Pier side change detected. Clearing calibration."));
1401         }
1402     }
1403 }
1404 
1405 void Guide::setMountStatus(ISD::Mount::Status newState)
1406 {
1407     m_MountStatus = newState;
1408 
1409     if (newState == ISD::Mount::MOUNT_PARKING || newState == ISD::Mount::MOUNT_SLEWING)
1410     {
1411         // reset the calibration if "Always reset calibration" is selected and the mount moves
1412         if (Options::resetGuideCalibration())
1413         {
1414             appendLogText(i18n("Mount is moving. Resetting calibration..."));
1415             clearCalibration();
1416         }
1417         else if (Options::reuseGuideCalibration() && (guiderType == GUIDE_INTERNAL))
1418         {
1419             // It will restore it with the reused one, and this way it reselects a guide star.
1420             calibrationComplete = false;
1421         }
1422         // GPG guide algorithm should be reset on any slew.
1423         if (Options::gPGEnabled())
1424             m_GuiderInstance->resetGPG();
1425 
1426         // If we're guiding, and the mount either slews or parks, then we abort.
1427         if (m_State == GUIDE_GUIDING || m_State == GUIDE_DITHERING)
1428         {
1429             if (newState == ISD::Mount::MOUNT_PARKING)
1430                 appendLogText(i18n("Mount is parking. Aborting guide..."));
1431             else
1432                 appendLogText(i18n("Mount is slewing. Aborting guide..."));
1433 
1434             abort();
1435         }
1436     }
1437 
1438     if (guiderType != GUIDE_INTERNAL)
1439         return;
1440 
1441     switch (newState)
1442     {
1443         case ISD::Mount::MOUNT_SLEWING:
1444         case ISD::Mount::MOUNT_PARKING:
1445         case ISD::Mount::MOUNT_MOVING:
1446             captureB->setEnabled(false);
1447             loopB->setEnabled(false);
1448             clearCalibrationB->setEnabled(false);
1449             manualPulseB->setEnabled(false);
1450             break;
1451 
1452         default:
1453             if (pi->isAnimated() == false)
1454             {
1455                 captureB->setEnabled(true);
1456                 loopB->setEnabled(true);
1457                 clearCalibrationB->setEnabled(true);
1458                 manualPulseB->setEnabled(true);
1459             }
1460     }
1461 }
1462 
1463 void Guide::setMountCoords(const SkyPoint &position, ISD::Mount::PierSide pierSide, const dms &ha)
1464 {
1465     Q_UNUSED(ha);
1466     m_GuiderInstance->setMountCoords(position, pierSide);
1467     m_ManaulPulse->setMountCoords(position);
1468 }
1469 
1470 void Guide::setExposure(double value)
1471 {
1472     guideExposure->setValue(value);
1473 }
1474 
1475 void Guide::setSubFrameEnabled(bool enable)
1476 {
1477     if (guideSubframe->isChecked() != enable)
1478         guideSubframe->setChecked(enable);
1479     if(guiderType == GUIDE_PHD2)
1480         setExternalGuiderBLOBEnabled(!enable);
1481 }
1482 
1483 void Guide::setAutoStarEnabled(bool enable)
1484 {
1485     if(guiderType == GUIDE_INTERNAL)
1486         guideAutoStar->setChecked(enable);
1487 }
1488 
1489 void Guide::clearCalibration()
1490 {
1491     calibrationComplete = false;
1492 
1493     m_GuiderInstance->clearCalibration();
1494 
1495     appendLogText(i18n("Calibration is cleared."));
1496 }
1497 
1498 void Guide::setStatus(Ekos::GuideState newState)
1499 {
1500     if (newState == m_State)
1501     {
1502         // pass through the aborted state
1503         if (newState == GUIDE_ABORTED)
1504             emit newStatus(m_State);
1505         return;
1506     }
1507 
1508     GuideState previousState = m_State;
1509 
1510     m_State = newState;
1511     emit newStatus(m_State);
1512 
1513     switch (m_State)
1514     {
1515         case GUIDE_CONNECTED:
1516             appendLogText(i18n("External guider connected."));
1517             externalConnectB->setEnabled(false);
1518             externalDisconnectB->setEnabled(true);
1519             clearCalibrationB->setEnabled(true);
1520             guideB->setEnabled(true);
1521 
1522             if(guiderType == GUIDE_PHD2)
1523             {
1524                 captureB->setEnabled(true);
1525                 loopB->setEnabled(true);
1526                 guideAutoStar->setEnabled(true);
1527                 configurePHD2Camera();
1528                 setExternalGuiderBLOBEnabled(!guideSubframe->isChecked());
1529                 guideSquareSize->setEnabled(true);
1530             }
1531             break;
1532 
1533         case GUIDE_DISCONNECTED:
1534             appendLogText(i18n("External guider disconnected."));
1535             setBusy(false); //This needs to come before caputureB since it will set it to enabled again.
1536             externalConnectB->setEnabled(true);
1537             externalDisconnectB->setEnabled(false);
1538             clearCalibrationB->setEnabled(false);
1539             guideB->setEnabled(false);
1540             captureB->setEnabled(false);
1541             loopB->setEnabled(false);
1542             guideAutoStar->setEnabled(false);
1543             guideSquareSize->setEnabled(false);
1544             //setExternalGuiderBLOBEnabled(true);
1545 #ifdef Q_OS_OSX
1546             repaint(); //This is a band-aid for a bug in QT 5.10.0
1547 #endif
1548             break;
1549 
1550         case GUIDE_CALIBRATION_SUCCESS:
1551             appendLogText(i18n("Calibration completed."));
1552             manualPulseB->setEnabled(true);
1553             calibrationComplete = true;
1554 
1555             if(guiderType !=
1556                     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.
1557                 guide();
1558             break;
1559 
1560         case GUIDE_IDLE:
1561         case GUIDE_CALIBRATION_ERROR:
1562             setBusy(false);
1563             manualDitherB->setEnabled(false);
1564             manualPulseB->setEnabled(true);
1565             break;
1566 
1567         case GUIDE_CALIBRATING:
1568             clearCalibrationGraphs();
1569             appendLogText(i18n("Calibration started."));
1570             setBusy(true);
1571             manualPulseB->setEnabled(false);
1572             break;
1573 
1574         case GUIDE_GUIDING:
1575             if (previousState == GUIDE_SUSPENDED || previousState == GUIDE_DITHERING_SUCCESS)
1576                 appendLogText(i18n("Guiding resumed."));
1577             else
1578             {
1579                 appendLogText(i18n("Autoguiding started."));
1580                 setBusy(true);
1581 
1582                 clearGuideGraphs();
1583                 guideTimer.start();
1584                 driftGraph->resetTimer();
1585                 driftGraph->refreshColorScheme();
1586             }
1587             manualDitherB->setEnabled(true);
1588             break;
1589 
1590         case GUIDE_ABORTED:
1591             appendLogText(i18n("Autoguiding aborted."));
1592             setBusy(false);
1593             break;
1594 
1595         case GUIDE_SUSPENDED:
1596             appendLogText(i18n("Guiding suspended."));
1597             break;
1598 
1599         case GUIDE_REACQUIRE:
1600             if (guiderType == GUIDE_INTERNAL)
1601                 capture();
1602             break;
1603 
1604         case GUIDE_MANUAL_DITHERING:
1605             appendLogText(i18n("Manual dithering in progress."));
1606             break;
1607 
1608         case GUIDE_DITHERING:
1609             appendLogText(i18n("Dithering in progress."));
1610             break;
1611 
1612         case GUIDE_DITHERING_SETTLE:
1613             appendLogText(i18np("Post-dither settling for %1 second...", "Post-dither settling for %1 seconds...",
1614                                 Options::ditherSettle()));
1615             break;
1616 
1617         case GUIDE_DITHERING_ERROR:
1618             appendLogText(i18n("Dithering failed."));
1619             // LinGuider guide continue after dithering failure
1620             if (guiderType != GUIDE_LINGUIDER)
1621             {
1622                 //state = GUIDE_IDLE;
1623                 m_State = GUIDE_ABORTED;
1624                 setBusy(false);
1625             }
1626             break;
1627 
1628         case GUIDE_DITHERING_SUCCESS:
1629             appendLogText(i18n("Dithering completed successfully."));
1630             // Go back to guiding state immediately if using regular guider
1631             if (Options::ditherNoGuiding() == false)
1632             {
1633                 setStatus(GUIDE_GUIDING);
1634                 // Only capture again if we are using internal guider
1635                 if (guiderType == GUIDE_INTERNAL)
1636                     capture();
1637             }
1638             break;
1639         default:
1640             break;
1641     }
1642 }
1643 
1644 void Guide::updateCCDBin(int index)
1645 {
1646     if (m_Camera == nullptr || guiderType != GUIDE_INTERNAL)
1647         return;
1648 
1649     ISD::CameraChip *targetChip = m_Camera->getChip(useGuideHead ? ISD::CameraChip::GUIDE_CCD : ISD::CameraChip::PRIMARY_CCD);
1650 
1651     targetChip->setBinning(index + 1, index + 1);
1652     guideBinIndex = index;
1653 
1654     QVariantMap settings      = frameSettings[targetChip];
1655     settings["binx"]          = index + 1;
1656     settings["biny"]          = index + 1;
1657     frameSettings[targetChip] = settings;
1658 
1659     m_GuiderInstance->setFrameParams(settings["x"].toInt(), settings["y"].toInt(), settings["w"].toInt(), settings["h"].toInt(),
1660                                      settings["binx"].toInt(), settings["biny"].toInt());
1661 }
1662 
1663 void Guide::updateProperty(INDI::Property prop)
1664 {
1665     if (m_Camera == nullptr || (prop.getDeviceName() != m_Camera->getDeviceName()) || guiderType != GUIDE_INTERNAL)
1666         return;
1667 
1668     if ((prop.isNameMatch("CCD_BINNING") && useGuideHead == false) ||
1669             (prop.isNameMatch("GUIDER_BINNING") && useGuideHead))
1670     {
1671         auto nvp = prop.getNumber();
1672         auto value = nvp->at(0)->getValue();
1673         if (guideBinIndex > (value - 1)) // INDI driver reports not supported binning
1674         {
1675             appendLogText(i18n("%1x%1 guide binning is not supported.", guideBinIndex + 1));
1676             guideBinning->setCurrentIndex( value - 1 );
1677             updateSetting("guideBinning", guideBinning->currentText());
1678         }
1679         else
1680         {
1681             guideBinning->setCurrentIndex(guideBinIndex);
1682         }
1683     }
1684 }
1685 
1686 void Guide::checkExposureValue(ISD::CameraChip * targetChip, double exposure, IPState expState)
1687 {
1688     // Ignore if not using internal guider, or chip belongs to a different camera.
1689     if (guiderType != GUIDE_INTERNAL || targetChip->getCCD() != m_Camera)
1690         return;
1691 
1692     INDI_UNUSED(exposure);
1693 
1694     if (expState == IPS_ALERT &&
1695             ((m_State == GUIDE_GUIDING) || (m_State == GUIDE_DITHERING) || (m_State == GUIDE_CALIBRATING)))
1696     {
1697         appendLogText(i18n("Exposure failed. Restarting exposure..."));
1698         m_Camera->setEncodingFormat("FITS");
1699         targetChip->capture(guideExposure->value());
1700     }
1701 }
1702 
1703 void Guide::configSEPMultistarOptions()
1704 {
1705     // SEP MultiStar always uses an automated guide star & doesn't subframe.
1706     if (internalGuider->SEPMultiStarEnabled())
1707     {
1708         guideSubframe->setChecked(false);
1709         guideSubframe->setEnabled(false);
1710         guideAutoStar->setChecked(true);
1711         guideAutoStar->setEnabled(false);
1712     }
1713     else
1714     {
1715         guideAutoStar->setEnabled(true);
1716         guideSubframe->setEnabled(true);
1717 
1718         auto subframed = m_Settings["guideSubframe"];
1719         if (subframed.isValid())
1720             guideSubframe->setChecked(subframed.toBool());
1721 
1722         auto autostar = m_Settings["guideAutoStar"];
1723         if (autostar.isValid())
1724             guideAutoStar->setChecked(autostar.toBool());
1725     }
1726 }
1727 
1728 void Guide::setDarkFrameEnabled(bool enable)
1729 {
1730     if (guideDarkFrame->isChecked() != enable)
1731         guideDarkFrame->setChecked(enable);
1732 }
1733 
1734 void Guide::saveDefaultGuideExposure()
1735 {
1736     if(guiderType == GUIDE_PHD2)
1737 
1738         phd2Guider->requestSetExposureTime(guideExposure->value() * 1000);
1739     else if (guiderType == GUIDE_INTERNAL)
1740     {
1741         internalGuider->setExposureTime();
1742     }
1743 }
1744 
1745 void Guide::setStarPosition(const QVector3D &newCenter, bool updateNow)
1746 {
1747     starCenter.setX(newCenter.x());
1748     starCenter.setY(newCenter.y());
1749     if (newCenter.z() > 0)
1750         starCenter.setZ(newCenter.z());
1751 
1752     if (updateNow)
1753         syncTrackingBoxPosition();
1754 }
1755 
1756 void Guide::syncTrackingBoxPosition()
1757 {
1758     if(!m_Camera || guiderType == GUIDE_LINGUIDER)
1759         return;
1760 
1761     if(guiderType == GUIDE_PHD2)
1762     {
1763         //This way it won't set the tracking box on the Guide Star Image.
1764         if(!m_ImageData.isNull())
1765         {
1766             if(m_ImageData->width() < 50)
1767             {
1768                 m_GuideView->setTrackingBoxEnabled(false);
1769                 return;
1770             }
1771         }
1772     }
1773 
1774     ISD::CameraChip *targetChip = m_Camera->getChip(useGuideHead ? ISD::CameraChip::GUIDE_CCD : ISD::CameraChip::PRIMARY_CCD);
1775     Q_ASSERT(targetChip);
1776 
1777     int subBinX = 1, subBinY = 1;
1778     targetChip->getBinning(&subBinX, &subBinY);
1779 
1780     if (starCenter.isNull() == false)
1781     {
1782         double boxSize = guideSquareSize->currentText().toInt();
1783         int x, y, w, h;
1784         targetChip->getFrame(&x, &y, &w, &h);
1785         // If box size is larger than image size, set it to lower index
1786         if (boxSize / subBinX >= w || boxSize / subBinY >= h)
1787         {
1788             int newIndex = guideSquareSize->currentIndex() - 1;
1789             if (newIndex >= 0)
1790                 guideSquareSize->setCurrentIndex(newIndex);
1791             return;
1792         }
1793 
1794         // If binning changed, update coords accordingly
1795         if (subBinX != starCenter.z())
1796         {
1797             if (starCenter.z() > 0)
1798             {
1799                 starCenter.setX(starCenter.x() * (starCenter.z() / subBinX));
1800                 starCenter.setY(starCenter.y() * (starCenter.z() / subBinY));
1801             }
1802 
1803             starCenter.setZ(subBinX);
1804         }
1805 
1806         QRect starRect = QRect(starCenter.x() - boxSize / (2 * subBinX), starCenter.y() - boxSize / (2 * subBinY),
1807                                boxSize / subBinX, boxSize / subBinY);
1808         m_GuideView->setTrackingBoxEnabled(true);
1809         m_GuideView->setTrackingBox(starRect);
1810     }
1811 }
1812 
1813 bool Guide::setGuiderType(int type)
1814 {
1815     // Use default guider option
1816     if (type == -1)
1817         type = Options::guiderType();
1818     else if (type == guiderType)
1819         return true;
1820 
1821     if (m_State == GUIDE_CALIBRATING || m_State == GUIDE_GUIDING || m_State == GUIDE_DITHERING)
1822     {
1823         appendLogText(i18n("Cannot change guider type while active."));
1824         return false;
1825     }
1826 
1827     if (m_GuiderInstance != nullptr)
1828     {
1829         // Disconnect from host
1830         if (m_GuiderInstance->isConnected())
1831             m_GuiderInstance->Disconnect();
1832 
1833         // Disconnect signals
1834         m_GuiderInstance->disconnect();
1835     }
1836 
1837     guiderType = static_cast<GuiderType>(type);
1838 
1839     switch (type)
1840     {
1841         case GUIDE_INTERNAL:
1842         {
1843             connect(internalGuider, &InternalGuider::newMultiPulse, this, &Guide::sendMultiPulse);
1844             connect(internalGuider, &InternalGuider::newSinglePulse, this, &Guide::sendSinglePulse);
1845             connect(internalGuider, &InternalGuider::DESwapChanged, this, &Guide::setDECSwap);
1846             connect(internalGuider, &InternalGuider::newStarPixmap, this, &Guide::newStarPixmap);
1847 
1848             m_GuiderInstance = internalGuider;
1849 
1850             internalGuider->setSquareAlgorithm(opsGuide->kcfg_GuideAlgorithm->currentIndex());
1851 
1852             clearCalibrationB->setEnabled(true);
1853             guideB->setEnabled(true);
1854             captureB->setEnabled(true);
1855             loopB->setEnabled(true);
1856 
1857             configSEPMultistarOptions();
1858             guideDarkFrame->setEnabled(true);
1859 
1860             guideExposure->setEnabled(true);
1861             guideBinning->setEnabled(true);
1862             guideSquareSize->setEnabled(true);
1863 
1864             externalConnectB->setEnabled(false);
1865             externalDisconnectB->setEnabled(false);
1866 
1867             opsGuide->controlGroup->setEnabled(true);
1868             infoGroup->setEnabled(true);
1869             l_Aperture->setEnabled(true);
1870             l_FOV->setEnabled(true);
1871             l_FbyD->setEnabled(true);
1872             l_Focal->setEnabled(true);
1873             driftGraphicsGroup->setEnabled(true);
1874 
1875             updateGuideParams();
1876         }
1877         break;
1878 
1879         case GUIDE_PHD2:
1880             if (phd2Guider.isNull())
1881                 phd2Guider = new PHD2();
1882 
1883             m_GuiderInstance = phd2Guider;
1884             phd2Guider->setGuideView(m_GuideView);
1885 
1886             connect(phd2Guider, SIGNAL(newStarPixmap(QPixmap &)), this, SIGNAL(newStarPixmap(QPixmap &)));
1887 
1888             clearCalibrationB->setEnabled(true);
1889             captureB->setEnabled(false);
1890             loopB->setEnabled(false);
1891             guideDarkFrame->setEnabled(false);
1892             guideSubframe->setEnabled(false);
1893             guideAutoStar->setEnabled(false);
1894             guideB->setEnabled(false); //This will be enabled later when equipment connects (or not)
1895             externalConnectB->setEnabled(false);
1896 
1897             rAGuideEnabled->setEnabled(false);
1898             eastRAGuideEnabled->setEnabled(false);
1899             westRAGuideEnabled->setEnabled(false);
1900 
1901             opsGuide->controlGroup->setEnabled(false);
1902             infoGroup->setEnabled(true);
1903             l_Aperture->setEnabled(false);
1904             l_FOV->setEnabled(false);
1905             l_FbyD->setEnabled(false);
1906             l_Focal->setEnabled(false);
1907             driftGraphicsGroup->setEnabled(true);
1908 
1909             guideExposure->setEnabled(true);
1910             guideBinning->setEnabled(false);
1911             guideSquareSize->setEnabled(false);
1912 
1913             if (Options::resetGuideCalibration())
1914                 appendLogText(i18n("Warning: Reset Guiding Calibration is enabled. It is recommended to turn this option off for PHD2."));
1915 
1916             updateGuideParams();
1917             break;
1918 
1919         case GUIDE_LINGUIDER:
1920             if (linGuider.isNull())
1921                 linGuider = new LinGuider();
1922 
1923             m_GuiderInstance = linGuider;
1924 
1925             clearCalibrationB->setEnabled(true);
1926             captureB->setEnabled(false);
1927             loopB->setEnabled(false);
1928             guideDarkFrame->setEnabled(false);
1929             guideSubframe->setEnabled(false);
1930             guideAutoStar->setEnabled(false);
1931             guideB->setEnabled(true);
1932             externalConnectB->setEnabled(true);
1933 
1934             opsGuide->controlGroup->setEnabled(false);
1935             infoGroup->setEnabled(false);
1936             driftGraphicsGroup->setEnabled(false);
1937 
1938             guideExposure->setEnabled(false);
1939             guideBinning->setEnabled(false);
1940             guideSquareSize->setEnabled(false);
1941 
1942             updateGuideParams();
1943 
1944             break;
1945     }
1946 
1947     if (m_GuiderInstance != nullptr)
1948     {
1949         connect(m_GuiderInstance, &Ekos::GuideInterface::frameCaptureRequested, this, &Ekos::Guide::capture);
1950         connect(m_GuiderInstance, &Ekos::GuideInterface::newLog, this, &Ekos::Guide::appendLogText);
1951         connect(m_GuiderInstance, &Ekos::GuideInterface::newStatus, this, &Ekos::Guide::setStatus);
1952         connect(m_GuiderInstance, &Ekos::GuideInterface::newStarPosition, this, &Ekos::Guide::setStarPosition);
1953         connect(m_GuiderInstance, &Ekos::GuideInterface::guideStats, this, &Ekos::Guide::guideStats);
1954 
1955         connect(m_GuiderInstance, &Ekos::GuideInterface::newAxisDelta, this, &Ekos::Guide::setAxisDelta);
1956         connect(m_GuiderInstance, &Ekos::GuideInterface::newAxisPulse, this, &Ekos::Guide::setAxisPulse);
1957         connect(m_GuiderInstance, &Ekos::GuideInterface::newAxisSigma, this, &Ekos::Guide::setAxisSigma);
1958         connect(m_GuiderInstance, &Ekos::GuideInterface::newSNR, this, &Ekos::Guide::setSNR);
1959         connect(m_GuiderInstance, &Ekos::GuideInterface::guideInfo, this, &Ekos::Guide::guideInfo);
1960         connect(m_GuiderInstance, &Ekos::GuideInterface::abortExposure, this, &Ekos::Guide::abortExposure);
1961 
1962         driftGraph->connectGuider(m_GuiderInstance);
1963         targetPlot->connectGuider(m_GuiderInstance);
1964 
1965         connect(m_GuiderInstance, &Ekos::GuideInterface::calibrationUpdate, this, &Ekos::Guide::calibrationUpdate);
1966 
1967         connect(m_GuiderInstance, &Ekos::GuideInterface::guideEquipmentUpdated, this, &Ekos::Guide::configurePHD2Camera);
1968     }
1969 
1970     externalConnectB->setEnabled(false);
1971     externalDisconnectB->setEnabled(false);
1972 
1973     if (m_GuiderInstance != nullptr && guiderType != GUIDE_INTERNAL)
1974     {
1975         externalConnectB->setEnabled(!m_GuiderInstance->isConnected());
1976         externalDisconnectB->setEnabled(m_GuiderInstance->isConnected());
1977     }
1978 
1979     if (m_GuiderInstance != nullptr)
1980         m_GuiderInstance->Connect();
1981 
1982     return true;
1983 }
1984 
1985 void Guide::guideInfo(const QString &info)
1986 {
1987     if (info.size() == 0)
1988     {
1989         guideInfoLabel->setVisible(false);
1990         guideInfoText->setVisible(false);
1991         return;
1992     }
1993     guideInfoLabel->setVisible(true);
1994     guideInfoLabel->setText("Detections");
1995     guideInfoText->setVisible(true);
1996     guideInfoText->setText(info);
1997 }
1998 
1999 void Guide::updateTrackingBoxSize(int currentIndex)
2000 {
2001     if (currentIndex >= 0)
2002     {
2003         if (guiderType == GUIDE_INTERNAL)
2004             dynamic_cast<InternalGuider *>(m_GuiderInstance)->setGuideBoxSize(guideSquareSize->currentText().toInt());
2005 
2006         syncTrackingBoxPosition();
2007     }
2008 }
2009 
2010 void Guide::onThresholdChanged(int index)
2011 {
2012     switch (guiderType)
2013     {
2014         case GUIDE_INTERNAL:
2015             dynamic_cast<InternalGuider *>(m_GuiderInstance)->setSquareAlgorithm(index);
2016             break;
2017 
2018         default:
2019             break;
2020     }
2021 }
2022 
2023 void Guide::onEnableDirRA()
2024 {
2025     // If RA guiding is enable or disabled, the GPG should be reset.
2026     if (Options::gPGEnabled())
2027         m_GuiderInstance->resetGPG();
2028 }
2029 
2030 void Guide::onEnableDirDEC()
2031 {
2032     onControlDirectionChanged();
2033 }
2034 
2035 void Guide::onControlDirectionChanged()
2036 {
2037     if(guiderType == GUIDE_PHD2)
2038         phd2Guider -> requestSetDEGuideMode(dECGuideEnabled->isChecked(), northDECGuideEnabled->isChecked(),
2039                                             southDECGuideEnabled->isChecked());
2040 }
2041 
2042 void Guide::updateDirectionsFromPHD2(const QString &mode)
2043 {
2044     //disable connections
2045     disconnect(dECGuideEnabled, &QCheckBox::toggled, this, &Ekos::Guide::onEnableDirDEC);
2046     disconnect(northDECGuideEnabled, &QCheckBox::toggled, this, &Ekos::Guide::onControlDirectionChanged);
2047     disconnect(southDECGuideEnabled, &QCheckBox::toggled, this, &Ekos::Guide::onControlDirectionChanged);
2048 
2049     if(mode == "Auto")
2050     {
2051         dECGuideEnabled->setChecked(true);
2052         northDECGuideEnabled->setChecked(true);
2053         southDECGuideEnabled->setChecked(true);
2054     }
2055     else if(mode == "North")
2056     {
2057         dECGuideEnabled->setChecked(true);
2058         northDECGuideEnabled->setChecked(true);
2059         southDECGuideEnabled->setChecked(false);
2060     }
2061     else if(mode == "South")
2062     {
2063         dECGuideEnabled->setChecked(true);
2064         northDECGuideEnabled->setChecked(false);
2065         southDECGuideEnabled->setChecked(true);
2066     }
2067     else //Off
2068     {
2069         dECGuideEnabled->setChecked(false);
2070         northDECGuideEnabled->setChecked(true);
2071         southDECGuideEnabled->setChecked(true);
2072     }
2073 
2074     //Re-enable connections
2075     connect(dECGuideEnabled, &QCheckBox::toggled, this, &Ekos::Guide::onEnableDirDEC);
2076     connect(northDECGuideEnabled, &QCheckBox::toggled, this, &Ekos::Guide::onControlDirectionChanged);
2077     connect(southDECGuideEnabled, &QCheckBox::toggled, this, &Ekos::Guide::onControlDirectionChanged);
2078 }
2079 
2080 void Guide::setTrackingStar(int x, int y)
2081 {
2082     QVector3D newStarPosition(x, y, -1);
2083     setStarPosition(newStarPosition, true);
2084 
2085     if(guiderType == GUIDE_PHD2)
2086     {
2087         //The Guide Star Image is 32 pixels across or less, so this guarantees it isn't that.
2088         if(!m_ImageData.isNull())
2089         {
2090             if(m_ImageData->width() > 50)
2091                 phd2Guider->setLockPosition(starCenter.x(), starCenter.y());
2092         }
2093     }
2094 
2095     if (operationStack.isEmpty() == false)
2096         executeOperationStack();
2097 }
2098 
2099 void Guide::setAxisDelta(double ra, double de)
2100 {
2101     //If PHD2 starts guiding because somebody pusted the button remotely, we want to set the state to guiding.
2102     //If guide pulses start coming in, it must be guiding.
2103     // 2020-04-10 sterne-jaeger: Will be resolved inside EKOS phd guiding.
2104     // if(guiderType == GUIDE_PHD2 && state != GUIDE_GUIDING)
2105     //     setStatus(GUIDE_GUIDING);
2106 
2107     ra = -ra;  //The ra is backwards in sign from how it should be displayed on the graph.
2108 
2109     int currentNumPoints = driftGraph->graph(GuideGraph::G_RA)->dataCount();
2110     guideSlider->setMaximum(currentNumPoints);
2111     if(graphOnLatestPt)
2112     {
2113         guideSlider->setValue(currentNumPoints);
2114     }
2115     l_DeltaRA->setText(QString::number(ra, 'f', 2));
2116     l_DeltaDEC->setText(QString::number(de, 'f', 2));
2117 
2118     emit newAxisDelta(ra, de);
2119 }
2120 
2121 void Guide::calibrationUpdate(GuideInterface::CalibrationUpdateType type, const QString &message,
2122                               double dx, double dy)
2123 {
2124     switch (type)
2125     {
2126         case GuideInterface::RA_OUT:
2127             calibrationPlot->graph(GuideGraph::G_RA)->addData(dx, dy);
2128             break;
2129         case GuideInterface::RA_IN:
2130             calibrationPlot->graph(GuideGraph::G_DEC)->addData(dx, dy);
2131             break;
2132         case GuideInterface::BACKLASH:
2133             calibrationPlot->graph(GuideGraph::G_RA_HIGHLIGHT)->addData(dx, dy);
2134             break;
2135         case GuideInterface::DEC_OUT:
2136             calibrationPlot->graph(GuideGraph::G_DEC_HIGHLIGHT)->addData(dx, dy);
2137             break;
2138         case GuideInterface::DEC_IN:
2139             calibrationPlot->graph(GuideGraph::G_RA_PULSE)->addData(dx, dy);
2140             break;
2141         case GuideInterface::CALIBRATION_MESSAGE_ONLY:
2142             ;
2143     }
2144     calLabel->setText(message);
2145     calibrationPlot->replot();
2146 }
2147 
2148 void Guide::setAxisSigma(double ra, double de)
2149 {
2150     l_ErrRA->setText(QString::number(ra, 'f', 2));
2151     l_ErrDEC->setText(QString::number(de, 'f', 2));
2152     const double total = std::hypot(ra, de);
2153     l_TotalRMS->setText(QString::number(total, 'f', 2));
2154 
2155     emit newAxisSigma(ra, de);
2156 }
2157 
2158 QList<double> Guide::axisDelta()
2159 {
2160     QList<double> delta;
2161 
2162     delta << l_DeltaRA->text().toDouble() << l_DeltaDEC->text().toDouble();
2163 
2164     return delta;
2165 }
2166 
2167 QList<double> Guide::axisSigma()
2168 {
2169     QList<double> sigma;
2170 
2171     sigma << l_ErrRA->text().toDouble() << l_ErrDEC->text().toDouble();
2172 
2173     return sigma;
2174 }
2175 
2176 void Guide::setAxisPulse(double ra, double de)
2177 {
2178     l_PulseRA->setText(QString::number(static_cast<int>(ra)));
2179     l_PulseDEC->setText(QString::number(static_cast<int>(de)));
2180 }
2181 
2182 void Guide::setSNR(double snr)
2183 {
2184     l_SNR->setText(QString::number(snr, 'f', 1));
2185 }
2186 
2187 void Guide::buildOperationStack(GuideState operation)
2188 {
2189     operationStack.clear();
2190 
2191     switch (operation)
2192     {
2193         case GUIDE_CAPTURE:
2194             if (guideDarkFrame->isChecked())
2195                 operationStack.push(GUIDE_DARK);
2196 
2197             operationStack.push(GUIDE_CAPTURE);
2198             operationStack.push(GUIDE_SUBFRAME);
2199             break;
2200 
2201         case GUIDE_CALIBRATING:
2202             operationStack.push(GUIDE_CALIBRATING);
2203             if (guiderType == GUIDE_INTERNAL)
2204             {
2205                 if (guideDarkFrame->isChecked())
2206                     operationStack.push(GUIDE_DARK);
2207 
2208                 // Auto Star Selected Path
2209                 if (guideAutoStar->isChecked() ||
2210                         // SEP MultiStar always uses an automated guide star.
2211                         internalGuider->SEPMultiStarEnabled())
2212                 {
2213                     // If subframe is enabled and we need to auto select a star, then we need to make the final capture
2214                     // of the subframed image. This is only done if we aren't already subframed.
2215                     if (subFramed == false && guideSubframe->isChecked())
2216                         operationStack.push(GUIDE_CAPTURE);
2217 
2218                     operationStack.push(GUIDE_SUBFRAME);
2219                     operationStack.push(GUIDE_STAR_SELECT);
2220 
2221 
2222                     operationStack.push(GUIDE_CAPTURE);
2223 
2224                     // If we are being ask to go full frame, let's do that first
2225                     if (subFramed == true && guideSubframe->isChecked() == false)
2226                         operationStack.push(GUIDE_SUBFRAME);
2227                 }
2228                 // Manual Star Selection Path
2229                 else
2230                 {
2231                     // Final capture before we start calibrating
2232                     if (subFramed == false && guideSubframe->isChecked())
2233                         operationStack.push(GUIDE_CAPTURE);
2234 
2235                     // Subframe if required
2236                     operationStack.push(GUIDE_SUBFRAME);
2237 
2238                     // First capture an image
2239                     operationStack.push(GUIDE_CAPTURE);
2240                 }
2241 
2242             }
2243             break;
2244 
2245         default:
2246             break;
2247     }
2248 }
2249 
2250 bool Guide::executeOperationStack()
2251 {
2252     if (operationStack.isEmpty())
2253         return false;
2254 
2255     GuideState nextOperation = operationStack.pop();
2256     // qCDebug(KSTARS_EKOS_GUIDE) << "Executing operation " << getGuideStatusString(nextOperation);
2257 
2258     bool actionRequired = false;
2259 
2260     switch (nextOperation)
2261     {
2262         case GUIDE_SUBFRAME:
2263             actionRequired = executeOneOperation(nextOperation);
2264             break;
2265 
2266         case GUIDE_DARK:
2267             actionRequired = executeOneOperation(nextOperation);
2268             break;
2269 
2270         case GUIDE_CAPTURE:
2271             actionRequired = captureOneFrame();
2272             break;
2273 
2274         case GUIDE_STAR_SELECT:
2275             actionRequired = executeOneOperation(nextOperation);
2276             break;
2277 
2278         case GUIDE_CALIBRATING:
2279             if (guiderType == GUIDE_INTERNAL)
2280             {
2281                 m_GuiderInstance->setStarPosition(starCenter);
2282 
2283                 // Tracking must be engaged
2284                 if (m_Mount && m_Mount->canControlTrack() && m_Mount->isTracking() == false)
2285                     m_Mount->setTrackEnabled(true);
2286             }
2287 
2288             if (m_GuiderInstance->calibrate())
2289             {
2290                 if (guiderType == GUIDE_INTERNAL)
2291                     disconnect(m_GuideView.get(), &FITSView::trackingStarSelected, this, &Guide::setTrackingStar);
2292                 setBusy(true);
2293             }
2294             else
2295             {
2296                 emit newStatus(GUIDE_CALIBRATION_ERROR);
2297                 m_State = GUIDE_IDLE;
2298                 appendLogText(i18n("Calibration failed to start."));
2299                 setBusy(false);
2300             }
2301             break;
2302 
2303         default:
2304             break;
2305     }
2306 
2307     // If an additional action is required, return return and continue later
2308     if (actionRequired)
2309         return true;
2310     // Otherwise, continue processing the stack
2311     else
2312         return executeOperationStack();
2313 }
2314 
2315 bool Guide::executeOneOperation(GuideState operation)
2316 {
2317     bool actionRequired = false;
2318 
2319     if (m_Camera == nullptr)
2320         return actionRequired;
2321 
2322     ISD::CameraChip *targetChip = m_Camera->getChip(useGuideHead ? ISD::CameraChip::GUIDE_CCD : ISD::CameraChip::PRIMARY_CCD);
2323     if (targetChip == nullptr)
2324         return false;
2325 
2326     int subBinX, subBinY;
2327     targetChip->getBinning(&subBinX, &subBinY);
2328 
2329     switch (operation)
2330     {
2331         case GUIDE_SUBFRAME:
2332         {
2333             // SEP MultiStar doesn't subframe.
2334             if ((guiderType == GUIDE_INTERNAL) && internalGuider->SEPMultiStarEnabled())
2335                 break;
2336             // Check if we need and can subframe
2337             if (subFramed == false && guideSubframe->isChecked() == true && targetChip->canSubframe())
2338             {
2339                 int minX, maxX, minY, maxY, minW, maxW, minH, maxH;
2340                 targetChip->getFrameMinMax(&minX, &maxX, &minY, &maxY, &minW, &maxW, &minH, &maxH);
2341 
2342                 int offset = guideSquareSize->currentText().toInt() / subBinX;
2343 
2344                 int x = starCenter.x();
2345                 int y = starCenter.y();
2346 
2347                 x     = (x - offset * 2) * subBinX;
2348                 y     = (y - offset * 2) * subBinY;
2349                 int w = offset * 4 * subBinX;
2350                 int h = offset * 4 * subBinY;
2351 
2352                 if (x < minX)
2353                     x = minX;
2354                 if (y < minY)
2355                     y = minY;
2356                 if ((x + w) > maxW)
2357                     w = maxW - x;
2358                 if ((y + h) > maxH)
2359                     h = maxH - y;
2360 
2361                 targetChip->setFrame(x, y, w, h);
2362 
2363                 subFramed            = true;
2364                 QVariantMap settings = frameSettings[targetChip];
2365                 settings["x"]        = x;
2366                 settings["y"]        = y;
2367                 settings["w"]        = w;
2368                 settings["h"]        = h;
2369                 settings["binx"]     = subBinX;
2370                 settings["biny"]     = subBinY;
2371 
2372                 frameSettings[targetChip] = settings;
2373 
2374                 starCenter.setX(w / (2 * subBinX));
2375                 starCenter.setY(h / (2 * subBinX));
2376             }
2377             // Otherwise check if we are already subframed
2378             // and we need to go back to full frame
2379             // or if we need to go back to full frame since we need
2380             // to reaquire a star
2381             else if (subFramed &&
2382                      (guideSubframe->isChecked() == false ||
2383                       m_State == GUIDE_REACQUIRE))
2384             {
2385                 targetChip->resetFrame();
2386 
2387                 int x, y, w, h;
2388                 targetChip->getFrame(&x, &y, &w, &h);
2389 
2390                 QVariantMap settings;
2391                 settings["x"]             = x;
2392                 settings["y"]             = y;
2393                 settings["w"]             = w;
2394                 settings["h"]             = h;
2395                 settings["binx"]          = subBinX;
2396                 settings["biny"]          = subBinY;
2397                 frameSettings[targetChip] = settings;
2398 
2399                 subFramed = false;
2400 
2401                 starCenter.setX(w / (2 * subBinX));
2402                 starCenter.setY(h / (2 * subBinX));
2403 
2404                 //starCenter.setX(0);
2405                 //starCenter.setY(0);
2406             }
2407         }
2408         break;
2409 
2410         case GUIDE_DARK:
2411         {
2412             // Do we need to take a dark frame?
2413             if (m_ImageData && guideDarkFrame->isChecked())
2414             {
2415                 QVariantMap settings = frameSettings[targetChip];
2416                 uint16_t offsetX = 0;
2417                 uint16_t offsetY = 0;
2418 
2419                 if (settings["x"].isValid() &&
2420                         settings["y"].isValid() &&
2421                         settings["binx"].isValid() &&
2422                         settings["biny"].isValid())
2423                 {
2424                     offsetX = settings["x"].toInt() / settings["binx"].toInt();
2425                     offsetY = settings["y"].toInt() / settings["biny"].toInt();
2426                 }
2427 
2428                 actionRequired = true;
2429                 targetChip->setCaptureFilter(FITS_NONE);
2430                 m_DarkProcessor->denoise(OpticalTrainManager::Instance()->id(opticalTrainCombo->currentText()),
2431                                          targetChip, m_ImageData, guideExposure->value(), offsetX, offsetY);
2432             }
2433         }
2434         break;
2435 
2436         case GUIDE_STAR_SELECT:
2437         {
2438             m_State = GUIDE_STAR_SELECT;
2439             emit newStatus(m_State);
2440 
2441             if (guideAutoStar->isChecked() ||
2442                     // SEP MultiStar always uses an automated guide star.
2443                     ((guiderType == GUIDE_INTERNAL) &&
2444                      internalGuider->SEPMultiStarEnabled()))
2445             {
2446                 bool autoStarCaptured = internalGuider->selectAutoStar();
2447                 if (autoStarCaptured)
2448                 {
2449                     appendLogText(i18n("Auto star selected."));
2450                 }
2451                 else
2452                 {
2453                     appendLogText(i18n("Failed to select an auto star."));
2454                     actionRequired = true;
2455                     m_State = GUIDE_CALIBRATION_ERROR;
2456                     emit newStatus(m_State);
2457                     setBusy(false);
2458                 }
2459             }
2460             else
2461             {
2462                 appendLogText(i18n("Select a guide star to calibrate."));
2463                 actionRequired = true;
2464             }
2465         }
2466         break;
2467 
2468         default:
2469             break;
2470     }
2471 
2472     return actionRequired;
2473 }
2474 
2475 void Guide::processGuideOptions()
2476 {
2477     if (Options::guiderType() != guiderType)
2478     {
2479         guiderType = static_cast<GuiderType>(Options::guiderType());
2480         setGuiderType(Options::guiderType());
2481     }
2482 }
2483 
2484 void Guide::showFITSViewer()
2485 {
2486     static int lastFVTabID = -1;
2487     if (m_ImageData)
2488     {
2489         QUrl url = QUrl::fromLocalFile("guide.fits");
2490         if (fv.isNull())
2491         {
2492             fv = KStars::Instance()->createFITSViewer();
2493             fv->loadData(m_ImageData, url, &lastFVTabID);
2494             connect(fv.get(), &FITSViewer::terminated, this, [this]()
2495             {
2496                 fv.clear();
2497             });
2498         }
2499         else if (fv->updateData(m_ImageData, url, lastFVTabID, &lastFVTabID) == false)
2500             fv->loadData(m_ImageData, url, &lastFVTabID);
2501 
2502         fv->show();
2503     }
2504 }
2505 
2506 void Guide::setExternalGuiderBLOBEnabled(bool enable)
2507 {
2508     // Nothing to do if guider is internal
2509     if (guiderType == GUIDE_INTERNAL)
2510         return;
2511 
2512     if(!m_Camera)
2513         return;
2514 
2515     m_Camera->setBLOBEnabled(enable);
2516 
2517     if(m_Camera->isBLOBEnabled())
2518     {
2519         checkUseGuideHead();
2520 
2521         auto targetChip = m_Camera->getChip(useGuideHead ? ISD::CameraChip::GUIDE_CCD : ISD::CameraChip::PRIMARY_CCD);
2522         if (targetChip)
2523             targetChip->setCaptureMode(FITS_GUIDE);
2524         syncCameraInfo();
2525     }
2526 
2527 }
2528 
2529 void Guide::resetNonGuidedDither()
2530 {
2531     // reset non guided dither total drift
2532     nonGuidedDitherRaOffsetMsec = 0;
2533     nonGuidedDitherDecOffsetMsec = 0;
2534     qCDebug(KSTARS_EKOS_GUIDE) << "Reset non guiding dithering position";
2535 
2536     // initialize random generator if not done before
2537     if (!isNonGuidedDitherInitialized)
2538     {
2539         auto seed = std::chrono::system_clock::now().time_since_epoch().count();
2540         nonGuidedPulseGenerator.seed(seed);
2541         isNonGuidedDitherInitialized = true;
2542         qCDebug(KSTARS_EKOS_GUIDE) << "Initialize non guiding dithering random generator";
2543     }
2544 }
2545 
2546 void Guide::nonGuidedDither()
2547 {
2548     double ditherPulse = Options::ditherNoGuidingPulse();
2549 
2550     // Randomize dithering position up to +/-dithePulse distance from original
2551     std::uniform_int_distribution<int> newPos(-ditherPulse, +ditherPulse);
2552 
2553     // Calculate the pulse needed to move to the new position, then save the new position and apply the pulse
2554 
2555     // for ra
2556     const int newRaOffsetMsec = newPos(nonGuidedPulseGenerator);
2557     const int raPulse = nonGuidedDitherRaOffsetMsec - newRaOffsetMsec;
2558     nonGuidedDitherRaOffsetMsec = newRaOffsetMsec;
2559     const int raMsec  = std::abs(raPulse);
2560     const int raPolarity = (raPulse >= 0 ? 1 : -1);
2561 
2562     // and for dec
2563     const int newDecOffsetMsec = newPos(nonGuidedPulseGenerator);
2564     const int decPulse = nonGuidedDitherDecOffsetMsec - newDecOffsetMsec;
2565     nonGuidedDitherDecOffsetMsec = newDecOffsetMsec;
2566     const int decMsec  = std::abs(decPulse);
2567     const int decPolarity = (decPulse >= 0 ? 1 : -1);
2568 
2569     qCInfo(KSTARS_EKOS_GUIDE) << "Starting non-guiding dither...";
2570     qCDebug(KSTARS_EKOS_GUIDE) << "dither ra_msec:" << raMsec << "ra_polarity:" << raPolarity << "de_msec:" << decMsec <<
2571                                "de_polarity:" << decPolarity;
2572 
2573     bool rc = sendMultiPulse(raPolarity > 0 ? RA_INC_DIR : RA_DEC_DIR, raMsec, decPolarity > 0 ? DEC_INC_DIR : DEC_DEC_DIR,
2574                              decMsec, DontCaptureAfterPulses);
2575 
2576     if (rc)
2577     {
2578         qCInfo(KSTARS_EKOS_GUIDE) << "Non-guiding dither successful.";
2579         QTimer::singleShot( (raMsec > decMsec ? raMsec : decMsec) + Options::ditherSettle() * 1000 + 100, this, [this]()
2580         {
2581             emit newStatus(GUIDE_DITHERING_SUCCESS);
2582             m_State = GUIDE_IDLE;
2583         });
2584     }
2585     else
2586     {
2587         qCWarning(KSTARS_EKOS_GUIDE) << "Non-guiding dither failed.";
2588         emit newStatus(GUIDE_DITHERING_ERROR);
2589         m_State = GUIDE_IDLE;
2590     }
2591 }
2592 
2593 void Guide::handleManualDither()
2594 {
2595     ISD::CameraChip *targetChip = m_Camera->getChip(useGuideHead ? ISD::CameraChip::GUIDE_CCD : ISD::CameraChip::PRIMARY_CCD);
2596     if (targetChip == nullptr)
2597         return;
2598 
2599     Ui::ManualDither ditherDialog;
2600     QDialog container(this);
2601     ditherDialog.setupUi(&container);
2602 
2603     if (guiderType != GUIDE_INTERNAL)
2604     {
2605         ditherDialog.coordinatesR->setEnabled(false);
2606         ditherDialog.x->setEnabled(false);
2607         ditherDialog.y->setEnabled(false);
2608     }
2609 
2610     int minX, maxX, minY, maxY, minW, maxW, minH, maxH;
2611     targetChip->getFrameMinMax(&minX, &maxX, &minY, &maxY, &minW, &maxW, &minH, &maxH);
2612 
2613     ditherDialog.x->setMinimum(minX);
2614     ditherDialog.x->setMaximum(maxX);
2615     ditherDialog.y->setMinimum(minY);
2616     ditherDialog.y->setMaximum(maxY);
2617 
2618     ditherDialog.x->setValue(starCenter.x());
2619     ditherDialog.y->setValue(starCenter.y());
2620 
2621     if (container.exec() == QDialog::Accepted)
2622     {
2623         if (ditherDialog.magnitudeR->isChecked())
2624             m_GuiderInstance->dither(ditherDialog.magnitude->value());
2625         else
2626         {
2627             InternalGuider * const ig = dynamic_cast<InternalGuider *>(m_GuiderInstance);
2628             if (ig)
2629                 ig->ditherXY(ditherDialog.x->value(), ditherDialog.y->value());
2630         }
2631     }
2632 }
2633 
2634 bool Guide::connectGuider()
2635 {
2636     setStatus(GUIDE_IDLE);
2637     return m_GuiderInstance->Connect();
2638 }
2639 
2640 bool Guide::disconnectGuider()
2641 {
2642     return m_GuiderInstance->Disconnect();
2643 }
2644 
2645 void Guide::initPlots()
2646 {
2647     initDriftGraph();
2648     initCalibrationPlot();
2649 
2650     connect(rightLayout, &QSplitter::splitterMoved, this, &Ekos::Guide::handleVerticalPlotSizeChange);
2651     connect(driftSplitter, &QSplitter::splitterMoved, this, &Ekos::Guide::handleHorizontalPlotSizeChange);
2652 
2653     buildTarget();
2654 }
2655 
2656 void Guide::initDriftGraph()
2657 {
2658     //Dragging and zooming settings
2659     // make bottom axis transfer its range to the top axis if the graph gets zoomed:
2660     connect(driftGraph->xAxis,  static_cast<void(QCPAxis::*)(const QCPRange &)>(&QCPAxis::rangeChanged),
2661             driftGraph->xAxis2, static_cast<void(QCPAxis::*)(const QCPRange &)>(&QCPAxis::setRange));
2662     // update the second vertical axis properly if the graph gets zoomed.
2663     connect(driftGraph->yAxis, static_cast<void(QCPAxis::*)(const QCPRange &)>(&QCPAxis::rangeChanged),
2664             [this]()
2665     {
2666         driftGraph->setCorrectionGraphScale(correctionSlider->value());
2667     });
2668 
2669     connect(driftGraph, &QCustomPlot::mouseMove, driftGraph, &GuideDriftGraph::mouseOverLine);
2670     connect(driftGraph, &QCustomPlot::mousePress, driftGraph, &GuideDriftGraph::mouseClicked);
2671 
2672     int scale =
2673         50;  //This is a scaling value between the left and the right axes of the driftGraph, it could be stored in kstars kcfg
2674     correctionSlider->setValue(scale);
2675 }
2676 
2677 void Guide::initCalibrationPlot()
2678 {
2679     calibrationPlot->setBackground(QBrush(Qt::black));
2680     calibrationPlot->setSelectionTolerance(10);
2681 
2682     calibrationPlot->xAxis->setBasePen(QPen(Qt::white, 1));
2683     calibrationPlot->yAxis->setBasePen(QPen(Qt::white, 1));
2684 
2685     calibrationPlot->xAxis->setTickPen(QPen(Qt::white, 1));
2686     calibrationPlot->yAxis->setTickPen(QPen(Qt::white, 1));
2687 
2688     calibrationPlot->xAxis->setSubTickPen(QPen(Qt::white, 1));
2689     calibrationPlot->yAxis->setSubTickPen(QPen(Qt::white, 1));
2690 
2691     calibrationPlot->xAxis->setTickLabelColor(Qt::white);
2692     calibrationPlot->yAxis->setTickLabelColor(Qt::white);
2693 
2694     calibrationPlot->xAxis->setLabelColor(Qt::white);
2695     calibrationPlot->yAxis->setLabelColor(Qt::white);
2696 
2697     calibrationPlot->xAxis->setLabelFont(QFont(font().family(), 10));
2698     calibrationPlot->yAxis->setLabelFont(QFont(font().family(), 10));
2699     calibrationPlot->xAxis->setTickLabelFont(QFont(font().family(), 9));
2700     calibrationPlot->yAxis->setTickLabelFont(QFont(font().family(), 9));
2701 
2702     calibrationPlot->xAxis->setLabelPadding(2);
2703     calibrationPlot->yAxis->setLabelPadding(2);
2704 
2705     calibrationPlot->xAxis->grid()->setPen(QPen(QColor(140, 140, 140), 1, Qt::DotLine));
2706     calibrationPlot->yAxis->grid()->setPen(QPen(QColor(140, 140, 140), 1, Qt::DotLine));
2707     calibrationPlot->xAxis->grid()->setSubGridPen(QPen(QColor(80, 80, 80), 1, Qt::DotLine));
2708     calibrationPlot->yAxis->grid()->setSubGridPen(QPen(QColor(80, 80, 80), 1, Qt::DotLine));
2709     calibrationPlot->xAxis->grid()->setZeroLinePen(QPen(Qt::gray));
2710     calibrationPlot->yAxis->grid()->setZeroLinePen(QPen(Qt::gray));
2711 
2712     calibrationPlot->xAxis->setLabel(i18n("x (pixels)"));
2713     calibrationPlot->yAxis->setLabel(i18n("y (pixels)"));
2714 
2715     calibrationPlot->xAxis->setRange(-20, 20);
2716     calibrationPlot->yAxis->setRange(-20, 20);
2717 
2718     calibrationPlot->setInteractions(QCP::iRangeZoom);
2719     calibrationPlot->setInteraction(QCP::iRangeDrag, true);
2720 
2721     calibrationPlot->addGraph();
2722     calibrationPlot->graph(GuideGraph::G_RA)->setLineStyle(QCPGraph::lsNone);
2723     calibrationPlot->graph(GuideGraph::G_RA)->setScatterStyle(QCPScatterStyle(QCPScatterStyle::ssDisc,
2724             QPen(KStarsData::Instance()->colorScheme()->colorNamed("RAGuideError"), 2), QBrush(), 6));
2725     calibrationPlot->graph(GuideGraph::G_RA)->setName("RA out");
2726 
2727     calibrationPlot->addGraph();
2728     calibrationPlot->graph(GuideGraph::G_DEC)->setLineStyle(QCPGraph::lsNone);
2729     calibrationPlot->graph(GuideGraph::G_DEC)->setScatterStyle(QCPScatterStyle(QCPScatterStyle::ssCircle, QPen(Qt::white, 2),
2730             QBrush(), 4));
2731     calibrationPlot->graph(GuideGraph::G_DEC)->setName("RA in");
2732 
2733     calibrationPlot->addGraph();
2734     calibrationPlot->graph(GuideGraph::G_RA_HIGHLIGHT)->setLineStyle(QCPGraph::lsNone);
2735     calibrationPlot->graph(GuideGraph::G_RA_HIGHLIGHT)->setScatterStyle(QCPScatterStyle(QCPScatterStyle::ssPlus, QPen(Qt::white,
2736             2),
2737             QBrush(), 6));
2738     calibrationPlot->graph(GuideGraph::G_RA_HIGHLIGHT)->setName("Backlash");
2739 
2740     calibrationPlot->addGraph();
2741     calibrationPlot->graph(GuideGraph::G_DEC_HIGHLIGHT)->setLineStyle(QCPGraph::lsNone);
2742     calibrationPlot->graph(GuideGraph::G_DEC_HIGHLIGHT)->setScatterStyle(QCPScatterStyle(QCPScatterStyle::ssDisc,
2743             QPen(KStarsData::Instance()->colorScheme()->colorNamed("DEGuideError"), 2), QBrush(), 6));
2744     calibrationPlot->graph(GuideGraph::G_DEC_HIGHLIGHT)->setName("DEC out");
2745 
2746     calibrationPlot->addGraph();
2747     calibrationPlot->graph(GuideGraph::G_RA_PULSE)->setLineStyle(QCPGraph::lsNone);
2748     calibrationPlot->graph(GuideGraph::G_RA_PULSE)->setScatterStyle(QCPScatterStyle(QCPScatterStyle::ssCircle, QPen(Qt::yellow,
2749             2),
2750             QBrush(), 4));
2751     calibrationPlot->graph(GuideGraph::G_RA_PULSE)->setName("DEC in");
2752 
2753     calLabel = new QCPItemText(calibrationPlot);
2754     calLabel->setColor(QColor(255, 255, 255));
2755     calLabel->setPositionAlignment(Qt::AlignTop | Qt::AlignHCenter);
2756     calLabel->position->setType(QCPItemPosition::ptAxisRectRatio);
2757     calLabel->position->setCoords(0.5, 0);
2758     calLabel->setText("");
2759     calLabel->setFont(QFont(font().family(), 10));
2760     calLabel->setVisible(true);
2761 
2762     calibrationPlot->resize(190, 190);
2763     calibrationPlot->replot();
2764 }
2765 
2766 void Guide::initView()
2767 {
2768     guideStateWidget = new GuideStateWidget();
2769     guideInfoLayout->insertWidget(-1, guideStateWidget);
2770 
2771     m_GuideView.reset(new GuideView(guideWidget, FITS_GUIDE));
2772     m_GuideView->setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Expanding);
2773     m_GuideView->setBaseSize(guideWidget->size());
2774     m_GuideView->createFloatingToolBar();
2775     QVBoxLayout *vlayout = new QVBoxLayout();
2776     vlayout->addWidget(m_GuideView.get());
2777     guideWidget->setLayout(vlayout);
2778     connect(m_GuideView.get(), &FITSView::trackingStarSelected, this, &Ekos::Guide::setTrackingStar);
2779     guideInfoLabel->setVisible(false);
2780     guideInfoText->setVisible(false);
2781 }
2782 
2783 void Guide::initConnections()
2784 {
2785     // Exposure Timeout
2786     captureTimeout.setSingleShot(true);
2787     connect(&captureTimeout, &QTimer::timeout, this, &Ekos::Guide::processCaptureTimeout);
2788 
2789     // Guiding Box Size
2790     connect(guideSquareSize, static_cast<void(QComboBox::*)(int)>(&QComboBox::currentIndexChanged), this,
2791             &Ekos::Guide::updateTrackingBoxSize);
2792 
2793     // Dark Frame Check
2794     connect(guideDarkFrame, &QCheckBox::toggled, this, &Ekos::Guide::setDarkFrameEnabled);
2795     // Subframe check
2796     if(guiderType != GUIDE_PHD2) //For PHD2, this is handled in the configurePHD2Camera method
2797         connect(guideSubframe, &QCheckBox::toggled, this, &Ekos::Guide::setSubFrameEnabled);
2798 
2799     // Binning Combo Selection
2800     connect(guideBinning, static_cast<void(QComboBox::*)(int)>(&QComboBox::currentIndexChanged), this,
2801             &Ekos::Guide::updateCCDBin);
2802 
2803     // RA/DEC Enable directions
2804     connect(rAGuideEnabled, &QCheckBox::toggled, this, &Ekos::Guide::onEnableDirRA);
2805     connect(dECGuideEnabled, &QCheckBox::toggled, this, &Ekos::Guide::onEnableDirDEC);
2806 
2807     // N/W and W/E direction enable
2808     connect(northDECGuideEnabled, &QCheckBox::toggled, this, &Ekos::Guide::onControlDirectionChanged);
2809     connect(southDECGuideEnabled, &QCheckBox::toggled, this, &Ekos::Guide::onControlDirectionChanged);
2810     connect(westRAGuideEnabled, &QCheckBox::toggled, this, &Ekos::Guide::onControlDirectionChanged);
2811     connect(eastRAGuideEnabled, &QCheckBox::toggled, this, &Ekos::Guide::onControlDirectionChanged);
2812 
2813     // Capture
2814     connect(captureB, &QPushButton::clicked, this, [this]()
2815     {
2816         m_State = GUIDE_CAPTURE;
2817         emit newStatus(m_State);
2818 
2819         if(guiderType == GUIDE_PHD2)
2820         {
2821             configurePHD2Camera();
2822             if(phd2Guider->isCurrentCameraNotInEkos())
2823                 appendLogText(
2824                     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."));
2825             else if(guideSubframe->isChecked())
2826             {
2827                 appendLogText(
2828                     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"));
2829                 guideSubframe->setChecked(false);
2830             }
2831             phd2Guider->captureSingleFrame();
2832         }
2833         else if (guiderType == GUIDE_INTERNAL)
2834             capture();
2835     });
2836 
2837     // Framing
2838     connect(loopB, &QPushButton::clicked, this, &Guide::loop);
2839 
2840     // Stop
2841     connect(stopB, &QPushButton::clicked, this, &Ekos::Guide::abort);
2842 
2843     // Clear Calibrate
2844     //connect(calibrateB, &QPushButton::clicked, this, &Ekos::Guide::calibrate()));
2845     connect(clearCalibrationB, &QPushButton::clicked, this, &Ekos::Guide::clearCalibration);
2846 
2847     // Guide
2848     connect(guideB, &QPushButton::clicked, this, &Ekos::Guide::guide);
2849 
2850     // Connect External Guide
2851     connect(externalConnectB, &QPushButton::clicked, this, [&]()
2852     {
2853         //setExternalGuiderBLOBEnabled(false);
2854         m_GuiderInstance->Connect();
2855     });
2856     connect(externalDisconnectB, &QPushButton::clicked, this, [&]()
2857     {
2858         //setExternalGuiderBLOBEnabled(true);
2859         m_GuiderInstance->Disconnect();
2860     });
2861 
2862     // Pulse Timer
2863     m_PulseTimer.setSingleShot(true);
2864     connect(&m_PulseTimer, &QTimer::timeout, this, &Ekos::Guide::capture);
2865 
2866     //This connects all the buttons and slider below the guide plots.
2867     connect(guiderAccuracyThreshold, static_cast<void(QDoubleSpinBox::*)(double)>(&QDoubleSpinBox::valueChanged), this,
2868             &Ekos::Guide::buildTarget);
2869     connect(guideSlider, &QSlider::sliderMoved, this, &Ekos::Guide::guideHistory);
2870     connect(latestCheck, &QCheckBox::toggled, this, &Ekos::Guide::setLatestGuidePoint);
2871     connect(rADisplayedOnGuideGraph, &QCheckBox::toggled, [this](bool isChecked)
2872     {
2873         driftGraph->toggleShowPlot(GuideGraph::G_RA, isChecked);
2874     });
2875     connect(dEDisplayedOnGuideGraph, &QCheckBox::toggled,  this, [this](bool isChecked)
2876     {
2877         driftGraph->toggleShowPlot(GuideGraph::G_DEC, isChecked);
2878     });
2879     connect(rACorrDisplayedOnGuideGraph, &QCheckBox::toggled,  this, [this](bool isChecked)
2880     {
2881         driftGraph->toggleShowPlot(GuideGraph::G_RA_PULSE, isChecked);
2882     });
2883     connect(dECorrDisplayedOnGuideGraph, &QCheckBox::toggled,  this, [this](bool isChecked)
2884     {
2885         driftGraph->toggleShowPlot(GuideGraph::G_DEC_PULSE, isChecked);
2886     });
2887     connect(sNRDisplayedOnGuideGraph, &QCheckBox::toggled,  this, [this](bool isChecked)
2888     {
2889         driftGraph->toggleShowPlot(GuideGraph::G_SNR, isChecked);
2890     });
2891     connect(rMSDisplayedOnGuideGraph, &QCheckBox::toggled,  this, [this](bool isChecked)
2892     {
2893         driftGraph->toggleShowPlot(GuideGraph::G_RMS, isChecked);
2894     });
2895     connect(correctionSlider, &QSlider::sliderMoved, driftGraph, &GuideDriftGraph::setCorrectionGraphScale);
2896 
2897     connect(manualDitherB, &QPushButton::clicked, this, &Guide::handleManualDither);
2898 
2899     connect(this, &Ekos::Guide::newStatus, guideStateWidget, &Ekos::GuideStateWidget::updateGuideStatus);
2900 }
2901 
2902 void Guide::removeDevice(const QSharedPointer<ISD::GenericDevice> &device)
2903 {
2904     auto name = device->getDeviceName();
2905 
2906     device->disconnect(this);
2907 
2908     // Mounts
2909     if (m_Mount && m_Mount->getDeviceName() == name)
2910     {
2911         m_Mount->disconnect(this);
2912         m_Mount = nullptr;
2913     }
2914 
2915 
2916     // Cameras
2917     if (m_Camera && m_Camera->getDeviceName() == name)
2918     {
2919         m_Camera->disconnect(this);
2920         m_Camera = nullptr;
2921     }
2922 
2923 
2924     // Guiders
2925     if (m_Guider && m_Guider->getDeviceName() == name)
2926     {
2927         m_Guider->disconnect(this);
2928         m_Guider = nullptr;
2929     }
2930 
2931     // Adaptive Optics
2932     // FIXME AO are not yet utilized property in Guide module
2933     if (m_AO && m_AO->getDeviceName() == name)
2934     {
2935         m_AO->disconnect(this);
2936         m_AO = nullptr;
2937     }
2938 }
2939 
2940 void Guide::loop()
2941 {
2942     m_State = GUIDE_LOOPING;
2943     emit newStatus(m_State);
2944 
2945     if(guiderType == GUIDE_PHD2)
2946     {
2947         configurePHD2Camera();
2948         if(phd2Guider->isCurrentCameraNotInEkos())
2949             appendLogText(
2950                 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."));
2951         else if(guideSubframe->isChecked())
2952         {
2953             appendLogText(
2954                 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"));
2955             guideSubframe->setChecked(false);
2956         }
2957         phd2Guider->loop();
2958         stopB->setEnabled(true);
2959     }
2960     else if (guiderType == GUIDE_INTERNAL)
2961         capture();
2962 }
2963 
2964 ///////////////////////////////////////////////////////////////////////////////////////////
2965 ///
2966 ///////////////////////////////////////////////////////////////////////////////////////////
2967 QVariantMap Guide::getAllSettings() const
2968 {
2969     QVariantMap settings;
2970 
2971     // All Combo Boxes
2972     for (auto &oneWidget : findChildren<QComboBox*>())
2973         settings.insert(oneWidget->objectName(), oneWidget->currentText());
2974 
2975     // All Double Spin Boxes
2976     for (auto &oneWidget : findChildren<QDoubleSpinBox*>())
2977         settings.insert(oneWidget->objectName(), oneWidget->value());
2978 
2979     // All Spin Boxes
2980     for (auto &oneWidget : findChildren<QSpinBox*>())
2981         settings.insert(oneWidget->objectName(), oneWidget->value());
2982 
2983     // All Checkboxes
2984     for (auto &oneWidget : findChildren<QCheckBox*>())
2985         settings.insert(oneWidget->objectName(), oneWidget->isChecked());
2986 
2987     return settings;
2988 }
2989 
2990 ///////////////////////////////////////////////////////////////////////////////////////////
2991 ///
2992 ///////////////////////////////////////////////////////////////////////////////////////////
2993 void Guide::setAllSettings(const QVariantMap &settings)
2994 {
2995     // Disconnect settings that we don't end up calling syncSettings while
2996     // performing the changes.
2997     disconnectSettings();
2998 
2999     for (auto &name : settings.keys())
3000     {
3001         // Combo
3002         auto comboBox = findChild<QComboBox*>(name);
3003         if (comboBox)
3004         {
3005             syncControl(settings, name, comboBox);
3006             continue;
3007         }
3008 
3009         // Double spinbox
3010         auto doubleSpinBox = findChild<QDoubleSpinBox*>(name);
3011         if (doubleSpinBox)
3012         {
3013             syncControl(settings, name, doubleSpinBox);
3014             continue;
3015         }
3016 
3017         // spinbox
3018         auto spinBox = findChild<QSpinBox*>(name);
3019         if (spinBox)
3020         {
3021             syncControl(settings, name, spinBox);
3022             continue;
3023         }
3024 
3025         // checkbox
3026         auto checkbox = findChild<QCheckBox*>(name);
3027         if (checkbox)
3028         {
3029             syncControl(settings, name, checkbox);
3030             continue;
3031         }
3032     }
3033 
3034     // Sync to options
3035     for (auto &key : settings.keys())
3036     {
3037         auto value = settings[key];
3038         // Save immediately
3039         Options::self()->setProperty(key.toLatin1(), value);
3040 
3041         m_Settings[key] = value;
3042         m_GlobalSettings[key] = value;
3043     }
3044 
3045     emit settingsUpdated(getAllSettings());
3046 
3047     // Save to optical train specific settings as well
3048     OpticalTrainSettings::Instance()->setOpticalTrainID(OpticalTrainManager::Instance()->id(opticalTrainCombo->currentText()));
3049     OpticalTrainSettings::Instance()->setOneSetting(OpticalTrainSettings::Guide, m_Settings);
3050 
3051     // Restablish connections
3052     connectSettings();
3053 }
3054 
3055 ///////////////////////////////////////////////////////////////////////////////////////////
3056 ///
3057 ///////////////////////////////////////////////////////////////////////////////////////////
3058 bool Guide::syncControl(const QVariantMap &settings, const QString &key, QWidget * widget)
3059 {
3060     QSpinBox *pSB = nullptr;
3061     QDoubleSpinBox *pDSB = nullptr;
3062     QCheckBox *pCB = nullptr;
3063     QComboBox *pComboBox = nullptr;
3064     bool ok = false;
3065 
3066     if ((pSB = qobject_cast<QSpinBox *>(widget)))
3067     {
3068         const int value = settings[key].toInt(&ok);
3069         if (ok)
3070         {
3071             pSB->setValue(value);
3072             return true;
3073         }
3074     }
3075     else if ((pDSB = qobject_cast<QDoubleSpinBox *>(widget)))
3076     {
3077         const double value = settings[key].toDouble(&ok);
3078         if (ok)
3079         {
3080             pDSB->setValue(value);
3081             return true;
3082         }
3083     }
3084     else if ((pCB = qobject_cast<QCheckBox *>(widget)))
3085     {
3086         const bool value = settings[key].toBool();
3087         pCB->setChecked(value);
3088         return true;
3089     }
3090     // ONLY FOR STRINGS, not INDEX
3091     else if ((pComboBox = qobject_cast<QComboBox *>(widget)))
3092     {
3093         const QString value = settings[key].toString();
3094         pComboBox->setCurrentText(value);
3095         return true;
3096     }
3097 
3098     return false;
3099 };
3100 
3101 void Guide::setupOpticalTrainManager()
3102 {
3103     connect(OpticalTrainManager::Instance(), &OpticalTrainManager::updated, this, &Guide::refreshOpticalTrain);
3104     connect(trainB, &QPushButton::clicked, this, [this]()
3105     {
3106         OpticalTrainManager::Instance()->openEditor(opticalTrainCombo->currentText());
3107     });
3108     connect(opticalTrainCombo, QOverload<int>::of(&QComboBox::currentIndexChanged), this, [this](int index)
3109     {
3110         if (guiderType == GUIDE_PHD2 && m_GuiderInstance->isConnected())
3111         {
3112             appendLogText(i18n("Cannot change active optical train while PHD2 is connected"));
3113             return;
3114         }
3115 
3116         ProfileSettings::Instance()->setOneSetting(ProfileSettings::GuideOpticalTrain,
3117                 OpticalTrainManager::Instance()->id(opticalTrainCombo->itemText(index)));
3118         refreshOpticalTrain();
3119         emit trainChanged();
3120     });
3121 }
3122 
3123 void Guide::refreshOpticalTrain()
3124 {
3125     opticalTrainCombo->blockSignals(true);
3126     opticalTrainCombo->clear();
3127     opticalTrainCombo->addItems(OpticalTrainManager::Instance()->getTrainNames());
3128     trainB->setEnabled(true);
3129 
3130     QVariant trainID = ProfileSettings::Instance()->getOneSetting(ProfileSettings::GuideOpticalTrain);
3131 
3132     if (trainID.isValid())
3133     {
3134         auto id = trainID.toUInt();
3135 
3136         // If train not found, select the first one available.
3137         if (OpticalTrainManager::Instance()->exists(id) == false)
3138         {
3139             qCWarning(KSTARS_EKOS_GUIDE) << "Optical train doesn't exist for id" << id;
3140             id = OpticalTrainManager::Instance()->id(opticalTrainCombo->itemText(0));
3141         }
3142 
3143         auto name = OpticalTrainManager::Instance()->name(id);
3144 
3145         opticalTrainCombo->setCurrentText(name);
3146 
3147         auto scope = OpticalTrainManager::Instance()->getScope(name);
3148         m_FocalLength = scope["focal_length"].toDouble(-1);
3149         m_Aperture = scope["aperture"].toDouble(-1);
3150         m_FocalRatio = scope["focal_ratio"].toDouble(-1);
3151         m_Reducer = OpticalTrainManager::Instance()->getReducer(name);
3152 
3153         // DSLR Lens Aperture
3154         if (m_Aperture < 0 && m_FocalRatio > 0)
3155             m_Aperture = m_FocalLength / m_FocalRatio;
3156 
3157         auto mount = OpticalTrainManager::Instance()->getMount(name);
3158         setMount(mount);
3159 
3160         auto camera = OpticalTrainManager::Instance()->getCamera(name);
3161         if (camera)
3162         {
3163             if (guiderType == GUIDE_INTERNAL)
3164                 starCenter = QVector3D();
3165 
3166             camera->setScopeInfo(m_FocalLength * m_Reducer, m_Aperture);
3167             opticalTrainCombo->setToolTip(QString("%1 @ %2").arg(camera->getDeviceName(), scope["name"].toString()));
3168         }
3169         setCamera(camera);
3170 
3171         syncTelescopeInfo();
3172 
3173         auto guider = OpticalTrainManager::Instance()->getGuider(name);
3174         setGuider(guider);
3175 
3176         auto ao = OpticalTrainManager::Instance()->getAdaptiveOptics(name);
3177         setAdaptiveOptics(ao);
3178 
3179         // Load train settings
3180         OpticalTrainSettings::Instance()->setOpticalTrainID(id);
3181         auto settings = OpticalTrainSettings::Instance()->getOneSetting(OpticalTrainSettings::Guide);
3182         if (settings.isValid())
3183             setAllSettings(settings.toJsonObject().toVariantMap());
3184         else
3185             m_Settings = m_GlobalSettings;
3186     }
3187 
3188     opticalTrainCombo->blockSignals(false);
3189 }
3190 
3191 void Guide::loadGlobalSettings()
3192 {
3193     QString key;
3194     QVariant value;
3195 
3196     QVariantMap settings;
3197     // All Combo Boxes
3198     for (auto &oneWidget : findChildren<QComboBox*>())
3199     {
3200         if (oneWidget->objectName() == "opticalTrainCombo")
3201             continue;
3202 
3203         key = oneWidget->objectName();
3204         value = Options::self()->property(key.toLatin1());
3205         if (value.isValid())
3206         {
3207             oneWidget->setCurrentText(value.toString());
3208             settings[key] = value;
3209         }
3210     }
3211 
3212     // All Double Spin Boxes
3213     for (auto &oneWidget : findChildren<QDoubleSpinBox*>())
3214     {
3215         key = oneWidget->objectName();
3216         value = Options::self()->property(key.toLatin1());
3217         if (value.isValid())
3218         {
3219             oneWidget->setValue(value.toDouble());
3220             settings[key] = value;
3221         }
3222     }
3223 
3224     // All Spin Boxes
3225     for (auto &oneWidget : findChildren<QSpinBox*>())
3226     {
3227         key = oneWidget->objectName();
3228         value = Options::self()->property(key.toLatin1());
3229         if (value.isValid())
3230         {
3231             oneWidget->setValue(value.toInt());
3232             settings[key] = value;
3233         }
3234     }
3235 
3236     // All Checkboxes
3237     for (auto &oneWidget : findChildren<QCheckBox*>())
3238     {
3239         key = oneWidget->objectName();
3240         value = Options::self()->property(key.toLatin1());
3241         if (value.isValid())
3242         {
3243             oneWidget->setChecked(value.toBool());
3244             settings[key] = value;
3245         }
3246     }
3247 
3248     m_GlobalSettings = m_Settings = settings;
3249 }
3250 
3251 void Guide::connectSettings()
3252 {
3253     // All Combo Boxes
3254     for (auto &oneWidget : findChildren<QComboBox*>())
3255         connect(oneWidget, QOverload<int>::of(&QComboBox::activated), this, &Ekos::Guide::syncSettings);
3256 
3257     // All Double Spin Boxes
3258     for (auto &oneWidget : findChildren<QDoubleSpinBox*>())
3259         connect(oneWidget, &QDoubleSpinBox::editingFinished, this, &Ekos::Guide::syncSettings);
3260 
3261     // All Spin Boxes
3262     for (auto &oneWidget : findChildren<QSpinBox*>())
3263         connect(oneWidget, &QSpinBox::editingFinished, this, &Ekos::Guide::syncSettings);
3264 
3265     // All Checkboxes
3266     for (auto &oneWidget : findChildren<QCheckBox*>())
3267         connect(oneWidget, &QCheckBox::toggled, this, &Ekos::Guide::syncSettings);
3268 
3269     // Train combo box should NOT be synced.
3270     disconnect(opticalTrainCombo, QOverload<int>::of(&QComboBox::activated), this, &Ekos::Guide::syncSettings);
3271 }
3272 
3273 void Guide::disconnectSettings()
3274 {
3275     // All Combo Boxes
3276     for (auto &oneWidget : findChildren<QComboBox*>())
3277         disconnect(oneWidget, QOverload<int>::of(&QComboBox::activated), this, &Ekos::Guide::syncSettings);
3278 
3279     // All Double Spin Boxes
3280     for (auto &oneWidget : findChildren<QDoubleSpinBox*>())
3281         disconnect(oneWidget, &QDoubleSpinBox::editingFinished, this, &Ekos::Guide::syncSettings);
3282 
3283     // All Spin Boxes
3284     for (auto &oneWidget : findChildren<QSpinBox*>())
3285         disconnect(oneWidget, &QSpinBox::editingFinished, this, &Ekos::Guide::syncSettings);
3286 
3287     // All Checkboxes
3288     for (auto &oneWidget : findChildren<QCheckBox*>())
3289         disconnect(oneWidget, &QCheckBox::toggled, this, &Ekos::Guide::syncSettings);
3290 
3291 }
3292 
3293 void Guide::updateSetting(const QString &key, const QVariant &value)
3294 {
3295     // Save immediately
3296     Options::self()->setProperty(key.toLatin1(), value);
3297     Options::self()->save();
3298 
3299     m_Settings[key] = value;
3300     m_GlobalSettings[key] = value;
3301 
3302     emit settingsUpdated(getAllSettings());
3303 
3304     // Save to optical train specific settings as well
3305     OpticalTrainSettings::Instance()->setOpticalTrainID(OpticalTrainManager::Instance()->id(opticalTrainCombo->currentText()));
3306     OpticalTrainSettings::Instance()->setOneSetting(OpticalTrainSettings::Guide, m_Settings);
3307 }
3308 
3309 void Guide::syncSettings()
3310 {
3311     QDoubleSpinBox *dsb = nullptr;
3312     QSpinBox *sb = nullptr;
3313     QCheckBox *cb = nullptr;
3314     QComboBox *cbox = nullptr;
3315 
3316     QString key;
3317     QVariant value;
3318 
3319     if ( (dsb = qobject_cast<QDoubleSpinBox*>(sender())))
3320     {
3321         key = dsb->objectName();
3322         value = dsb->value();
3323 
3324     }
3325     else if ( (sb = qobject_cast<QSpinBox*>(sender())))
3326     {
3327         key = sb->objectName();
3328         value = sb->value();
3329     }
3330     else if ( (cb = qobject_cast<QCheckBox*>(sender())))
3331     {
3332         key = cb->objectName();
3333         value = cb->isChecked();
3334     }
3335     else if ( (cbox = qobject_cast<QComboBox*>(sender())))
3336     {
3337         key = cbox->objectName();
3338         value = cbox->currentText();
3339     }
3340 
3341     updateSetting(key, value);
3342 }
3343 
3344 
3345 }