File indexing completed on 2024-05-05 15:55:10

0001 
0002 /*
0003     SPDX-FileCopyrightText: 2012 Jasem Mutlaq <mutlaqja@ikarustech.com>
0004 
0005     SPDX-License-Identifier: GPL-2.0-or-later
0006 */
0007 
0008 #include "adaptivefocus.h"
0009 #include "focusadaptor.h"
0010 #include "focusalgorithms.h"
0011 #include "focusfwhm.h"
0012 #include "aberrationinspector.h"
0013 #include "aberrationinspectorutils.h"
0014 #include "kstars.h"
0015 #include "kstarsdata.h"
0016 #include "Options.h"
0017 #include "stellarsolver.h"
0018 
0019 // Modules
0020 #include "ekos/guide/guide.h"
0021 #include "ekos/manager.h"
0022 
0023 // KStars Auxiliary
0024 #include "auxiliary/kspaths.h"
0025 #include "auxiliary/ksmessagebox.h"
0026 
0027 // Ekos Auxiliary
0028 #include "ekos/auxiliary/darklibrary.h"
0029 #include "ekos/auxiliary/darkprocessor.h"
0030 #include "ekos/auxiliary/profilesettings.h"
0031 #include "ekos/auxiliary/opticaltrainmanager.h"
0032 #include "ekos/auxiliary/opticaltrainsettings.h"
0033 #include "ekos/auxiliary/filtermanager.h"
0034 #include "ekos/auxiliary/stellarsolverprofileeditor.h"
0035 
0036 // FITS
0037 #include "fitsviewer/fitsdata.h"
0038 #include "fitsviewer/fitsview.h"
0039 #include "fitsviewer/fitsviewer.h"
0040 
0041 // Devices
0042 #include "indi/indifilterwheel.h"
0043 #include "ksnotification.h"
0044 #include "kconfigdialog.h"
0045 
0046 #include <basedevice.h>
0047 #include <gsl/gsl_fit.h>
0048 #include <gsl/gsl_vector.h>
0049 #include <gsl/gsl_min.h>
0050 
0051 #include <ekos_focus_debug.h>
0052 
0053 #include <cmath>
0054 
0055 #define MAXIMUM_ABS_ITERATIONS   30
0056 #define MAXIMUM_RESET_ITERATIONS 3
0057 #define AUTO_STAR_TIMEOUT        45000
0058 #define MINIMUM_PULSE_TIMER      32
0059 #define MAX_RECAPTURE_RETRIES    3
0060 #define MINIMUM_POLY_SOLUTIONS   2
0061 
0062 namespace Ekos
0063 {
0064 Focus::Focus() : QWidget()
0065 {
0066     // #1 Set the UI
0067     setupUi(this);
0068 
0069     // #1a Prepare UI
0070     prepareGUI();
0071 
0072     // #2 Register DBus
0073     qRegisterMetaType<Ekos::FocusState>("Ekos::FocusState");
0074     qDBusRegisterMetaType<Ekos::FocusState>();
0075     new FocusAdaptor(this);
0076     QDBusConnection::sessionBus().registerObject("/KStars/Ekos/Focus", this);
0077 
0078     // #3 Init connections
0079     initConnections();
0080 
0081     // #4 Init Plots
0082     initPlots();
0083 
0084     // #5 Init View
0085     initView();
0086 
0087     // #6 Reset all buttons to default states
0088     resetButtons();
0089 
0090     // #7 Load All settings
0091     loadGlobalSettings();
0092 
0093     // #8 Init Setting Connection now
0094     connectSyncSettings();
0095 
0096     // #9 Init Adaptive Focus
0097     adaptFocus.reset(new AdaptiveFocus(this));
0098 
0099     connect(&m_StarFinderWatcher, &QFutureWatcher<bool>::finished, this, &Focus::starDetectionFinished);
0100 
0101     //Note:  This is to prevent a button from being called the default button
0102     //and then executing when the user hits the enter key such as when on a Text Box
0103     QList<QPushButton *> qButtons = findChildren<QPushButton *>();
0104     for (auto &button : qButtons)
0105         button->setAutoDefault(false);
0106 
0107     appendLogText(i18n("Idle."));
0108 
0109     // Focus motion timeout
0110     m_FocusMotionTimer.setInterval(m_OpsFocusMechanics->focusMotionTimeout->value() * 1000);
0111     m_FocusMotionTimer.setSingleShot(true);
0112     connect(&m_FocusMotionTimer, &QTimer::timeout, this, &Focus::handleFocusMotionTimeout);
0113 
0114     // Create an autofocus CSV file, dated at startup time
0115     m_FocusLogFileName = QDir(KSPaths::writableLocation(QStandardPaths::AppLocalDataLocation)).filePath("focuslogs/autofocus-" +
0116                          QDateTime::currentDateTime().toString("yyyy-MM-ddThh-mm-ss") + ".txt");
0117     m_FocusLogFile.setFileName(m_FocusLogFileName);
0118 
0119     m_OpsFocusProcess->editFocusProfile->setIcon(QIcon::fromTheme("document-edit"));
0120     m_OpsFocusProcess->editFocusProfile->setAttribute(Qt::WA_LayoutUsesWidgetRect);
0121 
0122     connect(m_OpsFocusProcess->editFocusProfile, &QAbstractButton::clicked, this, [this]()
0123     {
0124         KConfigDialog *optionsEditor = new KConfigDialog(this, "OptionsProfileEditor", Options::self());
0125         optionsProfileEditor = new StellarSolverProfileEditor(this, Ekos::FocusProfiles, optionsEditor);
0126 #ifdef Q_OS_OSX
0127         optionsEditor->setWindowFlags(Qt::Tool | Qt::WindowStaysOnTopHint);
0128 #endif
0129         KPageWidgetItem *mainPage = optionsEditor->addPage(optionsProfileEditor, i18n("Focus Options Profile Editor"));
0130         mainPage->setIcon(QIcon::fromTheme("configure"));
0131         connect(optionsProfileEditor, &StellarSolverProfileEditor::optionsProfilesUpdated, this, &Focus::loadStellarSolverProfiles);
0132         optionsProfileEditor->loadProfile(m_OpsFocusProcess->focusSEPProfile->currentText());
0133         optionsEditor->show();
0134     });
0135 
0136     loadStellarSolverProfiles();
0137 
0138     // connect HFR plot widget
0139     connect(this, &Ekos::Focus::initHFRPlot, HFRPlot, &FocusHFRVPlot::init);
0140     connect(this, &Ekos::Focus::redrawHFRPlot, HFRPlot, &FocusHFRVPlot::redraw);
0141     connect(this, &Ekos::Focus::newHFRPlotPosition, HFRPlot, &FocusHFRVPlot::addPosition);
0142     connect(this, &Ekos::Focus::drawPolynomial, HFRPlot, &FocusHFRVPlot::drawPolynomial);
0143     // connect signal/slot for the curve plotting to the V-Curve widget
0144     connect(this, &Ekos::Focus::drawCurve, HFRPlot, &FocusHFRVPlot::drawCurve);
0145     connect(this, &Ekos::Focus::setTitle, HFRPlot, &FocusHFRVPlot::setTitle);
0146     connect(this, &Ekos::Focus::finalUpdates, HFRPlot, &FocusHFRVPlot::finalUpdates);
0147     connect(this, &Ekos::Focus::minimumFound, HFRPlot, &FocusHFRVPlot::drawMinimum);
0148     connect(this, &Ekos::Focus::drawCFZ, HFRPlot, &FocusHFRVPlot::drawCFZ);
0149 
0150     m_DarkProcessor = new DarkProcessor(this);
0151     connect(m_DarkProcessor, &DarkProcessor::newLog, this, &Ekos::Focus::appendLogText);
0152     connect(m_DarkProcessor, &DarkProcessor::darkFrameCompleted, this, [this](bool completed)
0153     {
0154         m_OpsFocusSettings->useFocusDarkFrame->setChecked(completed);
0155         m_FocusView->setProperty("suspended", false);
0156         if (completed)
0157         {
0158             m_FocusView->rescale(ZOOM_KEEP_LEVEL);
0159             m_FocusView->updateFrame();
0160         }
0161         setCaptureComplete();
0162         resetButtons();
0163     });
0164 
0165     setupOpticalTrainManager();
0166     // Needs to be done once
0167     connectFilterManager();
0168 }
0169 
0170 // Do once only preparation of GUI
0171 void Focus::prepareGUI()
0172 {
0173     // Parameters are handled by the KConfigDialog invoked by pressing the "Options..." button
0174     // on the Focus window. There are 3 pages of options.
0175     // Parameters are persisted per Optical Train, so when the user changes OT, the last persisted
0176     // parameters for the new OT are loaded. In addition the "current" parameter values are also
0177     // persisted locally using kstars.kcfg
0178     // KConfigDialog has the ability to persist parameters to kstars.kcfg but this functionality
0179     // is not used in Focus
0180     KConfigDialog *dialog = new KConfigDialog(this, "focussettings", Options::self());
0181     m_OpsFocusSettings = new OpsFocusSettings();
0182 #ifdef Q_OS_OSX
0183     dialog->setWindowFlags(Qt::Tool | Qt::WindowStaysOnTopHint);
0184 #endif
0185 
0186     KPageWidgetItem *page = dialog->addPage(m_OpsFocusSettings, i18n("Settings"), nullptr, i18n("Focus Settings"), false);
0187     page->setIcon(QIcon::fromTheme("configure"));
0188 
0189     m_OpsFocusProcess = new OpsFocusProcess();
0190     page = dialog->addPage(m_OpsFocusProcess, i18n("Process"), nullptr, i18n("Focus Process"), false);
0191     page->setIcon(QIcon::fromTheme("transform-move"));
0192 
0193     m_OpsFocusMechanics = new OpsFocusMechanics();
0194     page = dialog->addPage(m_OpsFocusMechanics, i18n("Mechanics"), nullptr, i18n("Focus Mechanics"), false);
0195     page->setIcon(QIcon::fromTheme("tool-measure"));
0196 
0197     // The CFZ is a tool so has its own dialog box.
0198     m_CFZDialog = new QDialog(this);
0199     m_CFZUI.reset(new Ui::focusCFZDialog());
0200     m_CFZUI->setupUi(m_CFZDialog);
0201 #ifdef Q_OS_OSX
0202     m_CFZDialog->setWindowFlags(Qt::Tool | Qt::WindowStaysOnTopHint);
0203 #endif
0204 
0205     // The Focus Advisor is a tool so has its own dialog box.
0206     m_AdvisorDialog = new QDialog(this);
0207     m_AdvisorUI.reset(new Ui::focusAdvisorDialog());
0208     m_AdvisorUI->setupUi(m_AdvisorDialog);
0209 #ifdef Q_OS_OSX
0210     m_AdvisorDialog->setWindowFlags(Qt::Tool | Qt::WindowStaysOnTopHint);
0211 #endif
0212 
0213     // Remove all widgets from the temporary bucket. These will then be loaded as required
0214     m_OpsFocusProcess->gridLayoutProcessBucket->removeWidget(m_OpsFocusProcess->focusMultiRowAverageLabel);
0215     m_OpsFocusProcess->gridLayoutProcessBucket->removeWidget(m_OpsFocusProcess->focusMultiRowAverage);
0216     m_OpsFocusProcess->gridLayoutProcessBucket->removeWidget(m_OpsFocusProcess->focusGaussianSigmaLabel);
0217     m_OpsFocusProcess->gridLayoutProcessBucket->removeWidget(m_OpsFocusProcess->focusGaussianSigma);
0218     m_OpsFocusProcess->gridLayoutProcessBucket->removeWidget(m_OpsFocusProcess->focusThresholdLabel);
0219     m_OpsFocusProcess->gridLayoutProcessBucket->removeWidget(m_OpsFocusProcess->focusThreshold);
0220     m_OpsFocusProcess->gridLayoutProcessBucket->removeWidget(m_OpsFocusProcess->focusGaussianKernelSizeLabel);
0221     m_OpsFocusProcess->gridLayoutProcessBucket->removeWidget(m_OpsFocusProcess->focusGaussianKernelSize);
0222     m_OpsFocusProcess->gridLayoutProcessBucket->removeWidget(m_OpsFocusProcess->focusToleranceLabel);
0223     m_OpsFocusProcess->gridLayoutProcessBucket->removeWidget(m_OpsFocusProcess->focusTolerance);
0224     delete m_OpsFocusProcess->gridLayoutProcessBucket;
0225 
0226     // Setup the Walk fields. OutSteps and NumSteps are either/or widgets so co-locate them
0227     m_OpsFocusMechanics->gridLayoutMechanics->replaceWidget(m_OpsFocusMechanics->focusOutStepsLabel,
0228             m_OpsFocusMechanics->focusNumStepsLabel);
0229     m_OpsFocusMechanics->gridLayoutMechanics->replaceWidget(m_OpsFocusMechanics->focusOutSteps,
0230             m_OpsFocusMechanics->focusNumSteps);
0231 
0232     // Some combo-boxes have changeable values depending on other settings so store the full list of options from the .ui
0233     // This helps keep some synchronisation with the .ui
0234     for (int i = 0; i < m_OpsFocusProcess->focusStarMeasure->count(); i++)
0235         m_StarMeasureText.append(m_OpsFocusProcess->focusStarMeasure->itemText(i));
0236     for (int i = 0; i < m_OpsFocusProcess->focusCurveFit->count(); i++)
0237         m_CurveFitText.append(m_OpsFocusProcess->focusCurveFit->itemText(i));
0238     for (int i = 0; i < m_OpsFocusMechanics->focusWalk->count(); i++)
0239         m_FocusWalkText.append(m_OpsFocusMechanics->focusWalk->itemText(i));
0240 }
0241 
0242 void Focus::loadStellarSolverProfiles()
0243 {
0244     QString savedOptionsProfiles = QDir(KSPaths::writableLocation(
0245                                             QStandardPaths::AppLocalDataLocation)).filePath("SavedFocusProfiles.ini");
0246     if(QFileInfo::exists(savedOptionsProfiles))
0247         m_StellarSolverProfiles = StellarSolver::loadSavedOptionsProfiles(savedOptionsProfiles);
0248     else
0249         m_StellarSolverProfiles = getDefaultFocusOptionsProfiles();
0250     m_OpsFocusProcess->focusSEPProfile->clear();
0251     for(auto &param : m_StellarSolverProfiles)
0252         m_OpsFocusProcess->focusSEPProfile->addItem(param.listName);
0253     auto profile = m_Settings["focusSEPProfile"];
0254     if (profile.isValid())
0255         m_OpsFocusProcess->focusSEPProfile->setCurrentText(profile.toString());
0256 }
0257 
0258 QStringList Focus::getStellarSolverProfiles()
0259 {
0260     QStringList profiles;
0261     for (auto param : m_StellarSolverProfiles)
0262         profiles << param.listName;
0263 
0264     return profiles;
0265 }
0266 
0267 Focus::~Focus()
0268 {
0269     if (focusingWidget->parent() == nullptr)
0270         toggleFocusingWidgetFullScreen();
0271 
0272     m_FocusLogFile.close();
0273 }
0274 
0275 void Focus::resetFrame()
0276 {
0277     if (m_Camera && m_Camera->isConnected())
0278     {
0279         ISD::CameraChip *targetChip = m_Camera->getChip(ISD::CameraChip::PRIMARY_CCD);
0280 
0281         if (targetChip)
0282         {
0283             //fx=fy=fw=fh=0;
0284             targetChip->resetFrame();
0285 
0286             int x, y, w, h;
0287             targetChip->getFrame(&x, &y, &w, &h);
0288 
0289             qCDebug(KSTARS_EKOS_FOCUS) << "Frame is reset. X:" << x << "Y:" << y << "W:" << w << "H:" << h << "binX:" << 1 << "binY:" <<
0290                                        1;
0291 
0292             QVariantMap settings;
0293             settings["x"]             = x;
0294             settings["y"]             = y;
0295             settings["w"]             = w;
0296             settings["h"]             = h;
0297             settings["binx"]          = 1;
0298             settings["biny"]          = 1;
0299             frameSettings[targetChip] = settings;
0300 
0301             starSelected = false;
0302             starCenter   = QVector3D();
0303             subFramed    = false;
0304 
0305             m_FocusView->setTrackingBox(QRect());
0306             checkMosaicMaskLimits();
0307         }
0308     }
0309 }
0310 
0311 QString Focus::camera()
0312 {
0313     if (m_Camera)
0314         return m_Camera->getDeviceName();
0315 
0316     return QString();
0317 }
0318 
0319 void Focus::checkCamera()
0320 {
0321     if (!m_Camera)
0322         return;
0323 
0324     // Do NOT perform checks when the camera is capturing or busy as this may result
0325     // in signals/slots getting disconnected.
0326     switch (state())
0327     {
0328         // Idle, can change camera.
0329         case FOCUS_IDLE:
0330         case FOCUS_COMPLETE:
0331         case FOCUS_FAILED:
0332         case FOCUS_ABORTED:
0333             break;
0334 
0335         // Busy, cannot change camera.
0336         case FOCUS_WAITING:
0337         case FOCUS_PROGRESS:
0338         case FOCUS_FRAMING:
0339         case FOCUS_CHANGING_FILTER:
0340             return;
0341     }
0342 
0343 
0344     ISD::CameraChip *targetChip = m_Camera->getChip(ISD::CameraChip::PRIMARY_CCD);
0345     if (targetChip && targetChip->isCapturing())
0346         return;
0347 
0348     if (targetChip)
0349     {
0350         focusBinning->setEnabled(targetChip->canBin());
0351         m_OpsFocusSettings->focusSubFrame->setEnabled(targetChip->canSubframe());
0352         if (targetChip->canBin())
0353         {
0354             int subBinX = 1, subBinY = 1;
0355             focusBinning->clear();
0356             targetChip->getMaxBin(&subBinX, &subBinY);
0357             for (int i = 1; i <= subBinX; i++)
0358                 focusBinning->addItem(QString("%1x%2").arg(i).arg(i));
0359 
0360             auto binning = m_Settings["focusBinning"];
0361             if (binning.isValid())
0362                 focusBinning->setCurrentText(binning.toString());
0363         }
0364 
0365         connect(m_Camera, &ISD::Camera::videoStreamToggled, this, &Ekos::Focus::setVideoStreamEnabled, Qt::UniqueConnection);
0366         liveVideoB->setEnabled(m_Camera->hasVideoStream());
0367         if (m_Camera->hasVideoStream())
0368             setVideoStreamEnabled(m_Camera->isStreamingEnabled());
0369         else
0370             liveVideoB->setIcon(QIcon::fromTheme("camera-off"));
0371 
0372     }
0373 
0374     syncCCDControls();
0375     syncCameraInfo();
0376 }
0377 
0378 void Focus::syncCCDControls()
0379 {
0380     if (m_Camera == nullptr)
0381         return;
0382 
0383     auto targetChip = m_Camera->getChip(ISD::CameraChip::PRIMARY_CCD);
0384     if (targetChip == nullptr || (targetChip && targetChip->isCapturing()))
0385         return;
0386 
0387     auto isoList = targetChip->getISOList();
0388     focusISO->clear();
0389 
0390     if (isoList.isEmpty())
0391     {
0392         focusISO->setEnabled(false);
0393         ISOLabel->setEnabled(false);
0394     }
0395     else
0396     {
0397         focusISO->setEnabled(true);
0398         ISOLabel->setEnabled(true);
0399         focusISO->addItems(isoList);
0400         focusISO->setCurrentIndex(targetChip->getISOIndex());
0401     }
0402 
0403     bool hasGain = m_Camera->hasGain();
0404     gainLabel->setEnabled(hasGain);
0405     focusGain->setEnabled(hasGain && m_Camera->getGainPermission() != IP_RO);
0406     if (hasGain)
0407     {
0408         double gain = 0, min = 0, max = 0, step = 1;
0409         m_Camera->getGainMinMaxStep(&min, &max, &step);
0410         if (m_Camera->getGain(&gain))
0411         {
0412             // Allow the possibility of no gain value at all.
0413             GainSpinSpecialValue = min - step;
0414             focusGain->setRange(GainSpinSpecialValue, max);
0415             focusGain->setSpecialValueText(i18n("--"));
0416             if (step > 0)
0417                 focusGain->setSingleStep(step);
0418 
0419             auto defaultGain = m_Settings["focusGain"];
0420             if (defaultGain.isValid())
0421                 focusGain->setValue(defaultGain.toDouble());
0422             else
0423                 focusGain->setValue(GainSpinSpecialValue);
0424         }
0425     }
0426     else
0427         focusGain->clear();
0428 }
0429 
0430 void Focus::syncCameraInfo()
0431 {
0432     if (m_Camera == nullptr)
0433         return;
0434 
0435     auto targetChip = m_Camera->getChip(ISD::CameraChip::PRIMARY_CCD);
0436     if (targetChip == nullptr || (targetChip && targetChip->isCapturing()))
0437         return;
0438 
0439     m_OpsFocusSettings->focusSubFrame->setEnabled(targetChip->canSubframe());
0440 
0441     if (frameSettings.contains(targetChip) == false)
0442     {
0443         int x, y, w, h;
0444         if (targetChip->getFrame(&x, &y, &w, &h))
0445         {
0446             int binx = 1, biny = 1;
0447             targetChip->getBinning(&binx, &biny);
0448             if (w > 0 && h > 0)
0449             {
0450                 int minX, maxX, minY, maxY, minW, maxW, minH, maxH;
0451                 targetChip->getFrameMinMax(&minX, &maxX, &minY, &maxY, &minW, &maxW, &minH, &maxH);
0452 
0453                 QVariantMap settings;
0454 
0455                 settings["x"]    = m_OpsFocusSettings->focusSubFrame->isChecked() ? x : minX;
0456                 settings["y"]    = m_OpsFocusSettings->focusSubFrame->isChecked() ? y : minY;
0457                 settings["w"]    = m_OpsFocusSettings->focusSubFrame->isChecked() ? w : maxW;
0458                 settings["h"]    = m_OpsFocusSettings->focusSubFrame->isChecked() ? h : maxH;
0459                 settings["binx"] = binx;
0460                 settings["biny"] = biny;
0461 
0462                 frameSettings[targetChip] = settings;
0463             }
0464         }
0465     }
0466 }
0467 
0468 bool Focus::setFilterWheel(ISD::FilterWheel *device)
0469 {
0470     if (m_FilterWheel && m_FilterWheel == device)
0471     {
0472         checkFilter();
0473         return false;
0474     }
0475 
0476     if (m_FilterWheel)
0477         m_FilterWheel->disconnect(this);
0478 
0479     m_FilterWheel = device;
0480 
0481     if (m_FilterWheel)
0482     {
0483         connect(m_FilterWheel, &ISD::ConcreteDevice::Connected, this, [this]()
0484         {
0485             FilterPosLabel->setEnabled(true);
0486             focusFilter->setEnabled(true);
0487             filterManagerB->setEnabled(true);
0488         });
0489         connect(m_FilterWheel, &ISD::ConcreteDevice::Disconnected, this, [this]()
0490         {
0491             FilterPosLabel->setEnabled(false);
0492             focusFilter->setEnabled(false);
0493             filterManagerB->setEnabled(false);
0494         });
0495     }
0496 
0497     auto isConnected = m_FilterWheel && m_FilterWheel->isConnected();
0498     FilterPosLabel->setEnabled(isConnected);
0499     focusFilter->setEnabled(isConnected);
0500     filterManagerB->setEnabled(isConnected);
0501 
0502     FilterPosLabel->setEnabled(true);
0503     focusFilter->setEnabled(true);
0504 
0505     checkFilter();
0506     return true;
0507 }
0508 
0509 bool Focus::addTemperatureSource(const QSharedPointer<ISD::GenericDevice> &device)
0510 {
0511     if (device.isNull())
0512         return false;
0513 
0514     for (auto &oneSource : m_TemperatureSources)
0515     {
0516         if (oneSource->getDeviceName() == device->getDeviceName())
0517             return false;
0518     }
0519 
0520     m_TemperatureSources.append(device);
0521     defaultFocusTemperatureSource->addItem(device->getDeviceName());
0522 
0523     auto targetSource = m_Settings["defaultFocusTemperatureSource"];
0524     if (targetSource.isValid())
0525         checkTemperatureSource(targetSource.toString());
0526     return true;
0527 }
0528 
0529 void Focus::checkTemperatureSource(const QString &name )
0530 {
0531     auto source = name;
0532     if (name.isEmpty())
0533     {
0534         source = defaultFocusTemperatureSource->currentText();
0535         if (source.isEmpty())
0536             return;
0537     }
0538 
0539     QSharedPointer<ISD::GenericDevice> currentSource;
0540 
0541     for (auto &oneSource : m_TemperatureSources)
0542     {
0543         if (oneSource->getDeviceName() == source)
0544         {
0545             currentSource = oneSource;
0546             break;
0547         }
0548     }
0549 
0550     m_LastSourceDeviceAutofocusTemperature = currentSource;
0551 
0552     // No valid device found
0553     if (!currentSource)
0554         return;
0555 
0556     // Disconnect all existing signals
0557     for (const auto &oneSource : m_TemperatureSources)
0558         disconnect(oneSource.get(), &ISD::GenericDevice::propertyUpdated, this, &Ekos::Focus::processTemperatureSource);
0559 
0560 
0561     if (findTemperatureElement(currentSource))
0562     {
0563         m_LastSourceAutofocusTemperature = currentTemperatureSourceElement->value;
0564         absoluteTemperatureLabel->setText(QString("%1 °C").arg(currentTemperatureSourceElement->value, 0, 'f', 2));
0565         deltaTemperatureLabel->setText(QString("%1 °C").arg(0.0, 0, 'f', 2));
0566     }
0567     else
0568     {
0569         m_LastSourceAutofocusTemperature = INVALID_VALUE;
0570     }
0571 
0572     connect(currentSource.get(), &ISD::GenericDevice::propertyUpdated, this, &Ekos::Focus::processTemperatureSource,
0573             Qt::UniqueConnection);
0574 }
0575 
0576 bool Focus::findTemperatureElement(const QSharedPointer<ISD::GenericDevice> &device)
0577 {
0578     // protect for nullptr
0579     if (device.isNull())
0580         return false;
0581 
0582     auto temperatureProperty = device->getProperty("FOCUS_TEMPERATURE");
0583     if (!temperatureProperty)
0584         temperatureProperty = device->getProperty("CCD_TEMPERATURE");
0585     if (temperatureProperty)
0586     {
0587         currentTemperatureSourceElement = temperatureProperty.getNumber()->at(0);
0588         return true;
0589     }
0590 
0591     temperatureProperty = device->getProperty("WEATHER_PARAMETERS");
0592     if (temperatureProperty)
0593     {
0594         for (int i = 0; i < temperatureProperty.getNumber()->count(); i++)
0595         {
0596             if (strstr(temperatureProperty.getNumber()->at(i)->getName(), "_TEMPERATURE"))
0597             {
0598                 currentTemperatureSourceElement = temperatureProperty.getNumber()->at(i);
0599                 return true;
0600             }
0601         }
0602     }
0603 
0604     return false;
0605 }
0606 
0607 QString Focus::filterWheel()
0608 {
0609     if (m_FilterWheel)
0610         return m_FilterWheel->getDeviceName();
0611 
0612     return QString();
0613 }
0614 
0615 
0616 bool Focus::setFilter(const QString &filter)
0617 {
0618     if (m_FilterWheel)
0619     {
0620         focusFilter->setCurrentText(filter);
0621         return true;
0622     }
0623 
0624     return false;
0625 }
0626 
0627 QString Focus::filter()
0628 {
0629     return focusFilter->currentText();
0630 }
0631 
0632 void Focus::checkFilter()
0633 {
0634     focusFilter->clear();
0635 
0636     if (!m_FilterWheel)
0637     {
0638         FilterPosLabel->setEnabled(false);
0639         focusFilter->setEnabled(false);
0640         filterManagerB->setEnabled(false);
0641 
0642         if (m_FilterManager)
0643         {
0644             m_FilterManager->disconnect(this);
0645             disconnect(m_FilterManager.get());
0646             m_FilterManager.reset();
0647         }
0648         return;
0649     }
0650 
0651     FilterPosLabel->setEnabled(true);
0652     focusFilter->setEnabled(true);
0653     filterManagerB->setEnabled(true);
0654 
0655     setupFilterManager();
0656 
0657     focusFilter->addItems(m_FilterManager->getFilterLabels());
0658 
0659     currentFilterPosition = m_FilterManager->getFilterPosition();
0660 
0661     focusFilter->setCurrentIndex(currentFilterPosition - 1);
0662 
0663     focusExposure->setValue(m_FilterManager->getFilterExposure());
0664 }
0665 
0666 bool Focus::setFocuser(ISD::Focuser *device)
0667 {
0668     if (m_Focuser && m_Focuser == device)
0669     {
0670         checkFocuser();
0671         return false;
0672     }
0673 
0674     if (m_Focuser)
0675         m_Focuser->disconnect(this);
0676 
0677     m_Focuser = device;
0678 
0679     if (m_Focuser)
0680     {
0681         connect(m_Focuser, &ISD::ConcreteDevice::Connected, this, [this]()
0682         {
0683             resetButtons();
0684         });
0685         connect(m_Focuser, &ISD::ConcreteDevice::Disconnected, this, [this]()
0686         {
0687             resetButtons();
0688         });
0689     }
0690 
0691     checkFocuser();
0692     return true;
0693 }
0694 
0695 QString Focus::focuser()
0696 {
0697     if (m_Focuser)
0698         return m_Focuser->getDeviceName();
0699 
0700     return QString();
0701 }
0702 
0703 void Focus::checkFocuser()
0704 {
0705     if (!m_Focuser)
0706     {
0707         if (m_FilterManager)
0708             m_FilterManager->setFocusReady(false);
0709         canAbsMove = canRelMove = canTimerMove = false;
0710         resetButtons();
0711         setFocusAlgorithm(static_cast<Algorithm> (m_OpsFocusProcess->focusAlgorithm->currentIndex()));
0712         return;
0713     }
0714     else
0715         focuserLabel->setText(m_Focuser->getDeviceName());
0716 
0717     if (m_FilterManager)
0718         m_FilterManager->setFocusReady(m_Focuser->isConnected());
0719 
0720     hasDeviation = m_Focuser->hasDeviation();
0721 
0722     canAbsMove = m_Focuser->canAbsMove();
0723 
0724     if (canAbsMove)
0725     {
0726         getAbsFocusPosition();
0727 
0728         absTicksSpin->setEnabled(true);
0729         absTicksLabel->setEnabled(true);
0730         startGotoB->setEnabled(true);
0731 
0732         // Now that Optical Trains are managing settings, no need to set absTicksSpin, except if it has a bad value
0733         if (absTicksSpin->value() <= 0)
0734             absTicksSpin->setValue(currentPosition);
0735     }
0736     else
0737     {
0738         absTicksSpin->setEnabled(false);
0739         absTicksLabel->setEnabled(false);
0740         startGotoB->setEnabled(false);
0741     }
0742 
0743     canRelMove = m_Focuser->canRelMove();
0744 
0745     // In case we have a purely relative focuser, we pretend
0746     // it is an absolute focuser with initial point set at 50,000.
0747     // This is done we can use the same algorithm used for absolute focuser.
0748     if (canAbsMove == false && canRelMove == true)
0749     {
0750         currentPosition = 50000;
0751         absMotionMax    = 100000;
0752         absMotionMin    = 0;
0753     }
0754 
0755     canTimerMove = m_Focuser->canTimerMove();
0756 
0757     // In case we have a timer-based focuser and using the linear focus algorithm,
0758     // we pretend it is an absolute focuser with initial point set at 50,000.
0759     // These variables don't have in impact on timer-based focusers if the algorithm
0760     // is not the linear focus algorithm.
0761     if (!canAbsMove && !canRelMove && canTimerMove)
0762     {
0763         currentPosition = 50000;
0764         absMotionMax    = 100000;
0765         absMotionMin    = 0;
0766     }
0767 
0768     m_FocusType = (canRelMove || canAbsMove || canTimerMove) ? FOCUS_AUTO : FOCUS_MANUAL;
0769     profilePlot->setFocusAuto(m_FocusType == FOCUS_AUTO);
0770 
0771     bool hasBacklash = m_Focuser->hasBacklash();
0772     m_OpsFocusMechanics->focusBacklash->setEnabled(hasBacklash);
0773     m_OpsFocusMechanics->focusBacklash->disconnect(this);
0774     if (hasBacklash)
0775     {
0776         double min = 0, max = 0, step = 0;
0777         m_Focuser->getMinMaxStep("FOCUS_BACKLASH_STEPS", "FOCUS_BACKLASH_VALUE", &min, &max, &step);
0778         m_OpsFocusMechanics->focusBacklash->setMinimum(min);
0779         m_OpsFocusMechanics->focusBacklash->setMaximum(max);
0780         m_OpsFocusMechanics->focusBacklash->setSingleStep(step);
0781         m_OpsFocusMechanics->focusBacklash->setValue(m_Focuser->getBacklash());
0782         connect(m_OpsFocusMechanics->focusBacklash, static_cast<void (QSpinBox::*)(int)>(&QSpinBox::valueChanged),
0783                 this, [this](int value)
0784         {
0785             if (m_Focuser)
0786             {
0787                 if (m_Focuser->getBacklash() == value)
0788                     // Prevent an event storm where a fast update of the backlash field, e.g. changing
0789                     // the value to "12" results in 2 events; "1", "12". As these events get
0790                     // processed in the driver and persisted, they in turn update backlash and start
0791                     // to conflict with this callback, getting stuck forever: "1", "12", "1', "12"
0792                     return;
0793                 m_Focuser->setBacklash(value);
0794             }
0795         });
0796         // Re-esablish connection to sync settings. Only need to reconnect if the focuser
0797         // has backlash as the value can't be changed if the focuser doesn't have the backlash property.
0798         connect(m_OpsFocusMechanics->focusBacklash, QOverload<int>::of(&QSpinBox::valueChanged), this, &Ekos::Focus::syncSettings);
0799     }
0800     else
0801     {
0802         m_OpsFocusMechanics->focusBacklash->setValue(0);
0803     }
0804 
0805     connect(m_Focuser, &ISD::Focuser::propertyUpdated, this, &Ekos::Focus::updateProperty, Qt::UniqueConnection);
0806 
0807     resetButtons();
0808     setFocusAlgorithm(static_cast<Algorithm> (m_OpsFocusProcess->focusAlgorithm->currentIndex()));
0809 }
0810 
0811 bool Focus::setCamera(ISD::Camera *device)
0812 {
0813     if (m_Camera && m_Camera == device)
0814     {
0815         checkCamera();
0816         return false;
0817     }
0818 
0819     if (m_Camera)
0820         m_Camera->disconnect(this);
0821 
0822     m_Camera = device;
0823 
0824     if (m_Camera)
0825     {
0826         connect(m_Camera, &ISD::ConcreteDevice::Connected, this, [this]()
0827         {
0828             focuserGroup->setEnabled(true);
0829             ccdGroup->setEnabled(true);
0830             toolsGroup->setEnabled(true);
0831         });
0832         connect(m_Camera, &ISD::ConcreteDevice::Disconnected, this, [this]()
0833         {
0834             focuserGroup->setEnabled(false);
0835             ccdGroup->setEnabled(false);
0836             toolsGroup->setEnabled(false);
0837         });
0838     }
0839 
0840     auto isConnected = m_Camera && m_Camera->isConnected();
0841     focuserGroup->setEnabled(isConnected);
0842     ccdGroup->setEnabled(isConnected);
0843     toolsGroup->setEnabled(isConnected);
0844 
0845     if (!m_Camera)
0846         return false;
0847 
0848     resetFrame();
0849 
0850     checkCamera();
0851     checkMosaicMaskLimits();
0852     return true;
0853 }
0854 
0855 void Focus::getAbsFocusPosition()
0856 {
0857     if (!canAbsMove)
0858         return;
0859 
0860     auto absMove = m_Focuser->getNumber("ABS_FOCUS_POSITION");
0861 
0862     if (absMove)
0863     {
0864         const auto &it = absMove->at(0);
0865         currentPosition = static_cast<int>(it->getValue());
0866         absMotionMax    = it->getMax();
0867         absMotionMin    = it->getMin();
0868 
0869         absTicksSpin->setMinimum(it->getMin());
0870         absTicksSpin->setMaximum(it->getMax());
0871         absTicksSpin->setSingleStep(it->getStep());
0872 
0873         // Restrict the travel if needed
0874         double const travel = std::abs(it->getMax() - it->getMin());
0875         m_OpsFocusMechanics->focusMaxTravel->setMaximum(travel);;
0876 
0877         absTicksLabel->setText(QString::number(currentPosition));
0878 
0879         m_OpsFocusMechanics->focusTicks->setMaximum(it->getMax() / 2);
0880     }
0881 }
0882 
0883 void Focus::processTemperatureSource(INDI::Property prop)
0884 {
0885     if (m_LastSourceAutofocusTemperature == INVALID_VALUE && m_LastSourceDeviceAutofocusTemperature
0886             && !currentTemperatureSourceElement )
0887     {
0888         if( findTemperatureElement( m_LastSourceDeviceAutofocusTemperature ) )
0889         {
0890             appendLogText(i18n("Finally found temperature source %1", QString(currentTemperatureSourceElement->nvp->name)));
0891             m_LastSourceAutofocusTemperature = currentTemperatureSourceElement->value;
0892         }
0893     }
0894 
0895     double delta = 0;
0896     if (currentTemperatureSourceElement && currentTemperatureSourceElement->nvp->name == prop.getName())
0897     {
0898         if (m_LastSourceAutofocusTemperature != INVALID_VALUE)
0899         {
0900             delta = currentTemperatureSourceElement->value - m_LastSourceAutofocusTemperature;
0901             emit newFocusTemperatureDelta(abs(delta), currentTemperatureSourceElement->value);
0902         }
0903         else
0904         {
0905             emit newFocusTemperatureDelta(0, currentTemperatureSourceElement->value);
0906         }
0907 
0908         absoluteTemperatureLabel->setText(QString("%1 °C").arg(currentTemperatureSourceElement->value, 0, 'f', 2));
0909         deltaTemperatureLabel->setText(QString("%1%2 °C").arg((delta > 0.0 ? "+" : "")).arg(delta, 0, 'f', 2));
0910         if (delta == 0)
0911             deltaTemperatureLabel->setStyleSheet("color: lightgreen");
0912         else if (delta > 0)
0913             deltaTemperatureLabel->setStyleSheet("color: lightcoral");
0914         else
0915             deltaTemperatureLabel->setStyleSheet("color: lightblue");
0916     }
0917 }
0918 
0919 void Focus::setLastFocusTemperature()
0920 {
0921     m_LastSourceAutofocusTemperature = currentTemperatureSourceElement ? currentTemperatureSourceElement->value : INVALID_VALUE;
0922 
0923     // Reset delta to zero now that we're just done with autofocus
0924     deltaTemperatureLabel->setText(QString("0 °C"));
0925     deltaTemperatureLabel->setStyleSheet("color: lightgreen");
0926 
0927     emit newFocusTemperatureDelta(0, -1e6);
0928 }
0929 
0930 void Focus::setLastFocusAlt()
0931 {
0932     if (mountAlt < 0.0 || mountAlt > 90.0)
0933         m_LastSourceAutofocusAlt = INVALID_VALUE;
0934     else
0935         m_LastSourceAutofocusAlt = mountAlt;
0936 }
0937 
0938 // Reset Adaptive Focus parameters.
0939 void Focus::resetAdaptiveFocus(bool enabled)
0940 {
0941     // Set the focusAdaptive switch so other modules such as Capture can access it
0942     Options::setFocusAdaptive(enabled);
0943 
0944     if (enabled)
0945         adaptFocus.reset(new AdaptiveFocus(this));
0946     adaptFocus->resetAdaptiveFocusCounters();
0947 }
0948 
0949 // Capture has signalled an Adaptive Focus run
0950 void Focus::adaptiveFocus()
0951 {
0952     // Invoke runAdaptiveFocus
0953     adaptFocus->runAdaptiveFocus(currentPosition, filter());
0954 }
0955 
0956 // Run Aberration Inspector
0957 void Focus::startAbIns()
0958 {
0959     m_abInsOn = canAbInsStart();
0960     start();
0961 }
0962 
0963 void Focus::start()
0964 {
0965     if (m_Focuser == nullptr)
0966     {
0967         appendLogText(i18n("No Focuser connected."));
0968         completeFocusProcedure(Ekos::FOCUS_ABORTED);
0969         return;
0970     }
0971 
0972     if (m_Camera == nullptr)
0973     {
0974         appendLogText(i18n("No CCD connected."));
0975         completeFocusProcedure(Ekos::FOCUS_ABORTED);
0976         return;
0977     }
0978 
0979     if (!canAbsMove && !canRelMove && m_OpsFocusMechanics->focusTicks->value() <= MINIMUM_PULSE_TIMER)
0980     {
0981         appendLogText(i18n("Starting pulse step is too low. Increase the step size to %1 or higher...",
0982                            MINIMUM_PULSE_TIMER * 5));
0983         completeFocusProcedure(Ekos::FOCUS_ABORTED);
0984         return;
0985     }
0986 
0987     if (inAutoFocus)
0988     {
0989         // If Autofocus is already running, just ignore this request. This condition should not happen
0990         // There is no point signalling Autofocus failure as that will trigger actions based on the
0991         // currently running Autofocus failing (whilst it is still running).
0992         appendLogText(i18n("Autofocus is already running, discarding start request."));
0993         return;
0994     }
0995     else if (inAdjustFocus)
0996     {
0997         if (++AFStartRetries < MAXIMUM_RESET_ITERATIONS)
0998         {
0999             // Its possible that a start request can be triggered whilst an Adjust Focus is completing
1000             // This was reported by a user. The conditions require align resetting a filter and an offset
1001             // on the filter needing to be applied. So retry 3 times (10s interval) and fail if still a problem
1002             appendLogText(i18n("Autofocus start request - Waiting 10sec for AdjustFocus to complete."));
1003             QTimer::singleShot(10 * 1000, this, [this]()
1004             {
1005                 start();
1006             });
1007             return;
1008         }
1009 
1010         appendLogText(i18n("Discarding Autofocus start request - AdjustFocus in progress."));
1011         completeFocusProcedure(Ekos::FOCUS_ABORTED);
1012         return;
1013     }
1014     else if (adaptFocus->inAdaptiveFocus())
1015     {
1016         // Protective code added as per the above else if. This scenario is unlikely
1017         if (++AFStartRetries < MAXIMUM_RESET_ITERATIONS)
1018         {
1019             appendLogText(i18n("Autofocus start request - Waiting 10sec for AdaptiveFocus to complete."));
1020             QTimer::singleShot(10 * 1000, this, [this]()
1021             {
1022                 start();
1023             });
1024             return;
1025         }
1026 
1027         appendLogText(i18n("Discarding Autofocus start request - AdaptiveFocus in progress."));
1028         completeFocusProcedure(Ekos::FOCUS_ABORTED);
1029         return;
1030     }
1031 
1032     inAutoFocus = true;
1033     m_AFRun++;
1034     AFStartRetries = 0;
1035     m_LastFocusDirection = FOCUS_NONE;
1036 
1037     waitStarSelectTimer.stop();
1038 
1039     // Reset the focus motion timer
1040     m_FocusMotionTimerCounter = 0;
1041     m_FocusMotionTimer.stop();
1042     m_FocusMotionTimer.setInterval(m_OpsFocusMechanics->focusMotionTimeout->value() * 1000);
1043 
1044     // Reset focuser reconnect counter
1045     m_FocuserReconnectCounter = 0;
1046 
1047     // Reset focuser reconnect counter
1048     m_FocuserReconnectCounter = 0;
1049     m_DebugFocuserCounter = 0;
1050 
1051     starsHFR.clear();
1052 
1053     lastHFR = 0;
1054 
1055     // Reset state variable that deals with retrying and aborting aurofocus
1056     m_RestartState = RESTART_NONE;
1057 
1058     // Keep the  last focus temperature, it can still be useful in case the autofocus fails
1059     // lastFocusTemperature
1060 
1061     if (canAbsMove)
1062     {
1063         absIterations = 0;
1064         getAbsFocusPosition();
1065         pulseDuration = m_OpsFocusMechanics->focusTicks->value();
1066     }
1067     else if (canRelMove)
1068     {
1069         //appendLogText(i18n("Setting dummy central position to 50000"));
1070         absIterations   = 0;
1071         pulseDuration   = m_OpsFocusMechanics->focusTicks->value();
1072         //currentPosition = 50000;
1073         absMotionMax    = 100000;
1074         absMotionMin    = 0;
1075     }
1076     else
1077     {
1078         pulseDuration   = m_OpsFocusMechanics->focusTicks->value();
1079         absIterations   = 0;
1080         absMotionMax    = 100000;
1081         absMotionMin    = 0;
1082     }
1083 
1084     focuserAdditionalMovement = 0;
1085     starMeasureFrames.clear();
1086 
1087     resetButtons();
1088 
1089     reverseDir = false;
1090 
1091     /*if (fw > 0 && fh > 0)
1092         starSelected= true;
1093     else
1094         starSelected= false;*/
1095 
1096     clearDataPoints();
1097     profilePlot->clear();
1098     FWHMOut->setText("");
1099 
1100     qCInfo(KSTARS_EKOS_FOCUS)  << "Starting Autofocus " << m_AFRun
1101                                << " on" << focuserLabel->text()
1102                                << " CanAbsMove: " << (canAbsMove ? "yes" : "no" )
1103                                << " CanRelMove: " << (canRelMove ? "yes" : "no" )
1104                                << " CanTimerMove: " << (canTimerMove ? "yes" : "no" )
1105                                << " Position:" << currentPosition
1106                                << " Filter:" << filter()
1107                                << " Exp:" << focusExposure->value()
1108                                << " Bin:" << focusBinning->currentText()
1109                                << " Gain:" << focusGain->value()
1110                                << " ISO:" << focusISO->currentText();
1111     qCInfo(KSTARS_EKOS_FOCUS)  << "Settings Tab."
1112                                << " Auto Select Star:" << ( m_OpsFocusSettings->focusAutoStarEnabled->isChecked() ? "yes" : "no" )
1113                                << " Dark Frame:" << ( m_OpsFocusSettings->useFocusDarkFrame->isChecked() ? "yes" : "no" )
1114                                << " Sub Frame:" << ( m_OpsFocusSettings->focusSubFrame->isChecked() ? "yes" : "no" )
1115                                << " Box:" << m_OpsFocusSettings->focusBoxSize->value()
1116                                << " Full frame:" << ( m_OpsFocusSettings->focusUseFullField->isChecked() ? "yes" : "no " )
1117                                << " Focus Mask: " << (m_OpsFocusSettings->focusNoMaskRB->isChecked() ? "Use all stars" :
1118                                        (m_OpsFocusSettings->focusRingMaskRB->isChecked() ? QString("Ring Mask: [%1%, %2%]").
1119                                         arg(m_OpsFocusSettings->focusFullFieldInnerRadius->value()).arg(m_OpsFocusSettings->focusFullFieldOuterRadius->value()) :
1120                                         QString("Mosaic Mask: [%1%, space=%2px]").
1121                                         arg(m_OpsFocusSettings->focusMosaicTileWidth->value()).arg(m_OpsFocusSettings->focusMosaicSpace->value())))
1122                                << " Suspend Guiding:" << ( m_OpsFocusSettings->focusSuspendGuiding->isChecked() ? "yes" : "no " )
1123                                << " Guide Settle:" << m_OpsFocusSettings->focusGuideSettleTime->value()
1124                                << " Display Units:" << m_OpsFocusSettings->focusUnits->currentText()
1125                                << " Adaptive Focus:" << ( m_OpsFocusSettings->focusAdaptive->isChecked() ? "yes" : "no" )
1126                                << " Min Move:" << m_OpsFocusSettings->focusAdaptiveMinMove->value()
1127                                << " Adapt Start:" << ( m_OpsFocusSettings->focusAdaptStart->isChecked() ? "yes" : "no" )
1128                                << " Max Total Move:" << m_OpsFocusSettings->focusAdaptiveMaxMove->value();
1129     qCInfo(KSTARS_EKOS_FOCUS)  << "Process Tab."
1130                                << " Detection:" << m_OpsFocusProcess->focusDetection->currentText()
1131                                << " SEP Profile:" << m_OpsFocusProcess->focusSEPProfile->currentText()
1132                                << " Algorithm:" << m_OpsFocusProcess->focusAlgorithm->currentText()
1133                                << " Curve Fit:" << m_OpsFocusProcess->focusCurveFit->currentText()
1134                                << " Measure:" << m_OpsFocusProcess->focusStarMeasure->currentText()
1135                                << " PSF:" << m_OpsFocusProcess->focusStarPSF->currentText()
1136                                << " Use Weights:" << ( m_OpsFocusProcess->focusUseWeights->isChecked() ? "yes" : "no" )
1137                                << " R2 Limit:" << m_OpsFocusProcess->focusR2Limit->value()
1138                                << " Refine Curve Fit:" << ( m_OpsFocusProcess->focusRefineCurveFit->isChecked() ? "yes" : "no" )
1139                                << " Average Over:" << m_OpsFocusProcess->focusFramesCount->value()
1140                                << " Num.of Rows:" << m_OpsFocusProcess->focusMultiRowAverage->value()
1141                                << " Sigma:" << m_OpsFocusProcess->focusGaussianSigma->value()
1142                                << " Threshold:" << m_OpsFocusProcess->focusThreshold->value()
1143                                << " Kernel size:" << m_OpsFocusProcess->focusGaussianKernelSize->value()
1144                                << " Tolerance:" << m_OpsFocusProcess->focusTolerance->value()
1145                                << " Donut Buster:" << ( m_OpsFocusProcess->focusDonut->isChecked() ? "yes" : "no" )
1146                                << " Donut Time Dilation:" << m_OpsFocusProcess->focusTimeDilation->value();
1147     qCInfo(KSTARS_EKOS_FOCUS)  << "Mechanics Tab."
1148                                << " Initial Step Size:" << m_OpsFocusMechanics->focusTicks->value()
1149                                << " Out Step Multiple:" << m_OpsFocusMechanics->focusOutSteps->value()
1150                                << " Number Steps:" << m_OpsFocusMechanics->focusNumSteps->value()
1151                                << " Max Travel:" << m_OpsFocusMechanics->focusMaxTravel->value()
1152                                << " Max Step Size:" << m_OpsFocusMechanics->focusMaxSingleStep->value()
1153                                << " Driver Backlash:" << m_OpsFocusMechanics->focusBacklash->value()
1154                                << " AF Overscan:" << m_OpsFocusMechanics->focusAFOverscan->value()
1155                                << " Focuser Settle:" << m_OpsFocusMechanics->focusSettleTime->value()
1156                                << " Walk:" << m_OpsFocusMechanics->focusWalk->currentText()
1157                                << " Capture Timeout:" << m_OpsFocusMechanics->focusCaptureTimeout->value()
1158                                << " Motion Timeout:" << m_OpsFocusMechanics->focusMotionTimeout->value();
1159     qCInfo(KSTARS_EKOS_FOCUS)  << "CFZ Tab."
1160                                << " Algorithm:" << m_CFZUI->focusCFZAlgorithm->currentText()
1161                                << " Tolerance:" << m_CFZUI->focusCFZTolerance->value()
1162                                << " Tolerance (Ï„):" << m_CFZUI->focusCFZTau->value()
1163                                << " Display:" << ( m_CFZUI->focusCFZDisplayVCurve->isChecked() ? "yes" : "no" )
1164                                << " Wavelength (λ):" << m_CFZUI->focusCFZWavelength->value()
1165                                << " Aperture (A):" << m_CFZUI->focusCFZAperture->value()
1166                                << " Focal Ratio (f):" << m_CFZUI->focusCFZFNumber->value()
1167                                << " Step Size:" << m_CFZUI->focusCFZStepSize->value()
1168                                << " FWHM (θ):" << m_CFZUI->focusCFZSeeing->value();
1169 
1170     if (currentTemperatureSourceElement)
1171         emit autofocusStarting(currentTemperatureSourceElement->value, filter());
1172     else
1173         // dummy temperature will be ignored
1174         emit autofocusStarting(INVALID_VALUE, filter());
1175 
1176     if (m_OpsFocusSettings->focusAutoStarEnabled->isChecked())
1177         appendLogText(i18n("Autofocus in progress..."));
1178     else if (!inAutoFocus)
1179         appendLogText(i18n("Please wait until image capture is complete..."));
1180 
1181     // Only suspend when we have Off-Axis Guider
1182     // If the guide camera is operating on a different OTA
1183     // then no need to suspend.
1184     if (m_GuidingSuspended == false && m_OpsFocusSettings->focusSuspendGuiding->isChecked())
1185     {
1186         m_GuidingSuspended = true;
1187         emit suspendGuiding();
1188     }
1189 
1190     //emit statusUpdated(true);
1191     setState(Ekos::FOCUS_PROGRESS);
1192 
1193     KSNotification::event(QLatin1String("FocusStarted"), i18n("Autofocus operation started"), KSNotification::Focus);
1194 
1195     // Used for all the focuser types.
1196     if (m_FocusAlgorithm == FOCUS_LINEAR || m_FocusAlgorithm == FOCUS_LINEAR1PASS)
1197     {
1198         QString AFfilter = filter();
1199         const int position = adaptFocus->adaptStartPosition(currentPosition, AFfilter);
1200 
1201         curveFitting.reset(new CurveFitting());
1202 
1203         FocusAlgorithmInterface::FocusParams params(curveFitting.get(),
1204                 m_OpsFocusMechanics->focusMaxTravel->value(), m_OpsFocusMechanics->focusTicks->value(), position, absMotionMin,
1205                 absMotionMax,
1206                 MAXIMUM_ABS_ITERATIONS, m_OpsFocusProcess->focusTolerance->value() / 100.0, AFfilter,
1207                 currentTemperatureSourceElement ? currentTemperatureSourceElement->value : INVALID_VALUE,
1208                 m_OpsFocusMechanics->focusOutSteps->value(), m_OpsFocusMechanics->focusNumSteps->value(),
1209                 m_FocusAlgorithm, m_OpsFocusMechanics->focusBacklash->value(), m_CurveFit, m_OpsFocusProcess->focusUseWeights->isChecked(),
1210                 m_StarMeasure, m_StarPSF, m_OpsFocusProcess->focusRefineCurveFit->isChecked(), m_FocusWalk,
1211                 m_OpsFocusProcess->focusDonut->isChecked(), m_OptDir, m_ScaleCalc);
1212 
1213         if (m_FocusAlgorithm == FOCUS_LINEAR1PASS)
1214         {
1215             // Curve fitting for stars and FWHM processing
1216             starFitting.reset(new CurveFitting());
1217             focusFWHM.reset(new FocusFWHM(m_ScaleCalc));
1218             focusFourierPower.reset(new FocusFourierPower(m_ScaleCalc));
1219             // Donut Buster
1220             initDonutProcessing();
1221         }
1222 
1223         if (canAbsMove)
1224             initialFocuserAbsPosition = position;
1225         linearFocuser.reset(MakeLinearFocuser(params));
1226         linearRequestedPosition = linearFocuser->initialPosition();
1227         if (!changeFocus(linearRequestedPosition - currentPosition))
1228             completeFocusProcedure(Ekos::FOCUS_ABORTED);
1229 
1230         // Avoid the capture below.
1231         return;
1232     }
1233     capture();
1234 }
1235 
1236 // Initialise donut buster
1237 void Focus::initDonutProcessing()
1238 {
1239     if (m_OpsFocusProcess->focusDonut->isChecked())
1240         m_donutOrigExposure = focusExposure->value();
1241 }
1242 
1243 // Reset donut buster
1244 void Focus::resetDonutProcessing()
1245 {
1246     // If donut busting variable focus exposures have been used, reset to starting value
1247     if (m_OpsFocusProcess->focusDonut->isChecked() && inAutoFocus)
1248         focusExposure->setValue(m_donutOrigExposure);
1249 }
1250 
1251 int Focus::adjustLinearPosition(int position, int newPosition, int overscan, bool updateDir)
1252 {
1253     if (overscan > 0 && newPosition > position)
1254     {
1255         // If user has set an overscan value then use it
1256         int adjustment = overscan;
1257         qCDebug(KSTARS_EKOS_FOCUS) << QString("Linear: extending outward movement by overscan %1").arg(adjustment);
1258 
1259         if (newPosition + adjustment > absMotionMax)
1260             adjustment = static_cast<int>(absMotionMax) - newPosition;
1261 
1262         focuserAdditionalMovement = adjustment;
1263         focuserAdditionalMovementUpdateDir = updateDir;
1264 
1265         return newPosition + adjustment;
1266     }
1267     return newPosition;
1268 }
1269 
1270 void Focus::checkStopFocus(bool abort)
1271 {
1272     // if abort, avoid try to restart
1273     if (abort)
1274         resetFocusIteration = MAXIMUM_RESET_ITERATIONS + 1;
1275 
1276     if (captureInProgress && inAutoFocus == false && inFocusLoop == false)
1277     {
1278         captureB->setEnabled(true);
1279         stopFocusB->setEnabled(false);
1280 
1281         appendLogText(i18n("Capture aborted."));
1282     }
1283 
1284     if (hfrInProgress)
1285     {
1286         stopFocusB->setEnabled(false);
1287         appendLogText(i18n("Detection in progress, please wait."));
1288         QTimer::singleShot(1000, this, [ &, abort]()
1289         {
1290             checkStopFocus(abort);
1291         });
1292     }
1293     else
1294     {
1295         completeFocusProcedure(abort ? Ekos::FOCUS_ABORTED : Ekos::FOCUS_FAILED);
1296     }
1297 }
1298 
1299 void Focus::meridianFlipStarted()
1300 {
1301     // if focusing is not running, do nothing
1302     if (state() == FOCUS_IDLE || state() == FOCUS_COMPLETE || state() == FOCUS_FAILED || state() == FOCUS_ABORTED)
1303         return;
1304 
1305     // store current focus iteration counter since abort() sets it to the maximal value to avoid restarting
1306     int old = resetFocusIteration;
1307     // abort focusing
1308     abort();
1309     // restore iteration counter
1310     resetFocusIteration = old;
1311 }
1312 
1313 void Focus::abort()
1314 {
1315     // No need to "abort" if not already in progress.
1316     if (state() <= FOCUS_ABORTED)
1317         return;
1318 
1319     bool focusLoop = inFocusLoop;
1320     checkStopFocus(true);
1321     appendLogText(i18n("Autofocus aborted."));
1322     if (!focusLoop)
1323         // For looping leave focuser where it is
1324         // Otherwise try to shift the focuser back to its initial position
1325         resetFocuser();
1326 }
1327 
1328 void Focus::stop(Ekos::FocusState completionState)
1329 {
1330     qCDebug(KSTARS_EKOS_FOCUS) << "Stopping Focus";
1331 
1332     captureTimeout.stop();
1333     m_FocusMotionTimer.stop();
1334     m_FocusMotionTimerCounter = 0;
1335     m_FocuserReconnectCounter = 0;
1336 
1337     opticalTrainCombo->setEnabled(true);
1338     resetDonutProcessing();
1339     inAutoFocus = false;
1340     inAdjustFocus = false;
1341     adaptFocus->setInAdaptiveFocus(false);
1342     inBuildOffsets = false;
1343     focuserAdditionalMovement = 0;
1344     focuserAdditionalMovementUpdateDir = true;
1345     inFocusLoop = false;
1346     captureInProgress = false;
1347     isVShapeSolution = false;
1348     captureFailureCounter = 0;
1349     minimumRequiredHFR = INVALID_STAR_MEASURE;
1350     noStarCount = 0;
1351     starMeasureFrames.clear();
1352     m_abInsOn = false;
1353     AFStartRetries = 0;
1354 
1355     // Check if CCD was not removed due to crash or other reasons.
1356     if (m_Camera)
1357     {
1358         disconnect(m_Camera, &ISD::Camera::newImage, this, &Ekos::Focus::processData);
1359         disconnect(m_Camera, &ISD::Camera::error, this, &Ekos::Focus::processCaptureError);
1360 
1361         if (rememberUploadMode != m_Camera->getUploadMode())
1362             m_Camera->setUploadMode(rememberUploadMode);
1363 
1364         // Remember to reset fast exposure if it was enabled before.
1365         if (m_RememberCameraFastExposure)
1366         {
1367             m_RememberCameraFastExposure = false;
1368             m_Camera->setFastExposureEnabled(true);
1369         }
1370 
1371         ISD::CameraChip *targetChip = m_Camera->getChip(ISD::CameraChip::PRIMARY_CCD);
1372         targetChip->abortExposure();
1373     }
1374 
1375     resetButtons();
1376 
1377     absIterations = 0;
1378     HFRInc        = 0;
1379     reverseDir    = false;
1380 
1381     if (m_GuidingSuspended)
1382     {
1383         emit resumeGuiding();
1384         m_GuidingSuspended = false;
1385     }
1386 
1387     if (completionState == Ekos::FOCUS_ABORTED || completionState == Ekos::FOCUS_FAILED)
1388         setState(completionState);
1389 }
1390 
1391 void Focus::capture(double settleTime)
1392 {
1393     // If capturing should be delayed by a given settling time, we start the capture timer.
1394     // This is intentionally designed re-entrant, i.e. multiple calls with settle time > 0 takes the last delay
1395     if (settleTime > 0 && captureInProgress == false)
1396     {
1397         captureTimer.start(static_cast<int>(settleTime * 1000));
1398         return;
1399     }
1400 
1401     if (captureInProgress)
1402     {
1403         qCWarning(KSTARS_EKOS_FOCUS) << "Capture called while already in progress. Capture is ignored.";
1404         return;
1405     }
1406 
1407     if (m_Camera == nullptr)
1408     {
1409         appendLogText(i18n("Error: No Camera detected."));
1410         checkStopFocus(true);
1411         return;
1412     }
1413 
1414     if (m_Camera->isConnected() == false)
1415     {
1416         appendLogText(i18n("Error: Lost connection to Camera."));
1417         checkStopFocus(true);
1418         return;
1419     }
1420 
1421     // reset timeout for receiving an image
1422     captureTimeout.stop();
1423     // reset timeout for focus star selection
1424     waitStarSelectTimer.stop();
1425 
1426     ISD::CameraChip *targetChip = m_Camera->getChip(ISD::CameraChip::PRIMARY_CCD);
1427 
1428     if (m_Camera->isBLOBEnabled() == false)
1429     {
1430         m_Camera->setBLOBEnabled(true);
1431     }
1432 
1433     if (focusFilter->currentIndex() != -1)
1434     {
1435         if (m_FilterWheel == nullptr)
1436         {
1437             appendLogText(i18n("Error: No Filter Wheel detected."));
1438             checkStopFocus(true);
1439             return;
1440         }
1441         if (m_FilterWheel->isConnected() == false)
1442         {
1443             appendLogText(i18n("Error: Lost connection to Filter Wheel."));
1444             checkStopFocus(true);
1445             return;
1446         }
1447 
1448         // In "regular" autofocus mode if the chosen filter has an associated lock filter then we need
1449         // to swap to the lock filter before running autofocus. However, AF is also called from build
1450         // offsets to run AF. In this case we need to ignore the lock filter and use the chosen filter
1451         int targetPosition = focusFilter->currentIndex() + 1;
1452         if (!inBuildOffsets)
1453         {
1454             QString lockedFilter = m_FilterManager->getFilterLock(filter());
1455 
1456             // We change filter if:
1457             // 1. Target position is not equal to current position.
1458             // 2. Locked filter of CURRENT filter is a different filter.
1459             if (lockedFilter != "--" && lockedFilter != filter())
1460             {
1461                 int lockedFilterIndex = focusFilter->findText(lockedFilter);
1462                 if (lockedFilterIndex >= 0)
1463                 {
1464                     // Go back to this filter once we are done
1465                     fallbackFilterPending = true;
1466                     fallbackFilterPosition = targetPosition;
1467                     targetPosition = lockedFilterIndex + 1;
1468                 }
1469             }
1470         }
1471 
1472         filterPositionPending = (targetPosition != currentFilterPosition);
1473         if (filterPositionPending)
1474         {
1475             // Change the filter. When done this will signal to update the focusFilter combo
1476             // Apply filter change policy if in Autofocus; otherwise apply change and offsets
1477             // Note that Autofocus doesn't need the change policy as Adapt Start Pos takes care of this
1478             FilterManager::FilterPolicy policy = (inAutoFocus) ?
1479                                                  static_cast<FilterManager::FilterPolicy>(FilterManager::CHANGE_POLICY) :
1480                                                  static_cast<FilterManager::FilterPolicy>(FilterManager::CHANGE_POLICY | FilterManager::OFFSET_POLICY);
1481             m_FilterManager->setFilterPosition(targetPosition, policy);
1482             return;
1483         }
1484         else if (targetPosition != focusFilter->currentIndex() + 1)
1485             focusFilter->setCurrentIndex(targetPosition - 1);
1486     }
1487 
1488     m_FocusView->setProperty("suspended", m_OpsFocusSettings->useFocusDarkFrame->isChecked());
1489     prepareCapture(targetChip);
1490 
1491     connect(m_Camera, &ISD::Camera::newImage, this, &Ekos::Focus::processData);
1492     connect(m_Camera, &ISD::Camera::error, this, &Ekos::Focus::processCaptureError);
1493 
1494     if (frameSettings.contains(targetChip))
1495     {
1496         QVariantMap settings = frameSettings[targetChip];
1497         targetChip->setFrame(settings["x"].toInt(), settings["y"].toInt(), settings["w"].toInt(),
1498                              settings["h"].toInt());
1499         settings["binx"]          = (focusBinning->currentIndex() + 1);
1500         settings["biny"]          = (focusBinning->currentIndex() + 1);
1501         frameSettings[targetChip] = settings;
1502     }
1503 
1504     captureInProgress = true;
1505     if (state() != FOCUS_PROGRESS)
1506         setState(FOCUS_PROGRESS);
1507 
1508     m_FocusView->setBaseSize(focusingWidget->size());
1509 
1510     if (targetChip->capture(focusExposure->value()))
1511     {
1512         // Timeout is exposure duration + timeout threshold in seconds
1513         //long const timeout = lround(ceil(focusExposure->value() * 1000)) + FOCUS_TIMEOUT_THRESHOLD;
1514         captureTimeout.start( (focusExposure->value() + m_OpsFocusMechanics->focusCaptureTimeout->value()) * 1000);
1515 
1516         if (inFocusLoop == false)
1517             appendLogText(i18n("Capturing image..."));
1518 
1519         resetButtons();
1520     }
1521     else if (inAutoFocus)
1522     {
1523         completeFocusProcedure(Ekos::FOCUS_ABORTED);
1524     }
1525 }
1526 
1527 void Focus::prepareCapture(ISD::CameraChip *targetChip)
1528 {
1529     if (m_Camera->getUploadMode() == ISD::Camera::UPLOAD_LOCAL)
1530     {
1531         rememberUploadMode = ISD::Camera::UPLOAD_LOCAL;
1532         m_Camera->setUploadMode(ISD::Camera::UPLOAD_CLIENT);
1533     }
1534 
1535     // We cannot use fast exposure in focus.
1536     if (m_Camera->isFastExposureEnabled())
1537     {
1538         m_RememberCameraFastExposure = true;
1539         m_Camera->setFastExposureEnabled(false);
1540     }
1541 
1542     m_Camera->setEncodingFormat("FITS");
1543     targetChip->setBatchMode(false);
1544     targetChip->setBinning((focusBinning->currentIndex() + 1), (focusBinning->currentIndex() + 1));
1545     targetChip->setCaptureMode(FITS_FOCUS);
1546     targetChip->setFrameType(FRAME_LIGHT);
1547     targetChip->setCaptureFilter(FITS_NONE);
1548 
1549     if (isFocusISOEnabled() && focusISO->currentIndex() != -1 &&
1550             targetChip->getISOIndex() != focusISO->currentIndex())
1551         targetChip->setISOIndex(focusISO->currentIndex());
1552 
1553     if (isFocusGainEnabled() && focusGain->value() != GainSpinSpecialValue)
1554         m_Camera->setGain(focusGain->value());
1555 }
1556 
1557 bool Focus::focusIn(int ms)
1558 {
1559     if (currentPosition == absMotionMin)
1560     {
1561         appendLogText(i18n("At minimum focus position %1...", absMotionMin));
1562         return false;
1563     }
1564     focusInB->setEnabled(false);
1565     focusOutB->setEnabled(false);
1566     startGotoB->setEnabled(false);
1567     if (ms <= 0)
1568         ms = m_OpsFocusMechanics->focusTicks->value();
1569     if (currentPosition - ms <= absMotionMin)
1570     {
1571         ms = currentPosition - absMotionMin;
1572         appendLogText(i18n("Moving to minimum focus position %1...", absMotionMin));
1573     }
1574     return changeFocus(-ms);
1575 }
1576 
1577 bool Focus::focusOut(int ms)
1578 {
1579     if (currentPosition == absMotionMax)
1580     {
1581         appendLogText(i18n("At maximum focus position %1...", absMotionMax));
1582         return false;
1583     }
1584     focusInB->setEnabled(false);
1585     focusOutB->setEnabled(false);
1586     startGotoB->setEnabled(false);
1587     if (ms <= 0)
1588         ms = m_OpsFocusMechanics->focusTicks->value();
1589     if (currentPosition + ms >= absMotionMax)
1590     {
1591         ms = absMotionMax - currentPosition;
1592         appendLogText(i18n("Moving to maximum focus position %1...", absMotionMax));
1593     }
1594     return changeFocus(ms);
1595 }
1596 
1597 // Routine to manage focus movements. All moves are now subject to overscan
1598 // + amount indicates a movement out; - amount indictaes a movement in
1599 bool Focus::changeFocus(int amount, bool updateDir)
1600 {
1601     // Retry capture if we stay at the same position
1602     // Allow 1 step of tolerance--Have seen stalls with amount==1.
1603     if (inAutoFocus && abs(amount) <= 1)
1604     {
1605         capture(m_OpsFocusMechanics->focusSettleTime->value());
1606         return true;
1607     }
1608 
1609     if (m_Focuser == nullptr)
1610     {
1611         appendLogText(i18n("Error: No Focuser detected."));
1612         checkStopFocus(true);
1613         return false;
1614     }
1615 
1616     if (m_Focuser->isConnected() == false)
1617     {
1618         appendLogText(i18n("Error: Lost connection to Focuser."));
1619         checkStopFocus(true);
1620         return false;
1621     }
1622 
1623     const int newPosition = adjustLinearPosition(currentPosition, currentPosition + amount,
1624                             m_OpsFocusMechanics->focusAFOverscan->value(),
1625                             updateDir);
1626     if (newPosition == currentPosition)
1627         return true;
1628 
1629     const int newAmount = newPosition - currentPosition;
1630     const int absNewAmount = abs(newAmount);
1631     const bool focusingOut = newAmount > 0;
1632     const QString dirStr = focusingOut ? i18n("outward") : i18n("inward");
1633     // update the m_LastFocusDirection unless in Iterative / Polynomial which controls this variable itself.
1634     if (updateDir)
1635         m_LastFocusDirection = (focusingOut) ? FOCUS_OUT : FOCUS_IN;
1636 
1637     if (focusingOut)
1638         m_Focuser->focusOut();
1639     else
1640         m_Focuser->focusIn();
1641 
1642     // Keep track of motion in case it gets stuck.
1643     m_FocusMotionTimer.start();
1644 
1645     if (canAbsMove)
1646     {
1647         m_LastFocusSteps = newPosition;
1648         m_Focuser->moveAbs(newPosition);
1649         appendLogText(i18n("Focusing %2 by %1 steps...", abs(absNewAmount), dirStr));
1650     }
1651     else if (canRelMove)
1652     {
1653         m_LastFocusSteps = absNewAmount;
1654         m_Focuser->moveRel(absNewAmount);
1655         appendLogText(i18np("Focusing %2 by %1 step...", "Focusing %2 by %1 steps...", absNewAmount, dirStr));
1656     }
1657     else
1658     {
1659         m_LastFocusSteps = absNewAmount;
1660         m_Focuser->moveByTimer(absNewAmount);
1661         appendLogText(i18n("Focusing %2 by %1 ms...", absNewAmount, dirStr));
1662     }
1663 
1664     return true;
1665 }
1666 
1667 void Focus::handleFocusMotionTimeout()
1668 {
1669     // handleFocusMotionTimeout is called when the focus motion timer times out. This is only
1670     // relevant to AutoFocus runs which could be unattended so make an attempt to recover. Other
1671     // types of focuser movement issues are logged and the user is expected to take action.
1672     // If set correctly, say 30 secs, this should only occur when there are comms issues
1673     // with the focuser.
1674     // Step 1: Just retry the last requested move. If the issue is a transient one-off issue
1675     //         this should resolve it. Try this twice.
1676     // Step 2: Step 1 didn't resolve the issue so try to restart the focus driver. In this case
1677     //         abort the inflight autofocus run and let it retry from the start. It will try to
1678     //         return the focuser to the start (last successful autofocus) position. Try twice.
1679     // Step 3: Step 2 didn't work either because the driver restart wasn't successful or we are
1680     //         still getting timeouts. In this case just stop the autoFocus process and return
1681     //         control to either the Scheduer or GUI. Note that here we cannot reset the focuser
1682     //         to the previous good position so if the focuser miraculously fixes itself the
1683     //         next autofocus run won't start from the best place.
1684 
1685     if (!inAutoFocus)
1686     {
1687         qCDebug(KSTARS_EKOS_FOCUS) << "handleFocusMotionTimeout() called while not in AutoFocus";
1688         return;
1689     }
1690 
1691     m_FocusMotionTimerCounter++;
1692 
1693     if (m_FocusMotionTimerCounter > 4)
1694     {
1695         // We've tried everything but still get timeouts so abort...
1696         appendLogText(i18n("Focuser is still timing out. Aborting..."));
1697         stop(Ekos::FOCUS_ABORTED);
1698         return;
1699     }
1700     else if (m_FocusMotionTimerCounter > 2)
1701     {
1702         QString focuser = m_Focuser->getDeviceName();
1703         appendLogText(i18n("Focus motion timed out (%1). Restarting focus driver %2", m_FocusMotionTimerCounter, focuser));
1704         emit focuserTimedout(focuser);
1705 
1706         QTimer::singleShot(5000, this, [ &, focuser]()
1707         {
1708             Focus::reconnectFocuser(focuser);
1709         });
1710         return;
1711     }
1712 
1713     if (!changeFocus(m_LastFocusSteps - currentPosition))
1714         appendLogText(i18n("Focus motion timed out (%1). Focusing to %2 steps...", m_FocusMotionTimerCounter, m_LastFocusSteps));
1715 }
1716 
1717 void Focus::selectImageMask()
1718 {
1719     const bool useFullField = m_OpsFocusSettings->focusUseFullField->isChecked();
1720     ImageMaskType masktype;
1721     if (m_OpsFocusSettings->focusRingMaskRB->isChecked())
1722         masktype = FOCUS_MASK_RING;
1723     else if (m_OpsFocusSettings->focusMosaicMaskRB->isChecked())
1724         masktype = FOCUS_MASK_MOSAIC;
1725     else
1726         masktype = FOCUS_MASK_NONE;
1727 
1728     // mask selection only enabled if full field should be used for focusing
1729     m_OpsFocusSettings->focusRingMaskRB->setEnabled(useFullField);
1730     m_OpsFocusSettings->focusMosaicMaskRB->setEnabled(useFullField);
1731     // ring mask
1732     m_OpsFocusSettings->focusFullFieldInnerRadius->setEnabled(useFullField && masktype == FOCUS_MASK_RING);
1733     m_OpsFocusSettings->focusFullFieldOuterRadius->setEnabled(useFullField && masktype == FOCUS_MASK_RING);
1734     // aberration inspector mosaic
1735     m_OpsFocusSettings->focusMosaicTileWidth->setEnabled(useFullField && masktype == FOCUS_MASK_MOSAIC);
1736     m_OpsFocusSettings->focusSpacerLabel->setEnabled(useFullField && masktype == FOCUS_MASK_MOSAIC);
1737     m_OpsFocusSettings->focusMosaicSpace->setEnabled(useFullField && masktype == FOCUS_MASK_MOSAIC);
1738 
1739     // create the type specific mask
1740     if (masktype == FOCUS_MASK_RING)
1741         m_FocusView->setImageMask(new ImageRingMask(m_OpsFocusSettings->focusFullFieldInnerRadius->value() / 100.0,
1742                                   m_OpsFocusSettings->focusFullFieldOuterRadius->value() / 100.0));
1743     else if (masktype == FOCUS_MASK_MOSAIC)
1744         m_FocusView->setImageMask(new ImageMosaicMask(m_OpsFocusSettings->focusMosaicTileWidth->value(),
1745                                   m_OpsFocusSettings->focusMosaicSpace->value()));
1746     else
1747         m_FocusView->setImageMask(nullptr);
1748 
1749     checkMosaicMaskLimits();
1750     m_currentImageMask = masktype;
1751     startAbInsB->setEnabled(canAbInsStart());
1752 }
1753 
1754 void Focus::reconnectFocuser(const QString &focuser)
1755 {
1756     m_FocuserReconnectCounter++;
1757 
1758     if (m_Focuser && m_Focuser->getDeviceName() == focuser)
1759     {
1760         appendLogText(i18n("Attempting to reconnect focuser: %1", focuser));
1761         refreshOpticalTrain();
1762         completeFocusProcedure(Ekos::FOCUS_ABORTED);
1763         return;
1764     }
1765 
1766     if (m_FocuserReconnectCounter > 12)
1767     {
1768         // We've waited a minute and can't reconnect the focuser so abort...
1769         appendLogText(i18n("Cannot reconnect focuser: %1. Aborting...", focuser));
1770         stop(Ekos::FOCUS_ABORTED);
1771         return;
1772     }
1773 
1774     QTimer::singleShot(5000, this, [ &, focuser]()
1775     {
1776         reconnectFocuser(focuser);
1777     });
1778 }
1779 
1780 void Focus::processData(const QSharedPointer<FITSData> &data)
1781 {
1782     // Ignore guide head if there is any.
1783     if (data->property("chip").toInt() == ISD::CameraChip::GUIDE_CCD)
1784         return;
1785 
1786     if (data)
1787     {
1788         m_FocusView->loadData(data);
1789         m_ImageData = data;
1790     }
1791     else
1792         m_ImageData.reset();
1793 
1794     captureTimeout.stop();
1795     captureTimeoutCounter = 0;
1796 
1797     ISD::CameraChip *targetChip = m_Camera->getChip(ISD::CameraChip::PRIMARY_CCD);
1798     disconnect(m_Camera, &ISD::Camera::newImage, this, &Ekos::Focus::processData);
1799     disconnect(m_Camera, &ISD::Camera::error, this, &Ekos::Focus::processCaptureError);
1800 
1801     if (m_ImageData && m_OpsFocusSettings->useFocusDarkFrame->isChecked())
1802     {
1803         QVariantMap settings = frameSettings[targetChip];
1804         uint16_t offsetX     = settings["x"].toInt() / settings["binx"].toInt();
1805         uint16_t offsetY     = settings["y"].toInt() / settings["biny"].toInt();
1806 
1807         m_DarkProcessor->denoise(OpticalTrainManager::Instance()->id(opticalTrainCombo->currentText()),
1808                                  targetChip, m_ImageData, focusExposure->value(), offsetX, offsetY);
1809         return;
1810     }
1811 
1812     setCaptureComplete();
1813     resetButtons();
1814 }
1815 
1816 void Focus::starDetectionFinished()
1817 {
1818     appendLogText(i18n("Detection complete."));
1819 
1820     // Beware as this HFR value is then treated specifically by the graph renderer
1821     double hfr = INVALID_STAR_MEASURE;
1822 
1823     if (m_StarFinderWatcher.result() == false)
1824     {
1825         qCWarning(KSTARS_EKOS_FOCUS) << "Failed to extract any stars.";
1826     }
1827     else
1828     {
1829         if (m_OpsFocusSettings->focusUseFullField->isChecked())
1830         {
1831             m_FocusView->filterStars();
1832 
1833             // Get the average HFR of the whole frame
1834             hfr = m_ImageData->getHFR(m_StarMeasure == FOCUS_STAR_HFR_ADJ ? HFR_ADJ_AVERAGE : HFR_AVERAGE);
1835         }
1836         else
1837         {
1838             m_FocusView->setTrackingBoxEnabled(true);
1839 
1840             // JM 2020-10-08: Try to get first the same HFR star already selected before
1841             // so that it doesn't keep jumping around
1842 
1843             if (starCenter.isNull() == false)
1844                 hfr = m_ImageData->getHFR(starCenter.x(), starCenter.y());
1845 
1846             // If not found, then get the MAX or MEDIAN depending on the selected algorithm.
1847             if (hfr < 0)
1848                 hfr = m_ImageData->getHFR(m_FocusDetection == ALGORITHM_SEP ? HFR_HIGH : HFR_MAX);
1849         }
1850     }
1851     // JEE Frig
1852     if (0)
1853     {
1854         if (hfr > 3)
1855             hfr = 3 - (hfr - 3);
1856         if (hfr < 0.5)
1857             hfr = INVALID_STAR_MEASURE;
1858     }
1859 
1860     hfrInProgress = false;
1861     currentHFR = hfr;
1862     currentNumStars = m_ImageData->getDetectedStars();
1863 
1864     // Setup with measure we are using (HFR, FWHM, etc)
1865     if (!inAutoFocus)
1866         currentMeasure = currentHFR;
1867     else
1868     {
1869         if (m_StarMeasure == FOCUS_STAR_NUM_STARS)
1870         {
1871             currentMeasure = currentNumStars;
1872             currentWeight = 1.0;
1873         }
1874         else if (m_StarMeasure == FOCUS_STAR_FWHM)
1875         {
1876             getFWHM(m_ImageData->getStarCenters(), &currentFWHM, &currentWeight);
1877             currentMeasure = currentFWHM;
1878         }
1879         else if (m_StarMeasure == FOCUS_STAR_FOURIER_POWER)
1880         {
1881             getFourierPower(&currentFourierPower, &currentWeight);
1882             currentMeasure = currentFourierPower;
1883         }
1884         else
1885         {
1886             currentMeasure = currentHFR;
1887             QList<Edge*> stars = m_ImageData->getStarCenters();
1888             std::vector<double> hfrs(stars.size());
1889             std::transform(stars.constBegin(), stars.constEnd(), hfrs.begin(), [](Edge * edge)
1890             {
1891                 return edge->HFR;
1892             });
1893             currentWeight = calculateStarWeight(m_OpsFocusProcess->focusUseWeights->isChecked(), hfrs);
1894         }
1895     }
1896     setCurrentMeasure();
1897 }
1898 
1899 // The image has been processed for star centroids and HFRs so now process it for star FWHMs
1900 void Focus::getFWHM(const QList<Edge *> &stars, double *FWHM, double *weight)
1901 {
1902     *FWHM = INVALID_STAR_MEASURE;
1903     *weight = 0.0;
1904 
1905     auto imageBuffer = m_ImageData->getImageBuffer();
1906 
1907     switch (m_ImageData->getStatistics().dataType)
1908     {
1909         case TBYTE:
1910             focusFWHM->processFWHM(reinterpret_cast<uint8_t const *>(imageBuffer), stars, m_ImageData, starFitting, FWHM, weight);
1911             break;
1912 
1913         case TSHORT: // Don't think short is used as its recorded as unsigned short
1914             focusFWHM->processFWHM(reinterpret_cast<short const *>(imageBuffer), stars, m_ImageData, starFitting, FWHM, weight);
1915             break;
1916 
1917         case TUSHORT:
1918             focusFWHM->processFWHM(reinterpret_cast<unsigned short const *>(imageBuffer), stars, m_ImageData, starFitting, FWHM,
1919                                    weight);
1920             break;
1921 
1922         case TLONG:  // Don't think long is used as its recorded as unsigned long
1923             focusFWHM->processFWHM(reinterpret_cast<long const *>(imageBuffer), stars, m_ImageData, starFitting, FWHM, weight);
1924             break;
1925 
1926         case TULONG:
1927             focusFWHM->processFWHM(reinterpret_cast<unsigned long const *>(imageBuffer), stars, m_ImageData, starFitting, FWHM, weight);
1928             break;
1929 
1930         case TFLOAT:
1931             focusFWHM->processFWHM(reinterpret_cast<float const *>(imageBuffer), stars, m_ImageData, starFitting, FWHM, weight);
1932             break;
1933 
1934         case TLONGLONG:
1935             focusFWHM->processFWHM(reinterpret_cast<long long const *>(imageBuffer), stars, m_ImageData, starFitting, FWHM, weight);
1936             break;
1937 
1938         case TDOUBLE:
1939             focusFWHM->processFWHM(reinterpret_cast<double const *>(imageBuffer), stars, m_ImageData, starFitting, FWHM, weight);
1940             break;
1941 
1942         default:
1943             qCDebug(KSTARS_EKOS_FOCUS) << "Unknown image buffer datatype " << m_ImageData->getStatistics().dataType <<
1944                                        " Cannot calc FWHM";
1945             break;
1946     }
1947 }
1948 
1949 // The image has been processed for star centroids and HFRs so now process it for star FWHMs
1950 void Focus::getFourierPower(double *fourierPower, double *weight, const int mosaicTile)
1951 {
1952     *fourierPower = INVALID_STAR_MEASURE;
1953     *weight = 1.0;
1954 
1955     auto imageBuffer = m_ImageData->getImageBuffer();
1956 
1957     switch (m_ImageData->getStatistics().dataType)
1958     {
1959         case TBYTE:
1960             focusFourierPower->processFourierPower(reinterpret_cast<uint8_t const *>(imageBuffer), m_ImageData,
1961                                                    m_FocusView->imageMask(), mosaicTile, fourierPower, weight);
1962             break;
1963 
1964         case TSHORT: // Don't think short is used as its recorded as unsigned short
1965             focusFourierPower->processFourierPower(reinterpret_cast<short const *>(imageBuffer), m_ImageData,
1966                                                    m_FocusView->imageMask(), mosaicTile, fourierPower, weight);
1967             break;
1968 
1969         case TUSHORT:
1970             focusFourierPower->processFourierPower(reinterpret_cast<unsigned short const *>(imageBuffer), m_ImageData,
1971                                                    m_FocusView->imageMask(), mosaicTile, fourierPower, weight);
1972             break;
1973 
1974         case TLONG:  // Don't think long is used as its recorded as unsigned long
1975             focusFourierPower->processFourierPower(reinterpret_cast<long const *>(imageBuffer), m_ImageData,
1976                                                    m_FocusView->imageMask(), mosaicTile, fourierPower, weight);
1977             break;
1978 
1979         case TULONG:
1980             focusFourierPower->processFourierPower(reinterpret_cast<unsigned long const *>(imageBuffer), m_ImageData,
1981                                                    m_FocusView->imageMask(), mosaicTile, fourierPower, weight);
1982             break;
1983 
1984         case TFLOAT:
1985             focusFourierPower->processFourierPower(reinterpret_cast<float const *>(imageBuffer), m_ImageData,
1986                                                    m_FocusView->imageMask(), mosaicTile, fourierPower, weight);
1987             break;
1988 
1989         case TLONGLONG:
1990             focusFourierPower->processFourierPower(reinterpret_cast<long long const *>(imageBuffer), m_ImageData,
1991                                                    m_FocusView->imageMask(), mosaicTile, fourierPower, weight);
1992             break;
1993 
1994         case TDOUBLE:
1995             focusFourierPower->processFourierPower(reinterpret_cast<double const *>(imageBuffer), m_ImageData,
1996                                                    m_FocusView->imageMask(), mosaicTile, fourierPower, weight);
1997             break;
1998 
1999         default:
2000             qCDebug(KSTARS_EKOS_FOCUS) << "Unknown image buffer datatype " << m_ImageData->getStatistics().dataType <<
2001                                        " Cannot calc Fourier Power";
2002             break;
2003     }
2004 }
2005 
2006 double Focus::calculateStarWeight(const bool useWeights, const std::vector<double> values)
2007 {
2008     if (!useWeights || values.size() <= 0)
2009         // If we can't calculate weight set to 1 = equal weights
2010         // Also if we are using numStars as the measure - don't use weights
2011         return 1.0f;
2012 
2013     return Mathematics::RobustStatistics::ComputeWeight(m_ScaleCalc, values);
2014 }
2015 
2016 void Focus::analyzeSources()
2017 {
2018     appendLogText(i18n("Detecting sources..."));
2019     hfrInProgress = true;
2020 
2021     QVariantMap extractionSettings;
2022     extractionSettings["optionsProfileIndex"] = m_OpsFocusProcess->focusSEPProfile->currentIndex();
2023     extractionSettings["optionsProfileGroup"] =  static_cast<int>(Ekos::FocusProfiles);
2024     m_ImageData->setSourceExtractorSettings(extractionSettings);
2025     // When we're using FULL field view, we always use either CENTROID algorithm which is the default
2026     // standard algorithm in KStars, or SEP. The other algorithms are too inefficient to run on full frames and require
2027     // a bounding box for them to be effective in near real-time application.
2028     if (m_OpsFocusSettings->focusUseFullField->isChecked())
2029     {
2030         m_FocusView->setTrackingBoxEnabled(false);
2031 
2032         if (m_FocusDetection != ALGORITHM_CENTROID && m_FocusDetection != ALGORITHM_SEP)
2033             m_StarFinderWatcher.setFuture(m_ImageData->findStars(ALGORITHM_CENTROID));
2034         else
2035             m_StarFinderWatcher.setFuture(m_ImageData->findStars(m_FocusDetection));
2036     }
2037     else
2038     {
2039         QRect searchBox = m_FocusView->isTrackingBoxEnabled() ? m_FocusView->getTrackingBox() : QRect();
2040         // If star is already selected then use whatever algorithm currently selected.
2041         if (starSelected)
2042         {
2043             m_StarFinderWatcher.setFuture(m_ImageData->findStars(m_FocusDetection, searchBox));
2044         }
2045         else
2046         {
2047             // Disable tracking box
2048             m_FocusView->setTrackingBoxEnabled(false);
2049 
2050             // If algorithm is set something other than Centeroid or SEP, then force Centroid
2051             // Since it is the most reliable detector when nothing was selected before.
2052             if (m_FocusDetection != ALGORITHM_CENTROID && m_FocusDetection != ALGORITHM_SEP)
2053                 m_StarFinderWatcher.setFuture(m_ImageData->findStars(ALGORITHM_CENTROID));
2054             else
2055                 // Otherwise, continue to find use using the selected algorithm
2056                 m_StarFinderWatcher.setFuture(m_ImageData->findStars(m_FocusDetection, searchBox));
2057         }
2058     }
2059 }
2060 
2061 bool Focus::appendMeasure(double newMeasure)
2062 {
2063     // Add new star measure (e.g. HFR, FWHM, etc) to existing values, even if invalid
2064     starMeasureFrames.append(newMeasure);
2065 
2066     // Prepare a work vector with valid HFR values
2067     QVector <double> samples(starMeasureFrames);
2068     samples.erase(std::remove_if(samples.begin(), samples.end(), [](const double measure)
2069     {
2070         return measure == INVALID_STAR_MEASURE;
2071     }), samples.end());
2072 
2073     // Consolidate the average star measure. Sigma clips outliers and averages remainder.
2074     if (!samples.isEmpty())
2075     {
2076         currentMeasure = Mathematics::RobustStatistics::ComputeLocation(
2077                              Mathematics::RobustStatistics::LOCATION_SIGMACLIPPING, std::vector<double>(samples.begin(), samples.end()));
2078 
2079         switch(m_StarMeasure)
2080         {
2081             case FOCUS_STAR_HFR:
2082             case FOCUS_STAR_HFR_ADJ:
2083                 currentHFR = currentMeasure;
2084                 break;
2085             case FOCUS_STAR_FWHM:
2086                 currentFWHM = currentMeasure;
2087                 break;
2088             case FOCUS_STAR_NUM_STARS:
2089                 currentNumStars = currentMeasure;
2090                 break;
2091             case FOCUS_STAR_FOURIER_POWER:
2092                 currentFourierPower = currentMeasure;
2093                 break;
2094             default:
2095                 break;
2096         }
2097     }
2098 
2099     // Save the focus frame
2100     saveFocusFrame();
2101     // Return whether we need more frame based on user requirement
2102     return starMeasureFrames.count() < m_OpsFocusProcess->focusFramesCount->value();
2103 }
2104 
2105 void Focus::settle(const FocusState completionState, const bool autoFocusUsed, const bool buildOffsetsUsed)
2106 {
2107     // TODO: check if the completion state can be emitted in all cases (sterne-jaeger 2023-09-12)
2108     m_state = completionState;
2109     if (completionState == Ekos::FOCUS_COMPLETE)
2110     {
2111         if (autoFocusUsed && fallbackFilterPending)
2112         {
2113             // Save the solution details for the filter used for the AF run before changing
2114             // filers to the fallback filter. Details for the fallback filter will be saved once that
2115             // filter has been processed and the offset applied
2116             m_FilterManager->setFilterAbsoluteFocusDetails(focusFilter->currentIndex(), currentPosition,
2117                     m_LastSourceAutofocusTemperature, m_LastSourceAutofocusAlt);
2118         }
2119 
2120         if (autoFocusUsed)
2121         {
2122             // Prepare the message for Analyze
2123             const int size = plot_position.size();
2124             QString analysis_results = "";
2125 
2126             for (int i = 0; i < size; ++i)
2127             {
2128                 analysis_results.append(QString("%1%2|%3")
2129                                         .arg(i == 0 ? "" : "|" )
2130                                         .arg(QString::number(plot_position[i], 'f', 0))
2131                                         .arg(QString::number(plot_value[i], 'f', 3)));
2132             }
2133 
2134             KSNotification::event(QLatin1String("FocusSuccessful"), i18n("Autofocus operation completed successfully"),
2135                                   KSNotification::Focus);
2136             if (m_FocusAlgorithm == FOCUS_LINEAR1PASS && curveFitting != nullptr)
2137                 emit autofocusComplete(filter(), analysis_results, curveFitting->serialize(), linearFocuser->getTextStatus(R2));
2138             else
2139                 emit autofocusComplete(filter(), analysis_results);
2140         }
2141     }
2142     else
2143     {
2144         if (autoFocusUsed)
2145         {
2146             KSNotification::event(QLatin1String("FocusFailed"), i18n("Autofocus operation failed"),
2147                                   KSNotification::Focus, KSNotification::Alert);
2148             emit autofocusAborted(filter(), "");
2149         }
2150     }
2151 
2152     qCDebug(KSTARS_EKOS_FOCUS) << "Settled. State:" << Ekos::getFocusStatusString(state());
2153 
2154     // Delay state notification if we have a locked filter pending return to original filter
2155     if (fallbackFilterPending)
2156     {
2157         FilterManager::FilterPolicy policy = (autoFocusUsed) ?
2158                                              static_cast<FilterManager::FilterPolicy>(FilterManager::CHANGE_POLICY) :
2159                                              static_cast<FilterManager::FilterPolicy>(FilterManager::CHANGE_POLICY | FilterManager::OFFSET_POLICY);
2160         m_FilterManager->setFilterPosition(fallbackFilterPosition, policy);
2161     }
2162     else
2163         emit newStatus(state());
2164 
2165     if (autoFocusUsed && buildOffsetsUsed)
2166         // If we are building filter offsets signal AF run is complete
2167         m_FilterManager->autoFocusComplete(state(), currentPosition, m_LastSourceAutofocusTemperature, m_LastSourceAutofocusAlt);
2168 
2169     resetButtons();
2170 }
2171 
2172 void Focus::completeFocusProcedure(FocusState completionState, bool plot)
2173 {
2174     if (inAutoFocus)
2175     {
2176         if (completionState == Ekos::FOCUS_COMPLETE)
2177         {
2178             if (plot)
2179                 emit redrawHFRPlot(polynomialFit.get(), currentPosition, currentHFR);
2180 
2181             // Update the plot_position and plot_value vectors (used by Analyze)
2182             updatePlotPosition();
2183 
2184             appendLogText(i18np("Focus procedure completed after %1 iteration.",
2185                                 "Focus procedure completed after %1 iterations.", plot_position.count()));
2186 
2187             setLastFocusTemperature();
2188             setLastFocusAlt();
2189             resetAdaptiveFocus(m_OpsFocusSettings->focusAdaptive->isChecked());
2190 
2191             // CR add auto focus position, temperature and filter to log in CSV format
2192             // this will help with setting up focus offsets and temperature compensation
2193             qCInfo(KSTARS_EKOS_FOCUS) << "Autofocus values: position," << currentPosition << ", temperature,"
2194                                       << m_LastSourceAutofocusTemperature << ", filter," << filter()
2195                                       << ", HFR," << currentHFR << ", altitude," << m_LastSourceAutofocusAlt;
2196 
2197             if (m_FocusAlgorithm == FOCUS_POLYNOMIAL)
2198             {
2199                 // Add the final polynomial values to the signal sent to Analyze.
2200                 plot_position.append(currentPosition);
2201                 plot_value.append(currentHFR);
2202             }
2203 
2204             appendFocusLogText(QString("%1, %2, %3, %4, %5\n")
2205                                .arg(QString::number(currentPosition))
2206                                .arg(QString::number(m_LastSourceAutofocusTemperature, 'f', 1))
2207                                .arg(filter())
2208                                .arg(QString::number(currentHFR, 'f', 3))
2209                                .arg(QString::number(m_LastSourceAutofocusAlt, 'f', 1)));
2210 
2211             // Replace user position with optimal position
2212             absTicksSpin->setValue(currentPosition);
2213         }
2214         // In case of failure, go back to last position if the focuser is absolute
2215         else if (canAbsMove && initialFocuserAbsPosition >= 0 && resetFocusIteration <= MAXIMUM_RESET_ITERATIONS
2216                  && m_RestartState != RESTART_ABORT)
2217         {
2218             // If we're doing in-sequence focusing using an absolute focuser, retry focusing once, starting from last known good position
2219             bool const retry_focusing = m_RestartState == RESTART_NONE && ++resetFocusIteration < MAXIMUM_RESET_ITERATIONS;
2220 
2221             // If retrying, before moving, reset focus frame in case the star in subframe was lost
2222             if (retry_focusing)
2223             {
2224                 m_RestartState = RESTART_NOW;
2225                 resetFrame();
2226             }
2227 
2228             resetFocuser();
2229 
2230             // Bypass the rest of the function if we retry - we will fail if we could not move the focuser
2231             if (retry_focusing)
2232             {
2233                 emit autofocusAborted(filter(), "");
2234                 return;
2235             }
2236             else
2237             {
2238                 // We're in Autofocus and we've hit our max retry limit, so...
2239                 // resetFocuser will have initiated a focuser reset back to its starting position
2240                 // so we need to wait for that move to complete before returning control.
2241                 // This is important if the scheduler is running autofocus as it will likely
2242                 // immediately retry. The startup process will take the current focuser position
2243                 // as the start position and so the focuser needs to have arrived at its starting
2244                 // position before this. So set m_RestartState to log this.
2245                 m_RestartState = RESTART_ABORT;
2246                 return;
2247             }
2248         }
2249 
2250         // Reset the retry count on success or maximum count
2251         resetFocusIteration = 0;
2252     }
2253 
2254     const bool autoFocusUsed = inAutoFocus;
2255     const bool inBuildOffsetsUsed = inBuildOffsets;
2256 
2257     // Refresh display if needed
2258     if (m_FocusAlgorithm == FOCUS_POLYNOMIAL && plot)
2259         emit drawPolynomial(polynomialFit.get(), isVShapeSolution, true);
2260 
2261     // Enforce settling duration. Note stop resets m_GuidingSuspended
2262     int const settleTime = m_GuidingSuspended ? m_OpsFocusSettings->focusGuideSettleTime->value() : 0;
2263 
2264     // Reset the autofocus flags
2265     stop(completionState);
2266 
2267     if (settleTime > 0)
2268         appendLogText(i18n("Settling for %1s...", settleTime));
2269 
2270     QTimer::singleShot(settleTime * 1000, this, [ &, settleTime, completionState, autoFocusUsed, inBuildOffsetsUsed]()
2271     {
2272         settle(completionState, autoFocusUsed, inBuildOffsetsUsed);
2273 
2274         if (settleTime > 0)
2275             appendLogText(i18n("Settling complete."));
2276     });
2277 }
2278 
2279 void Focus::resetFocuser()
2280 {
2281     // If we are able to and need to, move the focuser back to the initial position and let the procedure restart from its termination
2282     if (m_Focuser && m_Focuser->isConnected() && initialFocuserAbsPosition >= 0)
2283     {
2284         // HACK: If the focuser will not move, cheat a little to get the notification - see processNumber
2285         if (currentPosition == initialFocuserAbsPosition)
2286             currentPosition--;
2287 
2288         appendLogText(i18n("Autofocus failed, moving back to initial focus position %1.", initialFocuserAbsPosition));
2289         changeFocus(initialFocuserAbsPosition - currentPosition);
2290         /* Restart will be executed by the end-of-move notification from the device if needed by resetFocus */
2291     }
2292 }
2293 
2294 void Focus::setCurrentMeasure()
2295 {
2296     // Let's now report the current HFR
2297     qCDebug(KSTARS_EKOS_FOCUS) << "Focus newFITS #" << starMeasureFrames.count() + 1 << ": Current HFR " << currentHFR <<
2298                                " Num stars "
2299                                << (starSelected ? 1 : currentNumStars);
2300 
2301     // Take the new HFR into account, eventually continue to stack samples
2302     if (appendMeasure(currentMeasure))
2303     {
2304         capture();
2305         return;
2306     }
2307     else starMeasureFrames.clear();
2308 
2309     // Let signal the current HFR now depending on whether the focuser is absolute or relative
2310     // Outside of Focus we continue to rely on HFR and independent of which measure the user selected we always calculate HFR
2311     if (canAbsMove)
2312         emit newHFR(currentHFR, currentPosition, inAutoFocus);
2313     else
2314         emit newHFR(currentHFR, -1, inAutoFocus);
2315 
2316     // Format the labels under the V-curve
2317     HFROut->setText(QString("%1").arg(currentHFR * getStarUnits(m_StarMeasure, m_StarUnits), 0, 'f', 2));
2318     if (m_StarMeasure == FOCUS_STAR_FWHM)
2319         FWHMOut->setText(QString("%1").arg(currentFWHM * getStarUnits(m_StarMeasure, m_StarUnits), 0, 'f', 2));
2320     starsOut->setText(QString("%1").arg(m_ImageData->getDetectedStars()));
2321     iterOut->setText(QString("%1").arg(absIterations + 1));
2322 
2323     // Display message in case _last_ HFR was invalid
2324     if (lastHFR == INVALID_STAR_MEASURE)
2325         appendLogText(i18n("FITS received. No stars detected."));
2326 
2327     // If we have a valid HFR value
2328     if (currentHFR > 0)
2329     {
2330         // Check if we're done from polynomial fitting algorithm
2331         if (m_FocusAlgorithm == FOCUS_POLYNOMIAL && isVShapeSolution)
2332         {
2333             completeFocusProcedure(Ekos::FOCUS_COMPLETE);
2334             return;
2335         }
2336 
2337         Edge selectedHFRStarHFR = m_ImageData->getSelectedHFRStar();
2338 
2339         // Center tracking box around selected star (if it valid) either in:
2340         // 1. Autofocus
2341         // 2. CheckFocus (minimumHFRCheck)
2342         // The starCenter _must_ already be defined, otherwise, we proceed until
2343         // the latter half of the function searches for a star and define it.
2344         if (starCenter.isNull() == false && (inAutoFocus || minimumRequiredHFR >= 0))
2345         {
2346             // Now we have star selected in the frame
2347             starSelected = true;
2348             starCenter.setX(qMax(0, static_cast<int>(selectedHFRStarHFR.x)));
2349             starCenter.setY(qMax(0, static_cast<int>(selectedHFRStarHFR.y)));
2350 
2351             syncTrackingBoxPosition();
2352 
2353             // Record the star information (X, Y, currentHFR)
2354             QVector3D oneStar = starCenter;
2355             oneStar.setZ(currentHFR);
2356             starsHFR.append(oneStar);
2357         }
2358         else
2359         {
2360             // Record the star information (X, Y, currentHFR)
2361             QVector3D oneStar(starCenter.x(), starCenter.y(), currentHFR);
2362             starsHFR.append(oneStar);
2363         }
2364 
2365         if (currentHFR > maxHFR)
2366             maxHFR = currentHFR;
2367 
2368         // Append point to the #Iterations vs #HFR chart in case of looping or in case in autofocus with a focus
2369         // that does not support position feedback.
2370 
2371         // If inAutoFocus is true without canAbsMove and without canRelMove, canTimerMove must be true.
2372         // We'd only want to execute this if the focus linear algorithm is not being used, as that
2373         // algorithm simulates a position-based system even for timer-based focusers.
2374         if (inFocusLoop || (inAutoFocus && ! isPositionBased()))
2375         {
2376             int pos = plot_position.empty() ? 1 : plot_position.last() + 1;
2377             addPlotPosition(pos, currentHFR);
2378         }
2379     }
2380     else
2381     {
2382         // Let's record an invalid star result
2383         QVector3D oneStar(starCenter.x(), starCenter.y(), INVALID_STAR_MEASURE);
2384         starsHFR.append(oneStar);
2385     }
2386 
2387     // First check that we haven't already search for stars
2388     // Since star-searching algorithm are time-consuming, we should only search when necessary
2389     m_FocusView->updateFrame();
2390 
2391     setHFRComplete();
2392 
2393     if (m_abInsOn)
2394         calculateAbInsData();
2395 }
2396 
2397 // Save off focus frame during Autofocus for later debugging
2398 void Focus::saveFocusFrame()
2399 {
2400     if (inAutoFocus && Options::focusLogging() && Options::saveFocusImages())
2401     {
2402         QDir dir;
2403         QDateTime now = KStarsData::Instance()->lt();
2404         QString path = QDir(KSPaths::writableLocation(QStandardPaths::AppLocalDataLocation)).filePath("autofocus/" +
2405                        now.toString("yyyy-MM-dd"));
2406         dir.mkpath(path);
2407 
2408         // To help identify focus frames add run number, step and frame (for multiple frames at each step)
2409         QString detail;
2410         if (m_FocusAlgorithm == FOCUS_LINEAR1PASS)
2411         {
2412             const int currentStep = linearFocuser->currentStep() + 1;
2413             detail = QString("_%1_%2_%3").arg(m_AFRun).arg(currentStep).arg(starMeasureFrames.count());
2414         }
2415 
2416         // IS8601 contains colons but they are illegal under Windows OS, so replacing them with '-'
2417         // The timestamp is no longer ISO8601 but it should solve interoperality issues between different OS hosts
2418         QString name     = "autofocus_frame_" + now.toString("HH-mm-ss") + detail + ".fits";
2419         QString filename = path + QStringLiteral("/") + name;
2420         m_ImageData->saveImage(filename);
2421     }
2422 }
2423 
2424 void Focus::calculateAbInsData()
2425 {
2426     ImageMosaicMask *mosaicmask = dynamic_cast<ImageMosaicMask *>(m_FocusView->imageMask().get());
2427     const QVector<QRect> tiles = mosaicmask->tiles();
2428     auto stars = m_ImageData->getStarCenters();
2429     QVector<QList<Edge *>> tileStars(NUM_TILES);
2430 
2431     for (int star = 0; star < stars.count(); star++)
2432     {
2433         const int x = stars[star]->x;
2434         const int y = stars[star]->y;
2435         for (int tile = 0; tile < NUM_TILES; tile++)
2436         {
2437             QRect thisTile = tiles[tile];
2438             if (thisTile.contains(x, y))
2439             {
2440                 tileStars[tile].append(stars[star]);
2441                 break;
2442             }
2443         }
2444     }
2445 
2446     // Get the measure for each tile
2447     for (int tile = 0; tile < tileStars.count(); tile++)
2448     {
2449         double measure, weight;
2450 
2451         if (m_StarMeasure == FOCUS_STAR_NUM_STARS)
2452         {
2453             measure = tileStars[tile].count();
2454             weight = 1.0;
2455         }
2456         else if (m_StarMeasure == FOCUS_STAR_FWHM)
2457         {
2458             getFWHM(tileStars[tile], &measure, &weight);
2459         }
2460         else if (m_StarMeasure == FOCUS_STAR_FOURIER_POWER)
2461         {
2462             getFourierPower(&measure, &weight, tile);
2463         }
2464         else
2465         {
2466             // HFR or HFR_adj
2467             std::vector<double> HFRs;
2468 
2469             for (int star = 0; star < tileStars[tile].count(); star++)
2470             {
2471                 HFRs.push_back(tileStars[tile][star]->HFR);
2472             }
2473             measure = Mathematics::RobustStatistics::ComputeLocation(Mathematics::RobustStatistics::LOCATION_SIGMACLIPPING, HFRs, 2);
2474             weight = calculateStarWeight(m_OpsFocusProcess->focusUseWeights->isChecked(), HFRs);
2475         }
2476 
2477         m_abInsMeasure[tile].append(measure);
2478         m_abInsWeight[tile].append(weight);
2479         m_abInsNumStars[tile].append(tileStars[tile].count());
2480 
2481         if (!linearFocuser->isInFirstPass())
2482         {
2483             // This is the last datapoint so calculate average star position in the tile from the tile center
2484             // FOCUS_STAR_FOURIER_POWER doesn't use stars directly so no need to calculate offset. The other
2485             // measures all use stars: FOCUS_STAR_NUM_STARS, FOCUS_STAR_FWHM, FOCUS_STAR_HFR, FOCUS_STAR_HFR_ADJ
2486             int xAv = 0, yAv = 0;
2487             if (m_StarMeasure != FOCUS_STAR_FOURIER_POWER)
2488             {
2489                 QPoint tileCenter = tiles[tile].center();
2490                 int xSum = 0.0, ySum = 0.0;
2491                 for (int star = 0; star < tileStars[tile].count(); star++)
2492                 {
2493                     xSum += tileStars[tile][star]->x - tileCenter.x();
2494                     ySum += tileStars[tile][star]->y - tileCenter.y();
2495                 }
2496 
2497                 xAv = (tileStars[tile].count() <= 0) ? 0 : xSum / tileStars[tile].count();
2498                 yAv = (tileStars[tile].count() <= 0) ? 0 : ySum / tileStars[tile].count();
2499             }
2500             m_abInsTileCenterOffset.append(QPoint(xAv, yAv));
2501         }
2502     }
2503     m_abInsPosition.append(currentPosition);
2504 }
2505 
2506 void Focus::setCaptureComplete()
2507 {
2508     DarkLibrary::Instance()->disconnect(this);
2509 
2510     // If we have a box, sync the bounding box to its position.
2511     syncTrackingBoxPosition();
2512 
2513     // Notify user if we're not looping
2514     if (inFocusLoop == false)
2515         appendLogText(i18n("Image received."));
2516 
2517     if (captureInProgress && inFocusLoop == false && inAutoFocus == false)
2518         m_Camera->setUploadMode(rememberUploadMode);
2519 
2520     if (m_RememberCameraFastExposure && inFocusLoop == false && inAutoFocus == false)
2521     {
2522         m_RememberCameraFastExposure = false;
2523         m_Camera->setFastExposureEnabled(true);
2524     }
2525 
2526     captureInProgress = false;
2527     // update the limits from the real values
2528     checkMosaicMaskLimits();
2529 
2530     // Emit the whole image
2531     emit newImage(m_FocusView);
2532     // Emit the tracking (bounding) box view. Used in Summary View
2533     emit newStarPixmap(m_FocusView->getTrackingBoxPixmap(10));
2534 
2535     // If we are not looping; OR
2536     // If we are looping but we already have tracking box enabled; OR
2537     // If we are asked to analyze _all_ the stars within the field
2538     // THEN let's find stars in the image and get current HFR
2539     if (inFocusLoop == false || (inFocusLoop && (m_FocusView->isTrackingBoxEnabled()
2540                                  || m_OpsFocusSettings->focusUseFullField->isChecked())))
2541         analyzeSources();
2542     else
2543         setHFRComplete();
2544 }
2545 
2546 void Focus::setHFRComplete()
2547 {
2548     // If we are just framing, let's capture again
2549     if (inFocusLoop)
2550     {
2551         capture();
2552         return;
2553     }
2554 
2555     // Get target chip
2556     ISD::CameraChip *targetChip = m_Camera->getChip(ISD::CameraChip::PRIMARY_CCD);
2557 
2558     // Get target chip binning
2559     int subBinX = 1, subBinY = 1;
2560     if (!targetChip->getBinning(&subBinX, &subBinY))
2561         qCDebug(KSTARS_EKOS_FOCUS) << "Warning: target chip is reporting no binning property, using 1x1.";
2562 
2563     // If star is NOT yet selected in a non-full-frame situation
2564     // then let's now try to find the star. This step is skipped for full frames
2565     // since there isn't a single star to select as we are only interested in the overall average HFR.
2566     // We need to check if we can find the star right away, or if we need to _subframe_ around the
2567     // selected star.
2568     if (m_OpsFocusSettings->focusUseFullField->isChecked() == false && starCenter.isNull())
2569     {
2570         int x = 0, y = 0, w = 0, h = 0;
2571 
2572         // Let's get the stored frame settings for this particular chip
2573         if (frameSettings.contains(targetChip))
2574         {
2575             QVariantMap settings = frameSettings[targetChip];
2576             x                    = settings["x"].toInt();
2577             y                    = settings["y"].toInt();
2578             w                    = settings["w"].toInt();
2579             h                    = settings["h"].toInt();
2580         }
2581         else
2582             // Otherwise let's get the target chip frame coordinates.
2583             targetChip->getFrame(&x, &y, &w, &h);
2584 
2585         // In case auto star is selected.
2586         if (m_OpsFocusSettings->focusAutoStarEnabled->isChecked())
2587         {
2588             // Do we have a valid star detected?
2589             const Edge selectedHFRStar = m_ImageData->getSelectedHFRStar();
2590 
2591             if (selectedHFRStar.x == -1)
2592             {
2593                 appendLogText(i18n("Failed to automatically select a star. Please select a star manually."));
2594 
2595                 // Center the tracking box in the frame and display it
2596                 m_FocusView->setTrackingBox(QRect(w - m_OpsFocusSettings->focusBoxSize->value() / (subBinX * 2),
2597                                                   h - m_OpsFocusSettings->focusBoxSize->value() / (subBinY * 2),
2598                                                   m_OpsFocusSettings->focusBoxSize->value() / subBinX, m_OpsFocusSettings->focusBoxSize->value() / subBinY));
2599                 m_FocusView->setTrackingBoxEnabled(true);
2600 
2601                 // Use can now move it to select the desired star
2602                 setState(Ekos::FOCUS_WAITING);
2603 
2604                 // Start the wait timer so we abort after a timeout if the user does not make a choice
2605                 waitStarSelectTimer.start();
2606 
2607                 return;
2608             }
2609 
2610             // set the tracking box on selectedHFRStar
2611             starCenter.setX(selectedHFRStar.x);
2612             starCenter.setY(selectedHFRStar.y);
2613             starCenter.setZ(subBinX);
2614             starSelected = true;
2615             syncTrackingBoxPosition();
2616 
2617             // Do we need to subframe?
2618             if (subFramed == false && isFocusSubFrameEnabled() && m_OpsFocusSettings->focusSubFrame->isChecked())
2619             {
2620                 int offset = (static_cast<double>(m_OpsFocusSettings->focusBoxSize->value()) / subBinX) * 1.5;
2621                 int subX   = (selectedHFRStar.x - offset) * subBinX;
2622                 int subY   = (selectedHFRStar.y - offset) * subBinY;
2623                 int subW   = offset * 2 * subBinX;
2624                 int subH   = offset * 2 * subBinY;
2625 
2626                 int minX, maxX, minY, maxY, minW, maxW, minH, maxH;
2627                 targetChip->getFrameMinMax(&minX, &maxX, &minY, &maxY, &minW, &maxW, &minH, &maxH);
2628 
2629                 // Try to limit the subframed selection
2630                 if (subX < minX)
2631                     subX = minX;
2632                 if (subY < minY)
2633                     subY = minY;
2634                 if ((subW + subX) > maxW)
2635                     subW = maxW - subX;
2636                 if ((subH + subY) > maxH)
2637                     subH = maxH - subY;
2638 
2639                 // Now we store the subframe coordinates in the target chip frame settings so we
2640                 // reuse it later when we capture again.
2641                 QVariantMap settings = frameSettings[targetChip];
2642                 settings["x"]        = subX;
2643                 settings["y"]        = subY;
2644                 settings["w"]        = subW;
2645                 settings["h"]        = subH;
2646                 settings["binx"]     = subBinX;
2647                 settings["biny"]     = subBinY;
2648 
2649                 qCDebug(KSTARS_EKOS_FOCUS) << "Frame is subframed. X:" << subX << "Y:" << subY << "W:" << subW << "H:" << subH << "binX:" <<
2650                                            subBinX << "binY:" << subBinY;
2651 
2652                 starsHFR.clear();
2653 
2654                 frameSettings[targetChip] = settings;
2655 
2656                 // Set the star center in the center of the subframed coordinates
2657                 starCenter.setX(subW / (2 * subBinX));
2658                 starCenter.setY(subH / (2 * subBinY));
2659                 starCenter.setZ(subBinX);
2660 
2661                 subFramed = true;
2662 
2663                 m_FocusView->setFirstLoad(true);
2664 
2665                 // Now let's capture again for the actual requested subframed image.
2666                 capture();
2667                 return;
2668             }
2669             // If we're subframed or don't need subframe, let's record the max star coordinates
2670             else
2671             {
2672                 starCenter.setX(selectedHFRStar.x);
2673                 starCenter.setY(selectedHFRStar.y);
2674                 starCenter.setZ(subBinX);
2675 
2676                 // Let's now capture again if we're autofocusing
2677                 if (inAutoFocus)
2678                 {
2679                     capture();
2680                     return;
2681                 }
2682             }
2683         }
2684         // If manual selection is enabled then let's ask the user to select the focus star
2685         else
2686         {
2687             appendLogText(i18n("Capture complete. Select a star to focus."));
2688 
2689             starSelected = false;
2690 
2691             // Let's now display and set the tracking box in the center of the frame
2692             // so that the user moves it around to select the desired star.
2693             int subBinX = 1, subBinY = 1;
2694             targetChip->getBinning(&subBinX, &subBinY);
2695 
2696             m_FocusView->setTrackingBox(QRect((w - m_OpsFocusSettings->focusBoxSize->value()) / (subBinX * 2),
2697                                               (h - m_OpsFocusSettings->focusBoxSize->value()) / (2 * subBinY),
2698                                               m_OpsFocusSettings->focusBoxSize->value() / subBinX, m_OpsFocusSettings->focusBoxSize->value() / subBinY));
2699             m_FocusView->setTrackingBoxEnabled(true);
2700 
2701             // Now we wait
2702             setState(Ekos::FOCUS_WAITING);
2703 
2704             // If the user does not select for a timeout period, we abort.
2705             waitStarSelectTimer.start();
2706             return;
2707         }
2708     }
2709 
2710     // Check if the focus module is requested to verify if the minimum HFR value is met.
2711     if (minimumRequiredHFR >= 0)
2712     {
2713         // In case we failed to detected, we capture again.
2714         if (currentHFR == INVALID_STAR_MEASURE)
2715         {
2716             if (noStarCount++ < MAX_RECAPTURE_RETRIES)
2717             {
2718                 appendLogText(i18n("No stars detected while testing HFR, capturing again..."));
2719                 // On Last Attempt reset focus frame to capture full frame and recapture star if possible
2720                 if (noStarCount == MAX_RECAPTURE_RETRIES)
2721                     resetFrame();
2722                 capture();
2723                 return;
2724             }
2725             // If we exceeded maximum tries we abort
2726             else
2727             {
2728                 noStarCount = 0;
2729                 completeFocusProcedure(Ekos::FOCUS_ABORTED);
2730             }
2731         }
2732         // If the detect current HFR is more than the minimum required HFR
2733         // then we should start the autofocus process now to bring it down.
2734         else if (currentHFR > minimumRequiredHFR)
2735         {
2736             qCDebug(KSTARS_EKOS_FOCUS) << "Current HFR:" << currentHFR << "is above required minimum HFR:" << minimumRequiredHFR <<
2737                                        ". Starting AutoFocus...";
2738             minimumRequiredHFR = INVALID_STAR_MEASURE;
2739             start();
2740         }
2741         // Otherwise, the current HFR is fine and lower than the required minimum HFR so we announce success.
2742         else
2743         {
2744             qCDebug(KSTARS_EKOS_FOCUS) << "Current HFR:" << currentHFR << "is below required minimum HFR:" << minimumRequiredHFR <<
2745                                        ". Autofocus successful.";
2746             completeFocusProcedure(Ekos::FOCUS_COMPLETE, false);
2747         }
2748 
2749         // Nothing more for now
2750         return;
2751     }
2752 
2753     // If we are not in autofocus process, we're done.
2754     if (inAutoFocus == false)
2755     {
2756         // If we are done and there is no further autofocus,
2757         // we reset state to IDLE
2758         if (state() != Ekos::FOCUS_IDLE)
2759             setState(Ekos::FOCUS_IDLE);
2760 
2761         resetButtons();
2762         return;
2763     }
2764 
2765     // Set state to progress
2766     if (state() != Ekos::FOCUS_PROGRESS)
2767         setState(Ekos::FOCUS_PROGRESS);
2768 
2769     // Now let's kick in the algorithms
2770 
2771     if (m_FocusAlgorithm == FOCUS_LINEAR || m_FocusAlgorithm == FOCUS_LINEAR1PASS)
2772         autoFocusLinear();
2773     else if (canAbsMove || canRelMove)
2774         // Position-based algorithms
2775         autoFocusAbs();
2776     else
2777         // Time open-looped algorithms
2778         autoFocusRel();
2779 }
2780 
2781 QString Focus::getyAxisLabel(StarMeasure starMeasure)
2782 {
2783     QString str = "HFR";
2784     m_StarUnits == FOCUS_UNITS_ARCSEC ? str += " (\")" : str += " (pix)";
2785 
2786     if (inAutoFocus)
2787     {
2788         switch (starMeasure)
2789         {
2790             case FOCUS_STAR_HFR:
2791                 break;
2792             case FOCUS_STAR_HFR_ADJ:
2793                 str = "HFR Adj";
2794                 m_StarUnits == FOCUS_UNITS_ARCSEC ? str += " (\")" : str += " (pix)";
2795                 break;
2796             case FOCUS_STAR_FWHM:
2797                 str = "FWHM";
2798                 m_StarUnits == FOCUS_UNITS_ARCSEC ? str += " (\")" : str += " (pix)";
2799                 break;
2800             case FOCUS_STAR_NUM_STARS:
2801                 str = "# Stars";
2802                 break;
2803             case FOCUS_STAR_FOURIER_POWER:
2804                 str = "Fourier Power";
2805                 break;
2806             default:
2807                 break;
2808         }
2809     }
2810     return str;
2811 }
2812 
2813 void Focus::clearDataPoints()
2814 {
2815     maxHFR = 1;
2816     polynomialFit.reset();
2817     plot_position.clear();
2818     plot_value.clear();
2819     isVShapeSolution = false;
2820     m_abInsPosition.clear();
2821     m_abInsTileCenterOffset.clear();
2822     if (m_abInsMeasure.count() != NUM_TILES)
2823     {
2824         m_abInsMeasure.resize(NUM_TILES);
2825         m_abInsWeight.resize(NUM_TILES);
2826         m_abInsNumStars.resize(NUM_TILES);
2827     }
2828     else
2829     {
2830         for (int i = 0; i < m_abInsMeasure.count(); i++)
2831         {
2832             m_abInsMeasure[i].clear();
2833             m_abInsWeight[i].clear();
2834             m_abInsNumStars[i].clear();
2835         }
2836     }
2837 
2838     emit initHFRPlot(getyAxisLabel(m_StarMeasure), getStarUnits(m_StarMeasure, m_StarUnits),
2839                      m_OptDir == CurveFitting::OPTIMISATION_MINIMISE, m_OpsFocusProcess->focusUseWeights->isChecked(),
2840                      inFocusLoop == false && isPositionBased());
2841 }
2842 
2843 bool Focus::autoFocusChecks()
2844 {
2845     if (++absIterations > MAXIMUM_ABS_ITERATIONS)
2846     {
2847         appendLogText(i18n("Autofocus failed to reach proper focus. Try increasing tolerance value."));
2848         completeFocusProcedure(Ekos::FOCUS_ABORTED);
2849         return false;
2850     }
2851 
2852     // No stars detected, try to capture again
2853     if (currentHFR == INVALID_STAR_MEASURE)
2854     {
2855         if (noStarCount < MAX_RECAPTURE_RETRIES)
2856         {
2857             noStarCount++;
2858             appendLogText(i18n("No stars detected, capturing again..."));
2859             capture();
2860             return false;
2861         }
2862         else if (m_FocusAlgorithm == FOCUS_LINEAR)
2863         {
2864             appendLogText(i18n("Failed to detect any stars at position %1. Continuing...", currentPosition));
2865             noStarCount = 0;
2866         }
2867         else if(!m_OpsFocusProcess->focusDonut->isChecked())
2868         {
2869             // Carry on for donut detection
2870             appendLogText(i18n("Failed to detect any stars. Reset frame and try again."));
2871             completeFocusProcedure(Ekos::FOCUS_ABORTED);
2872             return false;
2873         }
2874     }
2875     else
2876         noStarCount = 0;
2877 
2878     return true;
2879 }
2880 
2881 void Focus::plotLinearFocus()
2882 {
2883     // I was hoping to avoid intermediate plotting, just set everything up then plot,
2884     // but this isn't working. For now, with plt=true, plot on every intermediate update.
2885     bool plt = true;
2886 
2887     // Get the data to plot.
2888     QVector<double> values, weights;
2889     QVector<int> positions;
2890     linearFocuser->getMeasurements(&positions, &values, &weights);
2891     const FocusAlgorithmInterface::FocusParams &params = linearFocuser->getParams();
2892 
2893     // As an optimization for slower machines, e.g. RPi4s, if the points are the same except for
2894     // the last point, just emit the last point instead of redrawing everything.
2895     static QVector<double> lastValues;
2896     static QVector<int> lastPositions;
2897 
2898     bool incrementalChange = false;
2899     if (positions.size() > 1 && positions.size() == lastPositions.size() + 1)
2900     {
2901         bool ok = true;
2902         for (int i = 0; i < positions.size() - 1; ++i)
2903             if (positions[i] != lastPositions[i] || values[i] != lastValues[i])
2904             {
2905                 ok = false;
2906                 break;
2907             }
2908         incrementalChange = ok;
2909     }
2910     lastPositions = positions;
2911     lastValues = values;
2912 
2913     const bool outlier = false;
2914     if (incrementalChange)
2915         emit newHFRPlotPosition(static_cast<double>(positions.last()), values.last(), (pow(weights.last(), -0.5)),
2916                                 outlier, params.initialStepSize, plt);
2917     else
2918     {
2919         emit initHFRPlot(getyAxisLabel(m_StarMeasure), getStarUnits(m_StarMeasure, m_StarUnits),
2920                          m_OptDir == CurveFitting::OPTIMISATION_MINIMISE, params.useWeights, plt);
2921         for (int i = 0; i < positions.size(); ++i)
2922             emit newHFRPlotPosition(static_cast<double>(positions[i]), values[i], (pow(weights.last(), -0.5)),
2923                                     outlier, params.initialStepSize, plt);
2924     }
2925 
2926     // Plot the polynomial, if there are enough points.
2927     if (values.size() > 3)
2928     {
2929         // The polynomial should only reflect 1st-pass samples.
2930         QVector<double> pass1Values, pass1Weights;
2931         QVector<int> pass1Positions;
2932         QVector<bool> pass1Outliers;
2933         double minPosition, minValue;
2934         double searchMin = std::max(params.minPositionAllowed, params.startPosition - params.maxTravel);
2935         double searchMax = std::min(params.maxPositionAllowed, params.startPosition + params.maxTravel);
2936 
2937         linearFocuser->getPass1Measurements(&pass1Positions, &pass1Values, &pass1Weights, &pass1Outliers);
2938         if (m_FocusAlgorithm == FOCUS_LINEAR || m_CurveFit == CurveFitting::FOCUS_QUADRATIC)
2939         {
2940             // TODO: Need to determine whether to change LINEAR over to the LM solver in CurveFitting
2941             // This will be determined after L1P's first release has been deemed successful.
2942             polynomialFit.reset(new PolynomialFit(2, pass1Positions, pass1Values));
2943 
2944             if (polynomialFit->findMinimum(params.startPosition, searchMin, searchMax, &minPosition, &minValue))
2945             {
2946                 emit drawPolynomial(polynomialFit.get(), true, true, plt);
2947 
2948                 // Only plot the first pass' min position if we're not done.
2949                 // Once we have a result, we don't want to display an intermediate minimum.
2950                 if (linearFocuser->isDone())
2951                     emit minimumFound(-1, -1, plt);
2952                 else
2953                     emit minimumFound(minPosition, minValue, plt);
2954             }
2955             else
2956             {
2957                 // Didn't get a good polynomial fit.
2958                 emit drawPolynomial(polynomialFit.get(), false, false, plt);
2959                 emit minimumFound(-1, -1, plt);
2960             }
2961 
2962         }
2963         else // Linear 1 Pass
2964         {
2965             if (curveFitting->findMinMax(params.startPosition, searchMin, searchMax, &minPosition, &minValue, params.curveFit,
2966                                          params.optimisationDirection))
2967             {
2968                 R2 = curveFitting->calculateR2(static_cast<CurveFitting::CurveFit>(params.curveFit));
2969                 emit drawCurve(curveFitting.get(), true, true, plt);
2970 
2971                 // For Linear 1 Pass always display the minimum on the graph
2972                 emit minimumFound(minPosition, minValue, plt);
2973             }
2974             else
2975             {
2976                 // Didn't get a good fit.
2977                 emit drawCurve(curveFitting.get(), false, false, plt);
2978                 emit minimumFound(-1, -1, plt);
2979             }
2980         }
2981     }
2982 
2983     // Linear focuser might change the latest hfr with its relativeHFR scheme.
2984     HFROut->setText(QString("%1").arg(currentHFR * getStarUnits(m_StarMeasure, m_StarUnits), 0, 'f', 2));
2985 
2986     emit setTitle(linearFocuser->getTextStatus(R2));
2987 
2988     if (!plt) HFRPlot->replot();
2989 }
2990 
2991 // Get the curve fitting goal
2992 CurveFitting::FittingGoal Focus::getGoal(int numSteps)
2993 {
2994     // The classic walk needs the STANDARD curve fitting
2995     if (m_FocusWalk == FOCUS_WALK_CLASSIC)
2996         return CurveFitting::FittingGoal::STANDARD;
2997 
2998     // Fixed step walks will use C, except for the last step which should be BEST
2999     return (numSteps >= m_OpsFocusMechanics->focusNumSteps->value()) ? CurveFitting::FittingGoal::BEST :
3000            CurveFitting::FittingGoal::STANDARD;
3001 }
3002 
3003 // Called after the first pass is complete and we're moving to the final focus position
3004 // Calculate R2 for the curve and update the graph.
3005 // Add the CFZ to the graph
3006 void Focus::plotLinearFinalUpdates()
3007 {
3008     bool plt = true;
3009     if (!m_OpsFocusProcess->focusRefineCurveFit->isChecked())
3010     {
3011         // Display the CFZ on the graph
3012         emit drawCFZ(linearFocuser->solution(), linearFocuser->solutionValue(), m_cfzSteps,
3013                      m_CFZUI->focusCFZDisplayVCurve->isChecked());
3014         // Final updates to the graph title
3015         emit finalUpdates(linearFocuser->getTextStatus(R2), plt);
3016     }
3017     else
3018     {
3019         // v-curve needs to be redrawn to reflect the data from the refining process
3020         // Get the data to plot.
3021         QVector<double> pass1Values, pass1Weights;
3022         QVector<int> pass1Positions;
3023         QVector<bool> pass1Outliers;
3024 
3025         linearFocuser->getPass1Measurements(&pass1Positions, &pass1Values, &pass1Weights, &pass1Outliers);
3026         const FocusAlgorithmInterface::FocusParams &params = linearFocuser->getParams();
3027 
3028         emit initHFRPlot(getyAxisLabel(m_StarMeasure), getStarUnits(m_StarMeasure, m_StarUnits),
3029                          m_OptDir == CurveFitting::OPTIMISATION_MINIMISE, m_OpsFocusProcess->focusUseWeights->isChecked(), plt);
3030 
3031         for (int i = 0; i < pass1Positions.size(); ++i)
3032             emit newHFRPlotPosition(static_cast<double>(pass1Positions[i]), pass1Values[i], (pow(pass1Weights[i], -0.5)),
3033                                     pass1Outliers[i], params.initialStepSize, plt);
3034 
3035         R2 = curveFitting->calculateR2(static_cast<CurveFitting::CurveFit>(params.curveFit));
3036         emit drawCurve(curveFitting.get(), true, true, false);
3037 
3038         // For Linear 1 Pass always display the minimum on the graph
3039         emit minimumFound(linearFocuser->solution(), linearFocuser->solutionValue(), plt);
3040         // Display the CFZ on the graph
3041         emit drawCFZ(linearFocuser->solution(), linearFocuser->solutionValue(), m_cfzSteps,
3042                      m_CFZUI->focusCFZDisplayVCurve->isChecked());
3043         // Update the graph title
3044         emit setTitle(linearFocuser->getTextStatus(R2), plt);
3045     }
3046 }
3047 
3048 void Focus::startAberrationInspector()
3049 {
3050     // Fill in the data structure to be passed to the Aberration Inspector
3051     AberrationInspector::abInsData data;
3052 
3053     ImageMosaicMask *mosaicmask = dynamic_cast<ImageMosaicMask *>(m_FocusView->imageMask().get());
3054     if (!mosaicmask)
3055     {
3056         appendLogText(i18n("Unable to launch Aberration Inspector run %1...", m_abInsRun));
3057         return;
3058     }
3059 
3060 
3061     data.run = ++m_abInsRun;
3062     data.curveFit = m_CurveFit;
3063     data.useWeights = m_OpsFocusProcess->focusUseWeights->isChecked();
3064     data.optDir = m_OptDir;
3065     data.sensorWidth = m_CcdWidth;
3066     data.sensorHeight = m_CcdHeight;
3067     data.pixelSize = m_CcdPixelSizeX;
3068     data.tileWidth = mosaicmask->tiles()[0].width();
3069     data.focuserStepMicrons = m_CFZUI->focusCFZStepSize->value();
3070     data.yAxisLabel = getyAxisLabel(m_StarMeasure);
3071     data.starUnits = getStarUnits(m_StarMeasure, m_StarUnits);
3072     data.cfzSteps = m_cfzSteps;
3073     data.isPositionBased = isPositionBased();
3074 
3075     // Launch the Aberration Inspector.
3076     appendLogText(i18n("Launching Aberration Inspector run %1...", m_abInsRun));
3077     QPointer<AberrationInspector> abIns(new AberrationInspector(data, m_abInsPosition, m_abInsMeasure, m_abInsWeight,
3078                                         m_abInsNumStars, m_abInsTileCenterOffset));
3079     abIns->setAttribute(Qt::WA_DeleteOnClose);
3080     abIns->show();
3081 }
3082 
3083 void Focus::autoFocusLinear()
3084 {
3085     if (!autoFocusChecks())
3086         return;
3087 
3088     if (!canAbsMove && !canRelMove && canTimerMove)
3089     {
3090         //const bool kFixPosition = true;
3091         if (linearRequestedPosition != currentPosition)
3092             //if (kFixPosition && (linearRequestedPosition != currentPosition))
3093         {
3094             qCDebug(KSTARS_EKOS_FOCUS) << "Linear: warning, changing position " << currentPosition << " to "
3095                                        << linearRequestedPosition;
3096 
3097             currentPosition = linearRequestedPosition;
3098         }
3099     }
3100 
3101     // Only use the relativeHFR algorithm if full field is enabled with one capture/measurement.
3102     bool useFocusStarsHFR = m_OpsFocusSettings->focusUseFullField->isChecked()
3103                             && m_OpsFocusProcess->focusFramesCount->value() == 1;
3104     auto focusStars = useFocusStarsHFR || (m_FocusAlgorithm == FOCUS_LINEAR1PASS) ? &(m_ImageData->getStarCenters()) : nullptr;
3105 
3106     linearRequestedPosition = linearFocuser->newMeasurement(currentPosition, currentMeasure, currentWeight, focusStars);
3107     if (m_FocusAlgorithm == FOCUS_LINEAR1PASS && linearFocuser->isDone() && linearFocuser->solution() != -1)
3108     {
3109         // Linear 1 Pass is done, graph is drawn, so just move to the focus position, and update the graph.
3110         plotLinearFinalUpdates();
3111         if (m_abInsOn)
3112             startAberrationInspector();
3113     }
3114     else
3115         // Update the graph with the next datapoint, draw the curve, etc.
3116         plotLinearFocus();
3117 
3118     if (linearFocuser->isDone())
3119     {
3120         if (linearFocuser->solution() != -1)
3121         {
3122             // Now test that the curve fit was acceptable. If not retry the focus process using standard retry process
3123             // R2 check is only available for Linear 1 Pass for Hyperbola and Parabola
3124             if (m_CurveFit == CurveFitting::FOCUS_QUADRATIC)
3125                 // Linear only uses Quadratic so no need to do the R2 check, just complete
3126                 completeFocusProcedure(Ekos::FOCUS_COMPLETE, false);
3127             else if (R2 >= m_OpsFocusProcess->focusR2Limit->value())
3128             {
3129                 qCDebug(KSTARS_EKOS_FOCUS) << QString("Linear Curve Fit check passed R2=%1 focusR2Limit=%2").arg(R2).arg(
3130                                                m_OpsFocusProcess->focusR2Limit->value());
3131                 completeFocusProcedure(Ekos::FOCUS_COMPLETE, false);
3132                 R2Retries = 0;
3133             }
3134             else if (R2Retries == 0)
3135             {
3136                 // Failed the R2 check for the first time so retry...
3137                 appendLogText(i18n("Curve Fit check failed R2=%1 focusR2Limit=%2 retrying...", R2,
3138                                    m_OpsFocusProcess->focusR2Limit->value()));
3139                 completeFocusProcedure(Ekos::FOCUS_ABORTED, false);
3140                 R2Retries++;
3141             }
3142             else
3143             {
3144                 // Retried after an R2 check fail but failed again so... log msg and continue
3145                 appendLogText(i18n("Curve Fit check failed again R2=%1 focusR2Limit=%2 but continuing...", R2,
3146                                    m_OpsFocusProcess->focusR2Limit->value()));
3147                 completeFocusProcedure(Ekos::FOCUS_COMPLETE, false);
3148                 R2Retries = 0;
3149             }
3150         }
3151         else
3152         {
3153             qCDebug(KSTARS_EKOS_FOCUS) << linearFocuser->doneReason();
3154             appendLogText("Linear autofocus algorithm aborted.");
3155             completeFocusProcedure(Ekos::FOCUS_ABORTED, false);
3156         }
3157         return;
3158     }
3159     else
3160     {
3161         const int delta = linearRequestedPosition - currentPosition;
3162 
3163         if (!changeFocus(delta))
3164             completeFocusProcedure(Ekos::FOCUS_ABORTED, false);
3165 
3166         return;
3167     }
3168 }
3169 
3170 void Focus::autoFocusAbs()
3171 {
3172     // Q_ASSERT_X(canAbsMove || canRelMove, __FUNCTION__, "Prerequisite: only absolute and relative focusers");
3173 
3174     static int minHFRPos = 0, focusOutLimit = 0, focusInLimit = 0, lastHFRPos = 0, fluctuations = 0;
3175     static double minHFR = 0, lastDelta = 0;
3176     double targetPosition = 0;
3177     bool ignoreLimitedDelta = false;
3178 
3179     QString deltaTxt = QString("%1").arg(fabs(currentHFR - minHFR) * 100.0, 0, 'g', 3);
3180     QString HFRText  = QString("%1").arg(currentHFR, 0, 'g', 3);
3181 
3182     qCDebug(KSTARS_EKOS_FOCUS) << "===============================";
3183     qCDebug(KSTARS_EKOS_FOCUS) << "Current HFR: " << currentHFR << " Current Position: " << currentPosition;
3184     qCDebug(KSTARS_EKOS_FOCUS) << "Last minHFR: " << minHFR << " Last MinHFR Pos: " << minHFRPos;
3185     qCDebug(KSTARS_EKOS_FOCUS) << "Delta: " << deltaTxt << "%";
3186     qCDebug(KSTARS_EKOS_FOCUS) << "========================================";
3187 
3188     if (minHFR)
3189         appendLogText(i18n("FITS received. HFR %1 @ %2. Delta (%3%)", HFRText, currentPosition, deltaTxt));
3190     else
3191         appendLogText(i18n("FITS received. HFR %1 @ %2.", HFRText, currentPosition));
3192 
3193     if (!autoFocusChecks())
3194         return;
3195 
3196     addPlotPosition(currentPosition, currentHFR);
3197 
3198     switch (m_LastFocusDirection)
3199     {
3200         case FOCUS_NONE:
3201             lastHFR                   = currentHFR;
3202             initialFocuserAbsPosition = currentPosition;
3203             minHFR                    = currentHFR;
3204             minHFRPos                 = currentPosition;
3205             HFRDec                    = 0;
3206             HFRInc                    = 0;
3207             focusOutLimit             = 0;
3208             focusInLimit              = 0;
3209             lastDelta                 = 0;
3210             fluctuations              = 0;
3211 
3212             // This is the first step, so clamp the initial target position to the device limits
3213             // If the focuser cannot move because it is at one end of the interval, try the opposite direction next
3214             if (absMotionMax < currentPosition + pulseDuration)
3215             {
3216                 if (currentPosition < absMotionMax)
3217                 {
3218                     pulseDuration = absMotionMax - currentPosition;
3219                 }
3220                 else
3221                 {
3222                     pulseDuration = 0;
3223                     m_LastFocusDirection = FOCUS_IN;
3224                 }
3225             }
3226             else if (currentPosition + pulseDuration < absMotionMin)
3227             {
3228                 if (absMotionMin < currentPosition)
3229                 {
3230                     pulseDuration = currentPosition - absMotionMin;
3231                 }
3232                 else
3233                 {
3234                     pulseDuration = 0;
3235                     m_LastFocusDirection = FOCUS_OUT;
3236                 }
3237             }
3238 
3239             m_LastFocusDirection = (pulseDuration > 0) ? FOCUS_OUT : FOCUS_IN;
3240             if (!changeFocus(pulseDuration, false))
3241                 completeFocusProcedure(Ekos::FOCUS_ABORTED);
3242 
3243             break;
3244 
3245         case FOCUS_IN:
3246         case FOCUS_OUT:
3247             if (reverseDir && focusInLimit && focusOutLimit &&
3248                     fabs(currentHFR - minHFR) < (m_OpsFocusProcess->focusTolerance->value() / 100.0) && HFRInc == 0)
3249             {
3250                 if (absIterations <= 2)
3251                 {
3252                     QString message = i18n("Change in HFR is too small. Try increasing the step size or decreasing the tolerance.");
3253                     appendLogText(message);
3254                     KSNotification::event(QLatin1String("FocusFailed"), message, KSNotification::Focus, KSNotification::Alert);
3255                     completeFocusProcedure(Ekos::FOCUS_ABORTED);
3256                 }
3257                 else if (noStarCount > 0)
3258                 {
3259                     QString message = i18n("Failed to detect focus star in frame. Capture and select a focus star.");
3260                     appendLogText(message);
3261                     KSNotification::event(QLatin1String("FocusFailed"), message, KSNotification::Focus, KSNotification::Alert);
3262                     completeFocusProcedure(Ekos::FOCUS_ABORTED);
3263                 }
3264                 else
3265                 {
3266                     completeFocusProcedure(Ekos::FOCUS_COMPLETE);
3267                 }
3268                 break;
3269             }
3270             else if (currentHFR < lastHFR)
3271             {
3272                 // Let's now limit the travel distance of the focuser
3273                 if (HFRInc >= 1 && m_LastFocusDirection == FOCUS_OUT && lastHFRPos < focusInLimit && fabs(currentHFR - lastHFR) > 0.1)
3274                 {
3275                     focusInLimit = lastHFRPos;
3276                     qCDebug(KSTARS_EKOS_FOCUS) << "New FocusInLimit " << focusInLimit;
3277                 }
3278                 else if (HFRInc >= 1 && m_LastFocusDirection == FOCUS_IN && lastHFRPos > focusOutLimit &&
3279                          fabs(currentHFR - lastHFR) > 0.1)
3280                 {
3281                     focusOutLimit = lastHFRPos;
3282                     qCDebug(KSTARS_EKOS_FOCUS) << "New FocusOutLimit " << focusOutLimit;
3283                 }
3284 
3285                 double factor = std::max(1.0, HFRDec / 2.0);
3286                 if (m_LastFocusDirection == FOCUS_IN)
3287                     targetPosition = currentPosition - (pulseDuration * factor);
3288                 else
3289                     targetPosition = currentPosition + (pulseDuration * factor);
3290 
3291                 qCDebug(KSTARS_EKOS_FOCUS) << "current Position" << currentPosition << " targetPosition " << targetPosition;
3292 
3293                 lastHFR = currentHFR;
3294 
3295                 // Let's keep track of the minimum HFR
3296                 if (lastHFR < minHFR)
3297                 {
3298                     minHFR    = lastHFR;
3299                     minHFRPos = currentPosition;
3300                     qCDebug(KSTARS_EKOS_FOCUS) << "new minHFR " << minHFR << " @ position " << minHFRPos;
3301                 }
3302 
3303                 lastHFRPos = currentPosition;
3304 
3305                 // HFR is decreasing, we are on the right direction
3306                 HFRDec++;
3307                 if (HFRInc > 0)
3308                 {
3309                     // Remove bad data point and mark fluctuation
3310                     if (plot_position.count() >= 2)
3311                     {
3312                         plot_position.remove(plot_position.count() - 2);
3313                         plot_value.remove(plot_value.count() - 2);
3314                     }
3315                     fluctuations++;
3316                 }
3317                 HFRInc = 0;
3318             }
3319             else
3320             {
3321                 // HFR increased, let's deal with it.
3322                 HFRInc++;
3323                 if (HFRDec > 0)
3324                     fluctuations++;
3325                 HFRDec = 0;
3326 
3327                 lastHFR      = currentHFR;
3328                 lastHFRPos   = currentPosition;
3329 
3330                 // Keep moving in same direction (even if bad) for one more iteration to gather data points.
3331                 if (HFRInc > 1)
3332                 {
3333                     // Looks like we're going away from optimal HFR
3334                     reverseDir   = true;
3335                     HFRInc       = 0;
3336 
3337                     qCDebug(KSTARS_EKOS_FOCUS) << "Focus is moving away from optimal HFR.";
3338 
3339                     // Let's set new limits
3340                     if (m_LastFocusDirection == FOCUS_IN)
3341                     {
3342                         focusInLimit = currentPosition;
3343                         qCDebug(KSTARS_EKOS_FOCUS) << "Setting focus IN limit to " << focusInLimit;
3344                     }
3345                     else
3346                     {
3347                         focusOutLimit = currentPosition;
3348                         qCDebug(KSTARS_EKOS_FOCUS) << "Setting focus OUT limit to " << focusOutLimit;
3349                     }
3350 
3351                     if (m_FocusAlgorithm == FOCUS_POLYNOMIAL && plot_position.count() > 4)
3352                     {
3353                         polynomialFit.reset(new PolynomialFit(2, 5, plot_position, plot_value));
3354                         double a = *std::min_element(plot_position.constBegin(), plot_position.constEnd());
3355                         double b = *std::max_element(plot_position.constBegin(), plot_position.constEnd());
3356                         double min_position = 0, min_hfr = 0;
3357                         isVShapeSolution = polynomialFit->findMinimum(minHFRPos, a, b, &min_position, &min_hfr);
3358                         qCDebug(KSTARS_EKOS_FOCUS) << "Found Minimum?" << (isVShapeSolution ? "Yes" : "No");
3359                         if (isVShapeSolution)
3360                         {
3361                             ignoreLimitedDelta = true;
3362                             qCDebug(KSTARS_EKOS_FOCUS) << "Minimum Solution:" << min_hfr << "@" << min_position;
3363                             targetPosition = round(min_position);
3364                             appendLogText(i18n("Found polynomial solution @ %1", QString::number(min_position, 'f', 0)));
3365 
3366                             emit drawPolynomial(polynomialFit.get(), isVShapeSolution, true);
3367                             emit minimumFound(min_position, min_hfr);
3368                         }
3369                         else
3370                         {
3371                             emit drawPolynomial(polynomialFit.get(), isVShapeSolution, false);
3372                         }
3373                     }
3374                 }
3375 
3376                 if (HFRInc == 1)
3377                 {
3378                     // Keep going at same stride even if we are going away from CFZ
3379                     // This is done to gather data points are the trough.
3380                     if (std::abs(lastDelta) > 0)
3381                         targetPosition = currentPosition + lastDelta;
3382                     else
3383                         targetPosition = currentPosition + pulseDuration;
3384                 }
3385                 else if (isVShapeSolution == false)
3386                 {
3387                     ignoreLimitedDelta = true;
3388                     // Let's get close to the minimum HFR position so far detected
3389                     if (m_LastFocusDirection == FOCUS_OUT)
3390                         targetPosition = minHFRPos - pulseDuration / 2;
3391                     else
3392                         targetPosition = minHFRPos + pulseDuration / 2;
3393                 }
3394 
3395                 qCDebug(KSTARS_EKOS_FOCUS) << "new targetPosition " << targetPosition;
3396             }
3397 
3398             // Limit target Pulse to algorithm limits
3399             if (focusInLimit != 0 && m_LastFocusDirection == FOCUS_IN && targetPosition < focusInLimit)
3400             {
3401                 targetPosition = focusInLimit;
3402                 qCDebug(KSTARS_EKOS_FOCUS) << "Limiting target pulse to focus in limit " << targetPosition;
3403             }
3404             else if (focusOutLimit != 0 && m_LastFocusDirection == FOCUS_OUT && targetPosition > focusOutLimit)
3405             {
3406                 targetPosition = focusOutLimit;
3407                 qCDebug(KSTARS_EKOS_FOCUS) << "Limiting target pulse to focus out limit " << targetPosition;
3408             }
3409 
3410             // Limit target pulse to focuser limits
3411             if (targetPosition < absMotionMin)
3412                 targetPosition = absMotionMin;
3413             else if (targetPosition > absMotionMax)
3414                 targetPosition = absMotionMax;
3415 
3416             // We cannot go any further because of the device limits, this is a failure
3417             if (targetPosition == currentPosition)
3418             {
3419                 // If case target position happens to be the minimal historical
3420                 // HFR position, accept this value instead of bailing out.
3421                 if (targetPosition == minHFRPos || isVShapeSolution)
3422                 {
3423                     appendLogText("Stopping at minimum recorded HFR position.");
3424                     completeFocusProcedure(Ekos::FOCUS_COMPLETE);
3425                 }
3426                 else
3427                 {
3428                     QString message = i18n("Focuser cannot move further, device limits reached. Autofocus aborted.");
3429                     appendLogText(message);
3430                     KSNotification::event(QLatin1String("FocusFailed"), message, KSNotification::Focus, KSNotification::Alert);
3431                     completeFocusProcedure(Ekos::FOCUS_ABORTED);
3432                 }
3433                 return;
3434             }
3435 
3436             // Too many fluctuatoins
3437             if (fluctuations >= MAXIMUM_FLUCTUATIONS)
3438             {
3439                 QString message = i18n("Unstable fluctuations. Try increasing initial step size or exposure time.");
3440                 appendLogText(message);
3441                 KSNotification::event(QLatin1String("FocusFailed"), message, KSNotification::Focus, KSNotification::Alert);
3442                 completeFocusProcedure(Ekos::FOCUS_ABORTED);
3443                 return;
3444             }
3445 
3446             // Ops, deadlock
3447             if (focusOutLimit && focusOutLimit == focusInLimit)
3448             {
3449                 QString message = i18n("Deadlock reached. Please try again with different settings.");
3450                 appendLogText(message);
3451                 KSNotification::event(QLatin1String("FocusFailed"), message, KSNotification::Focus, KSNotification::Alert);
3452                 completeFocusProcedure(Ekos::FOCUS_ABORTED);
3453                 return;
3454             }
3455 
3456             // Restrict the target position even more with the maximum travel option
3457             if (fabs(targetPosition - initialFocuserAbsPosition) > m_OpsFocusMechanics->focusMaxTravel->value())
3458             {
3459                 int minTravelLimit = qMax(0.0, initialFocuserAbsPosition - m_OpsFocusMechanics->focusMaxTravel->value());
3460                 int maxTravelLimit = qMin(absMotionMax, initialFocuserAbsPosition + m_OpsFocusMechanics->focusMaxTravel->value());
3461 
3462                 // In case we are asked to go below travel limit, but we are not there yet
3463                 // let us go there and see the result before aborting
3464                 if (fabs(currentPosition - minTravelLimit) > 10 && targetPosition < minTravelLimit)
3465                 {
3466                     targetPosition = minTravelLimit;
3467                 }
3468                 // Same for max travel
3469                 else if (fabs(currentPosition - maxTravelLimit) > 10 && targetPosition > maxTravelLimit)
3470                 {
3471                     targetPosition = maxTravelLimit;
3472                 }
3473                 else
3474                 {
3475                     qCDebug(KSTARS_EKOS_FOCUS) << "targetPosition (" << targetPosition << ") - initHFRAbsPos ("
3476                                                << initialFocuserAbsPosition << ") exceeds maxTravel distance of " << m_OpsFocusMechanics->focusMaxTravel->value();
3477 
3478                     QString message = i18n("Maximum travel limit reached. Autofocus aborted.");
3479                     appendLogText(message);
3480                     KSNotification::event(QLatin1String("FocusFailed"), message, KSNotification::Focus, KSNotification::Alert);
3481                     completeFocusProcedure(Ekos::FOCUS_ABORTED);
3482                     break;
3483                 }
3484             }
3485 
3486             // Get delta for next move
3487             lastDelta = (targetPosition - currentPosition);
3488 
3489             qCDebug(KSTARS_EKOS_FOCUS) << "delta (targetPosition - currentPosition) " << lastDelta;
3490 
3491             // Limit to Maximum permitted delta (Max Single Step Size)
3492             if (ignoreLimitedDelta == false)
3493             {
3494                 double limitedDelta = qMax(-1.0 * m_OpsFocusMechanics->focusMaxSingleStep->value(),
3495                                            qMin(1.0 * m_OpsFocusMechanics->focusMaxSingleStep->value(), lastDelta));
3496                 if (std::fabs(limitedDelta - lastDelta) > 0)
3497                 {
3498                     qCDebug(KSTARS_EKOS_FOCUS) << "Limited delta to maximum permitted single step " <<
3499                                                m_OpsFocusMechanics->focusMaxSingleStep->value();
3500                     lastDelta = limitedDelta;
3501                 }
3502             }
3503 
3504             m_LastFocusDirection = (lastDelta > 0) ? FOCUS_OUT : FOCUS_IN;
3505             if (!changeFocus(lastDelta, false))
3506                 completeFocusProcedure(Ekos::FOCUS_ABORTED);
3507 
3508             break;
3509     }
3510 }
3511 
3512 void Focus::addPlotPosition(int pos, double value, bool plot)
3513 {
3514     plot_position.append(pos);
3515     plot_value.append(value);
3516     if (plot)
3517         emit newHFRPlotPosition(static_cast<double>(pos), value, 1.0, false, pulseDuration);
3518 }
3519 
3520 // Synchronises the plot_position and plot_value vectors with the data used by linearFocuser
3521 // This keeps the 2 sets of data in sync for the Linear and Linear1Pass algorithms.
3522 // For Iterative and Polynomial these vectors are built during the focusing cycle so nothing to do here
3523 void Focus::updatePlotPosition()
3524 {
3525     if (m_FocusAlgorithm == FOCUS_LINEAR1PASS || m_FocusAlgorithm == FOCUS_LINEAR)
3526     {
3527         QVector<double> weights;
3528         QVector<int> positions;
3529         linearFocuser->getMeasurements(&positions, &plot_value, &weights);
3530         plot_position.clear();
3531         for (int i = 0; i < positions.count(); i++)
3532             plot_position.append(positions[i]);
3533         if (m_FocusAlgorithm == FOCUS_LINEAR1PASS)
3534         {
3535             // For L1P add in the solution datapoint. Linear already has this included.
3536             plot_position.append(linearFocuser->solution());
3537             plot_value.append(linearFocuser->solutionValue());
3538         }
3539     }
3540 }
3541 
3542 void Focus::autoFocusRel()
3543 {
3544     static int noStarCount = 0;
3545     static double minHFR   = 1e6;
3546     QString deltaTxt       = QString("%1").arg(fabs(currentHFR - minHFR) * 100.0, 0, 'g', 2);
3547     QString minHFRText     = QString("%1").arg(minHFR, 0, 'g', 3);
3548     QString HFRText        = QString("%1").arg(currentHFR, 0, 'g', 3);
3549 
3550     appendLogText(i18n("FITS received. HFR %1. Delta (%2%) Min HFR (%3)", HFRText, deltaTxt, minHFRText));
3551 
3552     if (pulseDuration <= MINIMUM_PULSE_TIMER)
3553     {
3554         appendLogText(i18n("Autofocus failed to reach proper focus. Try adjusting the tolerance value."));
3555         completeFocusProcedure(Ekos::FOCUS_ABORTED);
3556         return;
3557     }
3558 
3559     // No stars detected, try to capture again
3560     if (currentHFR == INVALID_STAR_MEASURE)
3561     {
3562         if (noStarCount < MAX_RECAPTURE_RETRIES)
3563         {
3564             noStarCount++;
3565             appendLogText(i18n("No stars detected, capturing again..."));
3566             capture();
3567             return;
3568         }
3569         else if (m_FocusAlgorithm == FOCUS_LINEAR || m_FocusAlgorithm == FOCUS_LINEAR1PASS)
3570         {
3571             appendLogText(i18n("Failed to detect any stars at position %1. Continuing...", currentPosition));
3572             noStarCount = 0;
3573         }
3574         else if(!m_OpsFocusProcess->focusDonut->isChecked())
3575         {
3576             // Carry on for donut detection
3577             appendLogText(i18n("Failed to detect any stars. Reset frame and try again."));
3578             completeFocusProcedure(Ekos::FOCUS_ABORTED);
3579             return;
3580         }
3581     }
3582     else
3583         noStarCount = 0;
3584 
3585     switch (m_LastFocusDirection)
3586     {
3587         case FOCUS_NONE:
3588             lastHFR = currentHFR;
3589             minHFR  = 1e6;
3590             m_LastFocusDirection = FOCUS_IN;
3591             changeFocus(-pulseDuration, false);
3592             break;
3593 
3594         case FOCUS_IN:
3595         case FOCUS_OUT:
3596             if (fabs(currentHFR - minHFR) < (m_OpsFocusProcess->focusTolerance->value() / 100.0) && HFRInc == 0)
3597             {
3598                 completeFocusProcedure(Ekos::FOCUS_COMPLETE);
3599             }
3600             else if (currentHFR < lastHFR)
3601             {
3602                 if (currentHFR < minHFR)
3603                     minHFR = currentHFR;
3604 
3605                 lastHFR = currentHFR;
3606                 changeFocus(m_LastFocusDirection == FOCUS_IN ? -pulseDuration : pulseDuration, false);
3607                 HFRInc = 0;
3608             }
3609             else
3610             {
3611                 //HFRInc++;
3612 
3613                 lastHFR = currentHFR;
3614 
3615                 HFRInc = 0;
3616 
3617                 pulseDuration *= 0.75;
3618 
3619                 if (!changeFocus(m_LastFocusDirection == FOCUS_IN ? pulseDuration : -pulseDuration, false))
3620                     completeFocusProcedure(Ekos::FOCUS_ABORTED);
3621 
3622                 // HFR getting worse so reverse direction
3623                 m_LastFocusDirection = (m_LastFocusDirection == FOCUS_IN) ? FOCUS_OUT : FOCUS_IN;
3624             }
3625             break;
3626     }
3627 }
3628 
3629 void Focus::autoFocusProcessPositionChange(IPState state)
3630 {
3631     if (state == IPS_OK)
3632     {
3633         // Normally, if we are auto-focusing, after we move the focuser we capture an image.
3634         // However, the Linear algorithm, at the start of its passes, requires two
3635         // consecutive focuser moves--the first out further than we want, and a second
3636         // move back in, so that we eliminate backlash and are always moving in before a capture.
3637         if (focuserAdditionalMovement > 0)
3638         {
3639             int temp = focuserAdditionalMovement;
3640             focuserAdditionalMovement = 0;
3641             qCDebug(KSTARS_EKOS_FOCUS) << QString("Undoing overscan extension. Moving back in by %1").arg(temp);
3642 
3643             if (!changeFocus(-temp, focuserAdditionalMovementUpdateDir))
3644             {
3645                 appendLogText(i18n("Focuser error, check INDI panel."));
3646                 completeFocusProcedure(Ekos::FOCUS_ABORTED);
3647             }
3648         }
3649         else if (inAutoFocus)
3650         {
3651             qCDebug(KSTARS_EKOS_FOCUS) << QString("Focus position reached at %1, starting capture in %2 seconds.").arg(
3652                                            currentPosition).arg(m_OpsFocusMechanics->focusSettleTime->value());
3653             // Adjust exposure if Donut Buster activated
3654             if (m_OpsFocusProcess->focusDonut->isChecked())
3655                 donutTimeDilation();
3656             capture(m_OpsFocusMechanics->focusSettleTime->value());
3657         }
3658     }
3659     else if (state == IPS_ALERT)
3660     {
3661         appendLogText(i18n("Focuser error, check INDI panel."));
3662         completeFocusProcedure(Ekos::FOCUS_ABORTED);
3663     }
3664     else
3665         qCDebug(KSTARS_EKOS_FOCUS) <<
3666                                    QString("autoFocusProcessPositionChange called with state %1 (%2), focuserAdditionalMovement=%3, inAutoFocus=%4, captureInProgress=%5, currentPosition=%6")
3667                                    .arg(state).arg(pstateStr(state)).arg(focuserAdditionalMovement).arg(inAutoFocus).arg(captureInProgress)
3668                                    .arg(currentPosition);
3669 }
3670 
3671 // This routine adjusts capture exposure during Autofocus depending on donut parameter settings
3672 void Focus::donutTimeDilation()
3673 {
3674     if (m_OpsFocusProcess->focusTimeDilation->value() == 1.0)
3675         return;
3676 
3677     // Get the max distance from focus to outer points
3678     const double centre = (m_OpsFocusMechanics->focusNumSteps->value() + 1.0) / 2.0;
3679     // Get the current step - treat the final
3680     const int currentStep = linearFocuser->currentStep() + 1;
3681     double distance;
3682     if (currentStep <= m_OpsFocusMechanics->focusNumSteps->value())
3683         distance = std::abs(centre - currentStep);
3684     else
3685         // Last step is always back to focus
3686         distance = 0.0;
3687     double multiplier = distance / (centre - 1.0) * m_OpsFocusProcess->focusTimeDilation->value();
3688     multiplier = std::max(multiplier, 1.0);
3689     const double exposure = multiplier * m_donutOrigExposure;
3690     focusExposure->setValue(exposure);
3691     qCDebug(KSTARS_EKOS_FOCUS) << "Donut time dilation for point " << currentStep << " from " << m_donutOrigExposure << " to "
3692                                << exposure;
3693 
3694 }
3695 
3696 void Focus::updateProperty(INDI::Property prop)
3697 {
3698     if (m_Focuser == nullptr || prop.getType() != INDI_NUMBER || prop.getDeviceName() != m_Focuser->getDeviceName())
3699         return;
3700 
3701     auto nvp = prop.getNumber();
3702 
3703     // Only process focus properties
3704     if (QString(nvp->getName()).contains("focus", Qt::CaseInsensitive) == false)
3705         return;
3706 
3707     if (nvp->isNameMatch("FOCUS_BACKLASH_STEPS"))
3708     {
3709         m_OpsFocusMechanics->focusBacklash->setValue(nvp->np[0].value);
3710         return;
3711     }
3712 
3713     if (nvp->isNameMatch("ABS_FOCUS_POSITION"))
3714     {
3715         if (m_DebugFocuser)
3716         {
3717             // Simulate focuser comms issues every 5 moves
3718             if (m_DebugFocuserCounter++ >= 10 && m_DebugFocuserCounter <= 14)
3719             {
3720                 if (m_DebugFocuserCounter == 14)
3721                     m_DebugFocuserCounter = 0;
3722                 appendLogText(i18n("Simulate focuser comms failure..."));
3723                 return;
3724             }
3725         }
3726 
3727         m_FocusMotionTimer.stop();
3728         INumber *pos = IUFindNumber(nvp, "FOCUS_ABSOLUTE_POSITION");
3729 
3730         // FIXME: We should check state validity, but some focusers do not care - make ignore an option!
3731         if (pos)
3732         {
3733             int newPosition = static_cast<int>(pos->value);
3734 
3735             // Some absolute focuser constantly report the position without a state change.
3736             // Therefore we ignore it if both value and state are the same as last time.
3737             // HACK: This would shortcut the autofocus procedure reset, see completeFocusProcedure for the small hack
3738             if (currentPosition == newPosition && currentPositionState == nvp->s)
3739             {
3740                 qCDebug(KSTARS_EKOS_FOCUS) << "Focuser position " << currentPosition << " and state:"
3741                                            << pstateStr(currentPositionState) << " unchanged";
3742                 return;
3743             }
3744 
3745             currentPositionState = nvp->s;
3746 
3747             if (currentPosition != newPosition)
3748             {
3749                 currentPosition = newPosition;
3750                 qCDebug(KSTARS_EKOS_FOCUS) << "Abs Focuser position changed to " << currentPosition << "State:" << pstateStr(
3751                                                currentPositionState);
3752                 absTicksLabel->setText(QString::number(currentPosition));
3753                 emit absolutePositionChanged(currentPosition);
3754             }
3755         }
3756 
3757         if (nvp->s != IPS_OK)
3758         {
3759             if (inAutoFocus || inAdjustFocus || adaptFocus->inAdaptiveFocus())
3760             {
3761                 // We had something back from the focuser but we're not done yet, so
3762                 // restart motion timer in case focuser gets stuck.
3763                 qCDebug(KSTARS_EKOS_FOCUS) << "Restarting focus motion timer, state " << pstateStr(nvp->s);
3764                 m_FocusMotionTimer.start();
3765             }
3766         }
3767         else
3768         {
3769             // Systematically reset UI when focuser finishes moving
3770             if (focuserAdditionalMovement == 0)
3771                 resetButtons();
3772 
3773             if (inAdjustFocus)
3774             {
3775                 if (focuserAdditionalMovement == 0)
3776                 {
3777                     inAdjustFocus = false;
3778                     emit focusPositionAdjusted();
3779                     return;
3780                 }
3781             }
3782 
3783             if (adaptFocus->inAdaptiveFocus())
3784             {
3785                 if (focuserAdditionalMovement == 0)
3786                 {
3787                     adaptFocus->adaptiveFocusAdmin(currentPosition, true, true);
3788                     return;
3789                 }
3790             }
3791 
3792             if (m_RestartState == RESTART_NOW && status() != Ekos::FOCUS_ABORTED)
3793             {
3794                 if (focuserAdditionalMovement == 0)
3795                 {
3796                     m_RestartState = RESTART_NONE;
3797                     inAutoFocus = inAdjustFocus = false;
3798                     adaptFocus->setInAdaptiveFocus(false);
3799                     appendLogText(i18n("Restarting autofocus process..."));
3800                     start();
3801                 }
3802             }
3803             else if (m_RestartState == RESTART_ABORT)
3804             {
3805                 // We are trying to abort an autofocus run
3806                 // This event means that the focuser has been reset and arrived at its starting point
3807                 // so we can finish processing the abort. Set inAutoFocus to avoid repeating
3808                 // processing already done in completeFocusProcedure
3809                 completeFocusProcedure(Ekos::FOCUS_ABORTED);
3810                 m_RestartState = RESTART_NONE;
3811                 inAutoFocus = inAdjustFocus = false;
3812                 adaptFocus->setInAdaptiveFocus(false);
3813             }
3814         }
3815 
3816         if (canAbsMove)
3817             autoFocusProcessPositionChange(nvp->s);
3818         else if (nvp->s == IPS_ALERT)
3819             appendLogText(i18n("Focuser error, check INDI panel."));
3820         return;
3821     }
3822 
3823     if (canAbsMove)
3824         return;
3825 
3826     if (nvp->isNameMatch("manualfocusdrive"))
3827     {
3828         if (m_DebugFocuser)
3829         {
3830             // Simulate focuser comms issues every 5 moves
3831             if (m_DebugFocuserCounter++ >= 10 && m_DebugFocuserCounter <= 14)
3832             {
3833                 if (m_DebugFocuserCounter == 14)
3834                     m_DebugFocuserCounter = 0;
3835                 appendLogText(i18n("Simulate focuser comms failure..."));
3836                 return;
3837             }
3838         }
3839 
3840         m_FocusMotionTimer.stop();
3841 
3842         INumber *pos = IUFindNumber(nvp, "manualfocusdrive");
3843         if (pos && nvp->s == IPS_OK)
3844         {
3845             currentPosition += pos->value;
3846             absTicksLabel->setText(QString::number(static_cast<int>(currentPosition)));
3847             emit absolutePositionChanged(currentPosition);
3848         }
3849 
3850         if (inAdjustFocus && nvp->s == IPS_OK)
3851         {
3852             if (focuserAdditionalMovement == 0)
3853             {
3854                 inAdjustFocus = false;
3855                 emit focusPositionAdjusted();
3856                 return;
3857             }
3858         }
3859 
3860         if (adaptFocus->inAdaptiveFocus() && nvp->s == IPS_OK)
3861         {
3862             if (focuserAdditionalMovement == 0)
3863             {
3864                 adaptFocus->adaptiveFocusAdmin(currentPosition, true, true);
3865                 return;
3866             }
3867         }
3868 
3869         // restart if focus movement has finished
3870         if (m_RestartState == RESTART_NOW && nvp->s == IPS_OK && status() != Ekos::FOCUS_ABORTED)
3871         {
3872             if (focuserAdditionalMovement == 0)
3873             {
3874                 m_RestartState = RESTART_NONE;
3875                 inAutoFocus = inAdjustFocus = false;
3876                 adaptFocus->setInAdaptiveFocus(false);
3877                 appendLogText(i18n("Restarting autofocus process..."));
3878                 start();
3879             }
3880         }
3881         else if (m_RestartState == RESTART_ABORT && nvp->s == IPS_OK)
3882         {
3883             // Abort the autofocus run now the focuser has finished moving to its start position
3884             completeFocusProcedure(Ekos::FOCUS_ABORTED);
3885             m_RestartState = RESTART_NONE;
3886             inAutoFocus = inAdjustFocus = false;
3887             adaptFocus->setInAdaptiveFocus(false);
3888         }
3889 
3890         if (canRelMove)
3891             autoFocusProcessPositionChange(nvp->s);
3892         else if (nvp->s == IPS_ALERT)
3893             appendLogText(i18n("Focuser error, check INDI panel."));
3894 
3895         return;
3896     }
3897 
3898     if (nvp->isNameMatch("REL_FOCUS_POSITION"))
3899     {
3900         m_FocusMotionTimer.stop();
3901 
3902         INumber *pos = IUFindNumber(nvp, "FOCUS_RELATIVE_POSITION");
3903         if (pos && nvp->s == IPS_OK)
3904         {
3905             currentPosition += pos->value * (m_LastFocusDirection == FOCUS_IN ? -1 : 1);
3906             qCDebug(KSTARS_EKOS_FOCUS)
3907                     << QString("Rel Focuser position moved %1 by %2 to %3")
3908                     .arg((m_LastFocusDirection == FOCUS_IN) ? "in" : "out").arg(pos->value).arg(currentPosition);
3909             absTicksLabel->setText(QString::number(static_cast<int>(currentPosition)));
3910             emit absolutePositionChanged(currentPosition);
3911         }
3912 
3913         if (inAdjustFocus && nvp->s == IPS_OK)
3914         {
3915             if (focuserAdditionalMovement == 0)
3916             {
3917                 inAdjustFocus = false;
3918                 emit focusPositionAdjusted();
3919                 return;
3920             }
3921         }
3922 
3923         if (adaptFocus->inAdaptiveFocus() && nvp->s == IPS_OK)
3924         {
3925             if (focuserAdditionalMovement == 0)
3926             {
3927                 adaptFocus->adaptiveFocusAdmin(currentPosition, true, true);
3928                 return;
3929             }
3930         }
3931 
3932         // restart if focus movement has finished
3933         if (m_RestartState == RESTART_NOW && nvp->s == IPS_OK && status() != Ekos::FOCUS_ABORTED)
3934         {
3935             if (focuserAdditionalMovement == 0)
3936             {
3937                 m_RestartState = RESTART_NONE;
3938                 inAutoFocus = inAdjustFocus = false;
3939                 adaptFocus->setInAdaptiveFocus(false);
3940                 appendLogText(i18n("Restarting autofocus process..."));
3941                 start();
3942             }
3943         }
3944         else if (m_RestartState == RESTART_ABORT && nvp->s == IPS_OK)
3945         {
3946             // Abort the autofocus run now the focuser has finished moving to its start position
3947             completeFocusProcedure(Ekos::FOCUS_ABORTED);
3948             m_RestartState = RESTART_NONE;
3949             inAutoFocus = inAdjustFocus = false;
3950             adaptFocus->setInAdaptiveFocus(false);
3951         }
3952 
3953         if (canRelMove)
3954             autoFocusProcessPositionChange(nvp->s);
3955         else if (nvp->s == IPS_ALERT)
3956             appendLogText(i18n("Focuser error, check INDI panel."));
3957 
3958         return;
3959     }
3960 
3961     if (canRelMove)
3962         return;
3963 
3964     if (nvp->isNameMatch("FOCUS_TIMER"))
3965     {
3966         m_FocusMotionTimer.stop();
3967         // restart if focus movement has finished
3968         if (m_RestartState == RESTART_NOW && nvp->s == IPS_OK && status() != Ekos::FOCUS_ABORTED)
3969         {
3970             if (focuserAdditionalMovement == 0)
3971             {
3972                 m_RestartState = RESTART_NONE;
3973                 inAutoFocus = inAdjustFocus = false;
3974                 adaptFocus->setInAdaptiveFocus(false);
3975                 appendLogText(i18n("Restarting autofocus process..."));
3976                 start();
3977             }
3978         }
3979         else if (m_RestartState == RESTART_ABORT && nvp->s == IPS_OK)
3980         {
3981             // Abort the autofocus run now the focuser has finished moving to its start position
3982             completeFocusProcedure(Ekos::FOCUS_ABORTED);
3983             m_RestartState = RESTART_NONE;
3984             inAutoFocus = inAdjustFocus = false;
3985             adaptFocus->setInAdaptiveFocus(false);
3986         }
3987 
3988         if (canAbsMove == false && canRelMove == false)
3989         {
3990             // Used by the linear focus algorithm. Ignored if that's not in use for the timer-focuser.
3991             INumber *pos = IUFindNumber(nvp, "FOCUS_TIMER_VALUE");
3992             if (pos)
3993             {
3994                 currentPosition += pos->value * (m_LastFocusDirection == FOCUS_IN ? -1 : 1);
3995                 qCDebug(KSTARS_EKOS_FOCUS)
3996                         << QString("Timer Focuser position moved %1 by %2 to %3")
3997                         .arg((m_LastFocusDirection == FOCUS_IN) ? "in" : "out").arg(pos->value).arg(currentPosition);
3998             }
3999 
4000             if (inAdjustFocus && nvp->s == IPS_OK)
4001             {
4002                 if (focuserAdditionalMovement == 0)
4003                 {
4004                     inAdjustFocus = false;
4005                     emit focusPositionAdjusted();
4006                     return;
4007                 }
4008             }
4009 
4010             if (adaptFocus->inAdaptiveFocus() && nvp->s == IPS_OK)
4011             {
4012                 if (focuserAdditionalMovement == 0)
4013                 {
4014                     adaptFocus->adaptiveFocusAdmin(true, true, true);
4015                     return;
4016                 }
4017             }
4018 
4019             autoFocusProcessPositionChange(nvp->s);
4020         }
4021         else if (nvp->s == IPS_ALERT)
4022             appendLogText(i18n("Focuser error, check INDI panel."));
4023 
4024         return;
4025     }
4026 }
4027 
4028 void Focus::appendLogText(const QString &text)
4029 {
4030     m_LogText.insert(0, i18nc("log entry; %1 is the date, %2 is the text", "%1 %2",
4031                               KStarsData::Instance()->lt().toString("yyyy-MM-ddThh:mm:ss"), text));
4032 
4033     qCInfo(KSTARS_EKOS_FOCUS) << text;
4034 
4035     emit newLog(text);
4036 }
4037 
4038 void Focus::clearLog()
4039 {
4040     m_LogText.clear();
4041     emit newLog(QString());
4042 }
4043 
4044 void Focus::appendFocusLogText(const QString &lines)
4045 {
4046     if (Options::focusLogging())
4047     {
4048 
4049         if (!m_FocusLogFile.exists())
4050         {
4051             // Create focus-specific log file and write the header record
4052             QDir dir(KSPaths::writableLocation(QStandardPaths::AppLocalDataLocation));
4053             dir.mkpath("focuslogs");
4054             m_FocusLogEnabled = m_FocusLogFile.open(QIODevice::WriteOnly | QIODevice::Text);
4055             if (m_FocusLogEnabled)
4056             {
4057                 QTextStream header(&m_FocusLogFile);
4058                 header << "date, time, position, temperature, filter, HFR, altitude\n";
4059                 header.flush();
4060             }
4061             else
4062                 qCWarning(KSTARS_EKOS_FOCUS) << "Failed to open focus log file: " << m_FocusLogFileName;
4063         }
4064 
4065         if (m_FocusLogEnabled)
4066         {
4067             QTextStream out(&m_FocusLogFile);
4068             out << QDateTime::currentDateTime().toString("yyyy-MM-dd, hh:mm:ss, ") << lines;
4069             out.flush();
4070         }
4071     }
4072 }
4073 
4074 void Focus::startFraming()
4075 {
4076     if (m_Camera == nullptr)
4077     {
4078         appendLogText(i18n("No CCD connected."));
4079         return;
4080     }
4081 
4082     waitStarSelectTimer.stop();
4083 
4084     inFocusLoop = true;
4085     starMeasureFrames.clear();
4086 
4087     clearDataPoints();
4088 
4089     //emit statusUpdated(true);
4090     setState(Ekos::FOCUS_FRAMING);
4091 
4092     resetButtons();
4093 
4094     appendLogText(i18n("Starting continuous exposure..."));
4095 
4096     capture();
4097 }
4098 
4099 void Focus::resetButtons()
4100 {
4101     if (inFocusLoop)
4102     {
4103         startFocusB->setEnabled(false);
4104         startAbInsB->setEnabled(false);
4105         startLoopB->setEnabled(false);
4106         focusOutB->setEnabled(true);
4107         focusInB->setEnabled(true);
4108         startGotoB->setEnabled(canAbsMove);
4109         stopFocusB->setEnabled(true);
4110         captureB->setEnabled(false);
4111         opticalTrainCombo->setEnabled(false);
4112         trainB->setEnabled(false);
4113         return;
4114     }
4115 
4116     if (inAutoFocus)
4117     {
4118         // During an Autofocus run we need to disable input widgets to stop the user changing them
4119         // We need to disable the widgets currently enabled and save a QVector of these to be
4120         // reinstated once the AF run completes.
4121         // Certain widgets (highlighted below) have the isEnabled() property used in the code to
4122         // determine functionality. So the isEnabled state for these is saved off before the
4123         // interface is disabled and these saved states used to control the code path and preserve
4124         // functionality.
4125         // Since this function can be called several times only load up widgets once
4126         if (disabledWidgets.empty())
4127         {
4128             AFDisable(trainLabel, false);
4129             AFDisable(opticalTrainCombo, false);
4130             AFDisable(trainB, false);
4131             AFDisable(focuserGroup, true);
4132             AFDisable(clearDataB, false);
4133 
4134             // In the ccdGroup save the enabled state of Gain and ISO
4135             m_FocusGainAFEnabled = focusGain->isEnabled();
4136             m_FocusISOAFEnabled = focusISO->isEnabled();
4137             AFDisable(ccdGroup, false);
4138 
4139             AFDisable(toolsGroup, false);
4140 
4141             // Save the enabled state of SubFrame
4142             m_FocusSubFrameAFEnabled = m_OpsFocusSettings->focusSubFrame->isEnabled();
4143 
4144             // Disable parameters and tools to prevent changes while Autofocus is running
4145             AFDisable(m_OpsFocusSettings, false);
4146             AFDisable(m_OpsFocusProcess, false);
4147             AFDisable(m_OpsFocusMechanics, false);
4148             AFDisable(m_AdvisorDialog, false);
4149             AFDisable(m_CFZDialog, false);
4150 
4151             // Enable the "stop" button so the user can abort an AF run
4152             stopFocusB->setEnabled(true);
4153         }
4154         return;
4155     }
4156 
4157     // Restore widgets that were disabled when Autofocus started
4158     for(int i = 0 ; i < disabledWidgets.size() ; i++)
4159         disabledWidgets[i]->setEnabled(true);
4160     disabledWidgets.clear();
4161 
4162     auto enableCaptureButtons = (captureInProgress == false && hfrInProgress == false);
4163 
4164     captureB->setEnabled(enableCaptureButtons);
4165     resetFrameB->setEnabled(enableCaptureButtons);
4166     startLoopB->setEnabled(enableCaptureButtons);
4167     m_OpsFocusSettings->focusAutoStarEnabled->setEnabled(enableCaptureButtons
4168             && m_OpsFocusSettings->focusUseFullField->isChecked() == false);
4169 
4170     if (m_Focuser && m_Focuser->isConnected())
4171     {
4172         focusOutB->setEnabled(true);
4173         focusInB->setEnabled(true);
4174 
4175         startFocusB->setEnabled(m_FocusType == FOCUS_AUTO);
4176         startAbInsB->setEnabled(canAbInsStart());
4177         stopFocusB->setEnabled(!enableCaptureButtons);
4178         startGotoB->setEnabled(canAbsMove);
4179         stopGotoB->setEnabled(true);
4180     }
4181     else
4182     {
4183         focusOutB->setEnabled(false);
4184         focusInB->setEnabled(false);
4185 
4186         startFocusB->setEnabled(false);
4187         startAbInsB->setEnabled(false);
4188         stopFocusB->setEnabled(false);
4189         startGotoB->setEnabled(false);
4190         stopGotoB->setEnabled(false);
4191     }
4192 }
4193 
4194 // Return whether the Aberration Inspector Start button should be enabled. The pre-requisties are:
4195 // - Absolute position focuser
4196 // - Mosaic Mask is on
4197 // - Algorithm is LINEAR 1 PASS
4198 bool Focus::canAbInsStart()
4199 {
4200     return canAbsMove && m_FocusAlgorithm == FOCUS_LINEAR1PASS && m_currentImageMask == FOCUS_MASK_MOSAIC;
4201 }
4202 
4203 // Disable input widgets during an Autofocus run. Keep a record so after the AF run, widgets can be re-enabled
4204 void Focus::AFDisable(QWidget * widget, const bool children)
4205 {
4206     if (children)
4207     {
4208         // The parent widget has been passed in so disable any enabled child widgets
4209         for(auto *wid : widget->findChildren<QWidget *>())
4210         {
4211             if (wid->isEnabled())
4212             {
4213                 wid->setEnabled(false);
4214                 disabledWidgets.push_back(wid);
4215             }
4216         }
4217 
4218     }
4219     else if (widget->isEnabled())
4220     {
4221         // Base level widget or group of widgets, so just disable the passed what was passed in
4222         widget->setEnabled(false);
4223         disabledWidgets.push_back(widget);
4224     }
4225 }
4226 
4227 bool Focus::isFocusGainEnabled()
4228 {
4229     return (inAutoFocus) ? m_FocusGainAFEnabled : focusGain->isEnabled();
4230 }
4231 
4232 bool Focus::isFocusISOEnabled()
4233 {
4234     return (inAutoFocus) ?  m_FocusISOAFEnabled : focusISO->isEnabled();
4235 }
4236 
4237 bool Focus::isFocusSubFrameEnabled()
4238 {
4239     return (inAutoFocus) ? m_FocusSubFrameAFEnabled : m_OpsFocusSettings->focusSubFrame->isEnabled();
4240 }
4241 
4242 void Focus::updateBoxSize(int value)
4243 {
4244     if (m_Camera == nullptr)
4245         return;
4246 
4247     ISD::CameraChip *targetChip = m_Camera->getChip(ISD::CameraChip::PRIMARY_CCD);
4248 
4249     if (targetChip == nullptr)
4250         return;
4251 
4252     int subBinX, subBinY;
4253     targetChip->getBinning(&subBinX, &subBinY);
4254 
4255     QRect trackBox = m_FocusView->getTrackingBox();
4256     QPoint center(trackBox.x() + (trackBox.width() / 2), trackBox.y() + (trackBox.height() / 2));
4257 
4258     trackBox =
4259         QRect(center.x() - value / (2 * subBinX), center.y() - value / (2 * subBinY), value / subBinX, value / subBinY);
4260 
4261     m_FocusView->setTrackingBox(trackBox);
4262 }
4263 
4264 void Focus::selectFocusStarFraction(double x, double y)
4265 {
4266     if (m_ImageData.isNull())
4267         return;
4268 
4269     focusStarSelected(x * m_ImageData->width(), y * m_ImageData->height());
4270     // Focus view timer takes 50ms second to update, so let's emit afterwards.
4271     QTimer::singleShot(250, this, [this]()
4272     {
4273         emit newImage(m_FocusView);
4274     });
4275 }
4276 
4277 void Focus::focusStarSelected(int x, int y)
4278 {
4279     if (state() == Ekos::FOCUS_PROGRESS)
4280         return;
4281 
4282     if (subFramed == false)
4283     {
4284         rememberStarCenter.setX(x);
4285         rememberStarCenter.setY(y);
4286     }
4287 
4288     ISD::CameraChip *targetChip = m_Camera->getChip(ISD::CameraChip::PRIMARY_CCD);
4289 
4290     int subBinX, subBinY;
4291     targetChip->getBinning(&subBinX, &subBinY);
4292 
4293     // If binning was changed outside of the focus module, recapture
4294     if (subBinX != (focusBinning->currentIndex() + 1))
4295     {
4296         capture();
4297         return;
4298     }
4299 
4300     int offset = (static_cast<double>(m_OpsFocusSettings->focusBoxSize->value()) / subBinX) * 1.5;
4301 
4302     QRect starRect;
4303 
4304     bool squareMovedOutside = false;
4305 
4306     if (subFramed == false && m_OpsFocusSettings->focusSubFrame->isChecked() && targetChip->canSubframe())
4307     {
4308         int minX, maxX, minY, maxY, minW, maxW, minH, maxH; //, fx,fy,fw,fh;
4309 
4310         targetChip->getFrameMinMax(&minX, &maxX, &minY, &maxY, &minW, &maxW, &minH, &maxH);
4311         //targetChip->getFrame(&fx, &fy, &fw, &fy);
4312 
4313         x     = (x - offset) * subBinX;
4314         y     = (y - offset) * subBinY;
4315         int w = offset * 2 * subBinX;
4316         int h = offset * 2 * subBinY;
4317 
4318         if (x < minX)
4319             x = minX;
4320         if (y < minY)
4321             y = minY;
4322         if ((x + w) > maxW)
4323             w = maxW - x;
4324         if ((y + h) > maxH)
4325             h = maxH - y;
4326 
4327         //fx += x;
4328         //fy += y;
4329         //fw = w;
4330         //fh = h;
4331 
4332         //targetChip->setFocusFrame(fx, fy, fw, fh);
4333         //frameModified=true;
4334 
4335         QVariantMap settings = frameSettings[targetChip];
4336         settings["x"]        = x;
4337         settings["y"]        = y;
4338         settings["w"]        = w;
4339         settings["h"]        = h;
4340         settings["binx"]     = subBinX;
4341         settings["biny"]     = subBinY;
4342 
4343         frameSettings[targetChip] = settings;
4344 
4345         subFramed = true;
4346 
4347         qCDebug(KSTARS_EKOS_FOCUS) << "Frame is subframed. X:" << x << "Y:" << y << "W:" << w << "H:" << h << "binX:" << subBinX <<
4348                                    "binY:" << subBinY;
4349 
4350         m_FocusView->setFirstLoad(true);
4351 
4352         capture();
4353 
4354         //starRect = QRect((w-focusBoxSize->value())/(subBinX*2), (h-focusBoxSize->value())/(subBinY*2), focusBoxSize->value()/subBinX, focusBoxSize->value()/subBinY);
4355         starCenter.setX(w / (2 * subBinX));
4356         starCenter.setY(h / (2 * subBinY));
4357     }
4358     else
4359     {
4360         //starRect = QRect(x-focusBoxSize->value()/(subBinX*2), y-focusBoxSize->value()/(subBinY*2), focusBoxSize->value()/subBinX, focusBoxSize->value()/subBinY);
4361         double dist = sqrt((starCenter.x() - x) * (starCenter.x() - x) + (starCenter.y() - y) * (starCenter.y() - y));
4362 
4363         squareMovedOutside = (dist > (static_cast<double>(m_OpsFocusSettings->focusBoxSize->value()) / subBinX));
4364         starCenter.setX(x);
4365         starCenter.setY(y);
4366         //starRect = QRect( starCenter.x()-focusBoxSize->value()/(2*subBinX), starCenter.y()-focusBoxSize->value()/(2*subBinY), focusBoxSize->value()/subBinX, focusBoxSize->value()/subBinY);
4367         starRect = QRect(starCenter.x() - m_OpsFocusSettings->focusBoxSize->value() / (2 * subBinX),
4368                          starCenter.y() - m_OpsFocusSettings->focusBoxSize->value() / (2 * subBinY),
4369                          m_OpsFocusSettings->focusBoxSize->value() / subBinX,
4370                          m_OpsFocusSettings->focusBoxSize->value() / subBinY);
4371         m_FocusView->setTrackingBox(starRect);
4372     }
4373 
4374     starsHFR.clear();
4375 
4376     starCenter.setZ(subBinX);
4377 
4378     if (squareMovedOutside && inAutoFocus == false && m_OpsFocusSettings->focusAutoStarEnabled->isChecked())
4379     {
4380         m_OpsFocusSettings->focusAutoStarEnabled->blockSignals(true);
4381         m_OpsFocusSettings->focusAutoStarEnabled->setChecked(false);
4382         m_OpsFocusSettings->focusAutoStarEnabled->blockSignals(false);
4383         appendLogText(i18n("Disabling Auto Star Selection as star selection box was moved manually."));
4384         starSelected = false;
4385     }
4386     else if (starSelected == false)
4387     {
4388         appendLogText(i18n("Focus star is selected."));
4389         starSelected = true;
4390         capture();
4391     }
4392 
4393     waitStarSelectTimer.stop();
4394     FocusState nextState = inAutoFocus ? FOCUS_PROGRESS : FOCUS_IDLE;
4395     if (nextState != state())
4396     {
4397         setState(nextState);
4398     }
4399 }
4400 
4401 void Focus::checkFocus(double requiredHFR)
4402 {
4403     if (inAutoFocus || inFocusLoop || inAdjustFocus || adaptFocus->inAdaptiveFocus() || inBuildOffsets)
4404     {
4405         qCDebug(KSTARS_EKOS_FOCUS) << "Check Focus rejected, focus procedure is already running.";
4406     }
4407     else
4408     {
4409         qCDebug(KSTARS_EKOS_FOCUS) << "Check Focus requested with minimum required HFR" << requiredHFR;
4410         minimumRequiredHFR = requiredHFR;
4411 
4412         appendLogText("Capturing to check HFR...");
4413         capture();
4414     }
4415 }
4416 
4417 // Start an AF run. This is called from Build Offsets but could be extended in the future
4418 void Focus::runAutoFocus(bool buildOffsets)
4419 {
4420     if (inAutoFocus || inFocusLoop || inAdjustFocus || adaptFocus->inAdaptiveFocus() || inBuildOffsets)
4421         qCDebug(KSTARS_EKOS_FOCUS) << "runAutoFocus rejected, focus procedure is already running.";
4422     else
4423     {
4424         // Set the inBuildOffsets flag and start the AF run
4425         inBuildOffsets = buildOffsets;
4426         start();
4427     }
4428 }
4429 
4430 void Focus::toggleSubframe(bool enable)
4431 {
4432     if (enable == false)
4433         resetFrame();
4434 
4435     starSelected = false;
4436     starCenter   = QVector3D();
4437 
4438     if (enable)
4439     {
4440         // sub frame focusing
4441         m_OpsFocusSettings->focusAutoStarEnabled->setEnabled(true);
4442         // disable focus image mask
4443         m_OpsFocusSettings->focusNoMaskRB->setChecked(true);
4444     }
4445     else
4446     {
4447         // full frame focusing
4448         m_OpsFocusSettings->focusAutoStarEnabled->setChecked(false);
4449         m_OpsFocusSettings->focusAutoStarEnabled->setEnabled(false);
4450     }
4451     // update image mask controls
4452     selectImageMask();
4453     // enable focus mask selection if full field is selected
4454     m_OpsFocusSettings->focusRingMaskRB->setEnabled(!enable);
4455     m_OpsFocusSettings->focusMosaicMaskRB->setEnabled(!enable);
4456 
4457     setUseWeights();
4458 }
4459 
4460 // Set the useWeights widget based on various other user selected parameters
4461 // 1. weights are only available with the LM solver used by Hyperbola and Parabola
4462 // 2. weights are only used for multiple stars so only if full frame is selected
4463 // 3. weights are only used for star measures involving multiple star measures: HFR, HFR_ADJ and FWHM
4464 void Focus::setUseWeights()
4465 {
4466     if (m_CurveFit == CurveFitting::FOCUS_QUADRATIC || !m_OpsFocusSettings->focusUseFullField->isChecked()
4467             || m_StarMeasure == FOCUS_STAR_NUM_STARS
4468             || m_StarMeasure == FOCUS_STAR_FOURIER_POWER)
4469     {
4470         m_OpsFocusProcess->focusUseWeights->setEnabled(false);
4471         m_OpsFocusProcess->focusUseWeights->setChecked(false);
4472     }
4473     else
4474         m_OpsFocusProcess->focusUseWeights->setEnabled(true);
4475 
4476 }
4477 
4478 // Set the setDonutBuster widget based on various other user selected parameters
4479 // 1. Donut Buster is only available for algorithm: Linear 1 Pass
4480 // 2. Donut Buster is available for measures: HFR, HFR Adj and FWHM
4481 // 3. Donut Buster is available for walks: Fixed and CFZ Shuffle
4482 void Focus::setDonutBuster()
4483 {
4484     if (m_FocusAlgorithm != FOCUS_LINEAR1PASS)
4485     {
4486         m_OpsFocusProcess->focusDonut->hide();
4487         m_OpsFocusProcess->focusDonut->setEnabled(false);
4488         m_OpsFocusProcess->focusDonut->setChecked(false);
4489     }
4490     else
4491     {
4492         m_OpsFocusProcess->focusDonut->show();
4493         if ((m_StarMeasure == FOCUS_STAR_HFR || m_StarMeasure == FOCUS_STAR_HFR_ADJ || m_StarMeasure == FOCUS_STAR_FWHM) &&
4494                 (m_FocusWalk == FOCUS_WALK_FIXED_STEPS || m_FocusWalk == FOCUS_WALK_CFZ_SHUFFLE))
4495             m_OpsFocusProcess->focusDonut->setEnabled(true);
4496         else
4497         {
4498             m_OpsFocusProcess->focusDonut->setEnabled(false);
4499             m_OpsFocusProcess->focusDonut->setChecked(false);
4500         }
4501     }
4502 }
4503 
4504 void Focus::setExposure(double value)
4505 {
4506     focusExposure->setValue(value);
4507 }
4508 
4509 void Focus::setBinning(int subBinX, int subBinY)
4510 {
4511     INDI_UNUSED(subBinY);
4512     focusBinning->setCurrentIndex(subBinX - 1);
4513 }
4514 
4515 void Focus::setAutoStarEnabled(bool enable)
4516 {
4517     m_OpsFocusSettings->focusAutoStarEnabled->setChecked(enable);
4518 }
4519 
4520 void Focus::setAutoSubFrameEnabled(bool enable)
4521 {
4522     m_OpsFocusSettings->focusSubFrame->setChecked(enable);
4523 }
4524 
4525 void Focus::setAutoFocusParameters(int boxSize, int stepSize, int maxTravel, double tolerance)
4526 {
4527     m_OpsFocusSettings->focusBoxSize->setValue(boxSize);
4528     m_OpsFocusMechanics->focusTicks->setValue(stepSize);
4529     m_OpsFocusMechanics->focusMaxTravel->setValue(maxTravel);
4530     m_OpsFocusProcess->focusTolerance->setValue(tolerance);
4531 }
4532 
4533 void Focus::checkAutoStarTimeout()
4534 {
4535     //if (starSelected == false && inAutoFocus)
4536     if (starCenter.isNull() && (inAutoFocus || minimumRequiredHFR > 0))
4537     {
4538         if (inAutoFocus)
4539         {
4540             if (rememberStarCenter.isNull() == false)
4541             {
4542                 focusStarSelected(rememberStarCenter.x(), rememberStarCenter.y());
4543                 appendLogText(i18n("No star was selected. Using last known position..."));
4544                 return;
4545             }
4546         }
4547 
4548         initialFocuserAbsPosition = -1;
4549         appendLogText(i18n("No star was selected. Aborting..."));
4550         completeFocusProcedure(Ekos::FOCUS_ABORTED);
4551     }
4552     else if (state() == FOCUS_WAITING)
4553         setState(FOCUS_IDLE);
4554 }
4555 
4556 void Focus::setAbsoluteFocusTicks()
4557 {
4558     if (absTicksSpin->value() == currentPosition)
4559     {
4560         appendLogText(i18n("Focuser already at %1...", currentPosition));
4561         return;
4562     }
4563     focusInB->setEnabled(false);
4564     focusOutB->setEnabled(false);
4565     startGotoB->setEnabled(false);
4566     if (!changeFocus(absTicksSpin->value() - currentPosition))
4567         qCDebug(KSTARS_EKOS_FOCUS) << "setAbsoluteFocusTicks unable to move focuser.";
4568 }
4569 
4570 void Focus::syncTrackingBoxPosition()
4571 {
4572     ISD::CameraChip *targetChip = m_Camera->getChip(ISD::CameraChip::PRIMARY_CCD);
4573     Q_ASSERT(targetChip);
4574 
4575     int subBinX = 1, subBinY = 1;
4576     targetChip->getBinning(&subBinX, &subBinY);
4577 
4578     if (starCenter.isNull() == false)
4579     {
4580         double boxSize = m_OpsFocusSettings->focusBoxSize->value();
4581         int x, y, w, h;
4582         targetChip->getFrame(&x, &y, &w, &h);
4583         // If box size is larger than image size, set it to lower index
4584         if (boxSize / subBinX >= w || boxSize / subBinY >= h)
4585         {
4586             m_OpsFocusSettings->focusBoxSize->setValue((boxSize / subBinX >= w) ? w : h);
4587             return;
4588         }
4589 
4590         // If binning changed, update coords accordingly
4591         if (subBinX != starCenter.z())
4592         {
4593             if (starCenter.z() > 0)
4594             {
4595                 starCenter.setX(starCenter.x() * (starCenter.z() / subBinX));
4596                 starCenter.setY(starCenter.y() * (starCenter.z() / subBinY));
4597             }
4598 
4599             starCenter.setZ(subBinX);
4600         }
4601 
4602         QRect starRect = QRect(starCenter.x() - boxSize / (2 * subBinX), starCenter.y() - boxSize / (2 * subBinY),
4603                                boxSize / subBinX, boxSize / subBinY);
4604         m_FocusView->setTrackingBoxEnabled(true);
4605         m_FocusView->setTrackingBox(starRect);
4606     }
4607 }
4608 
4609 void Focus::showFITSViewer()
4610 {
4611     static int lastFVTabID = -1;
4612     if (m_ImageData)
4613     {
4614         QUrl url = QUrl::fromLocalFile("focus.fits");
4615         if (fv.isNull())
4616         {
4617             fv = KStars::Instance()->createFITSViewer();
4618             fv->loadData(m_ImageData, url, &lastFVTabID);
4619             connect(fv.get(), &FITSViewer::terminated, this, [this]()
4620             {
4621                 fv.clear();
4622             });
4623         }
4624         else if (fv->updateData(m_ImageData, url, lastFVTabID, &lastFVTabID) == false)
4625             fv->loadData(m_ImageData, url, &lastFVTabID);
4626 
4627         fv->show();
4628     }
4629 }
4630 
4631 void Focus::adjustFocusOffset(int value, bool useAbsoluteOffset)
4632 {
4633     if (inAdjustFocus)
4634     {
4635         qCDebug(KSTARS_EKOS_FOCUS) << "adjustFocusOffset called whilst inAdjustFocus in progress. Ignoring...";
4636         return;
4637     }
4638 
4639     if (inFocusLoop)
4640     {
4641         qCDebug(KSTARS_EKOS_FOCUS) << "adjustFocusOffset called whilst inFocusLoop. Ignoring...";
4642         return;
4643 
4644     }
4645 
4646     if (adaptFocus->inAdaptiveFocus())
4647     {
4648         qCDebug(KSTARS_EKOS_FOCUS) << "adjustFocusOffset called whilst inAdaptiveFocus. Ignoring...";
4649         return;
4650     }
4651 
4652     inAdjustFocus = true;
4653 
4654     // Get the new position
4655     int newPosition = (useAbsoluteOffset) ? value : value + currentPosition;
4656 
4657     if (!changeFocus(newPosition - currentPosition))
4658         qCDebug(KSTARS_EKOS_FOCUS) << "adjustFocusOffset unable to move focuser";
4659 }
4660 
4661 void Focus::toggleFocusingWidgetFullScreen()
4662 {
4663     if (focusingWidget->parent() == nullptr)
4664     {
4665         focusingWidget->setParent(this);
4666         rightLayout->insertWidget(0, focusingWidget);
4667         focusingWidget->showNormal();
4668     }
4669     else
4670     {
4671         focusingWidget->setParent(nullptr);
4672         focusingWidget->setWindowTitle(i18nc("@title:window", "Focus Frame"));
4673         focusingWidget->setWindowFlags(Qt::Window | Qt::WindowTitleHint | Qt::CustomizeWindowHint);
4674         focusingWidget->showMaximized();
4675         focusingWidget->show();
4676     }
4677 }
4678 
4679 void Focus::setMountStatus(ISD::Mount::Status newState)
4680 {
4681     switch (newState)
4682     {
4683         case ISD::Mount::MOUNT_PARKING:
4684         case ISD::Mount::MOUNT_SLEWING:
4685         case ISD::Mount::MOUNT_MOVING:
4686             captureB->setEnabled(false);
4687             startFocusB->setEnabled(false);
4688             startAbInsB->setEnabled(false);
4689             startLoopB->setEnabled(false);
4690 
4691             // If mount is moved while we have a star selected and subframed
4692             // let us reset the frame.
4693             if (subFramed)
4694                 resetFrame();
4695 
4696             break;
4697 
4698         default:
4699             resetButtons();
4700             break;
4701     }
4702 }
4703 
4704 void Focus::setMountCoords(const SkyPoint &position, ISD::Mount::PierSide pierSide, const dms &ha)
4705 {
4706     Q_UNUSED(pierSide)
4707     Q_UNUSED(ha)
4708     mountAlt = position.alt().Degrees();
4709 }
4710 
4711 void Focus::removeDevice(const QSharedPointer<ISD::GenericDevice> &deviceRemoved)
4712 {
4713     auto name = deviceRemoved->getDeviceName();
4714 
4715     // Check in Focusers
4716 
4717     if (m_Focuser && m_Focuser->getDeviceName() == name)
4718     {
4719         m_Focuser->disconnect(this);
4720         m_Focuser = nullptr;
4721         QTimer::singleShot(1000, this, [this]()
4722         {
4723             checkFocuser();
4724             resetButtons();
4725         });
4726     }
4727 
4728     // Check in Temperature Sources.
4729     for (auto &oneSource : m_TemperatureSources)
4730     {
4731         if (oneSource->getDeviceName() == name)
4732         {
4733             // clear reference to avoid runtime exception in checkTemperatureSource()
4734             if (m_LastSourceDeviceAutofocusTemperature && m_LastSourceDeviceAutofocusTemperature->getDeviceName() == name)
4735                 m_LastSourceDeviceAutofocusTemperature.reset(nullptr);
4736 
4737             m_TemperatureSources.removeAll(oneSource);
4738             QTimer::singleShot(1000, this, [this, name]()
4739             {
4740                 defaultFocusTemperatureSource->removeItem(defaultFocusTemperatureSource->findText(name));
4741             });
4742             break;
4743         }
4744     }
4745 
4746     // Check camera
4747     if (m_Camera && m_Camera->getDeviceName() == name)
4748     {
4749         m_Camera->disconnect(this);
4750         m_Camera = nullptr;
4751 
4752         QTimer::singleShot(1000, this, [this]()
4753         {
4754             checkCamera();
4755             resetButtons();
4756         });
4757     }
4758 
4759     // Check Filter
4760     if (m_FilterWheel && m_FilterWheel->getDeviceName() == name)
4761     {
4762         m_FilterWheel->disconnect(this);
4763         m_FilterWheel = nullptr;
4764 
4765         QTimer::singleShot(1000, this, [this]()
4766         {
4767             checkFilter();
4768             resetButtons();
4769         });
4770     }
4771 }
4772 
4773 void Focus::setupFilterManager()
4774 {
4775     // Do we have an existing filter manager?
4776     if (m_FilterManager)
4777         m_FilterManager->disconnect(this);
4778 
4779     // Create new or refresh device
4780     Ekos::Manager::Instance()->createFilterManager(m_FilterWheel);
4781 
4782     // Return global filter manager for this filter wheel.
4783     Ekos::Manager::Instance()->getFilterManager(m_FilterWheel->getDeviceName(), m_FilterManager);
4784 
4785     // Focus Module ----> Filter Manager connections
4786 
4787     // Update focuser absolute position.
4788     connect(this, &Focus::absolutePositionChanged, m_FilterManager.get(), &FilterManager::setFocusAbsolutePosition);
4789 
4790     // Update Filter Manager state
4791     connect(this, &Focus::newStatus, this, [this](Ekos::FocusState state)
4792     {
4793         if (m_FilterManager)
4794         {
4795             m_FilterManager->setFocusStatus(state);
4796             if (focusFilter->currentIndex() != -1 && canAbsMove && state == Ekos::FOCUS_COMPLETE)
4797             {
4798                 m_FilterManager->setFilterAbsoluteFocusDetails(focusFilter->currentIndex(), currentPosition,
4799                         m_LastSourceAutofocusTemperature, m_LastSourceAutofocusAlt);
4800             }
4801         }
4802     });
4803 
4804     // Filter Manager ----> Focus Module connections
4805 
4806     // Suspend guiding if filter offset is change with OAG
4807     connect(m_FilterManager.get(), &FilterManager::newStatus, this, [this](Ekos::FilterState filterState)
4808     {
4809         // If we are changing filter offset while idle, then check if we need to suspend guiding.
4810         if (filterState == FILTER_OFFSET && state() != Ekos::FOCUS_PROGRESS)
4811         {
4812             if (m_GuidingSuspended == false && m_OpsFocusSettings->focusSuspendGuiding->isChecked())
4813             {
4814                 m_GuidingSuspended = true;
4815                 emit suspendGuiding();
4816             }
4817         }
4818     });
4819 
4820     // Take action once filter manager completes filter position
4821     connect(m_FilterManager.get(), &FilterManager::ready, this, [this]()
4822     {
4823         // Keep the focusFilter widget consistent with the filter wheel
4824         if (focusFilter->currentIndex() != currentFilterPosition - 1)
4825             focusFilter->setCurrentIndex(currentFilterPosition - 1);
4826 
4827         if (filterPositionPending)
4828         {
4829             filterPositionPending = false;
4830             capture();
4831         }
4832         else if (fallbackFilterPending)
4833         {
4834             fallbackFilterPending = false;
4835             emit newStatus(state());
4836         }
4837     });
4838 
4839     // Take action when filter operation fails
4840     connect(m_FilterManager.get(), &FilterManager::failed, this, [this]()
4841     {
4842         appendLogText(i18n("Filter operation failed."));
4843         completeFocusProcedure(Ekos::FOCUS_ABORTED);
4844     });
4845 
4846     // Run Autofocus if required by filter manager
4847     connect(m_FilterManager.get(), &FilterManager::runAutoFocus, this, &Focus::runAutoFocus);
4848 
4849     // Abort Autofocus if required by filter manager
4850     connect(m_FilterManager.get(), &FilterManager::abortAutoFocus, this, &Focus::abort);
4851 
4852     // Adjust focus offset
4853     connect(m_FilterManager.get(), &FilterManager::newFocusOffset, this, &Focus::adjustFocusOffset);
4854 
4855     // Update labels
4856     connect(m_FilterManager.get(), &FilterManager::labelsChanged, this, [this]()
4857     {
4858         focusFilter->clear();
4859         focusFilter->addItems(m_FilterManager->getFilterLabels());
4860         currentFilterPosition = m_FilterManager->getFilterPosition();
4861         focusFilter->setCurrentIndex(currentFilterPosition - 1);
4862     });
4863 
4864     // Position changed
4865     connect(m_FilterManager.get(), &FilterManager::positionChanged, this, [this]()
4866     {
4867         currentFilterPosition = m_FilterManager->getFilterPosition();
4868         focusFilter->setCurrentIndex(currentFilterPosition - 1);
4869     });
4870 
4871     // Exposure Changed
4872     connect(m_FilterManager.get(), &FilterManager::exposureChanged, this, [this]()
4873     {
4874         focusExposure->setValue(m_FilterManager->getFilterExposure());
4875     });
4876 
4877     // Wavelength Changed
4878     connect(m_FilterManager.get(), &FilterManager::wavelengthChanged, this, [this]()
4879     {
4880         wavelengthChanged();
4881     });
4882 }
4883 
4884 void Focus::connectFilterManager()
4885 {
4886     // Show filter manager if toggled.
4887     connect(filterManagerB, &QPushButton::clicked, this, [this]()
4888     {
4889         if (m_FilterManager)
4890         {
4891             m_FilterManager->refreshFilterModel();
4892             m_FilterManager->show();
4893             m_FilterManager->raise();
4894         }
4895     });
4896 
4897     // Resume guiding if suspended after focus position is adjusted.
4898     connect(this, &Focus::focusPositionAdjusted, this, [this]()
4899     {
4900         if (m_FilterManager)
4901             m_FilterManager->setFocusOffsetComplete();
4902         if (m_GuidingSuspended && state() != Ekos::FOCUS_PROGRESS)
4903         {
4904             QTimer::singleShot(m_OpsFocusMechanics->focusSettleTime->value() * 1000, this, [this]()
4905             {
4906                 m_GuidingSuspended = false;
4907                 emit resumeGuiding();
4908             });
4909         }
4910     });
4911 
4912     // Save focus exposure for a particular filter
4913     connect(focusExposure, &QDoubleSpinBox::editingFinished, this, [this]()
4914     {
4915         // Don't save if donut processing is changing this field
4916         if (inAutoFocus && m_OpsFocusProcess->focusDonut->isEnabled())
4917             return;
4918 
4919         if (m_FilterManager)
4920             m_FilterManager->setFilterExposure(focusFilter->currentIndex(), focusExposure->value());
4921     });
4922 
4923     // Load exposure if filter is changed.
4924     connect(focusFilter, &QComboBox::currentTextChanged, this, [this](const QString & text)
4925     {
4926         if (m_FilterManager)
4927         {
4928             focusExposure->setValue(m_FilterManager->getFilterExposure(text));
4929             // Update the CFZ for the new filter - force all data back to OT & filter values
4930             resetCFZToOT();
4931         }
4932     });
4933 
4934 }
4935 
4936 void Focus::toggleVideo(bool enabled)
4937 {
4938     if (m_Camera == nullptr)
4939         return;
4940 
4941     if (m_Camera->isBLOBEnabled() == false)
4942     {
4943 
4944         if (Options::guiderType() != Ekos::Guide::GUIDE_INTERNAL)
4945             m_Camera->setBLOBEnabled(true);
4946         else
4947         {
4948             connect(KSMessageBox::Instance(), &KSMessageBox::accepted, this, [this, enabled]()
4949             {
4950                 //QObject::disconnect(KSMessageBox::Instance(), &KSMessageBox::accepted, this, nullptr);
4951                 KSMessageBox::Instance()->disconnect(this);
4952                 m_Camera->setVideoStreamEnabled(enabled);
4953             });
4954             KSMessageBox::Instance()->questionYesNo(i18n("Image transfer is disabled for this camera. Would you like to enable it?"));
4955         }
4956     }
4957     else
4958         m_Camera->setVideoStreamEnabled(enabled);
4959 }
4960 
4961 //void Focus::setWeatherData(const std::vector<ISD::Weather::WeatherData> &data)
4962 //{
4963 //    auto pos = std::find_if(data.begin(), data.end(), [](ISD::Weather::WeatherData oneEntry)
4964 //    {
4965 //        return (oneEntry.name == "WEATHER_TEMPERATURE");
4966 //    });
4967 
4968 //    if (pos != data.end())
4969 //    {
4970 //        updateTemperature(OBSERVATORY_TEMPERATURE, pos->value);
4971 //    }
4972 //}
4973 
4974 void Focus::setVideoStreamEnabled(bool enabled)
4975 {
4976     if (enabled)
4977     {
4978         liveVideoB->setChecked(true);
4979         liveVideoB->setIcon(QIcon::fromTheme("camera-on"));
4980     }
4981     else
4982     {
4983         liveVideoB->setChecked(false);
4984         liveVideoB->setIcon(QIcon::fromTheme("camera-ready"));
4985     }
4986 }
4987 
4988 void Focus::processCaptureTimeout()
4989 {
4990     captureTimeoutCounter++;
4991 
4992     if (captureTimeoutCounter >= 3)
4993     {
4994         captureTimeoutCounter = 0;
4995         captureTimeout.stop();
4996         appendLogText(i18n("Exposure timeout. Aborting..."));
4997         completeFocusProcedure(Ekos::FOCUS_ABORTED);
4998     }
4999     else
5000     {
5001         appendLogText(i18n("Exposure timeout. Restarting exposure..."));
5002         ISD::CameraChip *targetChip = m_Camera->getChip(ISD::CameraChip::PRIMARY_CCD);
5003         targetChip->abortExposure();
5004 
5005         prepareCapture(targetChip);
5006 
5007         if (targetChip->capture(focusExposure->value()))
5008         {
5009             // Timeout is exposure duration + timeout threshold in seconds
5010             //long const timeout = lround(ceil(focusExposure->value() * 1000)) + FOCUS_TIMEOUT_THRESHOLD;
5011             captureTimeout.start( (focusExposure->value() + m_OpsFocusMechanics->focusCaptureTimeout->value()) * 1000);
5012 
5013             if (inFocusLoop == false)
5014                 appendLogText(i18n("Capturing image again..."));
5015 
5016             resetButtons();
5017         }
5018         else if (inAutoFocus)
5019         {
5020             completeFocusProcedure(Ekos::FOCUS_ABORTED);
5021         }
5022     }
5023 }
5024 
5025 void Focus::processCaptureError(ISD::Camera::ErrorType type)
5026 {
5027     if (type == ISD::Camera::ERROR_SAVE)
5028     {
5029         appendLogText(i18n("Failed to save image. Aborting..."));
5030         completeFocusProcedure(Ekos::FOCUS_ABORTED);
5031         return;
5032     }
5033 
5034     captureFailureCounter++;
5035 
5036     if (captureFailureCounter >= 3)
5037     {
5038         captureFailureCounter = 0;
5039         appendLogText(i18n("Exposure failure. Aborting..."));
5040         completeFocusProcedure(Ekos::FOCUS_ABORTED);
5041         return;
5042     }
5043 
5044     appendLogText(i18n("Exposure failure. Restarting exposure..."));
5045     ISD::CameraChip *targetChip = m_Camera->getChip(ISD::CameraChip::PRIMARY_CCD);
5046     targetChip->abortExposure();
5047     targetChip->capture(focusExposure->value());
5048 }
5049 
5050 void Focus::syncSettings()
5051 {
5052     QDoubleSpinBox *dsb = nullptr;
5053     QSpinBox *sb = nullptr;
5054     QCheckBox *cb = nullptr;
5055     QRadioButton *rb = nullptr;
5056     QComboBox *cbox = nullptr;
5057     QSplitter *s = nullptr;
5058 
5059     QString key;
5060     QVariant value;
5061 
5062     if ( (dsb = qobject_cast<QDoubleSpinBox*>(sender())))
5063     {
5064         key = dsb->objectName();
5065         value = dsb->value();
5066 
5067     }
5068     else if ( (sb = qobject_cast<QSpinBox*>(sender())))
5069     {
5070         key = sb->objectName();
5071         value = sb->value();
5072     }
5073     else if ( (cb = qobject_cast<QCheckBox*>(sender())))
5074     {
5075         key = cb->objectName();
5076         value = cb->isChecked();
5077     }
5078     else if ( (rb = qobject_cast<QRadioButton*>(sender())))
5079     {
5080         key = rb->objectName();
5081         value = rb->isChecked();
5082     }
5083     else if ( (cbox = qobject_cast<QComboBox*>(sender())))
5084     {
5085         key = cbox->objectName();
5086         value = cbox->currentText();
5087     }
5088     else if ( (s = qobject_cast<QSplitter*>(sender())))
5089     {
5090         key = s->objectName();
5091         // Convert from the QByteArray to QString using Base64
5092         value = QString::fromUtf8(s->saveState().toBase64());
5093     }
5094 
5095     // Save immediately
5096     Options::self()->setProperty(key.toLatin1(), value);
5097 
5098     m_Settings[key] = value;
5099     m_GlobalSettings[key] = value;
5100 
5101     emit settingsUpdated(getAllSettings());
5102 
5103     // Save to optical train specific settings as well
5104     OpticalTrainSettings::Instance()->setOpticalTrainID(OpticalTrainManager::Instance()->id(opticalTrainCombo->currentText()));
5105     OpticalTrainSettings::Instance()->setOneSetting(OpticalTrainSettings::Focus, m_Settings);
5106 
5107     // propagate image mask attributes
5108     selectImageMask();
5109 }
5110 
5111 void Focus::loadGlobalSettings()
5112 {
5113     QString key;
5114     QVariant value;
5115 
5116     QVariantMap settings;
5117     // All Combo Boxes
5118     for (auto &oneWidget : findChildren<QComboBox*>())
5119     {
5120         if (oneWidget->objectName() == "opticalTrainCombo")
5121             continue;
5122 
5123         key = oneWidget->objectName();
5124         value = Options::self()->property(key.toLatin1());
5125         if (value.isValid())
5126         {
5127             oneWidget->setCurrentText(value.toString());
5128             settings[key] = value;
5129         }
5130         else
5131             qCDebug(KSTARS_EKOS_FOCUS) << "Option" << key << "not found!";
5132     }
5133 
5134     // All Double Spin Boxes
5135     for (auto &oneWidget : findChildren<QDoubleSpinBox*>())
5136     {
5137         key = oneWidget->objectName();
5138         value = Options::self()->property(key.toLatin1());
5139         if (value.isValid())
5140         {
5141             oneWidget->setValue(value.toDouble());
5142             settings[key] = value;
5143         }
5144         else
5145             qCDebug(KSTARS_EKOS_FOCUS) << "Option" << key << "not found!";
5146     }
5147 
5148     // All Spin Boxes
5149     for (auto &oneWidget : findChildren<QSpinBox*>())
5150     {
5151         key = oneWidget->objectName();
5152         value = Options::self()->property(key.toLatin1());
5153         if (value.isValid())
5154         {
5155             oneWidget->setValue(value.toInt());
5156             settings[key] = value;
5157         }
5158         else
5159             qCDebug(KSTARS_EKOS_FOCUS) << "Option" << key << "not found!";
5160     }
5161 
5162     // All Checkboxes
5163     for (auto &oneWidget : findChildren<QCheckBox*>())
5164     {
5165         key = oneWidget->objectName();
5166         value = Options::self()->property(key.toLatin1());
5167         if (value.isValid())
5168         {
5169             oneWidget->setChecked(value.toBool());
5170             settings[key] = value;
5171         }
5172         else
5173             qCDebug(KSTARS_EKOS_FOCUS) << "Option" << key << "not found!";
5174     }
5175 
5176     // All Checkable Groupboxes
5177     for (auto &oneWidget : findChildren<QGroupBox*>())
5178     {
5179         if (oneWidget->isCheckable())
5180         {
5181             key = oneWidget->objectName();
5182             value = Options::self()->property(key.toLatin1());
5183             if (value.isValid())
5184             {
5185                 oneWidget->setChecked(value.toBool());
5186                 settings[key] = value;
5187             }
5188             else
5189                 qCDebug(KSTARS_EKOS_FOCUS) << "Option" << key << "not found!";
5190         }
5191     }
5192 
5193     // All Splitters
5194     for (auto &oneWidget : findChildren<QSplitter*>())
5195     {
5196         key = oneWidget->objectName();
5197         value = Options::self()->property(key.toLatin1());
5198         if (value.isValid())
5199         {
5200             // Convert the saved QString to a QByteArray using Base64
5201             auto valueBA = QByteArray::fromBase64(value.toString().toUtf8());
5202             oneWidget->restoreState(valueBA);
5203             settings[key] = valueBA;
5204         }
5205         else
5206             qCDebug(KSTARS_EKOS_FOCUS) << "Option" << key << "not found!";
5207     }
5208 
5209     // All Radio buttons
5210     for (auto &oneWidget : findChildren<QRadioButton*>())
5211     {
5212         key = oneWidget->objectName();
5213         value = Options::self()->property(key.toLatin1());
5214         if (value.isValid())
5215         {
5216             oneWidget->setChecked(value.toBool());
5217             settings[key] = value;
5218         }
5219     }
5220     // propagate image mask attributes
5221     selectImageMask();
5222     m_GlobalSettings = m_Settings = settings;
5223 }
5224 
5225 void Focus::checkMosaicMaskLimits()
5226 {
5227     if (m_Camera == nullptr || m_Camera->isConnected() == false)
5228         return;
5229     auto targetChip = m_Camera->getChip(ISD::CameraChip::PRIMARY_CCD);
5230     if (targetChip == nullptr || frameSettings.contains(targetChip) == false)
5231         return;
5232     auto settings = frameSettings[targetChip];
5233 
5234     // Watch out for invalid values.
5235     auto width = settings["w"].toInt();
5236     auto height = settings["h"].toInt();
5237     if (width == 0 || height == 0)
5238         return;
5239 
5240     // determine maximal square size
5241     auto min = std::min(width, height);
5242     // now check if the tile size is below this limit
5243     m_OpsFocusSettings->focusMosaicTileWidth->setMaximum(100 * min / (3 * width));
5244 }
5245 
5246 void Focus::connectSyncSettings()
5247 {
5248     // All Combo Boxes
5249     for (auto &oneWidget : findChildren<QComboBox*>())
5250         // Don't sync Optical Train combo
5251         if (oneWidget != opticalTrainCombo)
5252             connect(oneWidget, QOverload<int>::of(&QComboBox::activated), this, &Ekos::Focus::syncSettings);
5253 
5254     // All Double Spin Boxes
5255     for (auto &oneWidget : findChildren<QDoubleSpinBox*>())
5256         connect(oneWidget, QOverload<double>::of(&QDoubleSpinBox::valueChanged), this, &Ekos::Focus::syncSettings);
5257 
5258     // All Spin Boxes
5259     for (auto &oneWidget : findChildren<QSpinBox*>())
5260         connect(oneWidget, QOverload<int>::of(&QSpinBox::valueChanged), this, &Ekos::Focus::syncSettings);
5261 
5262     // All Checkboxes
5263     for (auto &oneWidget : findChildren<QCheckBox*>())
5264         connect(oneWidget, &QCheckBox::toggled, this, &Ekos::Focus::syncSettings);
5265 
5266     // All Checkable Groupboxes
5267     for (auto &oneWidget : findChildren<QGroupBox*>())
5268         if (oneWidget->isCheckable())
5269             connect(oneWidget, &QGroupBox::toggled, this, &Ekos::Focus::syncSettings);
5270 
5271     // All Splitters
5272     for (auto &oneWidget : findChildren<QSplitter*>())
5273         connect(oneWidget, &QSplitter::splitterMoved, this, &Ekos::Focus::syncSettings);
5274 
5275     // All Radio Buttons
5276     for (auto &oneWidget : findChildren<QRadioButton*>())
5277         connect(oneWidget, &QRadioButton::toggled, this, &Ekos::Focus::syncSettings);
5278 }
5279 
5280 void Focus::disconnectSyncSettings()
5281 {
5282     // All Combo Boxes
5283     for (auto &oneWidget : findChildren<QComboBox*>())
5284         disconnect(oneWidget, QOverload<int>::of(&QComboBox::activated), this, &Ekos::Focus::syncSettings);
5285 
5286     // All Double Spin Boxes
5287     for (auto &oneWidget : findChildren<QDoubleSpinBox*>())
5288         disconnect(oneWidget, QOverload<double>::of(&QDoubleSpinBox::valueChanged), this, &Ekos::Focus::syncSettings);
5289 
5290     // All Spin Boxes
5291     for (auto &oneWidget : findChildren<QSpinBox*>())
5292         disconnect(oneWidget, QOverload<int>::of(&QSpinBox::valueChanged), this, &Ekos::Focus::syncSettings);
5293 
5294     // All Checkboxes
5295     for (auto &oneWidget : findChildren<QCheckBox*>())
5296         disconnect(oneWidget, &QCheckBox::toggled, this, &Ekos::Focus::syncSettings);
5297 
5298     // All Checkable Groupboxes
5299     for (auto &oneWidget : findChildren<QGroupBox*>())
5300         if (oneWidget->isCheckable())
5301             disconnect(oneWidget, &QGroupBox::toggled, this, &Ekos::Focus::syncSettings);
5302 
5303     // All Splitters
5304     for (auto &oneWidget : findChildren<QSplitter*>())
5305         disconnect(oneWidget, &QSplitter::splitterMoved, this, &Ekos::Focus::syncSettings);
5306 
5307     // All Radio Buttons
5308     for (auto &oneWidget : findChildren<QRadioButton*>())
5309         disconnect(oneWidget, &QRadioButton::toggled, this, &Ekos::Focus::syncSettings);
5310 }
5311 
5312 void Focus::initPlots()
5313 {
5314     connect(clearDataB, &QPushButton::clicked, this, &Ekos::Focus::clearDataPoints);
5315 
5316     profileDialog = new QDialog(this);
5317     profileDialog->setWindowFlags(Qt::Tool | Qt::WindowStaysOnTopHint);
5318     QVBoxLayout *profileLayout = new QVBoxLayout(profileDialog);
5319     profileDialog->setWindowTitle(i18nc("@title:window", "Relative Profile"));
5320     profilePlot = new FocusProfilePlot(profileDialog);
5321 
5322     profileLayout->addWidget(profilePlot);
5323     profileDialog->setLayout(profileLayout);
5324     profileDialog->resize(400, 300);
5325 
5326     connect(relativeProfileB, &QPushButton::clicked, profileDialog, &QDialog::show);
5327     connect(this, &Ekos::Focus::newHFR, [this](double currentHFR, int pos)
5328     {
5329         Q_UNUSED(pos) profilePlot->drawProfilePlot(currentHFR);
5330     });
5331 }
5332 
5333 void Focus::initConnections()
5334 {
5335     // How long do we wait until the user select a star?
5336     waitStarSelectTimer.setInterval(AUTO_STAR_TIMEOUT);
5337     connect(&waitStarSelectTimer, &QTimer::timeout, this, &Ekos::Focus::checkAutoStarTimeout);
5338     connect(liveVideoB, &QPushButton::clicked, this, &Ekos::Focus::toggleVideo);
5339 
5340     // Show FITS Image in a new window
5341     showFITSViewerB->setIcon(QIcon::fromTheme("kstars_fitsviewer"));
5342     showFITSViewerB->setAttribute(Qt::WA_LayoutUsesWidgetRect);
5343     connect(showFITSViewerB, &QPushButton::clicked, this, &Ekos::Focus::showFITSViewer);
5344 
5345     // Toggle FITS View to full screen
5346     toggleFullScreenB->setIcon(QIcon::fromTheme("view-fullscreen"));
5347     toggleFullScreenB->setShortcut(Qt::Key_F4);
5348     toggleFullScreenB->setAttribute(Qt::WA_LayoutUsesWidgetRect);
5349     connect(toggleFullScreenB, &QPushButton::clicked, this, &Ekos::Focus::toggleFocusingWidgetFullScreen);
5350 
5351     // delayed capturing for waiting the scope to settle
5352     captureTimer.setSingleShot(true);
5353     connect(&captureTimer, &QTimer::timeout, this, [this]()
5354     {
5355         capture();
5356     });
5357 
5358     // How long do we wait until an exposure times out and needs a retry?
5359     captureTimeout.setSingleShot(true);
5360     connect(&captureTimeout, &QTimer::timeout, this, &Ekos::Focus::processCaptureTimeout);
5361 
5362     // Start/Stop focus
5363     connect(startFocusB, &QPushButton::clicked, this, &Ekos::Focus::start);
5364     connect(stopFocusB, &QPushButton::clicked, this, &Ekos::Focus::abort);
5365 
5366     // Focus IN/OUT
5367     connect(focusOutB, &QPushButton::clicked, this, &Ekos::Focus::focusOut);
5368     connect(focusInB, &QPushButton::clicked, this, &Ekos::Focus::focusIn);
5369 
5370     // Capture a single frame
5371     connect(captureB, &QPushButton::clicked, this, &Ekos::Focus::capture);
5372     // Start continuous capture
5373     connect(startLoopB, &QPushButton::clicked, this, &Ekos::Focus::startFraming);
5374     // Use a subframe when capturing
5375     connect(m_OpsFocusSettings->focusSubFrame, &QRadioButton::toggled, this, &Ekos::Focus::toggleSubframe);
5376     // Reset frame dimensions to default
5377     connect(resetFrameB, &QPushButton::clicked, this, &Ekos::Focus::resetFrame);
5378 
5379     // handle frame size changes
5380     connect(focusBinning, QOverload<int>::of(&QComboBox::activated), this, &Ekos::Focus::checkMosaicMaskLimits);
5381 
5382     // Sync settings if the temperature source selection is updated.
5383     connect(defaultFocusTemperatureSource, &QComboBox::currentTextChanged, this, &Ekos::Focus::checkTemperatureSource);
5384 
5385     // Set focuser absolute position
5386     connect(startGotoB, &QPushButton::clicked, this, &Ekos::Focus::setAbsoluteFocusTicks);
5387     connect(stopGotoB, &QPushButton::clicked, this, [this]()
5388     {
5389         if (m_Focuser)
5390             m_Focuser->stop();
5391     });
5392     // Update the focuser box size used to enclose a star
5393     connect(m_OpsFocusSettings->focusBoxSize, static_cast<void (QSpinBox::*)(int)>(&QSpinBox::valueChanged), this,
5394             &Ekos::Focus::updateBoxSize);
5395 
5396     // Setup the tools buttons
5397     connect(startAbInsB, &QPushButton::clicked, this, &Ekos::Focus::startAbIns);
5398     connect(cfzB, &QPushButton::clicked, this, [this]()
5399     {
5400         m_CFZDialog->show();
5401         m_CFZDialog->raise();
5402     });
5403     connect(advisorB, &QPushButton::clicked, this, [this]()
5404     {
5405         m_AdvisorDialog->show();
5406         m_AdvisorDialog->raise();
5407     });
5408 
5409     // Update the focuser star detection if the detection algorithm selection changes.
5410     connect(m_OpsFocusProcess->focusDetection, QOverload<int>::of(&QComboBox::currentIndexChanged), this, [&](int index)
5411     {
5412         setFocusDetection(static_cast<StarAlgorithm>(index));
5413     });
5414 
5415     // Update the focuser solution algorithm if the selection changes.
5416     connect(m_OpsFocusProcess->focusAlgorithm, QOverload<int>::of(&QComboBox::currentIndexChanged), this, [&](int index)
5417     {
5418         setFocusAlgorithm(static_cast<Algorithm>(index));
5419     });
5420 
5421     // Update the curve fit if the selection changes. Use the currentIndexChanged method rather than
5422     // activated as the former fires when the index is changed by the user AND if changed programmatically
5423     connect(m_OpsFocusProcess->focusCurveFit, static_cast<void (QComboBox::*)(int)>(&QComboBox::currentIndexChanged),
5424             this, [&](int index)
5425     {
5426         setCurveFit(static_cast<CurveFitting::CurveFit>(index));
5427     });
5428 
5429     // Update the star measure if the selection changes
5430     connect(m_OpsFocusProcess->focusStarMeasure, static_cast<void (QComboBox::*)(int)>(&QComboBox::currentIndexChanged),
5431             this, [&](int index)
5432     {
5433         setStarMeasure(static_cast<StarMeasure>(index));
5434     });
5435 
5436     // Update the star PSF if the selection changes
5437     connect(m_OpsFocusProcess->focusStarPSF, static_cast<void (QComboBox::*)(int)>(&QComboBox::currentIndexChanged),
5438             this, [&](int index)
5439     {
5440         setStarPSF(static_cast<StarPSF>(index));
5441     });
5442 
5443     // Update the units (pixels or arcsecs) if the selection changes
5444     connect(m_OpsFocusSettings->focusUnits, static_cast<void (QComboBox::*)(int)>(&QComboBox::currentIndexChanged),
5445             this, [&](int index)
5446     {
5447         setStarUnits(static_cast<StarUnits>(index));
5448     });
5449 
5450     // Update the walk if the selection changes
5451     connect(m_OpsFocusMechanics->focusWalk, static_cast<void (QComboBox::*)(int)>(&QComboBox::currentIndexChanged),
5452             this, [&](int index)
5453     {
5454         setWalk(static_cast<FocusWalk>(index));
5455     });
5456 
5457     // Adaptive Focus on/off switch toggled
5458     connect(m_OpsFocusSettings->focusAdaptive, &QCheckBox::toggled, this, &Ekos::Focus::resetAdaptiveFocus);
5459 
5460     // Reset star center on auto star check toggle
5461     connect(m_OpsFocusSettings->focusAutoStarEnabled, &QCheckBox::toggled, this, [&](bool enabled)
5462     {
5463         if (enabled)
5464         {
5465             starCenter   = QVector3D();
5466             starSelected = false;
5467             m_FocusView->setTrackingBox(QRect());
5468         }
5469     });
5470 
5471     // CFZ Panel
5472     connect(m_CFZUI->resetToOTB, &QPushButton::clicked, this, &Ekos::Focus::resetCFZToOT);
5473 
5474     connect(m_CFZUI->focusCFZDisplayVCurve, static_cast<void (QCheckBox::*)(int)>(&QCheckBox::stateChanged), this,
5475             &Ekos::Focus::calcCFZ);
5476 
5477     connect(m_CFZUI->focusCFZAlgorithm, static_cast<void (QComboBox::*)(int)>(&QComboBox::currentIndexChanged), this,
5478             &Ekos::Focus::calcCFZ);
5479 
5480     connect(m_CFZUI->focusCFZTolerance, QOverload<double>::of(&QDoubleSpinBox::valueChanged), this, &Ekos::Focus::calcCFZ);
5481 
5482     connect(m_CFZUI->focusCFZTau, QOverload<double>::of(&QDoubleSpinBox::valueChanged), [ = ](double d)
5483     {
5484         Q_UNUSED(d);
5485         calcCFZ();
5486     });
5487 
5488     connect(m_CFZUI->focusCFZWavelength, QOverload<double>::of(&QDoubleSpinBox::valueChanged), [ = ](int i)
5489     {
5490         Q_UNUSED(i);
5491         calcCFZ();
5492     });
5493 
5494     connect(m_CFZUI->focusCFZFNumber, QOverload<double>::of(&QDoubleSpinBox::valueChanged), [ = ](double d)
5495     {
5496         Q_UNUSED(d);
5497         calcCFZ();
5498     });
5499 
5500     connect(m_CFZUI->focusCFZStepSize, QOverload<double>::of(&QDoubleSpinBox::valueChanged), [ = ](double d)
5501     {
5502         Q_UNUSED(d);
5503         calcCFZ();
5504     });
5505 
5506     connect(m_CFZUI->focusCFZAperture, QOverload<int>::of(&QSpinBox::valueChanged), [ = ](int i)
5507     {
5508         Q_UNUSED(i);
5509         calcCFZ();
5510     });
5511 
5512     connect(m_CFZUI->focusCFZSeeing, QOverload<double>::of(&QDoubleSpinBox::valueChanged), [ = ](double d)
5513     {
5514         Q_UNUSED(d);
5515         calcCFZ();
5516     });
5517 
5518     // Focus Advisor Panel
5519     connect(m_AdvisorUI->focusAdvReset, &QPushButton::clicked, this, &Ekos::Focus::focusAdvisorAction);
5520     connect(m_AdvisorUI->focusAdvHelp, &QPushButton::clicked, this, &Ekos::Focus::focusAdvisorHelp);
5521     // Update the defaulted step size on the FA panel if the CFZ changes
5522     connect(m_CFZUI->focusCFZFinal, &QLineEdit::textChanged, this, [this]()
5523     {
5524         m_AdvisorUI->focusAdvSteps->setValue(m_cfzSteps);
5525     });
5526 }
5527 
5528 void Focus::setFocusDetection(StarAlgorithm starAlgorithm)
5529 {
5530     static bool first = true;
5531     if (!first && m_FocusDetection == starAlgorithm)
5532         return;
5533 
5534     first = false;
5535 
5536     m_FocusDetection = starAlgorithm;
5537 
5538     // setFocusAlgorithm displays the appropriate widgets for the selection
5539     setFocusAlgorithm(m_FocusAlgorithm);
5540 
5541     if (m_FocusDetection == ALGORITHM_BAHTINOV)
5542     {
5543         // In case of Bahtinov mask uncheck auto select star
5544         m_OpsFocusSettings->focusAutoStarEnabled->setChecked(false);
5545         m_OpsFocusSettings->focusBoxSize->setMaximum(512);
5546     }
5547     else
5548     {
5549         // When not using Bathinov mask, limit box size to 256 and make sure value stays within range.
5550         if (m_OpsFocusSettings->focusBoxSize->value() > 256)
5551         {
5552             // Focus box size changed, update control
5553             m_OpsFocusSettings->focusBoxSize->setValue(m_OpsFocusSettings->focusBoxSize->value());
5554         }
5555         m_OpsFocusSettings->focusBoxSize->setMaximum(256);
5556     }
5557     m_OpsFocusSettings->focusAutoStarEnabled->setEnabled(m_FocusDetection != ALGORITHM_BAHTINOV);
5558     setDonutBuster();
5559 }
5560 
5561 void Focus::setFocusAlgorithm(Algorithm algorithm)
5562 {
5563     m_FocusAlgorithm = algorithm;
5564     switch(algorithm)
5565     {
5566         case FOCUS_ITERATIVE:
5567             // Remove unused widgets from the grid and hide them
5568             m_OpsFocusProcess->gridLayoutProcess->removeWidget(m_OpsFocusProcess->focusMultiRowAverageLabel);
5569             m_OpsFocusProcess->focusMultiRowAverageLabel->hide();
5570             m_OpsFocusProcess->gridLayoutProcess->removeWidget(m_OpsFocusProcess->focusMultiRowAverage);
5571             m_OpsFocusProcess->focusMultiRowAverage->hide();
5572 
5573             m_OpsFocusProcess->gridLayoutProcess->removeWidget(m_OpsFocusProcess->focusGaussianSigmaLabel);
5574             m_OpsFocusProcess->focusGaussianSigmaLabel->hide();
5575             m_OpsFocusProcess->gridLayoutProcess->removeWidget(m_OpsFocusProcess->focusGaussianSigma);
5576             m_OpsFocusProcess->focusGaussianSigma->hide();
5577 
5578             m_OpsFocusProcess->gridLayoutProcess->removeWidget(m_OpsFocusProcess->focusGaussianKernelSizeLabel);
5579             m_OpsFocusProcess->focusGaussianKernelSizeLabel->hide();
5580             m_OpsFocusProcess->gridLayoutProcess->removeWidget(m_OpsFocusProcess->focusGaussianKernelSize);
5581             m_OpsFocusProcess->focusGaussianKernelSize->hide();
5582 
5583             m_OpsFocusProcess->gridLayoutProcess->removeWidget(m_OpsFocusProcess->focusStarMeasureLabel);
5584             m_OpsFocusProcess->focusStarMeasureLabel->hide();
5585             m_OpsFocusProcess->gridLayoutProcess->removeWidget(m_OpsFocusProcess->focusStarMeasure);
5586             m_OpsFocusProcess->focusStarMeasure->hide();
5587 
5588             m_OpsFocusProcess->gridLayoutProcess->removeWidget(m_OpsFocusProcess->focusStarPSFLabel);
5589             m_OpsFocusProcess->focusStarPSFLabel->hide();
5590             m_OpsFocusProcess->gridLayoutProcess->removeWidget(m_OpsFocusProcess->focusStarPSF);
5591             m_OpsFocusProcess->focusStarPSF->hide();
5592 
5593             m_OpsFocusProcess->gridLayoutProcess->removeWidget(m_OpsFocusProcess->focusUseWeights);
5594             m_OpsFocusProcess->focusUseWeights->hide();
5595 
5596             m_OpsFocusProcess->gridLayoutProcess->removeWidget(m_OpsFocusProcess->focusR2LimitLabel);
5597             m_OpsFocusProcess->focusR2LimitLabel->hide();
5598             m_OpsFocusProcess->gridLayoutProcess->removeWidget(m_OpsFocusProcess->focusR2Limit);
5599             m_OpsFocusProcess->focusR2Limit->hide();
5600 
5601             m_OpsFocusProcess->gridLayoutProcess->removeWidget(m_OpsFocusProcess->focusRefineCurveFit);
5602             m_OpsFocusProcess->focusRefineCurveFit->hide();
5603             m_OpsFocusProcess->focusRefineCurveFit->setChecked(false);
5604 
5605             m_OpsFocusProcess->gridLayoutProcess->removeWidget(m_OpsFocusProcess->focusToleranceLabel);
5606             m_OpsFocusProcess->focusToleranceLabel->hide();
5607             m_OpsFocusProcess->gridLayoutProcess->removeWidget(m_OpsFocusProcess->focusTolerance);
5608             m_OpsFocusProcess->focusTolerance->hide();
5609 
5610             m_OpsFocusProcess->gridLayoutProcess->removeWidget(m_OpsFocusProcess->focusThresholdLabel);
5611             m_OpsFocusProcess->focusThresholdLabel->hide();
5612             m_OpsFocusProcess->gridLayoutProcess->removeWidget(m_OpsFocusProcess->focusThreshold);
5613             m_OpsFocusProcess->focusThreshold->hide();
5614 
5615             m_OpsFocusProcess->gridLayoutProcess->removeWidget(m_OpsFocusProcess->focusCurveFitLabel);
5616             m_OpsFocusProcess->focusCurveFitLabel->hide();
5617             m_OpsFocusProcess->gridLayoutProcess->removeWidget(m_OpsFocusProcess->focusCurveFit);
5618             m_OpsFocusProcess->focusCurveFit->hide();
5619 
5620             m_OpsFocusProcess->focusDonut->hide();
5621             m_OpsFocusProcess->focusDonut->setChecked(false);
5622 
5623             // Although CurveFit is not used by Iterative setting to Quadratic will configure other widgets
5624             m_OpsFocusProcess->focusCurveFit->setCurrentIndex(CurveFitting::FOCUS_QUADRATIC);
5625 
5626             // Set Measure to just HFR
5627             if (m_OpsFocusProcess->focusStarMeasure->count() != 1)
5628             {
5629                 m_OpsFocusProcess->focusStarMeasure->clear();
5630                 m_OpsFocusProcess->focusStarMeasure->addItem(m_StarMeasureText.at(FOCUS_STAR_HFR));
5631                 m_OpsFocusProcess->focusStarMeasure->setCurrentIndex(FOCUS_STAR_HFR);
5632             }
5633 
5634             // Add necessary widgets to the grid
5635             m_OpsFocusProcess->gridLayoutProcess->addWidget(m_OpsFocusProcess->focusToleranceLabel, 3, 0);
5636             m_OpsFocusProcess->focusToleranceLabel->show();
5637             m_OpsFocusProcess->gridLayoutProcess->addWidget(m_OpsFocusProcess->focusTolerance, 3, 1);
5638             m_OpsFocusProcess->focusTolerance->show();
5639 
5640             m_OpsFocusProcess->gridLayoutProcess->addWidget(m_OpsFocusProcess->focusFramesCountLabel, 3, 2);
5641             m_OpsFocusProcess->focusFramesCountLabel->show();
5642             m_OpsFocusProcess->gridLayoutProcess->addWidget(m_OpsFocusProcess->focusFramesCount, 3, 3);
5643             m_OpsFocusProcess->focusFramesCount->show();
5644 
5645             if (m_FocusDetection == ALGORITHM_THRESHOLD)
5646             {
5647                 m_OpsFocusProcess->gridLayoutProcess->addWidget(m_OpsFocusProcess->focusThresholdLabel, 4, 0);
5648                 m_OpsFocusProcess->focusThresholdLabel->show();
5649                 m_OpsFocusProcess->gridLayoutProcess->addWidget(m_OpsFocusProcess->focusThreshold, 4, 1);
5650                 m_OpsFocusProcess->focusThreshold->show();
5651             }
5652             else if (m_FocusDetection == ALGORITHM_BAHTINOV)
5653             {
5654                 m_OpsFocusProcess->gridLayoutProcess->addWidget(m_OpsFocusProcess->focusMultiRowAverageLabel, 4, 0);
5655                 m_OpsFocusProcess->focusMultiRowAverageLabel->show();
5656                 m_OpsFocusProcess->gridLayoutProcess->addWidget(m_OpsFocusProcess->focusMultiRowAverage, 4, 1);
5657                 m_OpsFocusProcess->focusMultiRowAverage->show();
5658 
5659                 m_OpsFocusProcess->gridLayoutProcess->addWidget(m_OpsFocusProcess->focusGaussianSigmaLabel, 4, 2);
5660                 m_OpsFocusProcess->focusGaussianSigmaLabel->show();
5661                 m_OpsFocusProcess->gridLayoutProcess->addWidget(m_OpsFocusProcess->focusGaussianSigma, 4, 3);
5662                 m_OpsFocusProcess->focusGaussianSigma->show();
5663 
5664                 m_OpsFocusProcess->gridLayoutProcess->addWidget(m_OpsFocusProcess->focusGaussianKernelSizeLabel, 5, 0);
5665                 m_OpsFocusProcess->focusGaussianKernelSizeLabel->show();
5666                 m_OpsFocusProcess->gridLayoutProcess->addWidget(m_OpsFocusProcess->focusGaussianKernelSize, 5, 1);
5667                 m_OpsFocusProcess->focusGaussianKernelSize->show();
5668             }
5669 
5670             // Aberration Inspector button
5671             startAbInsB->setEnabled(canAbInsStart());
5672 
5673             // Settings changes
5674             // Disable adaptive focus
5675             m_OpsFocusSettings->focusAdaptive->setChecked(false);
5676             m_OpsFocusSettings->focusAdaptStart->setChecked(false);
5677             m_OpsFocusSettings->adaptiveFocusGroup->setEnabled(false);
5678 
5679             // Mechanics changes
5680             m_OpsFocusMechanics->focusMaxSingleStep->setEnabled(true);
5681             m_OpsFocusMechanics->focusOutSteps->setEnabled(false);
5682 
5683             // Set Walk to just Classic on 1st time through
5684             if (m_OpsFocusMechanics->focusWalk->count() != 1)
5685             {
5686                 m_OpsFocusMechanics->focusWalk->clear();
5687                 m_OpsFocusMechanics->focusWalk->addItem(m_FocusWalkText.at(FOCUS_WALK_CLASSIC));
5688                 m_OpsFocusMechanics->focusWalk->setCurrentIndex(FOCUS_WALK_CLASSIC);
5689             }
5690             break;
5691 
5692         case FOCUS_POLYNOMIAL:
5693             // Remove unused widgets from the grid and hide them
5694             m_OpsFocusProcess->gridLayoutProcess->removeWidget(m_OpsFocusProcess->focusMultiRowAverageLabel);
5695             m_OpsFocusProcess->focusMultiRowAverageLabel->hide();
5696             m_OpsFocusProcess->gridLayoutProcess->removeWidget(m_OpsFocusProcess->focusMultiRowAverage);
5697             m_OpsFocusProcess->focusMultiRowAverage->hide();
5698 
5699             m_OpsFocusProcess->gridLayoutProcess->removeWidget(m_OpsFocusProcess->focusGaussianSigmaLabel);
5700             m_OpsFocusProcess->focusGaussianSigmaLabel->hide();
5701             m_OpsFocusProcess->gridLayoutProcess->removeWidget(m_OpsFocusProcess->focusGaussianSigma);
5702             m_OpsFocusProcess->focusGaussianSigma->hide();
5703 
5704             m_OpsFocusProcess->gridLayoutProcess->removeWidget(m_OpsFocusProcess->focusGaussianKernelSizeLabel);
5705             m_OpsFocusProcess->focusGaussianKernelSizeLabel->hide();
5706             m_OpsFocusProcess->gridLayoutProcess->removeWidget(m_OpsFocusProcess->focusGaussianKernelSize);
5707             m_OpsFocusProcess->focusGaussianKernelSize->hide();
5708 
5709             m_OpsFocusProcess->gridLayoutProcess->removeWidget(m_OpsFocusProcess->focusStarPSFLabel);
5710             m_OpsFocusProcess->focusStarPSFLabel->hide();
5711             m_OpsFocusProcess->gridLayoutProcess->removeWidget(m_OpsFocusProcess->focusStarPSF);
5712             m_OpsFocusProcess->focusStarPSF->hide();
5713 
5714             m_OpsFocusProcess->gridLayoutProcess->removeWidget(m_OpsFocusProcess->focusUseWeights);
5715             m_OpsFocusProcess->focusUseWeights->hide();
5716 
5717             m_OpsFocusProcess->gridLayoutProcess->removeWidget(m_OpsFocusProcess->focusR2LimitLabel);
5718             m_OpsFocusProcess->focusR2LimitLabel->hide();
5719             m_OpsFocusProcess->gridLayoutProcess->removeWidget(m_OpsFocusProcess->focusR2Limit);
5720             m_OpsFocusProcess->focusR2Limit->hide();
5721 
5722             m_OpsFocusProcess->gridLayoutProcess->removeWidget(m_OpsFocusProcess->focusRefineCurveFit);
5723             m_OpsFocusProcess->focusRefineCurveFit->hide();
5724             m_OpsFocusProcess->focusRefineCurveFit->setChecked(false);
5725 
5726             m_OpsFocusProcess->gridLayoutProcess->removeWidget(m_OpsFocusProcess->focusToleranceLabel);
5727             m_OpsFocusProcess->focusToleranceLabel->hide();
5728             m_OpsFocusProcess->gridLayoutProcess->removeWidget(m_OpsFocusProcess->focusTolerance);
5729             m_OpsFocusProcess->focusTolerance->hide();
5730 
5731             m_OpsFocusProcess->gridLayoutProcess->removeWidget(m_OpsFocusProcess->focusThresholdLabel);
5732             m_OpsFocusProcess->focusThresholdLabel->hide();
5733             m_OpsFocusProcess->gridLayoutProcess->removeWidget(m_OpsFocusProcess->focusThreshold);
5734             m_OpsFocusProcess->focusThreshold->hide();
5735 
5736             // Donut buster not available
5737             m_OpsFocusProcess->focusDonut->hide();
5738             m_OpsFocusProcess->focusDonut->setChecked(false);
5739 
5740             // Set Measure to just HFR
5741             if (m_OpsFocusProcess->focusStarMeasure->count() != 1)
5742             {
5743                 m_OpsFocusProcess->focusStarMeasure->clear();
5744                 m_OpsFocusProcess->focusStarMeasure->addItem(m_StarMeasureText.at(FOCUS_STAR_HFR));
5745                 m_OpsFocusProcess->focusStarMeasure->setCurrentIndex(FOCUS_STAR_HFR);
5746             }
5747 
5748             // Add necessary widgets to the grid
5749             // Curve fit can only be QUADRATIC so only allow this value
5750             m_OpsFocusProcess->gridLayoutProcess->addWidget(m_OpsFocusProcess->focusCurveFitLabel, 1, 2);
5751             m_OpsFocusProcess->focusCurveFitLabel->show();
5752             m_OpsFocusProcess->gridLayoutProcess->addWidget(m_OpsFocusProcess->focusCurveFit, 1, 3);
5753             m_OpsFocusProcess->focusCurveFit->show();
5754             if (m_OpsFocusProcess->focusCurveFit->count() != 1)
5755             {
5756                 m_OpsFocusProcess->focusCurveFit->clear();
5757                 m_OpsFocusProcess->focusCurveFit->addItem(m_CurveFitText.at(CurveFitting::FOCUS_QUADRATIC));
5758                 m_OpsFocusProcess->focusCurveFit->setCurrentIndex(CurveFitting::FOCUS_QUADRATIC);
5759             }
5760 
5761             m_OpsFocusProcess->gridLayoutProcess->addWidget(m_OpsFocusProcess->focusToleranceLabel, 3, 0);
5762             m_OpsFocusProcess->focusToleranceLabel->show();
5763             m_OpsFocusProcess->gridLayoutProcess->addWidget(m_OpsFocusProcess->focusTolerance, 3, 1);
5764             m_OpsFocusProcess->focusTolerance->show();
5765 
5766             m_OpsFocusProcess->gridLayoutProcess->addWidget(m_OpsFocusProcess->focusFramesCountLabel, 3, 2);
5767             m_OpsFocusProcess->focusFramesCountLabel->show();
5768             m_OpsFocusProcess->gridLayoutProcess->addWidget(m_OpsFocusProcess->focusFramesCount, 3, 3);
5769             m_OpsFocusProcess->focusFramesCount->show();
5770 
5771             if (m_FocusDetection == ALGORITHM_THRESHOLD)
5772             {
5773                 m_OpsFocusProcess->gridLayoutProcess->addWidget(m_OpsFocusProcess->focusThresholdLabel, 4, 0);
5774                 m_OpsFocusProcess->focusThresholdLabel->show();
5775                 m_OpsFocusProcess->gridLayoutProcess->addWidget(m_OpsFocusProcess->focusThreshold, 4, 1);
5776                 m_OpsFocusProcess->focusThreshold->show();
5777             }
5778             else if (m_FocusDetection == ALGORITHM_BAHTINOV)
5779             {
5780                 m_OpsFocusProcess->gridLayoutProcess->addWidget(m_OpsFocusProcess->focusMultiRowAverageLabel, 4, 0);
5781                 m_OpsFocusProcess->focusMultiRowAverageLabel->show();
5782                 m_OpsFocusProcess->gridLayoutProcess->addWidget(m_OpsFocusProcess->focusMultiRowAverage, 4, 1);
5783                 m_OpsFocusProcess->focusMultiRowAverage->show();
5784 
5785                 m_OpsFocusProcess->gridLayoutProcess->addWidget(m_OpsFocusProcess->focusGaussianSigmaLabel, 4, 2);
5786                 m_OpsFocusProcess->focusGaussianSigmaLabel->show();
5787                 m_OpsFocusProcess->gridLayoutProcess->addWidget(m_OpsFocusProcess->focusGaussianSigma, 4, 3);
5788                 m_OpsFocusProcess->focusGaussianSigma->show();
5789 
5790                 m_OpsFocusProcess->gridLayoutProcess->addWidget(m_OpsFocusProcess->focusGaussianKernelSizeLabel, 5, 0);
5791                 m_OpsFocusProcess->focusGaussianKernelSizeLabel->show();
5792                 m_OpsFocusProcess->gridLayoutProcess->addWidget(m_OpsFocusProcess->focusGaussianKernelSize, 5, 1);
5793                 m_OpsFocusProcess->focusGaussianKernelSize->show();
5794             }
5795 
5796             // Aberration Inspector button
5797             startAbInsB->setEnabled(canAbInsStart());
5798 
5799             // Settings changes
5800             // Disable adaptive focus
5801             m_OpsFocusSettings->focusAdaptive->setChecked(false);
5802             m_OpsFocusSettings->focusAdaptStart->setChecked(false);
5803             m_OpsFocusSettings->adaptiveFocusGroup->setEnabled(false);
5804 
5805             // Mechanics changes
5806             m_OpsFocusMechanics->focusMaxSingleStep->setEnabled(true);
5807             m_OpsFocusMechanics->focusOutSteps->setEnabled(false);
5808 
5809             // Set Walk to just Classic on 1st time through
5810             if (m_OpsFocusMechanics->focusWalk->count() != 1)
5811             {
5812                 m_OpsFocusMechanics->focusWalk->clear();
5813                 m_OpsFocusMechanics->focusWalk->addItem(m_FocusWalkText.at(FOCUS_WALK_CLASSIC));
5814                 m_OpsFocusMechanics->focusWalk->setCurrentIndex(FOCUS_WALK_CLASSIC);
5815             }
5816             break;
5817 
5818         case FOCUS_LINEAR:
5819             // Remove unused widgets from the grid and hide them
5820             m_OpsFocusProcess->gridLayoutProcess->removeWidget(m_OpsFocusProcess->focusMultiRowAverageLabel);
5821             m_OpsFocusProcess->focusMultiRowAverageLabel->hide();
5822             m_OpsFocusProcess->gridLayoutProcess->removeWidget(m_OpsFocusProcess->focusMultiRowAverage);
5823             m_OpsFocusProcess->focusMultiRowAverage->hide();
5824 
5825             m_OpsFocusProcess->gridLayoutProcess->removeWidget(m_OpsFocusProcess->focusGaussianSigmaLabel);
5826             m_OpsFocusProcess->focusGaussianSigmaLabel->hide();
5827             m_OpsFocusProcess->gridLayoutProcess->removeWidget(m_OpsFocusProcess->focusGaussianSigma);
5828             m_OpsFocusProcess->focusGaussianSigma->hide();
5829 
5830             m_OpsFocusProcess->gridLayoutProcess->removeWidget(m_OpsFocusProcess->focusGaussianKernelSizeLabel);
5831             m_OpsFocusProcess->focusGaussianKernelSizeLabel->hide();
5832             m_OpsFocusProcess->gridLayoutProcess->removeWidget(m_OpsFocusProcess->focusGaussianKernelSize);
5833             m_OpsFocusProcess->focusGaussianKernelSize->hide();
5834 
5835             m_OpsFocusProcess->gridLayoutProcess->removeWidget(m_OpsFocusProcess->focusThresholdLabel);
5836             m_OpsFocusProcess->focusThresholdLabel->hide();
5837             m_OpsFocusProcess->gridLayoutProcess->removeWidget(m_OpsFocusProcess->focusThreshold);
5838             m_OpsFocusProcess->focusThreshold->hide();
5839 
5840             m_OpsFocusProcess->gridLayoutProcess->removeWidget(m_OpsFocusProcess->focusStarPSFLabel);
5841             m_OpsFocusProcess->focusStarPSFLabel->hide();
5842             m_OpsFocusProcess->gridLayoutProcess->removeWidget(m_OpsFocusProcess->focusStarPSF);
5843             m_OpsFocusProcess->focusStarPSF->hide();
5844 
5845             m_OpsFocusProcess->gridLayoutProcess->removeWidget(m_OpsFocusProcess->focusUseWeights);
5846             m_OpsFocusProcess->focusUseWeights->hide();
5847 
5848             m_OpsFocusProcess->gridLayoutProcess->removeWidget(m_OpsFocusProcess->focusR2LimitLabel);
5849             m_OpsFocusProcess->focusR2LimitLabel->hide();
5850             m_OpsFocusProcess->gridLayoutProcess->removeWidget(m_OpsFocusProcess->focusR2Limit);
5851             m_OpsFocusProcess->focusR2Limit->hide();
5852 
5853             m_OpsFocusProcess->gridLayoutProcess->removeWidget(m_OpsFocusProcess->focusRefineCurveFit);
5854             m_OpsFocusProcess->focusRefineCurveFit->hide();
5855             m_OpsFocusProcess->focusRefineCurveFit->setChecked(false);
5856 
5857             // Donut buster not available
5858             m_OpsFocusProcess->focusDonut->hide();
5859             m_OpsFocusProcess->focusDonut->setChecked(false);
5860 
5861             // Set Measure to just HFR
5862             if (m_OpsFocusProcess->focusStarMeasure->count() != 1)
5863             {
5864                 m_OpsFocusProcess->focusStarMeasure->clear();
5865                 m_OpsFocusProcess->focusStarMeasure->addItem(m_StarMeasureText.at(FOCUS_STAR_HFR));
5866                 m_OpsFocusProcess->focusStarMeasure->setCurrentIndex(FOCUS_STAR_HFR);
5867             }
5868 
5869             // Add necessary widgets to the grid
5870             // For Linear the only allowable CurveFit is Quadratic
5871             m_OpsFocusProcess->gridLayoutProcess->addWidget(m_OpsFocusProcess->focusCurveFitLabel, 1, 2);
5872             m_OpsFocusProcess->focusCurveFitLabel->show();
5873             m_OpsFocusProcess->gridLayoutProcess->addWidget(m_OpsFocusProcess->focusCurveFit, 1, 3);
5874             m_OpsFocusProcess->focusCurveFit->show();
5875             if (m_OpsFocusProcess->focusCurveFit->count() != 1)
5876             {
5877                 m_OpsFocusProcess->focusCurveFit->clear();
5878                 m_OpsFocusProcess->focusCurveFit->addItem(m_CurveFitText.at(CurveFitting::FOCUS_QUADRATIC));
5879                 m_OpsFocusProcess->focusCurveFit->setCurrentIndex(CurveFitting::FOCUS_QUADRATIC);
5880             }
5881 
5882             m_OpsFocusProcess->gridLayoutProcess->addWidget(m_OpsFocusProcess->focusToleranceLabel, 3, 0);
5883             m_OpsFocusProcess->focusToleranceLabel->show();
5884             m_OpsFocusProcess->gridLayoutProcess->addWidget(m_OpsFocusProcess->focusTolerance, 3, 1);
5885             m_OpsFocusProcess->focusTolerance->show();
5886 
5887             m_OpsFocusProcess->gridLayoutProcess->addWidget(m_OpsFocusProcess->focusFramesCountLabel, 3, 2);
5888             m_OpsFocusProcess->focusFramesCountLabel->show();
5889             m_OpsFocusProcess->gridLayoutProcess->addWidget(m_OpsFocusProcess->focusFramesCount, 3, 3);
5890             m_OpsFocusProcess->focusFramesCount->show();
5891 
5892             if (m_FocusDetection == ALGORITHM_THRESHOLD)
5893             {
5894                 m_OpsFocusProcess->gridLayoutProcess->addWidget(m_OpsFocusProcess->focusThresholdLabel, 4, 0);
5895                 m_OpsFocusProcess->focusThresholdLabel->show();
5896                 m_OpsFocusProcess->gridLayoutProcess->addWidget(m_OpsFocusProcess->focusThreshold, 4, 1);
5897                 m_OpsFocusProcess->focusThreshold->show();
5898             }
5899             else if (m_FocusDetection == ALGORITHM_BAHTINOV)
5900             {
5901                 m_OpsFocusProcess->gridLayoutProcess->addWidget(m_OpsFocusProcess->focusMultiRowAverageLabel, 4, 0);
5902                 m_OpsFocusProcess->focusMultiRowAverageLabel->show();
5903                 m_OpsFocusProcess->gridLayoutProcess->addWidget(m_OpsFocusProcess->focusMultiRowAverage, 4, 1);
5904                 m_OpsFocusProcess->focusMultiRowAverage->show();
5905 
5906                 m_OpsFocusProcess->gridLayoutProcess->addWidget(m_OpsFocusProcess->focusGaussianSigmaLabel, 4, 2);
5907                 m_OpsFocusProcess->focusGaussianSigmaLabel->show();
5908                 m_OpsFocusProcess->gridLayoutProcess->addWidget(m_OpsFocusProcess->focusGaussianSigma, 4, 3);
5909                 m_OpsFocusProcess->focusGaussianSigma->show();
5910 
5911                 m_OpsFocusProcess->gridLayoutProcess->addWidget(m_OpsFocusProcess->focusGaussianKernelSizeLabel, 5, 0);
5912                 m_OpsFocusProcess->focusGaussianKernelSizeLabel->show();
5913                 m_OpsFocusProcess->gridLayoutProcess->addWidget(m_OpsFocusProcess->focusGaussianKernelSize, 5, 1);
5914                 m_OpsFocusProcess->focusGaussianKernelSize->show();
5915             }
5916 
5917             // Aberration Inspector button
5918             startAbInsB->setEnabled(canAbInsStart());
5919 
5920             // Settings changes
5921             // Disable adaptive focus
5922             m_OpsFocusSettings->focusAdaptive->setChecked(false);
5923             m_OpsFocusSettings->focusAdaptStart->setChecked(false);
5924             m_OpsFocusSettings->adaptiveFocusGroup->setEnabled(false);
5925 
5926             // Mechanics changes
5927             m_OpsFocusMechanics->focusMaxSingleStep->setEnabled(false);
5928             m_OpsFocusMechanics->focusOutSteps->setEnabled(true);
5929 
5930             // Set Walk to just Classic on 1st time through
5931             if (m_OpsFocusMechanics->focusWalk->count() != 1)
5932             {
5933                 m_OpsFocusMechanics->focusWalk->clear();
5934                 m_OpsFocusMechanics->focusWalk->addItem(m_FocusWalkText.at(FOCUS_WALK_CLASSIC));
5935                 m_OpsFocusMechanics->focusWalk->setCurrentIndex(FOCUS_WALK_CLASSIC);
5936             }
5937             break;
5938 
5939         case FOCUS_LINEAR1PASS:
5940             // Remove unused widgets from the grid and hide them
5941             m_OpsFocusProcess->gridLayoutProcess->removeWidget(m_OpsFocusProcess->focusMultiRowAverageLabel);
5942             m_OpsFocusProcess->focusMultiRowAverageLabel->hide();
5943             m_OpsFocusProcess->gridLayoutProcess->removeWidget(m_OpsFocusProcess->focusMultiRowAverage);
5944             m_OpsFocusProcess->focusMultiRowAverage->hide();
5945 
5946             m_OpsFocusProcess->gridLayoutProcess->removeWidget(m_OpsFocusProcess->focusGaussianSigmaLabel);
5947             m_OpsFocusProcess->focusGaussianSigmaLabel->hide();
5948             m_OpsFocusProcess->gridLayoutProcess->removeWidget(m_OpsFocusProcess->focusGaussianSigma);
5949             m_OpsFocusProcess->focusGaussianSigma->hide();
5950 
5951             m_OpsFocusProcess->gridLayoutProcess->removeWidget(m_OpsFocusProcess->focusGaussianKernelSizeLabel);
5952             m_OpsFocusProcess->focusGaussianKernelSizeLabel->hide();
5953             m_OpsFocusProcess->gridLayoutProcess->removeWidget(m_OpsFocusProcess->focusGaussianKernelSize);
5954             m_OpsFocusProcess->focusGaussianKernelSize->hide();
5955 
5956             m_OpsFocusProcess->gridLayoutProcess->removeWidget(m_OpsFocusProcess->focusThresholdLabel);
5957             m_OpsFocusProcess->focusThresholdLabel->hide();
5958             m_OpsFocusProcess->gridLayoutProcess->removeWidget(m_OpsFocusProcess->focusThreshold);
5959             m_OpsFocusProcess->focusThreshold->hide();
5960 
5961             m_OpsFocusProcess->gridLayoutProcess->removeWidget(m_OpsFocusProcess->focusToleranceLabel);
5962             m_OpsFocusProcess->focusToleranceLabel->hide();
5963             m_OpsFocusProcess->gridLayoutProcess->removeWidget(m_OpsFocusProcess->focusTolerance);
5964             m_OpsFocusProcess->focusTolerance->hide();
5965 
5966             // Setup Measure with all options for detection=SEP and curveFit=HYPERBOLA or PARABOLA
5967             // or just HDR otherwise. Reset on 1st time through only
5968             if (m_FocusDetection == ALGORITHM_SEP && m_CurveFit != CurveFitting::FOCUS_QUADRATIC)
5969             {
5970                 if (m_OpsFocusProcess->focusStarMeasure->count() != m_StarMeasureText.count())
5971                 {
5972                     m_OpsFocusProcess->focusStarMeasure->clear();
5973                     m_OpsFocusProcess->focusStarMeasure->addItems(m_StarMeasureText);
5974                     m_OpsFocusProcess->focusStarMeasure->setCurrentIndex(FOCUS_STAR_HFR);
5975                 }
5976             }
5977             else if (m_FocusDetection != ALGORITHM_SEP || m_CurveFit == CurveFitting::FOCUS_QUADRATIC)
5978             {
5979                 if (m_OpsFocusProcess->focusStarMeasure->count() != 1)
5980                 {
5981                     m_OpsFocusProcess->focusStarMeasure->clear();
5982                     m_OpsFocusProcess->focusStarMeasure->addItem(m_StarMeasureText.at(FOCUS_STAR_HFR));
5983                     m_OpsFocusProcess->focusStarMeasure->setCurrentIndex(FOCUS_STAR_HFR);
5984                 }
5985             }
5986 
5987             // Add necessary widgets to the grid
5988             // All Curve Fits are available - default to Hyperbola which is the best option
5989             m_OpsFocusProcess->gridLayoutProcess->addWidget(m_OpsFocusProcess->focusCurveFitLabel, 1, 2);
5990             m_OpsFocusProcess->focusCurveFitLabel->show();
5991             m_OpsFocusProcess->gridLayoutProcess->addWidget(m_OpsFocusProcess->focusCurveFit, 1, 3);
5992             m_OpsFocusProcess->focusCurveFit->show();
5993             if (m_OpsFocusProcess->focusCurveFit->count() != m_CurveFitText.count())
5994             {
5995                 m_OpsFocusProcess->focusCurveFit->clear();
5996                 m_OpsFocusProcess->focusCurveFit->addItems(m_CurveFitText);
5997                 m_OpsFocusProcess->focusCurveFit->setCurrentIndex(CurveFitting::FOCUS_HYPERBOLA);
5998             }
5999 
6000             m_OpsFocusProcess->gridLayoutProcess->addWidget(m_OpsFocusProcess->focusUseWeights, 3, 0, 1, 2); // Spans 2 columns
6001             m_OpsFocusProcess->focusUseWeights->show();
6002 
6003             m_OpsFocusProcess->gridLayoutProcess->addWidget(m_OpsFocusProcess->focusR2LimitLabel, 3, 2);
6004             m_OpsFocusProcess->focusR2LimitLabel->show();
6005             m_OpsFocusProcess->gridLayoutProcess->addWidget(m_OpsFocusProcess->focusR2Limit, 3, 3);
6006             m_OpsFocusProcess->focusR2Limit->show();
6007 
6008             m_OpsFocusProcess->gridLayoutProcess->addWidget(m_OpsFocusProcess->focusRefineCurveFit, 4, 0, 1, 2); // Spans 2 columns
6009             m_OpsFocusProcess->focusRefineCurveFit->show();
6010 
6011             m_OpsFocusProcess->gridLayoutProcess->addWidget(m_OpsFocusProcess->focusFramesCountLabel, 4, 2);
6012             m_OpsFocusProcess->focusFramesCountLabel->show();
6013             m_OpsFocusProcess->gridLayoutProcess->addWidget(m_OpsFocusProcess->focusFramesCount, 4, 3);
6014             m_OpsFocusProcess->focusFramesCount->show();
6015 
6016             if (m_FocusDetection == ALGORITHM_THRESHOLD)
6017             {
6018                 m_OpsFocusProcess->gridLayoutProcess->addWidget(m_OpsFocusProcess->focusThresholdLabel, 5, 0);
6019                 m_OpsFocusProcess->focusThresholdLabel->show();
6020                 m_OpsFocusProcess->gridLayoutProcess->addWidget(m_OpsFocusProcess->focusThreshold, 5, 1);
6021                 m_OpsFocusProcess->focusThreshold->show();
6022             }
6023             else if (m_FocusDetection == ALGORITHM_BAHTINOV)
6024             {
6025                 m_OpsFocusProcess->gridLayoutProcess->addWidget(m_OpsFocusProcess->focusMultiRowAverageLabel, 5, 0);
6026                 m_OpsFocusProcess->focusMultiRowAverageLabel->show();
6027                 m_OpsFocusProcess->gridLayoutProcess->addWidget(m_OpsFocusProcess->focusMultiRowAverage, 5, 1);
6028                 m_OpsFocusProcess->focusMultiRowAverage->show();
6029 
6030                 m_OpsFocusProcess->gridLayoutProcess->addWidget(m_OpsFocusProcess->focusGaussianSigmaLabel, 5, 2);
6031                 m_OpsFocusProcess->focusGaussianSigmaLabel->show();
6032                 m_OpsFocusProcess->gridLayoutProcess->addWidget(m_OpsFocusProcess->focusGaussianSigma, 5, 3);
6033                 m_OpsFocusProcess->focusGaussianSigma->show();
6034 
6035                 m_OpsFocusProcess->gridLayoutProcess->addWidget(m_OpsFocusProcess->focusGaussianKernelSizeLabel, 6, 0);
6036                 m_OpsFocusProcess->focusGaussianKernelSizeLabel->show();
6037                 m_OpsFocusProcess->gridLayoutProcess->addWidget(m_OpsFocusProcess->focusGaussianKernelSize, 6, 1);
6038                 m_OpsFocusProcess->focusGaussianKernelSize->show();
6039             }
6040 
6041             // Donut Buster
6042             m_OpsFocusProcess->focusDonut->show();
6043             m_OpsFocusProcess->focusDonut->setEnabled(true);
6044 
6045             // Aberration Inspector button
6046             startAbInsB->setEnabled(canAbInsStart());
6047 
6048             // Settings changes
6049             // Enable adaptive focus for Absolute focusers
6050             m_OpsFocusSettings->adaptiveFocusGroup->setEnabled(canAbsMove);
6051 
6052             // Mechanics changes
6053             // Firstly max Single Step is not used by Linear 1 Pass
6054             m_OpsFocusMechanics->focusMaxSingleStep->setEnabled(false);
6055             m_OpsFocusMechanics->focusOutSteps->setEnabled(true);
6056 
6057             // Set Walk to just Classic for Quadratic, all options otherwise
6058             if (m_CurveFit == CurveFitting::FOCUS_QUADRATIC)
6059             {
6060                 if (m_OpsFocusMechanics->focusWalk->count() != 1)
6061                 {
6062                     m_OpsFocusMechanics->focusWalk->clear();
6063                     m_OpsFocusMechanics->focusWalk->addItem(m_FocusWalkText.at(FOCUS_WALK_CLASSIC));
6064                     m_OpsFocusMechanics->focusWalk->setCurrentIndex(FOCUS_WALK_CLASSIC);
6065                 }
6066             }
6067             else
6068             {
6069                 if (m_OpsFocusMechanics->focusWalk->count() != m_FocusWalkText.count())
6070                 {
6071                     m_OpsFocusMechanics->focusWalk->clear();
6072                     m_OpsFocusMechanics->focusWalk->addItems(m_FocusWalkText);
6073                     m_OpsFocusMechanics->focusWalk->setCurrentIndex(FOCUS_WALK_CLASSIC);
6074                 }
6075             }
6076             break;
6077     }
6078 }
6079 
6080 void Focus::setCurveFit(CurveFitting::CurveFit curve)
6081 {
6082     if (m_OpsFocusProcess->focusCurveFit->currentIndex() == -1)
6083         return;
6084 
6085     static bool first = true;
6086     if (!first && m_CurveFit == curve)
6087         return;
6088 
6089     first = false;
6090 
6091     m_CurveFit = curve;
6092     setFocusAlgorithm(static_cast<Algorithm> (m_OpsFocusProcess->focusAlgorithm->currentIndex()));
6093     setUseWeights();
6094     setDonutBuster();
6095 
6096     switch(m_CurveFit)
6097     {
6098         case CurveFitting::FOCUS_QUADRATIC:
6099             m_OpsFocusProcess->focusR2Limit->setEnabled(false);
6100             m_OpsFocusProcess->focusRefineCurveFit->setChecked(false);
6101             m_OpsFocusProcess->focusRefineCurveFit->setEnabled(false);
6102             break;
6103 
6104         case CurveFitting::FOCUS_HYPERBOLA:
6105             m_OpsFocusProcess->focusR2Limit->setEnabled(true);      // focusR2Limit allowed
6106             m_OpsFocusProcess->focusRefineCurveFit->setEnabled(true);
6107             break;
6108 
6109         case CurveFitting::FOCUS_PARABOLA:
6110             m_OpsFocusProcess->focusR2Limit->setEnabled(true);      // focusR2Limit allowed
6111             m_OpsFocusProcess->focusRefineCurveFit->setEnabled(true);
6112             break;
6113 
6114         default:
6115             break;
6116     }
6117 }
6118 
6119 void Focus::setStarMeasure(StarMeasure starMeasure)
6120 {
6121     if (m_OpsFocusProcess->focusStarMeasure->currentIndex() == -1)
6122         return;
6123 
6124     static bool first = true;
6125     if (!first && m_StarMeasure == starMeasure)
6126         return;
6127 
6128     first = false;
6129 
6130     m_StarMeasure = starMeasure;
6131     setFocusAlgorithm(static_cast<Algorithm> (m_OpsFocusProcess->focusAlgorithm->currentIndex()));
6132     setUseWeights();
6133     setDonutBuster();
6134 
6135     // So what is the best estimator of scale to use? Not much to choose from analysis on the sim.
6136     // Variance is the simplest but isn't robust in the presence of outliers.
6137     // MAD is probably better but Sn and Qn are theoretically a little better as they are both efficient and
6138     // able to deal with skew. Have picked Sn.
6139     switch(m_StarMeasure)
6140     {
6141         case FOCUS_STAR_HFR:
6142             m_OptDir = CurveFitting::OPTIMISATION_MINIMISE;
6143             m_ScaleCalc = Mathematics::RobustStatistics::ScaleCalculation::SCALE_SESTIMATOR;
6144             m_FocusView->setStarsHFREnabled(true);
6145 
6146             // Don't display the PSF widgets
6147             m_OpsFocusProcess->gridLayoutProcess->removeWidget(m_OpsFocusProcess->focusStarPSFLabel);
6148             m_OpsFocusProcess->focusStarPSFLabel->hide();
6149             m_OpsFocusProcess->gridLayoutProcess->removeWidget(m_OpsFocusProcess->focusStarPSF);
6150             m_OpsFocusProcess->focusStarPSF->hide();
6151             break;
6152 
6153         case FOCUS_STAR_HFR_ADJ:
6154             m_OptDir = CurveFitting::OPTIMISATION_MINIMISE;
6155             m_ScaleCalc = Mathematics::RobustStatistics::ScaleCalculation::SCALE_SESTIMATOR;
6156             m_FocusView->setStarsHFREnabled(false);
6157 
6158             // Don't display the PSF widgets
6159             m_OpsFocusProcess->gridLayoutProcess->removeWidget(m_OpsFocusProcess->focusStarPSFLabel);
6160             m_OpsFocusProcess->focusStarPSFLabel->hide();
6161             m_OpsFocusProcess->gridLayoutProcess->removeWidget(m_OpsFocusProcess->focusStarPSF);
6162             m_OpsFocusProcess->focusStarPSF->hide();
6163             break;
6164 
6165         case FOCUS_STAR_FWHM:
6166             m_OptDir = CurveFitting::OPTIMISATION_MINIMISE;
6167             m_ScaleCalc = Mathematics::RobustStatistics::ScaleCalculation::SCALE_SESTIMATOR;
6168             // Ideally the FITSViewer would display FWHM. Until then disable HFRs to avoid confusion
6169             m_FocusView->setStarsHFREnabled(false);
6170 
6171             // Display the PSF widgets
6172             m_OpsFocusProcess->gridLayoutProcess->addWidget(m_OpsFocusProcess->focusStarPSFLabel, 2, 2);
6173             m_OpsFocusProcess->focusStarPSFLabel->show();
6174             m_OpsFocusProcess->gridLayoutProcess->addWidget(m_OpsFocusProcess->focusStarPSF, 2, 3);
6175             m_OpsFocusProcess->focusStarPSF->show();
6176             break;
6177 
6178         case FOCUS_STAR_NUM_STARS:
6179             m_OptDir = CurveFitting::OPTIMISATION_MAXIMISE;
6180             m_ScaleCalc = Mathematics::RobustStatistics::ScaleCalculation::SCALE_SESTIMATOR;
6181             m_FocusView->setStarsHFREnabled(true);
6182 
6183             // Don't display the PSF widgets
6184             m_OpsFocusProcess->gridLayoutProcess->removeWidget(m_OpsFocusProcess->focusStarPSFLabel);
6185             m_OpsFocusProcess->focusStarPSFLabel->hide();
6186             m_OpsFocusProcess->gridLayoutProcess->removeWidget(m_OpsFocusProcess->focusStarPSF);
6187             m_OpsFocusProcess->focusStarPSF->hide();
6188             break;
6189 
6190         case FOCUS_STAR_FOURIER_POWER:
6191             m_OptDir = CurveFitting::OPTIMISATION_MAXIMISE;
6192             m_ScaleCalc = Mathematics::RobustStatistics::ScaleCalculation::SCALE_SESTIMATOR;
6193             m_FocusView->setStarsHFREnabled(true);
6194 
6195             // Don't display the PSF widgets
6196             m_OpsFocusProcess->gridLayoutProcess->removeWidget(m_OpsFocusProcess->focusStarPSFLabel);
6197             m_OpsFocusProcess->focusStarPSFLabel->hide();
6198             m_OpsFocusProcess->gridLayoutProcess->removeWidget(m_OpsFocusProcess->focusStarPSF);
6199             m_OpsFocusProcess->focusStarPSF->hide();
6200             break;
6201 
6202         default:
6203             break;
6204     }
6205 }
6206 
6207 void Focus::setStarPSF(StarPSF starPSF)
6208 {
6209     m_StarPSF = starPSF;
6210 }
6211 
6212 void Focus::setStarUnits(StarUnits starUnits)
6213 {
6214     m_StarUnits = starUnits;
6215 }
6216 
6217 void Focus::setWalk(FocusWalk walk)
6218 {
6219     if (m_OpsFocusMechanics->focusWalk->currentIndex() == -1)
6220         return;
6221 
6222     static bool first = true;
6223     if (!first && m_FocusWalk == walk)
6224         return;
6225 
6226     first = false;
6227 
6228     m_FocusWalk = walk;
6229 
6230     switch(m_FocusWalk)
6231     {
6232         case FOCUS_WALK_CLASSIC:
6233             m_OpsFocusMechanics->gridLayoutMechanics->replaceWidget(m_OpsFocusMechanics->focusNumStepsLabel,
6234                     m_OpsFocusMechanics->focusOutStepsLabel);
6235             m_OpsFocusMechanics->focusNumStepsLabel->hide();
6236             m_OpsFocusMechanics->focusOutStepsLabel->show();
6237             m_OpsFocusMechanics->gridLayoutMechanics->replaceWidget(m_OpsFocusMechanics->focusNumSteps,
6238                     m_OpsFocusMechanics->focusOutSteps);
6239             m_OpsFocusMechanics->focusNumSteps->hide();
6240             m_OpsFocusMechanics->focusOutSteps->show();
6241             break;
6242 
6243         case FOCUS_WALK_FIXED_STEPS:
6244         case FOCUS_WALK_CFZ_SHUFFLE:
6245             m_OpsFocusMechanics->gridLayoutMechanics->replaceWidget(m_OpsFocusMechanics->focusOutStepsLabel,
6246                     m_OpsFocusMechanics->focusNumStepsLabel);
6247             m_OpsFocusMechanics->focusOutStepsLabel->hide();
6248             m_OpsFocusMechanics->focusNumStepsLabel->show();
6249             m_OpsFocusMechanics->gridLayoutMechanics->replaceWidget(m_OpsFocusMechanics->focusOutSteps,
6250                     m_OpsFocusMechanics->focusNumSteps);
6251             m_OpsFocusMechanics->focusOutSteps->hide();
6252             m_OpsFocusMechanics->focusNumSteps->show();
6253             break;
6254 
6255         default:
6256             break;
6257     }
6258     setDonutBuster();
6259 }
6260 
6261 double Focus::getStarUnits(const StarMeasure starMeasure, const StarUnits starUnits)
6262 {
6263     if (starUnits == FOCUS_UNITS_PIXEL || starMeasure == FOCUS_STAR_NUM_STARS || starMeasure == FOCUS_STAR_FOURIER_POWER)
6264         return 1.0;
6265     if (m_CcdPixelSizeX <= 0.0 || m_FocalLength <= 0.0)
6266         return 1.0;
6267 
6268     // Convert to arcsecs from pixels. PixelSize / FocalLength * 206265
6269     return m_CcdPixelSizeX / m_FocalLength * 206.265;
6270 }
6271 
6272 void Focus::calcCFZ()
6273 {
6274     double cfzMicrons, cfzSteps;
6275     double cfzCameraSteps = calcCameraCFZ() / m_CFZUI->focusCFZStepSize->value();
6276 
6277     switch(static_cast<Focus::CFZAlgorithm> (m_CFZUI->focusCFZAlgorithm->currentIndex()))
6278     {
6279         case Focus::FOCUS_CFZ_CLASSIC:
6280             // CFZ = 4.88 t λ f2
6281             cfzMicrons = 4.88f * m_CFZUI->focusCFZTolerance->value() * m_CFZUI->focusCFZWavelength->value() / 1000.0f *
6282                          pow(m_CFZUI->focusCFZFNumber->value(), 2.0f);
6283             cfzSteps = cfzMicrons / m_CFZUI->focusCFZStepSize->value();
6284             m_cfzSteps = std::round(std::max(cfzSteps, cfzCameraSteps));
6285             m_CFZUI->focusCFZFormula->setText("CFZ = 4.88 t λ f²");
6286             m_CFZUI->focusCFZ->setText(QString("%1 μm").arg(cfzMicrons, 0, 'f', 0));
6287             m_CFZUI->focusCFZSteps->setText(QString("%1 steps").arg(cfzSteps, 0, 'f', 0));
6288             m_CFZUI->focusCFZCameraSteps->setText(QString("%1 steps").arg(cfzCameraSteps, 0, 'f', 0));
6289             m_CFZUI->focusCFZFinal->setText(QString("%1 steps").arg(m_cfzSteps, 0, 'f', 0));
6290 
6291             // Remove widgets not relevant to this algo
6292             m_CFZUI->gridLayoutCFZ->removeWidget(m_CFZUI->focusCFZTauLabel);
6293             m_CFZUI->focusCFZTauLabel->hide();
6294             m_CFZUI->gridLayoutCFZ->removeWidget(m_CFZUI->focusCFZTau);
6295             m_CFZUI->focusCFZTau->hide();
6296             m_CFZUI->gridLayoutCFZ->removeWidget(m_CFZUI->focusCFZSeeingLabel);
6297             m_CFZUI->focusCFZSeeingLabel->hide();
6298             m_CFZUI->gridLayoutCFZ->removeWidget(m_CFZUI->focusCFZSeeing);
6299             m_CFZUI->focusCFZSeeing->hide();
6300 
6301             // Add necessary widgets to the grid
6302             m_CFZUI->gridLayoutCFZ->addWidget(m_CFZUI->focusCFZToleranceLabel, 1, 0);
6303             m_CFZUI->focusCFZToleranceLabel->show();
6304             m_CFZUI->gridLayoutCFZ->addWidget(m_CFZUI->focusCFZTolerance, 1, 1);
6305             m_CFZUI->focusCFZTolerance->show();
6306             m_CFZUI->gridLayoutCFZ->addWidget(m_CFZUI->focusCFZWavelengthLabel, 2, 0);
6307             m_CFZUI->focusCFZWavelengthLabel->show();
6308             m_CFZUI->gridLayoutCFZ->addWidget(m_CFZUI->focusCFZWavelength, 2, 1);
6309             m_CFZUI->focusCFZWavelength->show();
6310             break;
6311 
6312         case Focus::FOCUS_CFZ_WAVEFRONT:
6313             // CFZ = 4 t λ f2
6314             cfzMicrons = 4.0f * m_CFZUI->focusCFZTolerance->value() * m_CFZUI->focusCFZWavelength->value() / 1000.0f * pow(
6315                              m_CFZUI->focusCFZFNumber->value(),
6316                              2.0f);
6317             cfzSteps = cfzMicrons / m_CFZUI->focusCFZStepSize->value();
6318             m_cfzSteps = std::round(std::max(cfzSteps, cfzCameraSteps));
6319             m_CFZUI->focusCFZFormula->setText("CFZ = 4 t λ f²");
6320             m_CFZUI->focusCFZ->setText(QString("%1 μm").arg(cfzMicrons, 0, 'f', 0));
6321             m_CFZUI->focusCFZSteps->setText(QString("%1 steps").arg(cfzSteps, 0, 'f', 0));
6322             m_CFZUI->focusCFZCameraSteps->setText(QString("%1 steps").arg(cfzCameraSteps, 0, 'f', 0));
6323             m_CFZUI->focusCFZFinal->setText(QString("%1 steps").arg(m_cfzSteps, 0, 'f', 0));
6324 
6325             // Remove widgets not relevant to this algo
6326             m_CFZUI->gridLayoutCFZ->removeWidget(m_CFZUI->focusCFZTauLabel);
6327             m_CFZUI->focusCFZTauLabel->hide();
6328             m_CFZUI->gridLayoutCFZ->removeWidget(m_CFZUI->focusCFZTau);
6329             m_CFZUI->focusCFZTau->hide();
6330             m_CFZUI->gridLayoutCFZ->removeWidget(m_CFZUI->focusCFZSeeingLabel);
6331             m_CFZUI->focusCFZSeeingLabel->hide();
6332             m_CFZUI->gridLayoutCFZ->removeWidget(m_CFZUI->focusCFZSeeing);
6333             m_CFZUI->focusCFZSeeing->hide();
6334 
6335             // Add necessary widgets to the grid
6336             m_CFZUI->gridLayoutCFZ->addWidget(m_CFZUI->focusCFZToleranceLabel, 1, 0);
6337             m_CFZUI->focusCFZToleranceLabel->show();
6338             m_CFZUI->gridLayoutCFZ->addWidget(m_CFZUI->focusCFZTolerance, 1, 1);
6339             m_CFZUI->focusCFZTolerance->show();
6340             m_CFZUI->gridLayoutCFZ->addWidget(m_CFZUI->focusCFZWavelengthLabel, 2, 0);
6341             m_CFZUI->focusCFZWavelengthLabel->show();
6342             m_CFZUI->gridLayoutCFZ->addWidget(m_CFZUI->focusCFZWavelength, 2, 1);
6343             m_CFZUI->focusCFZWavelength->show();
6344             break;
6345 
6346         case Focus::FOCUS_CFZ_GOLD:
6347             // CFZ = 0.00225 √τ θ f² A
6348             cfzMicrons = 0.00225f * pow(m_CFZUI->focusCFZTau->value(), 0.5f) * m_CFZUI->focusCFZSeeing->value()
6349                          * pow(m_CFZUI->focusCFZFNumber->value(), 2.0f) * m_CFZUI->focusCFZAperture->value();
6350             cfzSteps = cfzMicrons / m_CFZUI->focusCFZStepSize->value();
6351             m_cfzSteps = std::round(std::max(cfzSteps, cfzCameraSteps));
6352             m_CFZUI->focusCFZFormula->setText("CFZ = 0.00225 √τ θ f² A");
6353             m_CFZUI->focusCFZ->setText(QString("%1 μm").arg(cfzMicrons, 0, 'f', 0));
6354             m_CFZUI->focusCFZSteps->setText(QString("%1 steps").arg(cfzSteps, 0, 'f', 0));
6355             m_CFZUI->focusCFZCameraSteps->setText(QString("%1 steps").arg(cfzCameraSteps, 0, 'f', 0));
6356             m_CFZUI->focusCFZFinal->setText(QString("%1 steps").arg(m_cfzSteps, 0, 'f', 0));
6357 
6358             // Remove widgets not relevant to this algo
6359             m_CFZUI->gridLayoutCFZ->removeWidget(m_CFZUI->focusCFZToleranceLabel);
6360             m_CFZUI->focusCFZToleranceLabel->hide();
6361             m_CFZUI->gridLayoutCFZ->removeWidget(m_CFZUI->focusCFZTolerance);
6362             m_CFZUI->focusCFZTolerance->hide();
6363             m_CFZUI->gridLayoutCFZ->removeWidget(m_CFZUI->focusCFZWavelengthLabel);
6364             m_CFZUI->focusCFZWavelengthLabel->hide();
6365             m_CFZUI->gridLayoutCFZ->removeWidget(m_CFZUI->focusCFZWavelength);
6366             m_CFZUI->focusCFZWavelength->hide();
6367 
6368             // Add necessary widgets to the grid
6369             m_CFZUI->gridLayoutCFZ->addWidget(m_CFZUI->focusCFZTauLabel, 1, 0);
6370             m_CFZUI->focusCFZTauLabel->show();
6371             m_CFZUI->gridLayoutCFZ->addWidget(m_CFZUI->focusCFZTau, 1, 1);
6372             m_CFZUI->focusCFZTau->show();
6373             m_CFZUI->gridLayoutCFZ->addWidget(m_CFZUI->focusCFZSeeingLabel, 2, 0);
6374             m_CFZUI->focusCFZSeeingLabel->show();
6375             m_CFZUI->gridLayoutCFZ->addWidget(m_CFZUI->focusCFZSeeing, 2, 1);
6376             m_CFZUI->focusCFZSeeing->show();
6377             break;
6378     }
6379     if (linearFocuser != nullptr && linearFocuser->isDone())
6380         emit drawCFZ(linearFocuser->solution(), linearFocuser->solutionValue(), m_cfzSteps,
6381                      m_CFZUI->focusCFZDisplayVCurve->isChecked());
6382 }
6383 
6384 // Calculate the CFZ of the camera in microns using
6385 // CFZcamera = pixel_size * f^2 * A
6386 // pixel_size in microns and A in mm so divide A by 1000.
6387 double Focus::calcCameraCFZ()
6388 {
6389     return m_CcdPixelSizeX * pow(m_CFZUI->focusCFZFNumber->value(), 2.0) * m_CFZUI->focusCFZAperture->value() / 1000.0;
6390 }
6391 
6392 void Focus::wavelengthChanged()
6393 {
6394     // The static data for the wavelength held against the filter has been updated so use the new value
6395     if (m_FilterManager)
6396     {
6397         m_CFZUI->focusCFZWavelength->setValue(m_FilterManager->getFilterWavelength(filter()));
6398         calcCFZ();
6399     }
6400 }
6401 
6402 void Focus::resetCFZToOT()
6403 {
6404     // Set the aperture and focal ratio for the scope on the connected optical train
6405     m_CFZUI->focusCFZFNumber->setValue(m_FocalRatio);
6406     m_CFZUI->focusCFZAperture->setValue(m_Aperture);
6407 
6408     // Set the wavelength from the active filter
6409     if (m_FilterManager)
6410     {
6411         if (m_CFZUI->focusCFZWavelength->value() != m_FilterManager->getFilterWavelength(filter()))
6412             m_CFZUI->focusCFZWavelength->setValue(m_FilterManager->getFilterWavelength(filter()));
6413     }
6414     calcCFZ();
6415 }
6416 
6417 // Load up the Focus Advisor recommendations
6418 void Focus::focusAdvisorSetup()
6419 {
6420     bool longFL = m_FocalLength > 1500;
6421     double imageScale = getStarUnits(FOCUS_STAR_HFR, FOCUS_UNITS_ARCSEC);
6422     QString str;
6423     bool centralObstruction = scopeHasObstruction(m_ScopeType);
6424 
6425     // Set the FA label based on the optical train
6426     m_AdvisorUI->focusAdvLabel->setText(QString("Recommendations: %1 FL=%2 ImageScale=%3")
6427                                         .arg(m_ScopeType).arg(m_FocalLength).arg(imageScale, 0, 'f', 2));
6428 
6429     // Step Size - Recommend CFZ
6430     m_AdvisorUI->focusAdvSteps->setValue(m_cfzSteps);
6431 
6432     // Number steps - start with 5
6433     m_AdvisorUI->focusAdvOutStepMult->setValue(5);
6434     if (centralObstruction)
6435         str = "A good figure to start with is 5. You have a scope with a central obstruction that turns stars to donuts when\n"
6436               "they are out of focus. When this happens the system will struggle to identify stars correctly. To avoid this reduce\n"
6437               "either the step size or the number of steps.\n\n"
6438               "To check this situation, start at focus and move away by 'step size' * 'number of steps' steps. Take a focus frame\n"
6439               "and zoom in on the fitsviewer to see whether stars appear as stars or donuts.";
6440     else
6441         str = "A good figure to start with is 5.";
6442     m_AdvisorUI->focusAdvOutStepMult->setToolTip(str);
6443     m_AdvisorUI->focusAdvOutStepMultLabel->setToolTip(str);
6444 
6445     // Camera options: exposure and bining
6446     str = "Camera & Filter Wheel Parameters:\n";
6447     if (longFL)
6448     {
6449         FAExposure = 4.0;
6450         str.append("Exp=4.0\n");
6451     }
6452     else
6453     {
6454         FAExposure = 2.0;
6455         str.append("Exp=2.0\n");
6456     }
6457 
6458     FABinning = "";
6459     if (focusBinning->isEnabled())
6460     {
6461         // Only try and update the binning field if camera supports it (binning field enabled)
6462         QString binTarget = (imageScale < 1.0) ? "2x2" : "1x1";
6463 
6464         for (int i = 0; i < focusBinning->count(); i++)
6465         {
6466             if (focusBinning->itemText(i) == binTarget)
6467             {
6468                 FABinning = binTarget;
6469                 str.append(QString("Bin=%1\n").arg(binTarget));
6470                 break;
6471             }
6472         }
6473     }
6474 
6475     str.append("Gain ***Set Manually to Unity Gain***\n");
6476     str.append("Filter ***Set Manually***");
6477     m_AdvisorUI->focusAdvCameraLabel->setToolTip(str);
6478 
6479     // Settings
6480     str = "Settings Parameters:\n";
6481     FAAutoSelectStar = false;
6482     str.append("Auto Select Star=off\n");
6483 
6484     FADarkFrame = false;
6485     str.append("Dark Frame=off\n");
6486 
6487     FAFullFieldInnerRadius = 0.0;
6488     FAFullFieldOuterRadius = 80.0;
6489     str.append("Ring Mask 0%-80%\n");
6490 
6491     // Suspend Guilding, Guide Settle and Display Units won't affect Autofocus so don't set
6492 
6493     FAAdaptiveFocus = false;
6494     str.append("Adaptive Focus=off\n");
6495 
6496     FAAdaptStartPos = false;
6497     str.append("Adapt Start Pos=off");
6498 
6499     m_AdvisorUI->focusAdvSettingsLabel->setToolTip(str);
6500 
6501     // Process
6502     str = "Process Parameters:\n";
6503     FAFocusDetection = ALGORITHM_SEP;
6504     str.append("Detection=SEP\n");
6505 
6506     FAFocusSEPProfile = "";
6507     for (int i = 0; i < m_OpsFocusProcess->focusSEPProfile->count(); i++)
6508     {
6509         if (m_OpsFocusProcess->focusSEPProfile->itemText(i) == "1-Focus-Default")
6510         {
6511             FAFocusSEPProfile = "1-Focus-Default";
6512             str.append(QString("SEP Profile=%1\n").arg(FAFocusSEPProfile));
6513             break;
6514         }
6515     }
6516 
6517     FAFocusAlgorithm = FOCUS_LINEAR1PASS;
6518     str.append("Algorithm=Linear 1 Pass\n");
6519 
6520     FACurveFit = CurveFitting::FOCUS_HYPERBOLA;
6521     str.append("Curve Fit=Hyperbola\n");
6522 
6523     FAStarMeasure = FOCUS_STAR_HFR;
6524     str.append("Measure=HFR\n");
6525 
6526     FAUseWeights = true;
6527     str.append("Use Weights=on\n");
6528 
6529     FAFocusR2Limit = 0.8;
6530     str.append("R² Limit=0.8\n");
6531 
6532     FAFocusRefineCurveFit = true;
6533     str.append("Refine Curve Fit=on\n");
6534 
6535     FAFocusFramesCount = 1;
6536     str.append("Average Over=1");
6537 
6538     m_AdvisorUI->focusAdvProcessLabel->setToolTip(str);
6539 
6540     // Mechanics
6541     str = "Mechanics Parameters:\n";
6542     FAFocusWalk = FOCUS_WALK_CLASSIC;
6543     str.append("Walk=Classic\n");
6544 
6545     FAFocusSettleTime = 1.0;
6546     str.append("Focuser Settle=1\n");
6547 
6548     // Set Max travel to max value - no need to limit it
6549     FAFocusMaxTravel = m_OpsFocusMechanics->focusMaxTravel->maximum();
6550     str.append(QString("Max Travel=%1\n").arg(FAFocusMaxTravel));
6551 
6552     // Driver Backlash and AF Overscan are dealt with separately so inform user to do this
6553     str.append("Backlash ***Set Manually***\n");
6554     str.append("AF Overscan ***Set Manually***\n");
6555 
6556     FAFocusCaptureTimeout = 30;
6557     str.append(QString("Capture Timeout=%1\n").arg(FAFocusCaptureTimeout));
6558 
6559     FAFocusMotionTimeout = 30;
6560     str.append(QString("Motion Timeout=%1").arg(FAFocusMotionTimeout));
6561 
6562     m_AdvisorUI->focusAdvMechanicsLabel->setToolTip(str);
6563 }
6564 
6565 // Focus Advisor help popup
6566 void Focus::focusAdvisorHelp()
6567 {
6568     QString str = i18n("Focus Advisor (FA) is designed to help you with focus parameters.\n"
6569                        "It will not necessarily give you the perfect combination of parameters, you will "
6570                        "need to experiment yourself, but it will give you a basic set of parameters to "
6571                        "achieve focus.\n\n"
6572                        "FA will recommend values for the majority of parameters. A few, however, will need "
6573                        "extra work from you to setup. These are identified below along with a basic explanation "
6574                        "of how to set them.\n\n"
6575                        "The first step is to set backlash. Your focuser manual will likely explain how to do "
6576                        "this. Once you have a value for backlash for your system, set either the Backlash field "
6577                        "to have the driver perform backlash compensation or the AF Overscan field to have Autofocus "
6578                        "perform backlash compensation. Set only one field and set the other to 0.\n\n"
6579                        "The second step is to set Step Size. This can be defaulted from the Critical Focus Zone (CFZ) "
6580                        "for your equipment - so configure this now in the CFZ tab.\n\n"
6581                        "The third step is to set the Out Step Multiple. Start with the suggested default.");
6582 
6583     if (scopeHasObstruction(m_ScopeType))
6584         str.append(i18n(" You have a scope with a central obstruction so be careful not to move too far away from "
6585                         "focus as stars will appear as donuts and will not be detected properly. Experiment by "
6586                         "finding focus and moving Step Size * Out Step Multiple ticks away from focus and take a "
6587                         "focus frame. Zoom in to observe star detection. If it is poor then move the focuser back "
6588                         "towards focus until star detection is acceptable. Adjust Out Step Multiple to correspond to "
6589                         "this range of focuser motion."));
6590 
6591     str.append(i18n("\n\nThe fourth step is to set the remaining focus parameters to sensible values. Focus Advisor "
6592                     "will suggest values for 4 categories of parameters. Check the associated Update box to "
6593                     "accept these recommendations when you press Update Params.\n"
6594                     "1. Camera Properties - Note you need to ensure Gain is set appropriately, e.g. unity gain.\n"
6595                     "2. Focus Settings (Options Popup): These all have recommendations.\n"
6596                     "3. Focus Process (Options Popup): These all have recommendations.\n"
6597                     "4. Focus Mechanics (Options Popup): Note Step Size and Out Step Multiple are dealt with above.\n\n"
6598                     "Now move the focuser to approximate focus and select a broadband filter, e.g. Luminance\n"
6599                     "You are now ready to start an Autofocus run."));
6600 
6601     KMessageBox::information(nullptr, str, i18n("Focus Advisor"));
6602 }
6603 
6604 // Action the focus params recommendations
6605 void Focus::focusAdvisorAction()
6606 {
6607     if (m_AdvisorUI->focusAdvStepSize->isChecked())
6608         m_OpsFocusMechanics->focusTicks->setValue(m_AdvisorUI->focusAdvSteps->value());
6609 
6610     if (m_AdvisorUI->focusAdvOutStepMultiple->isChecked())
6611         m_OpsFocusMechanics->focusOutSteps->setValue(m_AdvisorUI->focusAdvOutStepMult->value());
6612 
6613     if (m_AdvisorUI->focusAdvCamera->isChecked())
6614     {
6615         focusExposure->setValue(FAExposure);
6616         if (focusBinning->isEnabled() && FABinning != "")
6617             // Only try and update the binning field if camera supports it (binning field enabled)
6618             focusBinning->setCurrentText(FABinning);
6619     }
6620 
6621     if (m_AdvisorUI->focusAdvSettingsTab->isChecked())
6622     {
6623         // Settings
6624         m_OpsFocusSettings->useFocusDarkFrame->setChecked(FADarkFrame);
6625         m_OpsFocusSettings->focusUseFullField->setChecked(true);
6626         m_OpsFocusSettings->focusAutoStarEnabled->setChecked(FAAutoSelectStar);
6627         m_OpsFocusSettings->focusFullFieldInnerRadius->setValue(FAFullFieldInnerRadius);
6628         m_OpsFocusSettings->focusFullFieldOuterRadius->setValue(FAFullFieldOuterRadius);
6629         m_OpsFocusSettings->focusRingMaskRB->setChecked(true);
6630         m_OpsFocusSettings->focusAdaptive->setChecked(FAAdaptiveFocus);
6631         m_OpsFocusSettings->focusAdaptStart->setChecked(FAAdaptStartPos);
6632     }
6633 
6634     if (m_AdvisorUI->focusAdvProcessTab->isChecked())
6635     {
6636         // Process
6637         m_OpsFocusProcess->focusDetection->setCurrentIndex(FAFocusDetection);
6638         if (FAFocusSEPProfile != "")
6639             m_OpsFocusProcess->focusSEPProfile->setCurrentText(FAFocusSEPProfile);
6640         m_OpsFocusProcess->focusAlgorithm->setCurrentIndex(FAFocusAlgorithm);
6641         m_OpsFocusProcess->focusCurveFit->setCurrentIndex(FACurveFit);
6642         m_OpsFocusProcess->focusStarMeasure->setCurrentIndex(FAStarMeasure);
6643         m_OpsFocusProcess->focusUseWeights->setChecked(FAUseWeights);
6644         m_OpsFocusProcess->focusR2Limit->setValue(FAFocusR2Limit);
6645         m_OpsFocusProcess->focusRefineCurveFit->setChecked(FAFocusRefineCurveFit);
6646         m_OpsFocusProcess->focusFramesCount->setValue(FAFocusFramesCount);
6647     }
6648 
6649     if (m_AdvisorUI->focusAdvMechanicsTab->isChecked())
6650     {
6651         m_OpsFocusMechanics->focusWalk->setCurrentIndex(FAFocusWalk);
6652         m_OpsFocusMechanics->focusSettleTime->setValue(FAFocusSettleTime);
6653         m_OpsFocusMechanics->focusMaxTravel->setValue(FAFocusMaxTravel);
6654         m_OpsFocusMechanics->focusCaptureTimeout->setValue(FAFocusCaptureTimeout);
6655         m_OpsFocusMechanics->focusMotionTimeout->setValue(FAFocusMotionTimeout);
6656     }
6657 }
6658 
6659 // Returns whether or not the passed in scopeType has a central obstruction or not. The scopeTypes
6660 // are defined in the equipmentWriter code. It would be better, probably, if that code included
6661 // a flag for central obstruction, rather than hard coding strings for the scopeType that are compared
6662 // in this routine.
6663 bool Focus::scopeHasObstruction(QString scopeType)
6664 {
6665     if (scopeType == "Refractor" || scopeType == "Kutter (Schiefspiegler)")
6666         return false;
6667     else
6668         return true;
6669 }
6670 
6671 void Focus::setState(FocusState newState)
6672 {
6673     qCDebug(KSTARS_EKOS_FOCUS) << "Focus State changes from" << getFocusStatusString(m_state) << "to" << getFocusStatusString(
6674                                    newState);
6675     m_state = newState;
6676     emit newStatus(m_state);
6677 }
6678 
6679 void Focus::initView()
6680 {
6681     m_FocusView.reset(new FITSView(focusingWidget, FITS_FOCUS));
6682     m_FocusView->setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Expanding);
6683     m_FocusView->setBaseSize(focusingWidget->size());
6684     m_FocusView->createFloatingToolBar();
6685     QVBoxLayout *vlayout = new QVBoxLayout();
6686     vlayout->addWidget(m_FocusView.get());
6687     focusingWidget->setLayout(vlayout);
6688     connect(m_FocusView.get(), &FITSView::trackingStarSelected, this, &Ekos::Focus::focusStarSelected, Qt::UniqueConnection);
6689     m_FocusView->setStarsEnabled(true);
6690     m_FocusView->setStarsHFREnabled(true);
6691 }
6692 
6693 QVariantMap Focus::getAllSettings() const
6694 {
6695     QVariantMap settings;
6696 
6697     // All Combo Boxes
6698     for (auto &oneWidget : findChildren<QComboBox*>())
6699         settings.insert(oneWidget->objectName(), oneWidget->currentText());
6700 
6701     // All Double Spin Boxes
6702     for (auto &oneWidget : findChildren<QDoubleSpinBox*>())
6703         settings.insert(oneWidget->objectName(), oneWidget->value());
6704 
6705     // All Spin Boxes
6706     for (auto &oneWidget : findChildren<QSpinBox*>())
6707         settings.insert(oneWidget->objectName(), oneWidget->value());
6708 
6709     // All Checkboxes
6710     for (auto &oneWidget : findChildren<QCheckBox*>())
6711         settings.insert(oneWidget->objectName(), oneWidget->isChecked());
6712 
6713     // All Checkable Groupboxes
6714     for (auto &oneWidget : findChildren<QGroupBox*>())
6715         if (oneWidget->isCheckable())
6716             settings.insert(oneWidget->objectName(), oneWidget->isChecked());
6717 
6718     // All Checkable Groupboxes
6719     for (auto &oneWidget : findChildren<QGroupBox*>())
6720         if (oneWidget->isCheckable())
6721             settings.insert(oneWidget->objectName(), oneWidget->isChecked());
6722 
6723     // All Splitters
6724     for (auto &oneWidget : findChildren<QSplitter*>())
6725         settings.insert(oneWidget->objectName(), QString::fromUtf8(oneWidget->saveState().toBase64()));
6726 
6727     // All Radio Buttons
6728     for (auto &oneWidget : findChildren<QRadioButton*>())
6729         settings.insert(oneWidget->objectName(), oneWidget->isChecked());
6730 
6731     return settings;
6732 }
6733 
6734 void Focus::setAllSettings(const QVariantMap &settings)
6735 {
6736     // Disconnect settings that we don't end up calling syncSettings while
6737     // performing the changes.
6738     disconnectSyncSettings();
6739 
6740     for (auto &name : settings.keys())
6741     {
6742         // Combo
6743         auto comboBox = findChild<QComboBox*>(name);
6744         if (comboBox)
6745         {
6746             syncControl(settings, name, comboBox);
6747             continue;
6748         }
6749 
6750         // Double spinbox
6751         auto doubleSpinBox = findChild<QDoubleSpinBox*>(name);
6752         if (doubleSpinBox)
6753         {
6754             syncControl(settings, name, doubleSpinBox);
6755             continue;
6756         }
6757 
6758         // spinbox
6759         auto spinBox = findChild<QSpinBox*>(name);
6760         if (spinBox)
6761         {
6762             syncControl(settings, name, spinBox);
6763             continue;
6764         }
6765 
6766         // checkbox
6767         auto checkbox = findChild<QCheckBox*>(name);
6768         if (checkbox)
6769         {
6770             syncControl(settings, name, checkbox);
6771             continue;
6772         }
6773 
6774         // Splitters
6775         auto splitter = findChild<QSplitter*>(name);
6776         if (splitter)
6777         {
6778             syncControl(settings, name, splitter);
6779             continue;
6780         }
6781 
6782         // Radio button
6783         auto radioButton = findChild<QRadioButton*>(name);
6784         if (radioButton)
6785         {
6786             syncControl(settings, name, radioButton);
6787             continue;
6788         }
6789     }
6790 
6791     // Sync to options
6792     for (auto &key : settings.keys())
6793     {
6794         auto value = settings[key];
6795         // Save immediately
6796         Options::self()->setProperty(key.toLatin1(), value);
6797 
6798         m_Settings[key] = value;
6799         m_GlobalSettings[key] = value;
6800     }
6801 
6802     emit settingsUpdated(getAllSettings());
6803 
6804     // Save to optical train specific settings as well
6805     OpticalTrainSettings::Instance()->setOpticalTrainID(OpticalTrainManager::Instance()->id(opticalTrainCombo->currentText()));
6806     OpticalTrainSettings::Instance()->setOneSetting(OpticalTrainSettings::Focus, m_Settings);
6807 
6808     // Restablish connections
6809     connectSyncSettings();
6810 
6811     // Once settings have been loaded run through routines to set state variables
6812     m_CurveFit = static_cast<CurveFitting::CurveFit> (m_OpsFocusProcess->focusCurveFit->currentIndex());
6813     setFocusDetection(static_cast<StarAlgorithm> (m_OpsFocusProcess->focusDetection->currentIndex()));
6814     setCurveFit(static_cast<CurveFitting::CurveFit>(m_OpsFocusProcess->focusCurveFit->currentIndex()));
6815     setStarMeasure(static_cast<StarMeasure>(m_OpsFocusProcess->focusStarMeasure->currentIndex()));
6816     setWalk(static_cast<FocusWalk>(m_OpsFocusMechanics->focusWalk->currentIndex()));
6817 }
6818 
6819 bool Focus::syncControl(const QVariantMap &settings, const QString &key, QWidget * widget)
6820 {
6821     QSpinBox *pSB = nullptr;
6822     QDoubleSpinBox *pDSB = nullptr;
6823     QCheckBox *pCB = nullptr;
6824     QComboBox *pComboBox = nullptr;
6825     QSplitter *pSplitter = nullptr;
6826     QRadioButton *pRadioButton = nullptr;
6827     bool ok = true;
6828 
6829     if ((pSB = qobject_cast<QSpinBox *>(widget)))
6830     {
6831         const int value = settings[key].toInt(&ok);
6832         if (ok)
6833         {
6834             pSB->setValue(value);
6835             return true;
6836         }
6837     }
6838     else if ((pDSB = qobject_cast<QDoubleSpinBox *>(widget)))
6839     {
6840         const double value = settings[key].toDouble(&ok);
6841         if (ok)
6842         {
6843             pDSB->setValue(value);
6844             return true;
6845         }
6846     }
6847     else if ((pCB = qobject_cast<QCheckBox *>(widget)))
6848     {
6849         const bool value = settings[key].toBool();
6850         pCB->setChecked(value);
6851         return true;
6852     }
6853     // ONLY FOR STRINGS, not INDEX
6854     else if ((pComboBox = qobject_cast<QComboBox *>(widget)))
6855     {
6856         const QString value = settings[key].toString();
6857         pComboBox->setCurrentText(value);
6858         return true;
6859     }
6860     else if ((pSplitter = qobject_cast<QSplitter *>(widget)))
6861     {
6862         const auto value = QByteArray::fromBase64(settings[key].toString().toUtf8());
6863         pSplitter->restoreState(value);
6864         return true;
6865     }
6866     else if ((pRadioButton = qobject_cast<QRadioButton *>(widget)))
6867     {
6868         const bool value = settings[key].toBool();
6869         pRadioButton->setChecked(value);
6870         return true;
6871     }
6872 
6873     return false;
6874 };
6875 
6876 void Focus::setupOpticalTrainManager()
6877 {
6878     connect(OpticalTrainManager::Instance(), &OpticalTrainManager::updated, this, &Focus::refreshOpticalTrain);
6879     connect(trainB, &QPushButton::clicked, this, [this]()
6880     {
6881         OpticalTrainManager::Instance()->openEditor(opticalTrainCombo->currentText());
6882     });
6883     connect(opticalTrainCombo, QOverload<int>::of(&QComboBox::currentIndexChanged), this, [this](int index)
6884     {
6885         ProfileSettings::Instance()->setOneSetting(ProfileSettings::FocusOpticalTrain,
6886                 OpticalTrainManager::Instance()->id(opticalTrainCombo->itemText(index)));
6887         refreshOpticalTrain();
6888         emit trainChanged();
6889     });
6890 }
6891 
6892 void Focus::refreshOpticalTrain()
6893 {
6894     opticalTrainCombo->blockSignals(true);
6895     opticalTrainCombo->clear();
6896     opticalTrainCombo->addItems(OpticalTrainManager::Instance()->getTrainNames());
6897     trainB->setEnabled(true);
6898 
6899     QVariant trainID = ProfileSettings::Instance()->getOneSetting(ProfileSettings::FocusOpticalTrain);
6900 
6901     if (trainID.isValid())
6902     {
6903         auto id = trainID.toUInt();
6904 
6905         // If train not found, select the first one available.
6906         if (OpticalTrainManager::Instance()->exists(id) == false)
6907         {
6908             qCWarning(KSTARS_EKOS_FOCUS) << "Optical train doesn't exist for id" << id;
6909             id = OpticalTrainManager::Instance()->id(opticalTrainCombo->itemText(0));
6910         }
6911 
6912         auto name = OpticalTrainManager::Instance()->name(id);
6913 
6914         opticalTrainCombo->setCurrentText(name);
6915 
6916         // Load train settings
6917         // This needs to be done near the start of this function as methods further down
6918         // cause settings to be updated, which in turn interferes with the persistence and
6919         // setup of settings in OpticalTrainSettings
6920         OpticalTrainSettings::Instance()->setOpticalTrainID(id);
6921         auto settings = OpticalTrainSettings::Instance()->getOneSetting(OpticalTrainSettings::Focus);
6922         if (settings.isValid())
6923             setAllSettings(settings.toJsonObject().toVariantMap());
6924         else
6925             m_Settings = m_GlobalSettings;
6926 
6927         auto focuser = OpticalTrainManager::Instance()->getFocuser(name);
6928         setFocuser(focuser);
6929 
6930         auto scope = OpticalTrainManager::Instance()->getScope(name);
6931 
6932         // CFZ and FA use scope parameters in their calcs - so update...
6933         m_Aperture = scope["aperture"].toDouble(-1);
6934         m_FocalLength = scope["focal_length"].toDouble(-1);
6935         m_FocalRatio = scope["focal_ratio"].toDouble(-1);
6936         m_ScopeType = scope["type"].toString();
6937         m_Reducer = OpticalTrainManager::Instance()->getReducer(name);
6938 
6939         // Adjust telescope FL and F# for any reducer
6940         if (m_Reducer > 0.0)
6941             m_FocalLength *= m_Reducer;
6942 
6943         // Use the adjusted focal length to calculate an adjusted focal ratio
6944         if (m_FocalRatio <= 0.0)
6945             // For a scope, FL and aperture are specified so calc the F#
6946             m_FocalRatio = (m_Aperture > 0.001) ? m_FocalLength / m_Aperture : 0.0f;
6947         else if (m_Aperture < 0.0)
6948             // DSLR Lens. FL and F# are specified so calc the aperture
6949             m_Aperture = m_FocalLength / m_FocalRatio;
6950 
6951         auto camera = OpticalTrainManager::Instance()->getCamera(name);
6952         if (camera)
6953         {
6954             opticalTrainCombo->setToolTip(QString("%1 @ %2").arg(camera->getDeviceName(), scope["name"].toString()));
6955 
6956             // Get the pixel size of the active camera for later calculations
6957             auto nvp = camera->getNumber("CCD_INFO");
6958             if (!nvp)
6959             {
6960                 m_CcdPixelSizeX = 0.0;
6961                 m_CcdWidth = m_CcdHeight = 0;
6962             }
6963             else
6964             {
6965                 auto np = nvp->findWidgetByName("CCD_PIXEL_SIZE_X");
6966                 if (np)
6967                     m_CcdPixelSizeX = np->getValue();
6968                 np = nvp->findWidgetByName("CCD_MAX_X");
6969                 if (np)
6970                     m_CcdWidth = np->getValue();
6971                 np = nvp->findWidgetByName("CCD_MAX_Y");
6972                 if (np)
6973                     m_CcdHeight = np->getValue();
6974             }
6975         }
6976         setCamera(camera);
6977 
6978         auto filterWheel = OpticalTrainManager::Instance()->getFilterWheel(name);
6979         setFilterWheel(filterWheel);
6980 
6981         // Update calcs for the CFZ and Focus Advisor based on the new OT
6982         resetCFZToOT();
6983         focusAdvisorSetup();
6984     }
6985 
6986     opticalTrainCombo->blockSignals(false);
6987 }
6988 
6989 }