1 /*
2     SPDX-FileCopyrightText: 2012 Jasem Mutlaq <mutlaqja@ikarustech.com>
3 
4     SPDX-License-Identifier: GPL-2.0-or-later
5 */
6 
7 #include "focus.h"
8 
9 #include "focusadaptor.h"
10 #include "focusalgorithms.h"
11 #include "polynomialfit.h"
12 #include "kstars.h"
13 #include "kstarsdata.h"
14 #include "Options.h"
15 #include "auxiliary/kspaths.h"
16 #include "auxiliary/ksmessagebox.h"
17 #include "ekos/manager.h"
18 #include "ekos/auxiliary/darklibrary.h"
19 #include "fitsviewer/fitsdata.h"
20 #include "fitsviewer/fitstab.h"
21 #include "fitsviewer/fitsview.h"
22 #include "indi/indifilter.h"
23 #include "ksnotification.h"
24 #include "kconfigdialog.h"
25 
26 #include <basedevice.h>
27 
28 #include <gsl/gsl_fit.h>
29 #include <gsl/gsl_vector.h>
30 #include <gsl/gsl_min.h>
31 
32 #include <ekos_focus_debug.h>
33 
34 #include <cmath>
35 
36 #define MAXIMUM_ABS_ITERATIONS   30
37 #define MAXIMUM_RESET_ITERATIONS 3
38 #define AUTO_STAR_TIMEOUT        45000
39 #define MINIMUM_PULSE_TIMER      32
40 #define MAX_RECAPTURE_RETRIES    3
41 #define MINIMUM_POLY_SOLUTIONS   2
42 
43 namespace Ekos
44 {
Focus()45 Focus::Focus()
46 {
47     // #1 Set the UI
48     setupUi(this);
49 
50     // #2 Register DBus
51     qRegisterMetaType<Ekos::FocusState>("Ekos::FocusState");
52     qDBusRegisterMetaType<Ekos::FocusState>();
53     new FocusAdaptor(this);
54     QDBusConnection::sessionBus().registerObject("/KStars/Ekos/Focus", this);
55 
56     // #3 Init connections
57     initConnections();
58 
59     // #4 Init Plots
60     initPlots();
61 
62     // #5 Init View
63     initView();
64 
65     // #6 Reset all buttons to default states
66     resetButtons();
67 
68     // #7 Image Effects - note there is already a no-op "--" in the gadget
69     filterCombo->addItems(FITSViewer::filterTypes);
70     connect(filterCombo, QOverload<int>::of(&QComboBox::activated), this, &Ekos::Focus::filterChangeWarning);
71 
72     // Check that the filter denominated by the Ekos option exists before setting it (count the no-op filter)
73     if (Options::focusEffect() < (uint) FITSViewer::filterTypes.count() + 1)
74         filterCombo->setCurrentIndex(Options::focusEffect());
75     filterChangeWarning(filterCombo->currentIndex());
76     defaultScale = static_cast<FITSScale>(Options::focusEffect());
77 
78     // #8 Load All settings
79     loadSettings();
80 
81     // #9 Init Setting Connection now
82     initSettingsConnections();
83 
84     connect(&m_StarFinderWatcher, &QFutureWatcher<bool>::finished, this, &Focus::calculateHFR);
85 
86     //Note:  This is to prevent a button from being called the default button
87     //and then executing when the user hits the enter key such as when on a Text Box
88     QList<QPushButton *> qButtons = findChildren<QPushButton *>();
89     for (auto &button : qButtons)
90         button->setAutoDefault(false);
91 
92     appendLogText(i18n("Idle."));
93 
94     // Focus motion timeout
95     m_FocusMotionTimer.setInterval(Options::focusMotionTimeout() * 1000);
96     connect(&m_FocusMotionTimer, &QTimer::timeout, this, &Focus::handleFocusMotionTimeout);
97 
98     // Create an autofocus CSV file, dated at startup time
99     m_FocusLogFileName = QDir(KSPaths::writableLocation(QStandardPaths::AppDataLocation)).filePath("focuslogs/autofocus-" +
100                          QDateTime::currentDateTime().toString("yyyy-MM-ddThh-mm-ss") + ".txt");
101     m_FocusLogFile.setFileName(m_FocusLogFileName);
102 
103     editFocusProfile->setIcon(QIcon::fromTheme("document-edit"));
104     editFocusProfile->setAttribute(Qt::WA_LayoutUsesWidgetRect);
105 
106     connect(editFocusProfile, &QAbstractButton::clicked, this, [this]()
107     {
108         KConfigDialog *optionsEditor = new KConfigDialog(this, "OptionsProfileEditor", Options::self());
109         optionsProfileEditor = new StellarSolverProfileEditor(this, Ekos::FocusProfiles, optionsEditor);
110 #ifdef Q_OS_OSX
111         optionsEditor->setWindowFlags(Qt::Tool | Qt::WindowStaysOnTopHint);
112 #endif
113         KPageWidgetItem *mainPage = optionsEditor->addPage(optionsProfileEditor, i18n("Focus Options Profile Editor"));
114         mainPage->setIcon(QIcon::fromTheme("configure"));
115         connect(optionsProfileEditor, &StellarSolverProfileEditor::optionsProfilesUpdated, this, &Focus::loadStellarSolverProfiles);
116         optionsProfileEditor->loadProfile(focusOptionsProfiles->currentIndex());
117         optionsEditor->show();
118     });
119 
120     loadStellarSolverProfiles();
121 
122     connect(focusOptionsProfiles, QOverload<int>::of(&QComboBox::activated), this, [](int index)
123     {
124         Options::setFocusOptionsProfile(index);
125     });
126 
127     // connect HFR plot widget
128     connect(this, &Ekos::Focus::initHFRPlot, HFRPlot, &FocusHFRVPlot::init);
129     connect(this, &Ekos::Focus::redrawHFRPlot, HFRPlot, &FocusHFRVPlot::redraw);
130     connect(this, &Ekos::Focus::newHFRPlotPosition, HFRPlot, &FocusHFRVPlot::addPosition);
131     connect(this, &Ekos::Focus::drawPolynomial, HFRPlot, &FocusHFRVPlot::drawPolynomial);
132     connect(this, &Ekos::Focus::setTitle, HFRPlot, &FocusHFRVPlot::setTitle);
133     connect(this, &Ekos::Focus::minimumFound, HFRPlot, &FocusHFRVPlot::drawMinimum);
134 
135     m_DarkProcessor = new DarkProcessor(this);
136     connect(m_DarkProcessor, &DarkProcessor::newLog, this, &Ekos::Focus::appendLogText);
137     connect(m_DarkProcessor, &DarkProcessor::darkFrameCompleted, this, [this](bool completed)
138     {
139         darkFrameCheck->setChecked(completed);
140         if (completed)
141         {
142             focusView->rescale(ZOOM_KEEP_LEVEL);
143             focusView->updateFrame();
144         }
145         setCaptureComplete();
146         resetButtons();
147     });
148 }
149 
loadStellarSolverProfiles()150 void Focus::loadStellarSolverProfiles()
151 {
152     QString savedOptionsProfiles = QDir(KSPaths::writableLocation(
153                                             QStandardPaths::AppDataLocation)).filePath("SavedFocusProfiles.ini");
154     if(QFile(savedOptionsProfiles).exists())
155         m_StellarSolverProfiles = StellarSolver::loadSavedOptionsProfiles(savedOptionsProfiles);
156     else
157         m_StellarSolverProfiles = getDefaultFocusOptionsProfiles();
158     focusOptionsProfiles->clear();
159     for(auto param : m_StellarSolverProfiles)
160         focusOptionsProfiles->addItem(param.listName);
161     focusOptionsProfiles->setCurrentIndex(Options::focusOptionsProfile());
162 }
163 
getStellarSolverProfiles()164 QStringList Focus::getStellarSolverProfiles()
165 {
166     QStringList profiles;
167     for (auto param : m_StellarSolverProfiles)
168         profiles << param.listName;
169 
170     return profiles;
171 }
172 
173 
~Focus()174 Focus::~Focus()
175 {
176     if (focusingWidget->parent() == nullptr)
177         toggleFocusingWidgetFullScreen();
178 
179     m_FocusLogFile.close();
180 }
181 
resetFrame()182 void Focus::resetFrame()
183 {
184     if (currentCCD && currentCCD->isConnected())
185     {
186         ISD::CCDChip *targetChip = currentCCD->getChip(ISD::CCDChip::PRIMARY_CCD);
187 
188         if (targetChip)
189         {
190             //fx=fy=fw=fh=0;
191             targetChip->resetFrame();
192 
193             int x, y, w, h;
194             targetChip->getFrame(&x, &y, &w, &h);
195 
196             qCDebug(KSTARS_EKOS_FOCUS) << "Frame is reset. X:" << x << "Y:" << y << "W:" << w << "H:" << h << "binX:" << 1 << "binY:" <<
197                                        1;
198 
199             QVariantMap settings;
200             settings["x"]             = x;
201             settings["y"]             = y;
202             settings["w"]             = w;
203             settings["h"]             = h;
204             settings["binx"]          = 1;
205             settings["biny"]          = 1;
206             frameSettings[targetChip] = settings;
207 
208             starSelected = false;
209             starCenter   = QVector3D();
210             subFramed    = false;
211 
212             focusView->setTrackingBox(QRect());
213         }
214     }
215 }
216 
setCamera(const QString & device)217 bool Focus::setCamera(const QString &device)
218 {
219     for (int i = 0; i < CCDCaptureCombo->count(); i++)
220         if (device == CCDCaptureCombo->itemText(i))
221         {
222             CCDCaptureCombo->setCurrentIndex(i);
223             checkCCD(i);
224             return true;
225         }
226 
227     return false;
228 }
229 
camera()230 QString Focus::camera()
231 {
232     if (currentCCD)
233         return currentCCD->getDeviceName();
234 
235     return QString();
236 }
237 
checkCCD(int ccdNum)238 void Focus::checkCCD(int ccdNum)
239 {
240     // Do NOT perform checks when the camera is capturing or busy as this may result
241     // in signals/slots getting disconnected.
242     switch (state)
243     {
244         // Idle, can change camera.
245         case FOCUS_IDLE:
246         case FOCUS_COMPLETE:
247         case FOCUS_FAILED:
248         case FOCUS_ABORTED:
249             break;
250 
251         // Busy, cannot change camera.
252         case FOCUS_WAITING:
253         case FOCUS_PROGRESS:
254         case FOCUS_FRAMING:
255         case FOCUS_CHANGING_FILTER:
256             return;
257     }
258 
259     if (ccdNum == -1)
260     {
261         ccdNum = CCDCaptureCombo->currentIndex();
262 
263         if (ccdNum == -1)
264             return;
265     }
266 
267     if (ccdNum >= 0 && ccdNum < CCDs.count())
268     {
269         currentCCD = CCDs.at(ccdNum);
270 
271         ISD::CCDChip *targetChip = currentCCD->getChip(ISD::CCDChip::PRIMARY_CCD);
272         if (targetChip && targetChip->isCapturing())
273             return;
274 
275         for (ISD::CCD *oneCCD : CCDs)
276         {
277             if (oneCCD == currentCCD)
278                 continue;
279             if (captureInProgress == false)
280                 oneCCD->disconnect(this);
281         }
282 
283         if (targetChip)
284         {
285             targetChip->setImageView(focusView, FITS_FOCUS);
286 
287             binningCombo->setEnabled(targetChip->canBin());
288             useSubFrame->setEnabled(targetChip->canSubframe());
289             if (targetChip->canBin())
290             {
291                 int subBinX = 1, subBinY = 1;
292                 binningCombo->clear();
293                 targetChip->getMaxBin(&subBinX, &subBinY);
294                 for (int i = 1; i <= subBinX; i++)
295                     binningCombo->addItem(QString("%1x%2").arg(i).arg(i));
296 
297                 activeBin = Options::focusXBin();
298                 binningCombo->setCurrentIndex(activeBin - 1);
299             }
300             else
301                 activeBin = 1;
302 
303             QStringList isoList = targetChip->getISOList();
304             ISOCombo->clear();
305 
306             if (isoList.isEmpty())
307             {
308                 ISOCombo->setEnabled(false);
309                 ISOLabel->setEnabled(false);
310             }
311             else
312             {
313                 ISOCombo->setEnabled(true);
314                 ISOLabel->setEnabled(true);
315                 ISOCombo->addItems(isoList);
316                 ISOCombo->setCurrentIndex(targetChip->getISOIndex());
317             }
318 
319             connect(currentCCD, &ISD::CCD::videoStreamToggled, this, &Ekos::Focus::setVideoStreamEnabled, Qt::UniqueConnection);
320 
321             liveVideoB->setEnabled(currentCCD->hasVideoStream());
322             if (currentCCD->hasVideoStream())
323                 setVideoStreamEnabled(currentCCD->isStreamingEnabled());
324             else
325                 liveVideoB->setIcon(QIcon::fromTheme("camera-off"));
326 
327 
328             bool hasGain = currentCCD->hasGain();
329             gainLabel->setEnabled(hasGain);
330             gainIN->setEnabled(hasGain && currentCCD->getGainPermission() != IP_RO);
331             if (hasGain)
332             {
333                 double gain = 0, min = 0, max = 0, step = 1;
334                 currentCCD->getGainMinMaxStep(&min, &max, &step);
335                 if (currentCCD->getGain(&gain))
336                 {
337                     gainIN->setMinimum(min);
338                     gainIN->setMaximum(max);
339                     if (step > 0)
340                         gainIN->setSingleStep(step);
341 
342                     double defaultGain = Options::focusGain();
343                     if (defaultGain > 0)
344                         gainIN->setValue(defaultGain);
345                     else
346                         gainIN->setValue(gain);
347                 }
348             }
349             else
350                 gainIN->clear();
351         }
352     }
353 
354     syncCCDInfo();
355 }
356 
syncCCDInfo()357 void Focus::syncCCDInfo()
358 {
359     if (currentCCD == nullptr)
360         return;
361 
362     ISD::CCDChip *targetChip = currentCCD->getChip(ISD::CCDChip::PRIMARY_CCD);
363 
364     useSubFrame->setEnabled(targetChip->canSubframe());
365 
366     if (frameSettings.contains(targetChip) == false)
367     {
368         int x, y, w, h;
369         if (targetChip->getFrame(&x, &y, &w, &h))
370         {
371             int binx = 1, biny = 1;
372             targetChip->getBinning(&binx, &biny);
373             if (w > 0 && h > 0)
374             {
375                 int minX, maxX, minY, maxY, minW, maxW, minH, maxH;
376                 targetChip->getFrameMinMax(&minX, &maxX, &minY, &maxY, &minW, &maxW, &minH, &maxH);
377 
378                 QVariantMap settings;
379 
380                 settings["x"]    = useSubFrame->isChecked() ? x : minX;
381                 settings["y"]    = useSubFrame->isChecked() ? y : minY;
382                 settings["w"]    = useSubFrame->isChecked() ? w : maxW;
383                 settings["h"]    = useSubFrame->isChecked() ? h : maxH;
384                 settings["binx"] = binx;
385                 settings["biny"] = biny;
386 
387                 frameSettings[targetChip] = settings;
388             }
389         }
390     }
391 }
392 
addFilter(ISD::GDInterface * newFilter)393 void Focus::addFilter(ISD::GDInterface *newFilter)
394 {
395     for (auto &oneFilter : Filters)
396     {
397         if (oneFilter->getDeviceName() == newFilter->getDeviceName())
398             return;
399     }
400 
401     FilterCaptureLabel->setEnabled(true);
402     FilterDevicesCombo->setEnabled(true);
403     FilterPosLabel->setEnabled(true);
404     FilterPosCombo->setEnabled(true);
405     filterManagerB->setEnabled(true);
406 
407     FilterDevicesCombo->addItem(newFilter->getDeviceName());
408 
409     Filters.append(static_cast<ISD::Filter *>(newFilter));
410 
411     int filterWheelIndex = 1;
412     if (Options::defaultFocusFilterWheel().isEmpty() == false)
413         filterWheelIndex = FilterDevicesCombo->findText(Options::defaultFocusFilterWheel());
414 
415     if (filterWheelIndex < 1)
416         filterWheelIndex = 1;
417 
418     checkFilter(filterWheelIndex);
419     FilterDevicesCombo->setCurrentIndex(filterWheelIndex);
420 
421     emit settingsUpdated(getSettings());
422 }
423 
addTemperatureSource(ISD::GDInterface * newSource)424 void Focus::addTemperatureSource(ISD::GDInterface *newSource)
425 {
426     if (!newSource)
427         return;
428 
429     for (const auto &oneSource : TemperatureSources)
430     {
431         if (oneSource->getDeviceName() == newSource->getDeviceName())
432             return;
433     }
434 
435     TemperatureSources.append(newSource);
436     temperatureSourceCombo->addItem(newSource->getDeviceName());
437 
438     int temperatureSourceIndex = temperatureSourceCombo->currentIndex();
439     if (Options::defaultFocusTemperatureSource().isEmpty())
440         Options::setDefaultFocusTemperatureSource(newSource->getDeviceName());
441     else
442         temperatureSourceIndex = temperatureSourceCombo->findText(Options::defaultFocusTemperatureSource());
443     if (temperatureSourceIndex < 0)
444         temperatureSourceIndex = 0;
445 
446     checkTemperatureSource(temperatureSourceIndex);
447 }
448 
checkTemperatureSource(int index)449 void Focus::checkTemperatureSource(int index)
450 {
451     if (index == -1)
452     {
453         index = temperatureSourceCombo->currentIndex();
454         if (index == -1)
455             return;
456     }
457 
458     QString deviceName;
459     if (index < TemperatureSources.count())
460         deviceName = temperatureSourceCombo->itemText(index);
461 
462     ISD::GDInterface *currentSource = nullptr;
463 
464     for (auto &oneSource : TemperatureSources)
465     {
466         if (oneSource->getDeviceName() == deviceName)
467         {
468             currentSource = oneSource;
469             break;
470         }
471     }
472 
473     // No valid device found
474     if (!currentSource)
475         return;
476 
477     QStringList deviceNames;
478     // Disconnect all existing signals
479     for (const auto &oneSource : TemperatureSources)
480     {
481         deviceNames << oneSource->getDeviceName();
482         disconnect(oneSource, &ISD::GDInterface::numberUpdated, this, &Ekos::Focus::processTemperatureSource);
483     }
484 
485     if (findTemperatureElement(currentSource))
486     {
487         m_LastSourceAutofocusTemperature = currentTemperatureSourceElement->value;
488         absoluteTemperatureLabel->setText(QString("%1 °C").arg(currentTemperatureSourceElement->value, 0, 'f', 2));
489         deltaTemperatureLabel->setText(QString("%1 °C").arg(0.0, 0, 'f', 2));
490     }
491     else
492         m_LastSourceAutofocusTemperature = INVALID_VALUE;
493     connect(currentSource, &ISD::GDInterface::numberUpdated, this, &Ekos::Focus::processTemperatureSource);
494 
495     temperatureSourceCombo->clear();
496     temperatureSourceCombo->addItems(deviceNames);
497     temperatureSourceCombo->setCurrentIndex(index);
498 }
499 
findTemperatureElement(ISD::GDInterface * device)500 bool Focus::findTemperatureElement(ISD::GDInterface *device)
501 {
502     INDI::Property *temperatureProperty = device->getProperty("FOCUS_TEMPERATURE");
503     if (!temperatureProperty)
504         temperatureProperty = device->getProperty("CCD_TEMPERATURE");
505     if (temperatureProperty)
506     {
507         currentTemperatureSourceElement = temperatureProperty->getNumber()->at(0);
508         return true;
509     }
510 
511     temperatureProperty = device->getProperty("WEATHER_PARAMETERS");
512     if (temperatureProperty)
513     {
514         for (int i = 0; i < temperatureProperty->getNumber()->count(); i++)
515         {
516             if (strstr(temperatureProperty->getNumber()->at(i)->getName(), "_TEMPERATURE"))
517             {
518                 currentTemperatureSourceElement = temperatureProperty->getNumber()->at(i);
519                 return true;
520             }
521         }
522     }
523 
524     return false;
525 }
526 
setFilterWheel(const QString & device)527 bool Focus::setFilterWheel(const QString &device)
528 {
529     bool deviceFound = false;
530 
531     for (int i = 1; i < FilterDevicesCombo->count(); i++)
532         if (device == FilterDevicesCombo->itemText(i))
533         {
534             checkFilter(i);
535             deviceFound = true;
536             break;
537         }
538 
539     if (deviceFound == false)
540         return false;
541 
542     return true;
543 }
544 
filterWheel()545 QString Focus::filterWheel()
546 {
547     if (FilterDevicesCombo->currentIndex() >= 1)
548         return FilterDevicesCombo->currentText();
549 
550     return QString();
551 }
552 
setFilter(const QString & filter)553 bool Focus::setFilter(const QString &filter)
554 {
555     if (FilterDevicesCombo->currentIndex() >= 1)
556     {
557         FilterPosCombo->setCurrentText(filter);
558         return true;
559     }
560 
561     return false;
562 }
563 
filter()564 QString Focus::filter()
565 {
566     return FilterPosCombo->currentText();
567 }
568 
checkFilter(int filterNum)569 void Focus::checkFilter(int filterNum)
570 {
571     if (filterNum == -1)
572     {
573         filterNum = FilterDevicesCombo->currentIndex();
574         if (filterNum == -1)
575             return;
576     }
577 
578     // "--" is no filter
579     if (filterNum == 0)
580     {
581         currentFilter = nullptr;
582         currentFilterPosition = -1;
583         FilterPosCombo->clear();
584         return;
585     }
586 
587     if (filterNum <= Filters.count())
588         currentFilter = Filters.at(filterNum - 1);
589 
590     //Options::setDefaultFocusFilterWheel(currentFilter->getDeviceName());
591 
592     filterManager->setCurrentFilterWheel(currentFilter);
593 
594     FilterPosCombo->clear();
595 
596     FilterPosCombo->addItems(filterManager->getFilterLabels());
597 
598     currentFilterPosition = filterManager->getFilterPosition();
599 
600     FilterPosCombo->setCurrentIndex(currentFilterPosition - 1);
601 
602     //Options::setDefaultFocusFilterWheelFilter(FilterPosCombo->currentText());
603 
604     exposureIN->setValue(filterManager->getFilterExposure());
605 }
606 
addFocuser(ISD::GDInterface * newFocuser)607 void Focus::addFocuser(ISD::GDInterface *newFocuser)
608 {
609     ISD::Focuser *oneFocuser = static_cast<ISD::Focuser *>(newFocuser);
610 
611     if (Focusers.contains(oneFocuser))
612         return;
613 
614     focuserCombo->addItem(oneFocuser->getDeviceName());
615 
616     Focusers.append(oneFocuser);
617 
618     currentFocuser = oneFocuser;
619 
620     checkFocuser();
621 }
622 
setFocuser(const QString & device)623 bool Focus::setFocuser(const QString &device)
624 {
625     for (int i = 0; i < focuserCombo->count(); i++)
626         if (device == focuserCombo->itemText(i))
627         {
628             focuserCombo->setCurrentIndex(i);
629             checkFocuser(i);
630             return true;
631         }
632 
633     return false;
634 }
635 
focuser()636 QString Focus::focuser()
637 {
638     if (currentFocuser)
639         return currentFocuser->getDeviceName();
640 
641     return QString();
642 }
643 
checkFocuser(int FocuserNum)644 void Focus::checkFocuser(int FocuserNum)
645 {
646     if (FocuserNum == -1)
647         FocuserNum = focuserCombo->currentIndex();
648 
649     if (FocuserNum == -1)
650     {
651         currentFocuser = nullptr;
652         return;
653     }
654 
655     if (FocuserNum < Focusers.count())
656         currentFocuser = Focusers.at(FocuserNum);
657 
658     filterManager->setFocusReady(currentFocuser->isConnected());
659 
660     // Disconnect all focusers
661     for (auto &oneFocuser : Focusers)
662     {
663         disconnect(oneFocuser, &ISD::GDInterface::numberUpdated, this, &Ekos::Focus::processFocusNumber);
664     }
665 
666     hasDeviation = currentFocuser->hasDeviation();
667 
668     canAbsMove = currentFocuser->canAbsMove();
669 
670     if (canAbsMove)
671     {
672         getAbsFocusPosition();
673 
674         absTicksSpin->setEnabled(true);
675         absTicksLabel->setEnabled(true);
676         startGotoB->setEnabled(true);
677 
678         absTicksSpin->setValue(currentPosition);
679     }
680     else
681     {
682         absTicksSpin->setEnabled(false);
683         absTicksLabel->setEnabled(false);
684         startGotoB->setEnabled(false);
685     }
686 
687     canRelMove = currentFocuser->canRelMove();
688 
689     // In case we have a purely relative focuser, we pretend
690     // it is an absolute focuser with initial point set at 50,000.
691     // This is done we can use the same algorithm used for absolute focuser.
692     if (canAbsMove == false && canRelMove == true)
693     {
694         currentPosition = 50000;
695         absMotionMax    = 100000;
696         absMotionMin    = 0;
697     }
698 
699     canTimerMove = currentFocuser->canTimerMove();
700 
701     // In case we have a timer-based focuser and using the linear focus algorithm,
702     // we pretend it is an absolute focuser with initial point set at 50,000.
703     // These variables don't have in impact on timer-based focusers if the algorithm
704     // is not the linear focus algorithm.
705     if (!canAbsMove && !canRelMove && canTimerMove)
706     {
707         currentPosition = 50000;
708         absMotionMax    = 100000;
709         absMotionMin    = 0;
710     }
711 
712     focusType = (canRelMove || canAbsMove || canTimerMove) ? FOCUS_AUTO : FOCUS_MANUAL;
713     profilePlot->setFocusAuto(focusType == FOCUS_AUTO);
714 
715     bool hasBacklash = currentFocuser->hasBacklash();
716     focusBacklashSpin->setEnabled(hasBacklash);
717     focusBacklashSpin->disconnect(this);
718     if (hasBacklash)
719     {
720         double min = 0, max = 0, step = 0;
721         currentFocuser->getMinMaxStep("FOCUS_BACKLASH_STEPS", "FOCUS_BACKLASH_VALUE", &min, &max, &step);
722         focusBacklashSpin->setMinimum(min);
723         focusBacklashSpin->setMaximum(max);
724         focusBacklashSpin->setSingleStep(step);
725         focusBacklashSpin->setValue(currentFocuser->getBacklash());
726         connect(focusBacklashSpin, static_cast<void (QSpinBox::*)(int)>(&QSpinBox::valueChanged), this, [this](int value)
727         {
728             if (currentFocuser)
729                 currentFocuser->setBacklash(value);
730         });
731     }
732     else
733     {
734         focusBacklashSpin->setValue(0);
735     }
736 
737     //initializeFocuserTemperature();
738 
739     connect(currentFocuser, &ISD::GDInterface::numberUpdated, this, &Ekos::Focus::processFocusNumber, Qt::UniqueConnection);
740     //connect(currentFocuser, SIGNAL(propertyDefined(INDI::Property*)), this, &Ekos::Focus::(registerFocusProperty(INDI::Property*)), Qt::UniqueConnection);
741 
742     resetButtons();
743 
744     //if (!inAutoFocus && !inFocusLoop && !captureInProgress && !inSequenceFocus)
745     //  emit autoFocusFinished(true, -1);
746 }
747 
addCCD(ISD::GDInterface * newCCD)748 void Focus::addCCD(ISD::GDInterface *newCCD)
749 {
750     if (CCDs.contains(static_cast<ISD::CCD *>(newCCD)))
751         return;
752 
753     CCDs.append(static_cast<ISD::CCD *>(newCCD));
754 
755     CCDCaptureCombo->addItem(newCCD->getDeviceName());
756 
757     checkCCD();
758 }
759 
getAbsFocusPosition()760 void Focus::getAbsFocusPosition()
761 {
762     if (!canAbsMove)
763         return;
764 
765     auto absMove = currentFocuser->getBaseDevice()->getNumber("ABS_FOCUS_POSITION");
766 
767     if (absMove)
768     {
769         const auto &it = absMove->at(0);
770         currentPosition = static_cast<int>(it->getValue());
771         absMotionMax    = it->getMax();
772         absMotionMin    = it->getMin();
773 
774         absTicksSpin->setMinimum(it->getMin());
775         absTicksSpin->setMaximum(it->getMax());
776         absTicksSpin->setSingleStep(it->getStep());
777 
778         // Restrict the travel if needed
779         double const travel = std::abs(it->getMax() - it->getMin());
780         if (travel < maxTravelIN->maximum())
781             maxTravelIN->setMaximum(travel);
782 
783         absTicksLabel->setText(QString::number(currentPosition));
784 
785         stepIN->setMaximum(it->getMax() / 2);
786         //absTicksSpin->setValue(currentPosition);
787     }
788 }
789 
processTemperatureSource(INumberVectorProperty * nvp)790 void Focus::processTemperatureSource(INumberVectorProperty *nvp)
791 {
792     double delta = 0;
793     if (currentTemperatureSourceElement && currentTemperatureSourceElement->nvp == nvp)
794     {
795         if (m_LastSourceAutofocusTemperature != INVALID_VALUE)
796         {
797             delta = currentTemperatureSourceElement->value - m_LastSourceAutofocusTemperature;
798             emit newFocusTemperatureDelta(abs(delta), currentTemperatureSourceElement->value);
799         }
800         else
801         {
802             emit newFocusTemperatureDelta(0, currentTemperatureSourceElement->value);
803         }
804 
805         absoluteTemperatureLabel->setText(QString("%1 °C").arg(currentTemperatureSourceElement->value, 0, 'f', 2));
806         deltaTemperatureLabel->setText(QString("%1%2 °C").arg((delta > 0.0 ? "+" : "")).arg(delta, 0, 'f', 2));
807         if (delta == 0)
808             deltaTemperatureLabel->setStyleSheet("color: lightgreen");
809         else if (delta > 0)
810             deltaTemperatureLabel->setStyleSheet("color: lightcoral");
811         else
812             deltaTemperatureLabel->setStyleSheet("color: lightblue");
813     }
814 }
815 
setLastFocusTemperature()816 void Focus::setLastFocusTemperature()
817 {
818     m_LastSourceAutofocusTemperature = currentTemperatureSourceElement ? currentTemperatureSourceElement->value : INVALID_VALUE;
819 
820     // Reset delta to zero now that we're just done with autofocus
821     deltaTemperatureLabel->setText(QString("0 °C"));
822     deltaTemperatureLabel->setStyleSheet("color: lightgreen");
823 
824     emit newFocusTemperatureDelta(0, -1e6);
825 }
826 
827 #if 0
828 void Focus::initializeFocuserTemperature()
829 {
830     auto temperatureProperty = currentFocuser->getBaseDevice()->getNumber("FOCUS_TEMPERATURE");
831 
832     if (temperatureProperty && temperatureProperty->getState() != IPS_ALERT)
833     {
834         focuserTemperature = temperatureProperty->at(0)->getValue();
835         qCDebug(KSTARS_EKOS_FOCUS) << QString("Setting current focuser temperature: %1").arg(focuserTemperature, 0, 'f', 2);
836     }
837     else
838     {
839         focuserTemperature = INVALID_VALUE;
840         qCDebug(KSTARS_EKOS_FOCUS) << QString("Focuser temperature is not available");
841     }
842 }
843 
844 void Focus::setLastFocusTemperature()
845 {
846     // The focus temperature is taken by default from the focuser.
847     // If unavailable, fallback to the observatory temperature.
848     if (focuserTemperature != INVALID_VALUE)
849     {
850         lastFocusTemperature = focuserTemperature;
851         lastFocusTemperatureSource = FOCUSER_TEMPERATURE;
852     }
853     else if (observatoryTemperature != INVALID_VALUE)
854     {
855         lastFocusTemperature = observatoryTemperature;
856         lastFocusTemperatureSource = OBSERVATORY_TEMPERATURE;
857     }
858     else
859     {
860         lastFocusTemperature = INVALID_VALUE;
861         lastFocusTemperatureSource = NO_TEMPERATURE;
862     }
863 
864     emit newFocusTemperatureDelta(0, -1e6);
865 }
866 
867 
868 void Focus::updateTemperature(TemperatureSource source, double newTemperature)
869 {
870     if (source == FOCUSER_TEMPERATURE && focuserTemperature != newTemperature)
871     {
872         focuserTemperature = newTemperature;
873         emitTemperatureEvents(source, newTemperature);
874     }
875     else if (source == OBSERVATORY_TEMPERATURE && observatoryTemperature != newTemperature)
876     {
877         observatoryTemperature = newTemperature;
878         emitTemperatureEvents(source, newTemperature);
879     }
880 }
881 
882 void Focus::emitTemperatureEvents(TemperatureSource source, double newTemperature)
883 {
884     if (source != lastFocusTemperatureSource)
885     {
886         return;
887     }
888 
889     if (lastFocusTemperature != INVALID_VALUE && newTemperature != INVALID_VALUE)
890     {
891         emit newFocusTemperatureDelta(abs(newTemperature - lastFocusTemperature), newTemperature);
892     }
893     else
894     {
895         emit newFocusTemperatureDelta(0, newTemperature);
896     }
897 }
898 #endif
899 
start()900 void Focus::start()
901 {
902     if (currentFocuser == nullptr)
903     {
904         appendLogText(i18n("No Focuser connected."));
905         completeFocusProcedure(Ekos::FOCUS_ABORTED);
906         return;
907     }
908 
909     if (currentCCD == nullptr)
910     {
911         appendLogText(i18n("No CCD connected."));
912         completeFocusProcedure(Ekos::FOCUS_ABORTED);
913         return;
914     }
915 
916     if (!canAbsMove && !canRelMove && stepIN->value() <= MINIMUM_PULSE_TIMER)
917     {
918         appendLogText(i18n("Starting pulse step is too low. Increase the step size to %1 or higher...",
919                            MINIMUM_PULSE_TIMER * 5));
920         completeFocusProcedure(Ekos::FOCUS_ABORTED);
921         return;
922     }
923 
924     if (inAutoFocus)
925     {
926         appendLogText(i18n("Autofocus is already running, discarding start request."));
927         return;
928     }
929     else inAutoFocus = true;
930 
931     m_LastFocusDirection = FOCUS_NONE;
932 
933     polySolutionFound = 0;
934 
935     waitStarSelectTimer.stop();
936 
937     starsHFR.clear();
938 
939     lastHFR = 0;
940 
941     // Keep the  last focus temperature, it can still be useful in case the autofocus fails
942     // lastFocusTemperature
943 
944     if (canAbsMove)
945     {
946         absIterations = 0;
947         getAbsFocusPosition();
948         pulseDuration = stepIN->value();
949     }
950     else if (canRelMove)
951     {
952         //appendLogText(i18n("Setting dummy central position to 50000"));
953         absIterations   = 0;
954         pulseDuration   = stepIN->value();
955         //currentPosition = 50000;
956         absMotionMax    = 100000;
957         absMotionMin    = 0;
958     }
959     else
960     {
961         pulseDuration   = stepIN->value();
962         absIterations   = 0;
963         absMotionMax    = 100000;
964         absMotionMin    = 0;
965     }
966 
967     focuserAdditionalMovement = 0;
968     HFRFrames.clear();
969 
970     resetButtons();
971 
972     reverseDir = false;
973 
974     /*if (fw > 0 && fh > 0)
975         starSelected= true;
976     else
977         starSelected= false;*/
978 
979     clearDataPoints();
980     profilePlot->clear();
981 
982     //    Options::setFocusTicks(stepIN->value());
983     //    Options::setFocusTolerance(toleranceIN->value());
984     //    Options::setFocusExposure(exposureIN->value());
985     //    Options::setFocusMaxTravel(maxTravelIN->value());
986     //    Options::setFocusBoxSize(focusBoxSize->value());
987     //    Options::setFocusSubFrame(useSubFrame->isChecked());
988     //    Options::setFocusAutoStarEnabled(useAutoStar->isChecked());
989     //    Options::setSuspendGuiding(suspendGuideCheck->isChecked());
990     //    Options::setUseFocusDarkFrame(darkFrameCheck->isChecked());
991     //    Options::setFocusFramesCount(focusFramesSpin->value());
992     //    Options::setFocusUseFullField(useFullField->isChecked());
993 
994     qCDebug(KSTARS_EKOS_FOCUS)  << "Starting focus with Detection: " << focusDetectionCombo->currentText()
995                                 << " Algorithm: " << focusAlgorithmCombo->currentText()
996                                 << " Box size: " << focusBoxSize->value()
997                                 << " Subframe: " << ( useSubFrame->isChecked() ? "yes" : "no" )
998                                 << " Autostar: " << ( useAutoStar->isChecked() ? "yes" : "no" )
999                                 << " Full frame: " << ( useFullField->isChecked() ? "yes" : "no " )
1000                                 << " [" << fullFieldInnerRing->value() << "%," << fullFieldOuterRing->value() << "%]"
1001                                 << " Step Size: " << stepIN->value() << " Threshold: " << thresholdSpin->value()
1002                                 << " Gaussian Sigma: " << gaussianSigmaSpin->value()
1003                                 << " Gaussian Kernel size: " << gaussianKernelSizeSpin->value()
1004                                 << " Multi row average: " << multiRowAverageSpin->value()
1005                                 << " Tolerance: " << toleranceIN->value()
1006                                 << " Frames: " << 1 /*focusFramesSpin->value()*/ << " Maximum Travel: " << maxTravelIN->value();
1007 
1008     if (currentTemperatureSourceElement)
1009         emit autofocusStarting(currentTemperatureSourceElement->value, filter());
1010 
1011     if (useAutoStar->isChecked())
1012         appendLogText(i18n("Autofocus in progress..."));
1013     else if (!inAutoFocus)
1014         appendLogText(i18n("Please wait until image capture is complete..."));
1015 
1016     // Only suspend when we have Off-Axis Guider
1017     // If the guide camera is operating on a different OTA
1018     // then no need to suspend.
1019     const bool isOAG = currentCCD->getTelescopeType() == Options::guideScopeType();
1020     if (isOAG && m_GuidingSuspended == false && suspendGuideCheck->isChecked())
1021     {
1022         m_GuidingSuspended = true;
1023         emit suspendGuiding();
1024     }
1025 
1026     //emit statusUpdated(true);
1027     state = Ekos::FOCUS_PROGRESS;
1028     qCDebug(KSTARS_EKOS_FOCUS) << "State:" << Ekos::getFocusStatusString(state);
1029     emit newStatus(state);
1030 
1031     // Denoise with median filter
1032     //defaultScale = FITS_MEDIAN;
1033 
1034     KSNotification::event(QLatin1String("FocusStarted"), i18n("Autofocus operation started"));
1035 
1036     // Used for all the focuser types.
1037     if (focusAlgorithm == FOCUS_LINEAR)
1038     {
1039         const int position = static_cast<int>(currentPosition);
1040         FocusAlgorithmInterface::FocusParams params(
1041             maxTravelIN->value(), stepIN->value(), position, absMotionMin, absMotionMax,
1042             MAXIMUM_ABS_ITERATIONS, toleranceIN->value() / 100.0, filter(),
1043             currentTemperatureSourceElement ? currentTemperatureSourceElement->value : INVALID_VALUE,
1044             Options::initialFocusOutSteps());
1045         if (canAbsMove)
1046             initialFocuserAbsPosition = position;
1047         linearFocuser.reset(MakeLinearFocuser(params));
1048         linearRequestedPosition = linearFocuser->initialPosition();
1049         const int newPosition = adjustLinearPosition(position, linearRequestedPosition);
1050         if (newPosition != position)
1051         {
1052             if (!changeFocus(newPosition - position))
1053             {
1054                 completeFocusProcedure(Ekos::FOCUS_ABORTED);
1055             }
1056             // Avoid the capture below.
1057             return;
1058         }
1059     }
1060     capture();
1061 }
1062 
adjustLinearPosition(int position,int newPosition)1063 int Focus::adjustLinearPosition(int position, int newPosition)
1064 {
1065     if (newPosition > position)
1066     {
1067         constexpr int extraMotionSteps = 5;
1068         int adjustment = extraMotionSteps * stepIN->value();
1069         if (newPosition + adjustment > absMotionMax)
1070             adjustment = static_cast<int>(absMotionMax) - newPosition;
1071 
1072         focuserAdditionalMovement = adjustment;
1073         qCDebug(KSTARS_EKOS_FOCUS) << QString("LinearFocuser: extending outward movement by %1").arg(adjustment);
1074 
1075         return newPosition + adjustment;
1076     }
1077     return newPosition;
1078 }
1079 
checkStopFocus(bool abort)1080 void Focus::checkStopFocus(bool abort)
1081 {
1082     // if abort, avoid try to restart
1083     if (abort)
1084         resetFocusIteration = MAXIMUM_RESET_ITERATIONS + 1;
1085 
1086     if (captureInProgress && inAutoFocus == false && inFocusLoop == false)
1087     {
1088         captureB->setEnabled(true);
1089         stopFocusB->setEnabled(false);
1090 
1091         appendLogText(i18n("Capture aborted."));
1092     }
1093 
1094     if (hfrInProgress)
1095     {
1096         stopFocusB->setEnabled(false);
1097         appendLogText(i18n("Detection in progress, please wait."));
1098         QTimer::singleShot(1000, this, [ &, abort]()
1099         {
1100             checkStopFocus(abort);
1101         });
1102     }
1103     else
1104     {
1105         completeFocusProcedure(abort ? Ekos::FOCUS_ABORTED : Ekos::FOCUS_FAILED);
1106     }
1107 }
1108 
meridianFlipStarted()1109 void Focus::meridianFlipStarted()
1110 {
1111     // if focusing is not running, do nothing
1112     if (state == FOCUS_IDLE || state == FOCUS_COMPLETE || state == FOCUS_FAILED || state == FOCUS_ABORTED)
1113         return;
1114 
1115     // store current focus iteration counter since abort() sets it to the maximal value to avoid restarting
1116     int old = resetFocusIteration;
1117     // abort focusing
1118     abort();
1119     // try to shift the focuser back to its initial position
1120     resetFocuser();
1121     // restore iteration counter
1122     resetFocusIteration = old;
1123 }
1124 
abort()1125 void Focus::abort()
1126 {
1127     // No need to "abort" if not already in progress.
1128     if (state <= FOCUS_ABORTED)
1129         return;
1130 
1131     checkStopFocus(true);
1132     appendLogText(i18n("Autofocus aborted."));
1133 }
1134 
stop(Ekos::FocusState completionState)1135 void Focus::stop(Ekos::FocusState completionState)
1136 {
1137     qCDebug(KSTARS_EKOS_FOCUS) << "Stopping Focus";
1138 
1139     captureTimeout.stop();
1140     m_FocusMotionTimer.stop();
1141     m_FocusMotionTimerCounter = 0;
1142 
1143     inAutoFocus     = false;
1144     focuserAdditionalMovement = 0;
1145     inFocusLoop     = false;
1146     polySolutionFound  = 0;
1147     captureInProgress  = false;
1148     captureFailureCounter = 0;
1149     minimumRequiredHFR = -1;
1150     noStarCount        = 0;
1151     HFRFrames.clear();
1152 
1153     // Check if CCD was not removed due to crash or other reasons.
1154     if (currentCCD)
1155     {
1156         disconnect(currentCCD, &ISD::CCD::newImage, this, &Ekos::Focus::processData);
1157         disconnect(currentCCD, &ISD::CCD::error, this, &Ekos::Focus::processCaptureError);
1158 
1159         if (rememberUploadMode != currentCCD->getUploadMode())
1160             currentCCD->setUploadMode(rememberUploadMode);
1161 
1162         // Remember to reset fast exposure if it was enabled before.
1163         if (m_RememberCameraFastExposure)
1164         {
1165             m_RememberCameraFastExposure = false;
1166             currentCCD->setFastExposureEnabled(true);
1167         }
1168 
1169         ISD::CCDChip *targetChip = currentCCD->getChip(ISD::CCDChip::PRIMARY_CCD);
1170         targetChip->abortExposure();
1171     }
1172 
1173     resetButtons();
1174 
1175     absIterations = 0;
1176     HFRInc        = 0;
1177     reverseDir    = false;
1178 
1179     if (m_GuidingSuspended)
1180     {
1181         emit resumeGuiding();
1182         m_GuidingSuspended = false;
1183     }
1184 
1185     if (completionState == Ekos::FOCUS_ABORTED || completionState == Ekos::FOCUS_FAILED)
1186     {
1187         state = completionState;
1188         qCDebug(KSTARS_EKOS_FOCUS) << "State:" << Ekos::getFocusStatusString(state);
1189         emit newStatus(state);
1190     }
1191 }
1192 
capture(double settleTime)1193 void Focus::capture(double settleTime)
1194 {
1195     // If capturing should be delayed by a given settling time, we start the capture timer.
1196     // This is intentionally designed re-entrant, i.e. multiple calls with settle time > 0 takes the last delay
1197     if (settleTime > 0 && captureInProgress == false)
1198     {
1199         captureTimer.start(static_cast<int>(settleTime * 1000));
1200         return;
1201     }
1202 
1203     if (captureInProgress)
1204     {
1205         qCWarning(KSTARS_EKOS_FOCUS) << "Capture called while already in progress. Capture is ignored.";
1206         return;
1207     }
1208 
1209     if (currentCCD == nullptr)
1210     {
1211         appendLogText(i18n("Error: No Camera detected."));
1212         checkStopFocus(true);
1213         return;
1214     }
1215 
1216     if (currentCCD->isConnected() == false)
1217     {
1218         appendLogText(i18n("Error: Lost connection to Camera."));
1219         checkStopFocus(true);
1220         return;
1221     }
1222 
1223     // reset timeout for receiving an image
1224     captureTimeout.stop();
1225     // reset timeout for focus star selection
1226     waitStarSelectTimer.stop();
1227 
1228     ISD::CCDChip *targetChip = currentCCD->getChip(ISD::CCDChip::PRIMARY_CCD);
1229 
1230     if (currentCCD->isBLOBEnabled() == false)
1231     {
1232         currentCCD->setBLOBEnabled(true);
1233     }
1234 
1235     if (FilterPosCombo->currentIndex() != -1)
1236     {
1237         if (currentFilter == nullptr)
1238         {
1239             appendLogText(i18n("Error: No Filter Wheel detected."));
1240             checkStopFocus(true);
1241             return;
1242         }
1243         if (currentFilter->isConnected() == false)
1244         {
1245             appendLogText(i18n("Error: Lost connection to Filter Wheel."));
1246             checkStopFocus(true);
1247             return;
1248         }
1249 
1250         int targetPosition = FilterPosCombo->currentIndex() + 1;
1251         QString lockedFilter = filterManager->getFilterLock(FilterPosCombo->currentText());
1252 
1253         // We change filter if:
1254         // 1. Target position is not equal to current position.
1255         // 2. Locked filter of CURRENT filter is a different filter.
1256         if (lockedFilter != "--" && lockedFilter != FilterPosCombo->currentText())
1257         {
1258             int lockedFilterIndex = FilterPosCombo->findText(lockedFilter);
1259             if (lockedFilterIndex >= 0)
1260             {
1261                 // Go back to this filter one we are done
1262                 fallbackFilterPending = true;
1263                 fallbackFilterPosition = targetPosition;
1264                 targetPosition = lockedFilterIndex + 1;
1265             }
1266         }
1267 
1268         filterPositionPending = (targetPosition != currentFilterPosition);
1269         // If either the target position is not equal to the current position, OR
1270         if (filterPositionPending)
1271         {
1272             // Apply all policies except autofocus since we are already in autofocus module doh.
1273             filterManager->setFilterPosition(targetPosition,
1274                                              static_cast<FilterManager::FilterPolicy>(FilterManager::CHANGE_POLICY | FilterManager::OFFSET_POLICY));
1275             return;
1276         }
1277     }
1278 
1279     prepareCapture(targetChip);
1280 
1281     connect(currentCCD, &ISD::CCD::newImage, this, &Ekos::Focus::processData);
1282     connect(currentCCD, &ISD::CCD::error, this, &Ekos::Focus::processCaptureError);
1283 
1284     if (frameSettings.contains(targetChip))
1285     {
1286         QVariantMap settings = frameSettings[targetChip];
1287         targetChip->setFrame(settings["x"].toInt(), settings["y"].toInt(), settings["w"].toInt(),
1288                              settings["h"].toInt());
1289         settings["binx"]          = activeBin;
1290         settings["biny"]          = activeBin;
1291         frameSettings[targetChip] = settings;
1292     }
1293 
1294     captureInProgress = true;
1295     if (state != FOCUS_PROGRESS)
1296     {
1297         state = FOCUS_PROGRESS;
1298         emit newStatus(state);
1299     }
1300 
1301     focusView->setBaseSize(focusingWidget->size());
1302 
1303     if (targetChip->capture(exposureIN->value()))
1304     {
1305         // Timeout is exposure duration + timeout threshold in seconds
1306         //long const timeout = lround(ceil(exposureIN->value() * 1000)) + FOCUS_TIMEOUT_THRESHOLD;
1307         captureTimeout.start(Options::focusCaptureTimeout() * 1000);
1308 
1309         if (inFocusLoop == false)
1310             appendLogText(i18n("Capturing image..."));
1311 
1312         resetButtons();
1313     }
1314     else if (inAutoFocus)
1315     {
1316         completeFocusProcedure(Ekos::FOCUS_ABORTED);
1317     }
1318 }
1319 
prepareCapture(ISD::CCDChip * targetChip)1320 void Focus::prepareCapture(ISD::CCDChip *targetChip)
1321 {
1322     if (currentCCD->getUploadMode() == ISD::CCD::UPLOAD_LOCAL)
1323     {
1324         rememberUploadMode = ISD::CCD::UPLOAD_LOCAL;
1325         currentCCD->setUploadMode(ISD::CCD::UPLOAD_CLIENT);
1326     }
1327 
1328     // We cannot use fast exposure in focus.
1329     if (currentCCD->isFastExposureEnabled())
1330     {
1331         m_RememberCameraFastExposure = true;
1332         currentCCD->setFastExposureEnabled(false);
1333     }
1334 
1335     currentCCD->setTransformFormat(ISD::CCD::FORMAT_FITS);
1336     targetChip->setBatchMode(false);
1337     targetChip->setBinning(activeBin, activeBin);
1338     targetChip->setCaptureMode(FITS_FOCUS);
1339     targetChip->setFrameType(FRAME_LIGHT);
1340 
1341     // Always disable filtering if using a dark frame and then re-apply after subtraction. TODO: Implement this in capture and guide and align
1342     if (darkFrameCheck->isChecked())
1343         targetChip->setCaptureFilter(FITS_NONE);
1344     else
1345         targetChip->setCaptureFilter(defaultScale);
1346 
1347     if (ISOCombo->isEnabled() && ISOCombo->currentIndex() != -1 &&
1348             targetChip->getISOIndex() != ISOCombo->currentIndex())
1349         targetChip->setISOIndex(ISOCombo->currentIndex());
1350 
1351     if (gainIN->isEnabled())
1352         currentCCD->setGain(gainIN->value());
1353 }
1354 
focusIn(int ms)1355 bool Focus::focusIn(int ms)
1356 {
1357     if (ms == -1)
1358         ms = stepIN->value();
1359     return changeFocus(-ms);
1360 }
1361 
focusOut(int ms)1362 bool Focus::focusOut(int ms)
1363 {
1364     if (ms == -1)
1365         ms = stepIN->value();
1366     return changeFocus(ms);
1367 }
1368 
1369 // If amount > 0 we focus out, otherwise in.
changeFocus(int amount)1370 bool Focus::changeFocus(int amount)
1371 {
1372     const int absAmount = abs(amount);
1373 
1374     // Retry capture if we stay at the same position
1375     // Allow 1 step of tolerance--Have seen stalls with amount==1.
1376     if (inAutoFocus && absAmount <= 1)
1377     {
1378         capture(FocusSettleTime->value());
1379         return true;
1380     }
1381 
1382     if (currentFocuser == nullptr)
1383     {
1384         appendLogText(i18n("Error: No Focuser detected."));
1385         checkStopFocus(true);
1386         return false;
1387     }
1388 
1389     if (currentFocuser->isConnected() == false)
1390     {
1391         appendLogText(i18n("Error: Lost connection to Focuser."));
1392         checkStopFocus(true);
1393         return false;
1394     }
1395 
1396     const bool focusingOut = amount > 0;
1397     const QString dirStr = focusingOut ? i18n("outward") : i18n("inward");
1398     m_LastFocusDirection = focusingOut ? FOCUS_OUT : FOCUS_IN;
1399 
1400     if (focusingOut)
1401         currentFocuser->focusOut();
1402     else
1403         currentFocuser->focusIn();
1404 
1405     // Keep track of motion in case it gets stuck.
1406     m_FocusMotionTimerCounter = 0;
1407     m_FocusMotionTimer.start();
1408 
1409     if (canAbsMove)
1410     {
1411         m_LastFocusSteps = currentPosition + amount;
1412         currentFocuser->moveAbs(currentPosition + amount);
1413         appendLogText(i18n("Focusing %2 by %1 steps...", absAmount, dirStr));
1414     }
1415     else if (canRelMove)
1416     {
1417         m_LastFocusSteps = absAmount;
1418         currentFocuser->moveRel(absAmount);
1419         appendLogText(i18np("Focusing %2 by %1 step...", "Focusing %2 by %1 steps...", absAmount, dirStr));
1420     }
1421     else
1422     {
1423         m_LastFocusSteps = absAmount;
1424         currentFocuser->moveByTimer(absAmount);
1425         appendLogText(i18n("Focusing %2 by %1 ms...", absAmount, dirStr));
1426     }
1427 
1428     return true;
1429 }
1430 
1431 ///////////////////////////////////////////////////////////////////////////////////////////////////
1432 ///
1433 ///////////////////////////////////////////////////////////////////////////////////////////////////
handleFocusMotionTimeout()1434 void Focus::handleFocusMotionTimeout()
1435 {
1436     if (++m_FocusMotionTimerCounter > 3)
1437     {
1438         appendLogText(i18n("Focuser is not responding to commands. Aborting..."));
1439         completeFocusProcedure(Ekos::FOCUS_ABORTED);
1440     }
1441 
1442     const QString dirStr = m_LastFocusDirection == FOCUS_OUT ? i18n("outward") : i18n("inward");
1443     if (canAbsMove)
1444     {
1445         currentFocuser->moveAbs(m_LastFocusSteps);
1446         appendLogText(i18n("Focus motion timed out. Focusing to %1 steps...", m_LastFocusSteps));
1447     }
1448     else if (canRelMove)
1449     {
1450         currentFocuser->moveRel(m_LastFocusSteps);
1451         appendLogText(i18n("Focus motion timed out. Focusing %2 by %1 steps...", m_LastFocusSteps,
1452                            dirStr));
1453     }
1454     else
1455     {
1456         currentFocuser->moveByTimer(m_LastFocusSteps);
1457         appendLogText(i18n("Focus motion timed out. Focusing %2 by %1 ms...",
1458                            m_LastFocusSteps, dirStr));
1459     }
1460 }
1461 
processData(const QSharedPointer<FITSData> & data)1462 void Focus::processData(const QSharedPointer<FITSData> &data)
1463 {
1464     // Ignore guide head if there is any.
1465     if (data->property("chip").toInt() == ISD::CCDChip::GUIDE_CCD)
1466         return;
1467 
1468     if (data)
1469         m_ImageData = data;
1470     else
1471         m_ImageData.reset();
1472 
1473     captureTimeout.stop();
1474     captureTimeoutCounter = 0;
1475 
1476     ISD::CCDChip *targetChip = currentCCD->getChip(ISD::CCDChip::PRIMARY_CCD);
1477     disconnect(currentCCD, &ISD::CCD::newImage, this, &Ekos::Focus::processData);
1478     disconnect(currentCCD, &ISD::CCD::error, this, &Ekos::Focus::processCaptureError);
1479 
1480     if (m_ImageData && darkFrameCheck->isChecked())
1481     {
1482         QVariantMap settings = frameSettings[targetChip];
1483         uint16_t offsetX     = settings["x"].toInt() / settings["binx"].toInt();
1484         uint16_t offsetY     = settings["y"].toInt() / settings["biny"].toInt();
1485 
1486         //targetChip->setCaptureFilter(defaultScale);
1487         m_DarkProcessor->denoise(targetChip, m_ImageData, exposureIN->value(), offsetX, offsetY);
1488         return;
1489     }
1490 
1491     setCaptureComplete();
1492     resetButtons();
1493 }
1494 
calculateHFR()1495 void Focus::calculateHFR()
1496 {
1497     appendLogText(i18n("Detection complete."));
1498 
1499     // Beware as this HFR value is then treated specifically by the graph renderer
1500     double hfr = FocusAlgorithmInterface::IGNORED_HFR;
1501 
1502     if (m_StarFinderWatcher.result() == false)
1503     {
1504         qCWarning(KSTARS_EKOS_FOCUS) << "Failed to extract any stars.";
1505     }
1506     else
1507     {
1508         if (Options::focusUseFullField())
1509         {
1510             focusView->setStarFilterRange(static_cast <float> (fullFieldInnerRing->value() / 100.0),
1511                                           static_cast <float> (fullFieldOuterRing->value() / 100.0));
1512             focusView->filterStars();
1513 
1514             // Get the average HFR of the whole frame
1515             hfr = m_ImageData->getHFR(HFR_AVERAGE);
1516         }
1517         else
1518         {
1519             focusView->setTrackingBoxEnabled(true);
1520 
1521             // JM 2020-10-08: Try to get first the same HFR star already selected before
1522             // so that it doesn't keep jumping around
1523 
1524             if (starCenter.isNull() == false)
1525                 hfr = m_ImageData->getHFR(starCenter.x(), starCenter.y());
1526 
1527             // If not found, then get the MAX or MEDIAN depending on the selected algorithm.
1528             if (hfr < 0)
1529                 hfr = m_ImageData->getHFR(focusDetection == ALGORITHM_SEP ? HFR_HIGH : HFR_MAX);
1530         }
1531     }
1532 
1533     hfrInProgress = false;
1534     resetButtons();
1535     setCurrentHFR(hfr);
1536 }
1537 
analyzeSources()1538 void Focus::analyzeSources()
1539 {
1540     appendLogText(i18n("Detecting sources..."));
1541     hfrInProgress = true;
1542 
1543     QVariantMap extractionSettings;
1544     extractionSettings["optionsProfileIndex"] = Options::focusOptionsProfile();
1545     extractionSettings["optionsProfileGroup"] =  static_cast<int>(Ekos::FocusProfiles);
1546     m_ImageData->setSourceExtractorSettings(extractionSettings);
1547     // When we're using FULL field view, we always use either CENTROID algorithm which is the default
1548     // standard algorithm in KStars, or SEP. The other algorithms are too inefficient to run on full frames and require
1549     // a bounding box for them to be effective in near real-time application.
1550     if (Options::focusUseFullField())
1551     {
1552         focusView->setTrackingBoxEnabled(false);
1553 
1554         if (focusDetection != ALGORITHM_CENTROID && focusDetection != ALGORITHM_SEP)
1555             m_StarFinderWatcher.setFuture(m_ImageData->findStars(ALGORITHM_CENTROID));
1556         else
1557             m_StarFinderWatcher.setFuture(m_ImageData->findStars(focusDetection));
1558     }
1559     else
1560     {
1561         QRect searchBox = focusView->isTrackingBoxEnabled() ? focusView->getTrackingBox() : QRect();
1562         // If star is already selected then use whatever algorithm currently selected.
1563         if (starSelected)
1564         {
1565             m_StarFinderWatcher.setFuture(m_ImageData->findStars(focusDetection, searchBox));
1566         }
1567         else
1568         {
1569             // Disable tracking box
1570             focusView->setTrackingBoxEnabled(false);
1571 
1572             // If algorithm is set something other than Centeroid or SEP, then force Centroid
1573             // Since it is the most reliable detector when nothing was selected before.
1574             if (focusDetection != ALGORITHM_CENTROID && focusDetection != ALGORITHM_SEP)
1575                 m_StarFinderWatcher.setFuture(m_ImageData->findStars(ALGORITHM_CENTROID));
1576             else
1577                 // Otherwise, continue to find use using the selected algorithm
1578                 m_StarFinderWatcher.setFuture(m_ImageData->findStars(focusDetection, searchBox));
1579         }
1580     }
1581 }
1582 
appendHFR(double newHFR)1583 bool Focus::appendHFR(double newHFR)
1584 {
1585     // Add new HFR to existing values, even if invalid
1586     HFRFrames.append(newHFR);
1587 
1588     // Prepare a work vector with valid HFR values
1589     QVector <double> samples(HFRFrames);
1590     samples.erase(std::remove_if(samples.begin(), samples.end(), [](const double HFR)
1591     {
1592         return HFR == FocusAlgorithmInterface::IGNORED_HFR;
1593     }), samples.end());
1594 
1595     // Perform simple sigma clipping if more than a few samples
1596     if (samples.count() > 3)
1597     {
1598         // Sort all HFRs and extract the median
1599         std::sort(samples.begin(), samples.end());
1600         const auto median =
1601             ((samples.size() % 2) ?
1602              samples[samples.size() / 2] :
1603              (static_cast<double>(samples[samples.size() / 2 - 1]) + samples[samples.size() / 2]) * .5);
1604 
1605         // Extract the mean
1606         const auto mean = std::accumulate(samples.begin(), samples.end(), .0) / samples.size();
1607 
1608         // Extract the variance
1609         double variance = 0;
1610         foreach (auto val, samples)
1611             variance += (val - mean) * (val - mean);
1612 
1613         // Deduce the standard deviation
1614         const double stddev = sqrt(variance / samples.size());
1615 
1616         // Reject those 2 sigma away from median
1617         const double sigmaHigh = median + stddev * 2;
1618         const double sigmaLow  = median - stddev * 2;
1619 
1620         // FIXME: why is the first value not considered?
1621         // FIXME: what if there are less than 3 samples after clipping?
1622         QMutableVectorIterator<double> i(samples);
1623         while (i.hasNext())
1624         {
1625             auto val = i.next();
1626             if (val > sigmaHigh || val < sigmaLow)
1627                 i.remove();
1628         }
1629     }
1630 
1631     // Consolidate the average HFR
1632     currentHFR = samples.isEmpty() ? -1 : std::accumulate(samples.begin(), samples.end(), .0) / samples.size();
1633 
1634     // Return whether we need more frame based on user requirement
1635     return HFRFrames.count() < focusFramesSpin->value();
1636 }
1637 
settle(const FocusState completionState,const bool autoFocusUsed)1638 void Focus::settle(const FocusState completionState, const bool autoFocusUsed)
1639 {
1640     state = completionState;
1641     if (completionState == Ekos::FOCUS_COMPLETE)
1642     {
1643         if (autoFocusUsed)
1644         {
1645             // Prepare the message for Analyze
1646             const int size = hfr_position.size();
1647             QString analysis_results = "";
1648 
1649             for (int i = 0; i < size; ++i)
1650             {
1651                 analysis_results.append(QString("%1%2|%3")
1652                                         .arg(i == 0 ? "" : "|" )
1653                                         .arg(QString::number(hfr_position[i], 'f', 0))
1654                                         .arg(QString::number(hfr_value[i], 'f', 3)));
1655             }
1656 
1657             KSNotification::event(QLatin1String("FocusSuccessful"), i18n("Autofocus operation completed successfully"));
1658             emit autofocusComplete(filter(), analysis_results);
1659         }
1660     }
1661     else
1662     {
1663         if (autoFocusUsed)
1664         {
1665             KSNotification::event(QLatin1String("FocusFailed"), i18n("Autofocus operation failed"),
1666                                   KSNotification::EVENT_ALERT);
1667             emit autofocusAborted(filter(), "");
1668         }
1669     }
1670 
1671     qCDebug(KSTARS_EKOS_FOCUS) << "Settled. State:" << Ekos::getFocusStatusString(state);
1672 
1673     // Delay state notification if we have a locked filter pending return to original filter
1674     if (fallbackFilterPending)
1675     {
1676         filterManager->setFilterPosition(fallbackFilterPosition,
1677                                          static_cast<FilterManager::FilterPolicy>(FilterManager::CHANGE_POLICY | FilterManager::OFFSET_POLICY));
1678     }
1679     else
1680         emit newStatus(state);
1681 
1682     resetButtons();
1683 }
1684 
completeFocusProcedure(FocusState completionState,bool plot)1685 void Focus::completeFocusProcedure(FocusState completionState, bool plot)
1686 {
1687     if (inAutoFocus)
1688     {
1689         if (completionState == Ekos::FOCUS_COMPLETE)
1690         {
1691             if (plot)
1692                 emit redrawHFRPlot(polynomialFit.get(), currentPosition, currentHFR);
1693             appendLogText(i18np("Focus procedure completed after %1 iteration.",
1694                                 "Focus procedure completed after %1 iterations.", hfr_position.count()));
1695 
1696             setLastFocusTemperature();
1697 
1698             // CR add auto focus position, temperature and filter to log in CSV format
1699             // this will help with setting up focus offsets and temperature compensation
1700             qCInfo(KSTARS_EKOS_FOCUS) << "Autofocus values: position," << currentPosition << ", temperature,"
1701                                       << m_LastSourceAutofocusTemperature << ", filter," << filter()
1702                                       << ", HFR," << currentHFR << ", altitude," << mountAlt;
1703 
1704             appendFocusLogText(QString("%1, %2, %3, %4, %5\n")
1705                                .arg(QString::number(currentPosition))
1706                                .arg(QString::number(m_LastSourceAutofocusTemperature, 'f', 1))
1707                                .arg(filter())
1708                                .arg(QString::number(currentHFR, 'f', 3))
1709                                .arg(QString::number(mountAlt, 'f', 1)));
1710 
1711             // Replace user position with optimal position
1712             absTicksSpin->setValue(currentPosition);
1713         }
1714         // In case of failure, go back to last position if the focuser is absolute
1715         else if (canAbsMove && initialFocuserAbsPosition >= 0 && resetFocusIteration <= MAXIMUM_RESET_ITERATIONS)
1716         {
1717             // If we're doing in-sequence focusing using an absolute focuser, retry focusing once, starting from last known good position
1718             bool const retry_focusing = !restartFocus && ++resetFocusIteration < MAXIMUM_RESET_ITERATIONS;
1719 
1720             // If retrying, before moving, reset focus frame in case the star in subframe was lost
1721             if (retry_focusing)
1722             {
1723                 restartFocus = true;
1724                 resetFrame();
1725             }
1726 
1727             resetFocuser();
1728 
1729             // Bypass the rest of the function if we retry - we will fail if we could not move the focuser
1730             if (retry_focusing)
1731                 return;
1732         }
1733 
1734         // Reset the retry count on success or maximum count
1735         resetFocusIteration = 0;
1736     }
1737 
1738     const bool autoFocusUsed = inAutoFocus;
1739 
1740     // Reset the autofocus flags
1741     stop(completionState);
1742 
1743     // Refresh display if needed
1744     if (focusAlgorithm == FOCUS_POLYNOMIAL && plot)
1745         emit drawPolynomial(polynomialFit.get(), isVShapeSolution, true);
1746 
1747     // Enforce settling duration
1748     int const settleTime = m_GuidingSuspended ? GuideSettleTime->value() : 0;
1749 
1750     if (settleTime > 0)
1751         appendLogText(i18n("Settling for %1s...", settleTime));
1752 
1753     QTimer::singleShot(settleTime * 1000, this, [ &, settleTime, completionState, autoFocusUsed]()
1754     {
1755         settle(completionState, autoFocusUsed);
1756 
1757         if (settleTime > 0)
1758             appendLogText(i18n("Settling complete."));
1759     });
1760 }
1761 
resetFocuser()1762 void Focus::resetFocuser()
1763 {
1764     // If we are able to and need to, move the focuser back to the initial position and let the procedure restart from its termination
1765     if (currentFocuser && currentFocuser->isConnected() && initialFocuserAbsPosition >= 0)
1766     {
1767         // HACK: If the focuser will not move, cheat a little to get the notification - see processNumber
1768         if (currentPosition == initialFocuserAbsPosition)
1769             currentPosition--;
1770 
1771         appendLogText(i18n("Autofocus failed, moving back to initial focus position %1.", initialFocuserAbsPosition));
1772         currentFocuser->moveAbs(initialFocuserAbsPosition);
1773         /* Restart will be executed by the end-of-move notification from the device if needed by resetFocus */
1774     }
1775 }
1776 
setCurrentHFR(double value)1777 void Focus::setCurrentHFR(double value)
1778 {
1779     currentHFR = value;
1780 
1781     // Let's now report the current HFR
1782     qCDebug(KSTARS_EKOS_FOCUS) << "Focus newFITS #" << HFRFrames.count() + 1 << ": Current HFR " << currentHFR << " Num stars "
1783                                << (starSelected ? 1 : m_ImageData->getDetectedStars());
1784 
1785     // Take the new HFR into account, eventually continue to stack samples
1786     if (appendHFR(currentHFR))
1787     {
1788         capture();
1789         return;
1790     }
1791     else HFRFrames.clear();
1792 
1793     // Let signal the current HFR now depending on whether the focuser is absolute or relative
1794     if (canAbsMove)
1795         emit newHFR(currentHFR, currentPosition);
1796     else
1797         emit newHFR(currentHFR, -1);
1798 
1799     // Format the HFR value into a string
1800     QString HFRText = QString("%1").arg(currentHFR, 0, 'f', 2);
1801     HFROut->setText(HFRText);
1802     starsOut->setText(QString("%1").arg(m_ImageData->getDetectedStars()));
1803     iterOut->setText(QString("%1").arg(absIterations + 1));
1804 
1805     // Display message in case _last_ HFR was negative
1806     if (lastHFR == FocusAlgorithmInterface::IGNORED_HFR)
1807         appendLogText(i18n("FITS received. No stars detected."));
1808 
1809     // If we have a valid HFR value
1810     if (currentHFR > 0)
1811     {
1812         // Check if we're done from polynomial fitting algorithm
1813         if (focusAlgorithm == FOCUS_POLYNOMIAL && polySolutionFound == MINIMUM_POLY_SOLUTIONS)
1814         {
1815             polySolutionFound = 0;
1816             completeFocusProcedure(Ekos::FOCUS_COMPLETE);
1817             return;
1818         }
1819 
1820         Edge *selectedHFRStarHFR = nullptr;
1821 
1822         // Center tracking box around selected star (if it valid) either in:
1823         // 1. Autofocus
1824         // 2. CheckFocus (minimumHFRCheck)
1825         // The starCenter _must_ already be defined, otherwise, we proceed until
1826         // the latter half of the function searches for a star and define it.
1827         if (starCenter.isNull() == false && (inAutoFocus || minimumRequiredHFR >= 0) &&
1828                 (selectedHFRStarHFR = m_ImageData->getSelectedHFRStar()) != nullptr)
1829         {
1830             // Now we have star selected in the frame
1831             starSelected = true;
1832             starCenter.setX(qMax(0, static_cast<int>(selectedHFRStarHFR->x)));
1833             starCenter.setY(qMax(0, static_cast<int>(selectedHFRStarHFR->y)));
1834 
1835             syncTrackingBoxPosition();
1836 
1837             // Record the star information (X, Y, currentHFR)
1838             QVector3D oneStar = starCenter;
1839             oneStar.setZ(currentHFR);
1840             starsHFR.append(oneStar);
1841         }
1842         else
1843         {
1844             // Record the star information (X, Y, currentHFR)
1845             QVector3D oneStar(starCenter.x(), starCenter.y(), currentHFR);
1846             starsHFR.append(oneStar);
1847         }
1848 
1849         if (currentHFR > maxHFR)
1850             maxHFR = currentHFR;
1851 
1852         // Append point to the #Iterations vs #HFR chart in case of looping or in case in autofocus with a focus
1853         // that does not support position feedback.
1854 
1855         // If inAutoFocus is true without canAbsMove and without canRelMove, canTimerMove must be true.
1856         // We'd only want to execute this if the focus linear algorithm is not being used, as that
1857         // algorithm simulates a position-based system even for timer-based focusers.
1858         if (inFocusLoop || (inAutoFocus && ! isPositionBased()))
1859         {
1860             int pos = hfr_position.empty() ? 1 : hfr_position.last() + 1;
1861             addPlotPosition(pos, currentHFR);
1862         }
1863     }
1864     else
1865     {
1866         // Let's record an invalid star result - IGNORED_HFR must be suitable as invalid star HFR!
1867         QVector3D oneStar(starCenter.x(), starCenter.y(), FocusAlgorithmInterface::IGNORED_HFR);
1868         starsHFR.append(oneStar);
1869     }
1870 
1871     // First check that we haven't already search for stars
1872     // Since star-searching algorithm are time-consuming, we should only search when necessary
1873     focusView->updateFrame();
1874 
1875     setHFRComplete();
1876 }
1877 
setCaptureComplete()1878 void Focus::setCaptureComplete()
1879 {
1880     DarkLibrary::Instance()->disconnect(this);
1881 
1882     // If we have a box, sync the bounding box to its position.
1883     syncTrackingBoxPosition();
1884 
1885     // Notify user if we're not looping
1886     if (inFocusLoop == false)
1887         appendLogText(i18n("Image received."));
1888 
1889     if (captureInProgress && inFocusLoop == false && inAutoFocus == false)
1890         currentCCD->setUploadMode(rememberUploadMode);
1891 
1892     if (m_RememberCameraFastExposure && inFocusLoop == false && inAutoFocus == false)
1893     {
1894         m_RememberCameraFastExposure = false;
1895         currentCCD->setFastExposureEnabled(true);
1896     }
1897 
1898     captureInProgress = false;
1899 
1900 
1901     // Emit the whole image
1902     emit newImage(focusView);
1903     // Emit the tracking (bounding) box view. Used in Summary View
1904     emit newStarPixmap(focusView->getTrackingBoxPixmap(10));
1905 
1906     // If we are not looping; OR
1907     // If we are looping but we already have tracking box enabled; OR
1908     // If we are asked to analyze _all_ the stars within the field
1909     // THEN let's find stars in the image and get current HFR
1910     if (inFocusLoop == false || (inFocusLoop && (focusView->isTrackingBoxEnabled() || Options::focusUseFullField())))
1911     {
1912         if (m_ImageData->areStarsSearched() == false)
1913         {
1914             analyzeSources();
1915         }
1916     }
1917     else
1918         setHFRComplete();
1919 }
1920 
setHFRComplete()1921 void Focus::setHFRComplete()
1922 {
1923     // If we are just framing, let's capture again
1924     if (inFocusLoop)
1925     {
1926         capture();
1927         return;
1928     }
1929 
1930     // Get target chip
1931     ISD::CCDChip *targetChip = currentCCD->getChip(ISD::CCDChip::PRIMARY_CCD);
1932 
1933     // Get target chip binning
1934     int subBinX = 1, subBinY = 1;
1935     if (!targetChip->getBinning(&subBinX, &subBinY))
1936         qCDebug(KSTARS_EKOS_FOCUS) << "Warning: target chip is reporting no binning property, using 1x1.";
1937 
1938     // If star is NOT yet selected in a non-full-frame situation
1939     // then let's now try to find the star. This step is skipped for full frames
1940     // since there isn't a single star to select as we are only interested in the overall average HFR.
1941     // We need to check if we can find the star right away, or if we need to _subframe_ around the
1942     // selected star.
1943     if (Options::focusUseFullField() == false && starCenter.isNull())
1944     {
1945         int x = 0, y = 0, w = 0, h = 0;
1946 
1947         // Let's get the stored frame settings for this particular chip
1948         if (frameSettings.contains(targetChip))
1949         {
1950             QVariantMap settings = frameSettings[targetChip];
1951             x                    = settings["x"].toInt();
1952             y                    = settings["y"].toInt();
1953             w                    = settings["w"].toInt();
1954             h                    = settings["h"].toInt();
1955         }
1956         else
1957             // Otherwise let's get the target chip frame coordinates.
1958             targetChip->getFrame(&x, &y, &w, &h);
1959 
1960         // In case auto star is selected.
1961         if (useAutoStar->isChecked())
1962         {
1963             // Do we have a valid star detected?
1964             Edge *selectedHFRStar = m_ImageData->getSelectedHFRStar();
1965 
1966             if (selectedHFRStar == nullptr)
1967             {
1968                 appendLogText(i18n("Failed to automatically select a star. Please select a star manually."));
1969 
1970                 // Center the tracking box in the frame and display it
1971                 focusView->setTrackingBox(QRect(w - focusBoxSize->value() / (subBinX * 2),
1972                                                 h - focusBoxSize->value() / (subBinY * 2),
1973                                                 focusBoxSize->value() / subBinX, focusBoxSize->value() / subBinY));
1974                 focusView->setTrackingBoxEnabled(true);
1975 
1976                 // Use can now move it to select the desired star
1977                 state = Ekos::FOCUS_WAITING;
1978                 qCDebug(KSTARS_EKOS_FOCUS) << "State:" << Ekos::getFocusStatusString(state);
1979                 emit newStatus(state);
1980 
1981                 // Start the wait timer so we abort after a timeout if the user does not make a choice
1982                 waitStarSelectTimer.start();
1983 
1984                 return;
1985             }
1986 
1987             // set the tracking box on selectedHFRStar
1988             starCenter.setX(selectedHFRStar->x);
1989             starCenter.setY(selectedHFRStar->y);
1990             starCenter.setZ(subBinX);
1991             starSelected = true;
1992             syncTrackingBoxPosition();
1993 
1994             defaultScale = static_cast<FITSScale>(filterCombo->currentIndex());
1995 
1996             // Do we need to subframe?
1997             if (subFramed == false && useSubFrame->isEnabled() && useSubFrame->isChecked())
1998             {
1999                 int offset = (static_cast<double>(focusBoxSize->value()) / subBinX) * 1.5;
2000                 int subX   = (selectedHFRStar->x - offset) * subBinX;
2001                 int subY   = (selectedHFRStar->y - offset) * subBinY;
2002                 int subW   = offset * 2 * subBinX;
2003                 int subH   = offset * 2 * subBinY;
2004 
2005                 int minX, maxX, minY, maxY, minW, maxW, minH, maxH;
2006                 targetChip->getFrameMinMax(&minX, &maxX, &minY, &maxY, &minW, &maxW, &minH, &maxH);
2007 
2008                 // Try to limit the subframed selection
2009                 if (subX < minX)
2010                     subX = minX;
2011                 if (subY < minY)
2012                     subY = minY;
2013                 if ((subW + subX) > maxW)
2014                     subW = maxW - subX;
2015                 if ((subH + subY) > maxH)
2016                     subH = maxH - subY;
2017 
2018                 // Now we store the subframe coordinates in the target chip frame settings so we
2019                 // reuse it later when we capture again.
2020                 QVariantMap settings = frameSettings[targetChip];
2021                 settings["x"]        = subX;
2022                 settings["y"]        = subY;
2023                 settings["w"]        = subW;
2024                 settings["h"]        = subH;
2025                 settings["binx"]     = subBinX;
2026                 settings["biny"]     = subBinY;
2027 
2028                 qCDebug(KSTARS_EKOS_FOCUS) << "Frame is subframed. X:" << subX << "Y:" << subY << "W:" << subW << "H:" << subH << "binX:" <<
2029                                            subBinX << "binY:" << subBinY;
2030 
2031                 starsHFR.clear();
2032 
2033                 frameSettings[targetChip] = settings;
2034 
2035                 // Set the star center in the center of the subframed coordinates
2036                 starCenter.setX(subW / (2 * subBinX));
2037                 starCenter.setY(subH / (2 * subBinY));
2038                 starCenter.setZ(subBinX);
2039 
2040                 subFramed = true;
2041 
2042                 focusView->setFirstLoad(true);
2043 
2044                 // Now let's capture again for the actual requested subframed image.
2045                 capture();
2046                 return;
2047             }
2048             // If we're subframed or don't need subframe, let's record the max star coordinates
2049             else
2050             {
2051                 starCenter.setX(selectedHFRStar->x);
2052                 starCenter.setY(selectedHFRStar->y);
2053                 starCenter.setZ(subBinX);
2054 
2055                 // Let's now capture again if we're autofocusing
2056                 if (inAutoFocus)
2057                 {
2058                     capture();
2059                     return;
2060                 }
2061             }
2062         }
2063         // If manual selection is enabled then let's ask the user to select the focus star
2064         else
2065         {
2066             appendLogText(i18n("Capture complete. Select a star to focus."));
2067 
2068             starSelected = false;
2069 
2070             // Let's now display and set the tracking box in the center of the frame
2071             // so that the user moves it around to select the desired star.
2072             int subBinX = 1, subBinY = 1;
2073             targetChip->getBinning(&subBinX, &subBinY);
2074 
2075             focusView->setTrackingBox(QRect((w - focusBoxSize->value()) / (subBinX * 2),
2076                                             (h - focusBoxSize->value()) / (2 * subBinY),
2077                                             focusBoxSize->value() / subBinX, focusBoxSize->value() / subBinY));
2078             focusView->setTrackingBoxEnabled(true);
2079 
2080             // Now we wait
2081             state = Ekos::FOCUS_WAITING;
2082             qCDebug(KSTARS_EKOS_FOCUS) << "State:" << Ekos::getFocusStatusString(state);
2083             emit newStatus(state);
2084 
2085             // If the user does not select for a timeout period, we abort.
2086             waitStarSelectTimer.start();
2087             return;
2088         }
2089     }
2090 
2091     // Check if the focus module is requested to verify if the minimum HFR value is met.
2092     if (minimumRequiredHFR >= 0)
2093     {
2094         // In case we failed to detected, we capture again.
2095         if (currentHFR == -1)
2096         {
2097             if (noStarCount++ < MAX_RECAPTURE_RETRIES)
2098             {
2099                 appendLogText(i18n("No stars detected while testing HFR, capturing again..."));
2100                 // On Last Attempt reset focus frame to capture full frame and recapture star if possible
2101                 if (noStarCount == MAX_RECAPTURE_RETRIES)
2102                     resetFrame();
2103                 capture();
2104                 return;
2105             }
2106             // If we exceeded maximum tries we abort
2107             else
2108             {
2109                 noStarCount = 0;
2110                 completeFocusProcedure(Ekos::FOCUS_ABORTED);
2111             }
2112         }
2113         // If the detect current HFR is more than the minimum required HFR
2114         // then we should start the autofocus process now to bring it down.
2115         else if (currentHFR > minimumRequiredHFR)
2116         {
2117             qCDebug(KSTARS_EKOS_FOCUS) << "Current HFR:" << currentHFR << "is above required minimum HFR:" << minimumRequiredHFR <<
2118                                        ". Starting AutoFocus...";
2119             minimumRequiredHFR = -1;
2120             start();
2121         }
2122         // Otherwise, the current HFR is fine and lower than the required minimum HFR so we announce success.
2123         else
2124         {
2125             qCDebug(KSTARS_EKOS_FOCUS) << "Current HFR:" << currentHFR << "is below required minimum HFR:" << minimumRequiredHFR <<
2126                                        ". Autofocus successful.";
2127             completeFocusProcedure(Ekos::FOCUS_COMPLETE);
2128         }
2129 
2130         // Nothing more for now
2131         return;
2132     }
2133 
2134     // If focus logging is enabled, let's save the frame.
2135     if (Options::focusLogging() && Options::saveFocusImages())
2136     {
2137         QDir dir;
2138         QDateTime now = KStarsData::Instance()->lt();
2139         QString path = QDir(KSPaths::writableLocation(QStandardPaths::AppDataLocation)).filePath("autofocus/" +
2140                        now.toString("yyyy-MM-dd"));
2141         dir.mkpath(path);
2142         // IS8601 contains colons but they are illegal under Windows OS, so replacing them with '-'
2143         // The timestamp is no longer ISO8601 but it should solve interoperality issues between different OS hosts
2144         QString name     = "autofocus_frame_" + now.toString("HH-mm-ss") + ".fits";
2145         QString filename = path + QStringLiteral("/") + name;
2146         m_ImageData->saveImage(filename);
2147     }
2148 
2149     // If we are not in autofocus process, we're done.
2150     if (inAutoFocus == false)
2151     {
2152         // If we are done and there is no further autofocus,
2153         // we reset state to IDLE
2154         if (state != Ekos::FOCUS_IDLE)
2155         {
2156             state = Ekos::FOCUS_IDLE;
2157             qCDebug(KSTARS_EKOS_FOCUS) << "State:" << Ekos::getFocusStatusString(state);
2158             emit newStatus(state);
2159         }
2160 
2161         resetButtons();
2162         return;
2163     }
2164 
2165     // Set state to progress
2166     if (state != Ekos::FOCUS_PROGRESS)
2167     {
2168         state = Ekos::FOCUS_PROGRESS;
2169         qCDebug(KSTARS_EKOS_FOCUS) << "State:" << Ekos::getFocusStatusString(state);
2170         emit newStatus(state);
2171     }
2172 
2173     // Now let's kick in the algorithms
2174 
2175     if (focusAlgorithm == FOCUS_LINEAR)
2176         autoFocusLinear();
2177     else if (canAbsMove || canRelMove)
2178         // Position-based algorithms
2179         autoFocusAbs();
2180     else
2181         // Time open-looped algorithms
2182         autoFocusRel();
2183 }
2184 
clearDataPoints()2185 void Focus::clearDataPoints()
2186 {
2187     maxHFR = 1;
2188     polynomialFit.reset();
2189     hfr_position.clear();
2190     hfr_value.clear();
2191     isVShapeSolution = false;
2192     emit initHFRPlot(inFocusLoop == false && isPositionBased());
2193 }
2194 
autoFocusChecks()2195 bool Focus::autoFocusChecks()
2196 {
2197     if (++absIterations > MAXIMUM_ABS_ITERATIONS)
2198     {
2199         appendLogText(i18n("Autofocus failed to reach proper focus. Try increasing tolerance value."));
2200         completeFocusProcedure(Ekos::FOCUS_ABORTED);
2201         return false;
2202     }
2203 
2204     // No stars detected, try to capture again
2205     if (currentHFR == FocusAlgorithmInterface::IGNORED_HFR)
2206     {
2207         if (noStarCount < MAX_RECAPTURE_RETRIES)
2208         {
2209             noStarCount++;
2210             appendLogText(i18n("No stars detected, capturing again..."));
2211             capture();
2212             return false;
2213         }
2214         else if (focusAlgorithm == FOCUS_LINEAR)
2215         {
2216             appendLogText(i18n("Failed to detect any stars at position %1. Continuing...", currentPosition));
2217             noStarCount = 0;
2218         }
2219         else
2220         {
2221             appendLogText(i18n("Failed to detect any stars. Reset frame and try again."));
2222             completeFocusProcedure(Ekos::FOCUS_ABORTED);
2223             return false;
2224         }
2225     }
2226     else
2227         noStarCount = 0;
2228 
2229     return true;
2230 }
2231 
plotLinearFocus()2232 void Focus::plotLinearFocus()
2233 {
2234     // I was hoping to avoid intermediate plotting, just set everything up then plot,
2235     // but this isn't working. For now, with plt=true, plot on every intermediate update.
2236     bool plt = true;
2237 
2238     // Get the data to plot.
2239     QVector<double> HFRs;
2240     QVector<int> positions;
2241     linearFocuser->getMeasurements(&positions, &HFRs);
2242     const FocusAlgorithmInterface::FocusParams &params = linearFocuser->getParams();
2243 
2244     // As an optimization for slower machines, e.g. RPi4s, if the points are the same except for
2245     // the last point, just emit the last point instead of redrawing everything.
2246     static QVector<double> lastHFRs;
2247     static QVector<int> lastPositions;
2248     bool incrementalChange = false;
2249     if (positions.size() > 1 && positions.size() == lastPositions.size() + 1)
2250     {
2251         bool ok = true;
2252         for (int i = 0; i < positions.size() - 1; ++i)
2253             if (positions[i] != lastPositions[i] || HFRs[i] != lastHFRs[i])
2254             {
2255                 ok = false;
2256                 break;
2257             }
2258         incrementalChange = ok;
2259     }
2260     lastPositions = positions;
2261     lastHFRs = HFRs;
2262 
2263     if (incrementalChange)
2264         emit newHFRPlotPosition(static_cast<double>(positions.last()), HFRs.last(), params.initialStepSize, plt);
2265     else
2266     {
2267         emit initHFRPlot(true);
2268         for (int i = 0; i < positions.size(); ++i)
2269             emit newHFRPlotPosition(static_cast<double>(positions[i]), HFRs[i], params.initialStepSize, plt);
2270     }
2271 
2272     // Plot the polynomial, if there are enough points.
2273     if (HFRs.size() > 3)
2274     {
2275         // The polynomial should only reflect 1st-pass samples.
2276         QVector<double> pass1HFRs;
2277         QVector<int> pass1Positions;
2278         linearFocuser->getPass1Measurements(&pass1Positions, &pass1HFRs);
2279         polynomialFit.reset(new PolynomialFit(2, pass1Positions, pass1HFRs));
2280 
2281         double minPosition, minValue;
2282         double searchMin = std::max(params.minPositionAllowed, params.startPosition - params.maxTravel);
2283         double searchMax = std::min(params.maxPositionAllowed, params.startPosition + params.maxTravel);
2284         if (polynomialFit->findMinimum(params.startPosition, searchMin, searchMax, &minPosition, &minValue))
2285         {
2286             emit drawPolynomial(polynomialFit.get(), true, true, plt);
2287 
2288             // Only plot the first pass' min position if we're not done.
2289             // Once we have a result, we don't want to display an intermediate minimum.
2290             if (linearFocuser->isDone())
2291                 emit minimumFound(-1, -1, plt);
2292             else
2293                 emit minimumFound(minPosition, minValue, plt);
2294         }
2295         else
2296         {
2297             // Didn't get a good polynomial fit.
2298             emit drawPolynomial(polynomialFit.get(), false, false, plt);
2299             emit minimumFound(-1, -1, plt);
2300         }
2301     }
2302 
2303     // Linear focuser might change the latest hfr with its relativeHFR scheme.
2304     HFROut->setText(QString("%1").arg(linearFocuser->latestHFR(), 0, 'f', 2));
2305 
2306     emit setTitle(linearFocuser->getTextStatus());
2307 
2308     if (!plt) HFRPlot->replot();
2309 }
2310 
autoFocusLinear()2311 void Focus::autoFocusLinear()
2312 {
2313     if (!autoFocusChecks())
2314         return;
2315 
2316     if (!canAbsMove && !canRelMove && canTimerMove)
2317     {
2318         //const bool kFixPosition = true;
2319         if (linearRequestedPosition != currentPosition)
2320             //if (kFixPosition && (linearRequestedPosition != currentPosition))
2321         {
2322             qCDebug(KSTARS_EKOS_FOCUS) << "Linear: warning, changing position " << currentPosition << " to "
2323                                        << linearRequestedPosition;
2324 
2325             currentPosition = linearRequestedPosition;
2326         }
2327     }
2328 
2329     addPlotPosition(currentPosition, currentHFR, false);
2330 
2331     // Only use the relativeHFR algorithm if full field is enabled with one capture/measurement.
2332     bool useFocusStarsHFR = Options::focusUseFullField() && focusFramesSpin->value() == 1;
2333     auto focusStars = useFocusStarsHFR ? &(m_ImageData->getStarCenters()) : nullptr;
2334 
2335     linearRequestedPosition = linearFocuser->newMeasurement(currentPosition, currentHFR, focusStars);
2336     plotLinearFocus();
2337 
2338     const int nextPosition = adjustLinearPosition(currentPosition, linearRequestedPosition);
2339     if (linearRequestedPosition == -1)
2340     {
2341         if (linearFocuser->isDone() && linearFocuser->solution() != -1)
2342         {
2343             completeFocusProcedure(Ekos::FOCUS_COMPLETE, false);
2344         }
2345         else
2346         {
2347             qCDebug(KSTARS_EKOS_FOCUS) << linearFocuser->doneReason();
2348             appendLogText("Linear autofocus algorithm aborted.");
2349             completeFocusProcedure(Ekos::FOCUS_ABORTED, false);
2350         }
2351         return;
2352     }
2353     else
2354     {
2355         const int delta = nextPosition - currentPosition;
2356 
2357         if (!changeFocus(delta))
2358             completeFocusProcedure(Ekos::FOCUS_ABORTED, false);
2359 
2360         return;
2361     }
2362 }
2363 
autoFocusAbs()2364 void Focus::autoFocusAbs()
2365 {
2366     // Q_ASSERT_X(canAbsMove || canRelMove, __FUNCTION__, "Prerequisite: only absolute and relative focusers");
2367 
2368     static int minHFRPos = 0, focusOutLimit = 0, focusInLimit = 0;
2369     static double minHFR = 0;
2370     double targetPosition = 0, delta = 0;
2371 
2372     QString deltaTxt = QString("%1").arg(fabs(currentHFR - minHFR) * 100.0, 0, 'g', 3);
2373     QString HFRText  = QString("%1").arg(currentHFR, 0, 'g', 3);
2374 
2375     qCDebug(KSTARS_EKOS_FOCUS) << "========================================";
2376     qCDebug(KSTARS_EKOS_FOCUS) << "Current HFR: " << currentHFR << " Current Position: " << currentPosition;
2377     qCDebug(KSTARS_EKOS_FOCUS) << "Last minHFR: " << minHFR << " Last MinHFR Pos: " << minHFRPos;
2378     qCDebug(KSTARS_EKOS_FOCUS) << "Delta: " << deltaTxt << "%";
2379     qCDebug(KSTARS_EKOS_FOCUS) << "========================================";
2380 
2381     if (minHFR)
2382         appendLogText(i18n("FITS received. HFR %1 @ %2. Delta (%3%)", HFRText, currentPosition, deltaTxt));
2383     else
2384         appendLogText(i18n("FITS received. HFR %1 @ %2.", HFRText, currentPosition));
2385 
2386     if (!autoFocusChecks())
2387         return;
2388 
2389     addPlotPosition(currentPosition, currentHFR);
2390 
2391     switch (m_LastFocusDirection)
2392     {
2393         case FOCUS_NONE:
2394             lastHFR                   = currentHFR;
2395             initialFocuserAbsPosition = currentPosition;
2396             minHFR                    = currentHFR;
2397             minHFRPos                 = currentPosition;
2398             HFRDec                    = 0;
2399             HFRInc                    = 0;
2400             focusOutLimit             = 0;
2401             focusInLimit              = 0;
2402 
2403             // This is the first step, so clamp the initial target position to the device limits
2404             // If the focuser cannot move because it is at one end of the interval, try the opposite direction next
2405             if (absMotionMax < currentPosition + pulseDuration)
2406             {
2407                 if (currentPosition < absMotionMax)
2408                 {
2409                     pulseDuration = absMotionMax - currentPosition;
2410                 }
2411                 else
2412                 {
2413                     pulseDuration = 0;
2414                     m_LastFocusDirection = FOCUS_IN;
2415                 }
2416             }
2417             else if (currentPosition + pulseDuration < absMotionMin)
2418             {
2419                 if (absMotionMin < currentPosition)
2420                 {
2421                     pulseDuration = currentPosition - absMotionMin;
2422                 }
2423                 else
2424                 {
2425                     pulseDuration = 0;
2426                     m_LastFocusDirection = FOCUS_OUT;
2427                 }
2428             }
2429 
2430             if (!changeFocus(pulseDuration))
2431                 completeFocusProcedure(Ekos::FOCUS_ABORTED);
2432 
2433             break;
2434 
2435         case FOCUS_IN:
2436         case FOCUS_OUT:
2437             static int lastHFRPos = 0, initSlopePos = 0;
2438             static double initSlopeHFR = 0;
2439 
2440             if (reverseDir && focusInLimit && focusOutLimit &&
2441                     fabs(currentHFR - minHFR) < (toleranceIN->value() / 100.0) && HFRInc == 0)
2442             {
2443                 if (absIterations <= 2)
2444                 {
2445                     appendLogText(
2446                         i18n("Change in HFR is too small. Try increasing the step size or decreasing the tolerance."));
2447                     completeFocusProcedure(Ekos::FOCUS_ABORTED);
2448                 }
2449                 else if (noStarCount > 0)
2450                 {
2451                     appendLogText(i18n("Failed to detect focus star in frame. Capture and select a focus star."));
2452                     completeFocusProcedure(Ekos::FOCUS_ABORTED);
2453                 }
2454                 else
2455                 {
2456                     completeFocusProcedure(Ekos::FOCUS_COMPLETE);
2457                 }
2458                 break;
2459             }
2460             else if (currentHFR < lastHFR)
2461             {
2462                 double slope = 0;
2463 
2464                 // Let's try to calculate slope of the V curve.
2465                 if (initSlopeHFR == 0 && HFRInc == 0 && HFRDec >= 1)
2466                 {
2467                     initSlopeHFR = lastHFR;
2468                     initSlopePos = lastHFRPos;
2469 
2470                     qCDebug(KSTARS_EKOS_FOCUS) << "Setting initial slop to " << initSlopePos << " @ HFR " << initSlopeHFR;
2471                 }
2472 
2473                 // Let's now limit the travel distance of the focuser
2474                 if (m_LastFocusDirection == FOCUS_OUT && lastHFRPos < focusInLimit && fabs(currentHFR - lastHFR) > 0.1)
2475                 {
2476                     focusInLimit = lastHFRPos;
2477                     qCDebug(KSTARS_EKOS_FOCUS) << "New FocusInLimit " << focusInLimit;
2478                 }
2479                 else if (m_LastFocusDirection == FOCUS_IN && lastHFRPos > focusOutLimit &&
2480                          fabs(currentHFR - lastHFR) > 0.1)
2481                 {
2482                     focusOutLimit = lastHFRPos;
2483                     qCDebug(KSTARS_EKOS_FOCUS) << "New FocusOutLimit " << focusOutLimit;
2484                 }
2485 
2486                 // If we have slope, get next target position
2487                 if (initSlopeHFR && absMotionMax > 50)
2488                 {
2489                     double factor = 0.5;
2490                     slope         = (currentHFR - initSlopeHFR) / (currentPosition - initSlopePos);
2491                     if (fabs(currentHFR - minHFR) * 100.0 < 0.5)
2492                         factor = 1 - fabs(currentHFR - minHFR) * 10;
2493                     targetPosition = currentPosition + (currentHFR * factor - currentHFR) / slope;
2494                     if (targetPosition < 0)
2495                     {
2496                         factor = 1;
2497                         while (targetPosition < 0 && factor > 0)
2498                         {
2499                             factor -= 0.005;
2500                             targetPosition = currentPosition + (currentHFR * factor - currentHFR) / slope;
2501                         }
2502                     }
2503                     qCDebug(KSTARS_EKOS_FOCUS) << "Using slope to calculate target pulse...";
2504                 }
2505                 // Otherwise proceed iteratively
2506                 else
2507                 {
2508                     if (m_LastFocusDirection == FOCUS_IN)
2509                         targetPosition = currentPosition - pulseDuration;
2510                     else
2511                         targetPosition = currentPosition + pulseDuration;
2512 
2513                     qCDebug(KSTARS_EKOS_FOCUS) << "Proceeding iteratively to next target pulse ...";
2514                 }
2515 
2516                 qCDebug(KSTARS_EKOS_FOCUS) << "V-Curve Slope " << slope << " current Position " << currentPosition
2517                                            << " targetPosition " << targetPosition;
2518 
2519                 lastHFR = currentHFR;
2520 
2521                 // Let's keep track of the minimum HFR
2522                 if (lastHFR < minHFR)
2523                 {
2524                     minHFR    = lastHFR;
2525                     minHFRPos = currentPosition;
2526                     qCDebug(KSTARS_EKOS_FOCUS) << "new minHFR " << minHFR << " @ position " << minHFRPos;
2527                 }
2528 
2529                 lastHFRPos = currentPosition;
2530 
2531                 // HFR is decreasing, we are on the right direction
2532                 HFRDec++;
2533                 HFRInc = 0;
2534             }
2535             else
2536 
2537             {
2538                 // HFR increased, let's deal with it.
2539                 //HFRInc++;
2540                 HFRDec = 0;
2541 
2542                 // Reality Check: If it's first time, let's capture again and see if it changes.
2543                 /*if (HFRInc <= 1 && reverseDir == false)
2544                 {
2545                     capture();
2546                     return;
2547                 }
2548                 // Looks like we're going away from optimal HFR
2549                 else
2550                 {*/
2551                 reverseDir   = true;
2552                 lastHFR      = currentHFR;
2553                 lastHFRPos   = currentPosition;
2554                 initSlopeHFR = 0;
2555                 HFRInc       = 0;
2556 
2557                 qCDebug(KSTARS_EKOS_FOCUS) << "Focus is moving away from optimal HFR.";
2558 
2559                 // Let's set new limits
2560                 if (m_LastFocusDirection == FOCUS_IN)
2561                 {
2562                     focusInLimit = currentPosition;
2563                     qCDebug(KSTARS_EKOS_FOCUS) << "Setting focus IN limit to " << focusInLimit;
2564 
2565                     if (hfr_position.count() > 3)
2566                     {
2567                         focusOutLimit = hfr_position[hfr_position.count() - 3];
2568                         qCDebug(KSTARS_EKOS_FOCUS) << "Setting focus OUT limit to " << focusOutLimit;
2569                     }
2570                 }
2571                 else
2572                 {
2573                     focusOutLimit = currentPosition;
2574                     qCDebug(KSTARS_EKOS_FOCUS) << "Setting focus OUT limit to " << focusOutLimit;
2575 
2576                     if (hfr_position.count() > 3)
2577                     {
2578                         focusInLimit = hfr_position[hfr_position.count() - 3];
2579                         qCDebug(KSTARS_EKOS_FOCUS) << "Setting focus IN limit to " << focusInLimit;
2580                     }
2581                 }
2582 
2583                 if (focusAlgorithm == FOCUS_POLYNOMIAL && hfr_position.count() > 5)
2584                 {
2585                     polynomialFit.reset(new PolynomialFit(3, hfr_position, hfr_value));
2586                     double a = *std::min_element(hfr_position.constBegin(), hfr_position.constEnd());
2587                     double b = *std::max_element(hfr_position.constBegin(), hfr_position.constEnd());
2588                     double min_position = 0, min_hfr = 0;
2589                     isVShapeSolution = polynomialFit->findMinimum(minHFRPos, a, b, &min_position, &min_hfr);
2590                     qCDebug(KSTARS_EKOS_FOCUS) << "Found Minimum?" << (isVShapeSolution ? "Yes" : "No");
2591                     if (isVShapeSolution)
2592                     {
2593                         qCDebug(KSTARS_EKOS_FOCUS) << "Minimum Solution:" << min_hfr << "@" << min_position;
2594                         polySolutionFound++;
2595                         targetPosition = round(min_position);
2596                         appendLogText(i18n("Found polynomial solution @ %1", QString::number(min_position, 'f', 0)));
2597 
2598                         emit drawPolynomial(polynomialFit.get(), isVShapeSolution, true);
2599                         emit minimumFound(min_position, min_hfr);
2600                     }
2601                     else
2602                     {
2603                         emit drawPolynomial(polynomialFit.get(), isVShapeSolution, false);
2604                     }
2605                 }
2606 
2607                 if (isVShapeSolution == false)
2608                 {
2609                     // Decrease pulse
2610                     pulseDuration = pulseDuration * 0.75;
2611 
2612                     // Let's get close to the minimum HFR position so far detected
2613                     if (m_LastFocusDirection == FOCUS_OUT)
2614                         targetPosition = minHFRPos - pulseDuration / 2;
2615                     else
2616                         targetPosition = minHFRPos + pulseDuration / 2;
2617                 }
2618 
2619                 qCDebug(KSTARS_EKOS_FOCUS) << "new targetPosition " << targetPosition;
2620             }
2621 
2622             // Limit target Pulse to algorithm limits
2623             if (focusInLimit != 0 && m_LastFocusDirection == FOCUS_IN && targetPosition < focusInLimit)
2624             {
2625                 targetPosition = focusInLimit;
2626                 qCDebug(KSTARS_EKOS_FOCUS) << "Limiting target pulse to focus in limit " << targetPosition;
2627             }
2628             else if (focusOutLimit != 0 && m_LastFocusDirection == FOCUS_OUT && targetPosition > focusOutLimit)
2629             {
2630                 targetPosition = focusOutLimit;
2631                 qCDebug(KSTARS_EKOS_FOCUS) << "Limiting target pulse to focus out limit " << targetPosition;
2632             }
2633 
2634             // Limit target pulse to focuser limits
2635             if (targetPosition < absMotionMin)
2636                 targetPosition = absMotionMin;
2637             else if (targetPosition > absMotionMax)
2638                 targetPosition = absMotionMax;
2639 
2640             // We cannot go any further because of the device limits, this is a failure
2641             if (targetPosition == currentPosition)
2642             {
2643                 // If case target position happens to be the minimal historical
2644                 // HFR position, accept this value instead of bailing out.
2645                 if (targetPosition == minHFRPos)
2646                 {
2647                     appendLogText("Stopping at minimum recorded HFR position.");
2648                     completeFocusProcedure(Ekos::FOCUS_COMPLETE);
2649                 }
2650                 else
2651                 {
2652                     appendLogText("Focuser cannot move further, device limits reached. Autofocus aborted.");
2653                     qCDebug(KSTARS_EKOS_FOCUS) << "Focuser cannot move further, restricted by device limits at " << targetPosition;
2654                     completeFocusProcedure(Ekos::FOCUS_ABORTED);
2655                 }
2656                 return;
2657             }
2658 
2659             // Ops, deadlock
2660             if (focusOutLimit && focusOutLimit == focusInLimit)
2661             {
2662                 appendLogText(i18n("Deadlock reached. Please try again with different settings."));
2663                 completeFocusProcedure(Ekos::FOCUS_ABORTED);
2664                 return;
2665             }
2666 
2667             // Restrict the target position even more with the maximum travel option
2668             if (fabs(targetPosition - initialFocuserAbsPosition) > maxTravelIN->value())
2669             {
2670                 int minTravelLimit = qMax(0.0, initialFocuserAbsPosition - maxTravelIN->value());
2671                 int maxTravelLimit = qMin(absMotionMax, initialFocuserAbsPosition + maxTravelIN->value());
2672 
2673                 // In case we are asked to go below travel limit, but we are not there yet
2674                 // let us go there and see the result before aborting
2675                 if (fabs(currentPosition - minTravelLimit) > 10 && targetPosition < minTravelLimit)
2676                 {
2677                     targetPosition = minTravelLimit;
2678                 }
2679                 // Same for max travel
2680                 else if (fabs(currentPosition - maxTravelLimit) > 10 && targetPosition > maxTravelLimit)
2681                 {
2682                     targetPosition = maxTravelLimit;
2683                 }
2684                 else
2685                 {
2686                     qCDebug(KSTARS_EKOS_FOCUS) << "targetPosition (" << targetPosition << ") - initHFRAbsPos ("
2687                                                << initialFocuserAbsPosition << ") exceeds maxTravel distance of " << maxTravelIN->value();
2688 
2689                     appendLogText("Maximum travel limit reached. Autofocus aborted.");
2690                     completeFocusProcedure(Ekos::FOCUS_ABORTED);
2691                     break;
2692                 }
2693             }
2694 
2695             // Get delta for next move
2696             delta = (targetPosition - currentPosition);
2697 
2698             qCDebug(KSTARS_EKOS_FOCUS) << "delta (targetPosition - currentPosition) " << delta;
2699 
2700             // Limit to Maximum permitted delta (Max Single Step Size)
2701             double limitedDelta = qMax(-1.0 * maxSingleStepIN->value(), qMin(1.0 * maxSingleStepIN->value(), delta));
2702             if (std::fabs(limitedDelta - delta) > 0)
2703             {
2704                 qCDebug(KSTARS_EKOS_FOCUS) << "Limited delta to maximum permitted single step " << maxSingleStepIN->value();
2705                 delta = limitedDelta;
2706             }
2707 
2708             // Now cross your fingers and wait
2709             if (!changeFocus(delta))
2710                 completeFocusProcedure(Ekos::FOCUS_ABORTED);
2711 
2712             break;
2713     }
2714 }
2715 
addPlotPosition(int pos,double hfr,bool plot)2716 void Focus::addPlotPosition(int pos, double hfr, bool plot)
2717 {
2718     hfr_position.append(pos);
2719     hfr_value.append(hfr);
2720     if (plot)
2721         emit newHFRPlotPosition(pos, hfr, pulseDuration);
2722 }
2723 
autoFocusRel()2724 void Focus::autoFocusRel()
2725 {
2726     static int noStarCount = 0;
2727     static double minHFR   = 1e6;
2728     QString deltaTxt       = QString("%1").arg(fabs(currentHFR - minHFR) * 100.0, 0, 'g', 2);
2729     QString minHFRText     = QString("%1").arg(minHFR, 0, 'g', 3);
2730     QString HFRText        = QString("%1").arg(currentHFR, 0, 'g', 3);
2731 
2732     appendLogText(i18n("FITS received. HFR %1. Delta (%2%) Min HFR (%3)", HFRText, deltaTxt, minHFRText));
2733 
2734     if (pulseDuration <= MINIMUM_PULSE_TIMER)
2735     {
2736         appendLogText(i18n("Autofocus failed to reach proper focus. Try adjusting the tolerance value."));
2737         completeFocusProcedure(Ekos::FOCUS_ABORTED);
2738         return;
2739     }
2740 
2741     // No stars detected, try to capture again
2742     if (currentHFR == FocusAlgorithmInterface::IGNORED_HFR)
2743     {
2744         if (noStarCount < MAX_RECAPTURE_RETRIES)
2745         {
2746             noStarCount++;
2747             appendLogText(i18n("No stars detected, capturing again..."));
2748             capture();
2749             return;
2750         }
2751         else if (focusAlgorithm == FOCUS_LINEAR)
2752         {
2753             appendLogText(i18n("Failed to detect any stars at position %1. Continuing...", currentPosition));
2754             noStarCount = 0;
2755         }
2756         else
2757         {
2758             appendLogText(i18n("Failed to detect any stars. Reset frame and try again."));
2759             completeFocusProcedure(Ekos::FOCUS_ABORTED);
2760             return;
2761         }
2762     }
2763     else
2764         noStarCount = 0;
2765 
2766     switch (m_LastFocusDirection)
2767     {
2768         case FOCUS_NONE:
2769             lastHFR = currentHFR;
2770             minHFR  = 1e6;
2771             changeFocus(-pulseDuration);
2772             break;
2773 
2774         case FOCUS_IN:
2775         case FOCUS_OUT:
2776             if (fabs(currentHFR - minHFR) < (toleranceIN->value() / 100.0) && HFRInc == 0)
2777             {
2778                 completeFocusProcedure(Ekos::FOCUS_COMPLETE);
2779             }
2780             else if (currentHFR < lastHFR)
2781             {
2782                 if (currentHFR < minHFR)
2783                     minHFR = currentHFR;
2784 
2785                 lastHFR = currentHFR;
2786                 changeFocus(m_LastFocusDirection == FOCUS_IN ? -pulseDuration : pulseDuration);
2787                 HFRInc = 0;
2788             }
2789             else
2790             {
2791                 //HFRInc++;
2792 
2793                 lastHFR = currentHFR;
2794 
2795                 HFRInc = 0;
2796 
2797                 pulseDuration *= 0.75;
2798 
2799                 if (!changeFocus(m_LastFocusDirection == FOCUS_IN ? pulseDuration : -pulseDuration))
2800                     completeFocusProcedure(Ekos::FOCUS_ABORTED);
2801             }
2802             break;
2803     }
2804 }
2805 
2806 /*void Focus::registerFocusProperty(INDI::Property prop)
2807 {
2808     // Return if it is not our current focuser
2809     if (strcmp(prop->getDeviceName(), currentFocuser->getDeviceName()))
2810         return;
2811 
2812     // Do not make unnecessary function call
2813     // Check if current focuser supports absolute mode
2814     if (canAbsMove == false && currentFocuser->canAbsMove())
2815     {
2816         canAbsMove = true;
2817         getAbsFocusPosition();
2818 
2819         absTicksSpin->setEnabled(true);
2820         absTicksLabel->setEnabled(true);
2821         startGotoB->setEnabled(true);
2822     }
2823 
2824     // Do not make unnecessary function call
2825     // Check if current focuser supports relative mode
2826     if (canRelMove == false && currentFocuser->canRelMove())
2827         canRelMove = true;
2828 
2829     if (canTimerMove == false && currentFocuser->canTimerMove())
2830     {
2831         canTimerMove = true;
2832         resetButtons();
2833     }
2834 }*/
2835 
autoFocusProcessPositionChange(IPState state)2836 void Focus::autoFocusProcessPositionChange(IPState state)
2837 {
2838     if (state == IPS_OK && captureInProgress == false)
2839     {
2840         // Normally, if we are auto-focusing, after we move the focuser we capture an image.
2841         // However, the Linear algorithm, at the start of its passes, requires two
2842         // consecutive focuser moves--the first out further than we want, and a second
2843         // move back in, so that we eliminate backlash and are always moving in before a capture.
2844         if (focuserAdditionalMovement > 0)
2845         {
2846             int temp = focuserAdditionalMovement;
2847             focuserAdditionalMovement = 0;
2848             qCDebug(KSTARS_EKOS_FOCUS) << QString("LinearFocuser: un-doing extension. Moving back in by %1").arg(temp);
2849 
2850             if (!focusIn(temp))
2851             {
2852                 appendLogText(i18n("Focuser error, check INDI panel."));
2853                 completeFocusProcedure(Ekos::FOCUS_ABORTED);
2854             }
2855         }
2856         else
2857         {
2858             qCDebug(KSTARS_EKOS_FOCUS) << QString("Focus position reached at %1, starting capture in %2 seconds.").arg(
2859                                            currentPosition).arg(FocusSettleTime->value());
2860             capture(FocusSettleTime->value());
2861         }
2862     }
2863     else if (state == IPS_ALERT)
2864     {
2865         appendLogText(i18n("Focuser error, check INDI panel."));
2866         completeFocusProcedure(Ekos::FOCUS_ABORTED);
2867     }
2868 }
2869 
processFocusNumber(INumberVectorProperty * nvp)2870 void Focus::processFocusNumber(INumberVectorProperty *nvp)
2871 {
2872     if (currentFocuser == nullptr)
2873         return;
2874 
2875     // Return if it is not our current focuser
2876     if (nvp->device != currentFocuser->getDeviceName())
2877         return;
2878 
2879     // Only process focus properties
2880     if (QString(nvp->name).contains("focus", Qt::CaseInsensitive) == false)
2881         return;
2882 
2883     if (!strcmp(nvp->name, "FOCUS_BACKLASH_STEPS"))
2884     {
2885         focusBacklashSpin->setValue(nvp->np[0].value);
2886         return;
2887     }
2888 
2889     if (!strcmp(nvp->name, "ABS_FOCUS_POSITION"))
2890     {
2891         m_FocusMotionTimer.stop();
2892         INumber *pos = IUFindNumber(nvp, "FOCUS_ABSOLUTE_POSITION");
2893 
2894         // FIXME: We should check state validity, but some focusers do not care - make ignore an option!
2895         if (pos)
2896         {
2897             int newPosition = static_cast<int>(pos->value);
2898 
2899             // Some absolute focuser constantly report the position without a state change.
2900             // Therefore we ignore it if both value and state are the same as last time.
2901             // HACK: This would shortcut the autofocus procedure reset, see completeFocusProcedure for the small hack
2902             if (currentPosition == newPosition && currentPositionState == nvp->s)
2903                 return;
2904 
2905             currentPositionState = nvp->s;
2906 
2907             if (currentPosition != newPosition)
2908             {
2909                 currentPosition = newPosition;
2910                 qCDebug(KSTARS_EKOS_FOCUS) << "Abs Focuser position changed to " << currentPosition << "State:" << pstateStr(
2911                                                currentPositionState);
2912                 absTicksLabel->setText(QString::number(currentPosition));
2913                 emit absolutePositionChanged(currentPosition);
2914             }
2915         }
2916 
2917         if (nvp->s == IPS_OK)
2918         {
2919             // Systematically reset UI when focuser finishes moving
2920             resetButtons();
2921 
2922             if (adjustFocus)
2923             {
2924                 adjustFocus = false;
2925                 m_LastFocusDirection = FOCUS_NONE;
2926                 emit focusPositionAdjusted();
2927                 return;
2928             }
2929 
2930             if (restartFocus && status() != Ekos::FOCUS_ABORTED)
2931             {
2932                 restartFocus = false;
2933                 inAutoFocus = false;
2934                 appendLogText(i18n("Restarting autofocus process..."));
2935                 start();
2936             }
2937         }
2938 
2939         if (canAbsMove && inAutoFocus)
2940         {
2941             autoFocusProcessPositionChange(nvp->s);
2942         }
2943         else if (nvp->s == IPS_ALERT)
2944             appendLogText(i18n("Focuser error, check INDI panel."));
2945         return;
2946     }
2947 
2948     if (canAbsMove)
2949         return;
2950 
2951     if (!strcmp(nvp->name, "manualfocusdrive"))
2952     {
2953         m_FocusMotionTimer.stop();
2954 
2955         INumber *pos = IUFindNumber(nvp, "manualfocusdrive");
2956         if (pos && nvp->s == IPS_OK)
2957         {
2958             currentPosition += pos->value;
2959             absTicksLabel->setText(QString::number(static_cast<int>(currentPosition)));
2960             emit absolutePositionChanged(currentPosition);
2961         }
2962 
2963         if (adjustFocus && nvp->s == IPS_OK)
2964         {
2965             adjustFocus = false;
2966             m_LastFocusDirection = FOCUS_NONE;
2967             emit focusPositionAdjusted();
2968             return;
2969         }
2970 
2971         // restart if focus movement has finished
2972         if (restartFocus && nvp->s == IPS_OK && status() != Ekos::FOCUS_ABORTED)
2973         {
2974             restartFocus = false;
2975             inAutoFocus = false;
2976             appendLogText(i18n("Restarting autofocus process..."));
2977             start();
2978         }
2979 
2980         if (canRelMove && inAutoFocus)
2981         {
2982             autoFocusProcessPositionChange(nvp->s);
2983         }
2984         else if (nvp->s == IPS_ALERT)
2985             appendLogText(i18n("Focuser error, check INDI panel."));
2986 
2987         return;
2988     }
2989 
2990     if (!strcmp(nvp->name, "REL_FOCUS_POSITION"))
2991     {
2992         m_FocusMotionTimer.stop();
2993 
2994         INumber *pos = IUFindNumber(nvp, "FOCUS_RELATIVE_POSITION");
2995         if (pos && nvp->s == IPS_OK)
2996         {
2997             currentPosition += pos->value * (m_LastFocusDirection == FOCUS_IN ? -1 : 1);
2998             qCDebug(KSTARS_EKOS_FOCUS)
2999                     << QString("Rel Focuser position changed by %1 to %2")
3000                     .arg(pos->value).arg(currentPosition);
3001             absTicksLabel->setText(QString::number(static_cast<int>(currentPosition)));
3002             emit absolutePositionChanged(currentPosition);
3003         }
3004 
3005         if (adjustFocus && nvp->s == IPS_OK)
3006         {
3007             adjustFocus = false;
3008             m_LastFocusDirection = FOCUS_NONE;
3009             emit focusPositionAdjusted();
3010             return;
3011         }
3012 
3013         // restart if focus movement has finished
3014         if (restartFocus && nvp->s == IPS_OK && status() != Ekos::FOCUS_ABORTED)
3015         {
3016             restartFocus = false;
3017             inAutoFocus = false;
3018             appendLogText(i18n("Restarting autofocus process..."));
3019             start();
3020         }
3021 
3022         if (canRelMove && inAutoFocus)
3023         {
3024             autoFocusProcessPositionChange(nvp->s);
3025         }
3026         else if (nvp->s == IPS_ALERT)
3027             appendLogText(i18n("Focuser error, check INDI panel."));
3028 
3029         return;
3030     }
3031 
3032     if (canRelMove)
3033         return;
3034 
3035     if (!strcmp(nvp->name, "FOCUS_TIMER"))
3036     {
3037         m_FocusMotionTimer.stop();
3038         // restart if focus movement has finished
3039         if (restartFocus && nvp->s == IPS_OK && status() != Ekos::FOCUS_ABORTED)
3040         {
3041             restartFocus = false;
3042             inAutoFocus = false;
3043             appendLogText(i18n("Restarting autofocus process..."));
3044             start();
3045         }
3046 
3047         if (canAbsMove == false && canRelMove == false && inAutoFocus)
3048         {
3049             // Used by the linear focus algorithm. Ignored if that's not in use for the timer-focuser.
3050             INumber *pos = IUFindNumber(nvp, "FOCUS_TIMER_VALUE");
3051             if (pos)
3052             {
3053                 currentPosition += pos->value * (m_LastFocusDirection == FOCUS_IN ? -1 : 1);
3054                 qCDebug(KSTARS_EKOS_FOCUS)
3055                         << QString("Timer Focuser position changed by %1 to %2")
3056                         .arg(pos->value).arg(currentPosition);
3057             }
3058             autoFocusProcessPositionChange(nvp->s);
3059         }
3060         else if (nvp->s == IPS_ALERT)
3061             appendLogText(i18n("Focuser error, check INDI panel."));
3062 
3063         return;
3064     }
3065 }
3066 
appendLogText(const QString & text)3067 void Focus::appendLogText(const QString &text)
3068 {
3069     m_LogText.insert(0, i18nc("log entry; %1 is the date, %2 is the text", "%1 %2",
3070                               KStarsData::Instance()->lt().toString("yyyy-MM-ddThh:mm:ss"), text));
3071 
3072     qCInfo(KSTARS_EKOS_FOCUS) << text;
3073 
3074     emit newLog(text);
3075 }
3076 
clearLog()3077 void Focus::clearLog()
3078 {
3079     m_LogText.clear();
3080     emit newLog(QString());
3081 }
3082 
appendFocusLogText(const QString & lines)3083 void Focus::appendFocusLogText(const QString &lines)
3084 {
3085     if (Options::focusLogging())
3086     {
3087 
3088         if (!m_FocusLogFile.exists())
3089         {
3090             // Create focus-specific log file and write the header record
3091             QDir dir(KSPaths::writableLocation(QStandardPaths::AppDataLocation));
3092             dir.mkpath("focuslogs");
3093             m_FocusLogEnabled = m_FocusLogFile.open(QIODevice::WriteOnly | QIODevice::Text);
3094             if (m_FocusLogEnabled)
3095             {
3096                 QTextStream header(&m_FocusLogFile);
3097                 header << "date, time, position, temperature, filter, HFR, altitude\n";
3098                 header.flush();
3099             }
3100             else
3101                 qCWarning(KSTARS_EKOS_FOCUS) << "Failed to open focus log file: " << m_FocusLogFileName;
3102         }
3103 
3104         if (m_FocusLogEnabled)
3105         {
3106             QTextStream out(&m_FocusLogFile);
3107             out << QDateTime::currentDateTime().toString("yyyy-MM-dd, hh:mm:ss, ") << lines;
3108             out.flush();
3109         }
3110     }
3111 }
3112 
startFraming()3113 void Focus::startFraming()
3114 {
3115     if (currentCCD == nullptr)
3116     {
3117         appendLogText(i18n("No CCD connected."));
3118         return;
3119     }
3120 
3121     waitStarSelectTimer.stop();
3122 
3123     inFocusLoop = true;
3124     HFRFrames.clear();
3125 
3126     clearDataPoints();
3127 
3128     //emit statusUpdated(true);
3129     state = Ekos::FOCUS_FRAMING;
3130     qCDebug(KSTARS_EKOS_FOCUS) << "State:" << Ekos::getFocusStatusString(state);
3131     emit newStatus(state);
3132 
3133     resetButtons();
3134 
3135     appendLogText(i18n("Starting continuous exposure..."));
3136 
3137     capture();
3138 }
3139 
resetButtons()3140 void Focus::resetButtons()
3141 {
3142     if (inFocusLoop)
3143     {
3144         startFocusB->setEnabled(false);
3145         startLoopB->setEnabled(false);
3146         stopFocusB->setEnabled(true);
3147 
3148         captureB->setEnabled(false);
3149 
3150         return;
3151     }
3152 
3153     if (inAutoFocus)
3154     {
3155         stopFocusB->setEnabled(true);
3156 
3157         startFocusB->setEnabled(false);
3158         startLoopB->setEnabled(false);
3159         captureB->setEnabled(false);
3160         focusOutB->setEnabled(false);
3161         focusInB->setEnabled(false);
3162         startGotoB->setEnabled(false);
3163         stopGotoB->setEnabled(false);
3164 
3165         resetFrameB->setEnabled(false);
3166 
3167         return;
3168     }
3169 
3170     bool const enableCaptureButtons = captureInProgress == false && hfrInProgress == false;
3171 
3172     captureB->setEnabled(enableCaptureButtons);
3173     resetFrameB->setEnabled(enableCaptureButtons);
3174     startLoopB->setEnabled(enableCaptureButtons);
3175 
3176     if (currentFocuser)
3177     {
3178         focusOutB->setEnabled(true);
3179         focusInB->setEnabled(true);
3180 
3181         startFocusB->setEnabled(focusType == FOCUS_AUTO);
3182         stopFocusB->setEnabled(!enableCaptureButtons);
3183         startGotoB->setEnabled(canAbsMove);
3184         stopGotoB->setEnabled(true);
3185     }
3186     else
3187     {
3188         focusOutB->setEnabled(false);
3189         focusInB->setEnabled(false);
3190 
3191         startFocusB->setEnabled(false);
3192         stopFocusB->setEnabled(false);
3193         startGotoB->setEnabled(false);
3194         stopGotoB->setEnabled(false);
3195     }
3196 }
3197 
updateBoxSize(int value)3198 void Focus::updateBoxSize(int value)
3199 {
3200     if (currentCCD == nullptr)
3201         return;
3202 
3203     ISD::CCDChip *targetChip = currentCCD->getChip(ISD::CCDChip::PRIMARY_CCD);
3204 
3205     if (targetChip == nullptr)
3206         return;
3207 
3208     int subBinX, subBinY;
3209     targetChip->getBinning(&subBinX, &subBinY);
3210 
3211     QRect trackBox = focusView->getTrackingBox();
3212     QPoint center(trackBox.x() + (trackBox.width() / 2), trackBox.y() + (trackBox.height() / 2));
3213 
3214     trackBox =
3215         QRect(center.x() - value / (2 * subBinX), center.y() - value / (2 * subBinY), value / subBinX, value / subBinY);
3216 
3217     focusView->setTrackingBox(trackBox);
3218 }
3219 
selectFocusStarFraction(double x,double y)3220 void Focus::selectFocusStarFraction(double x, double y)
3221 {
3222     if (m_ImageData.isNull())
3223         return;
3224 
3225     focusStarSelected(x * m_ImageData->width(), y * m_ImageData->height());
3226     // Focus view timer takes 50ms second to update, so let's emit afterwards.
3227     QTimer::singleShot(250, this, [this]()
3228     {
3229         emit newImage(focusView);
3230     });
3231 }
3232 
focusStarSelected(int x,int y)3233 void Focus::focusStarSelected(int x, int y)
3234 {
3235     if (state == Ekos::FOCUS_PROGRESS)
3236         return;
3237 
3238     if (subFramed == false)
3239     {
3240         rememberStarCenter.setX(x);
3241         rememberStarCenter.setY(y);
3242     }
3243 
3244     ISD::CCDChip *targetChip = currentCCD->getChip(ISD::CCDChip::PRIMARY_CCD);
3245 
3246     int subBinX, subBinY;
3247     targetChip->getBinning(&subBinX, &subBinY);
3248 
3249     // If binning was changed outside of the focus module, recapture
3250     if (subBinX != activeBin)
3251     {
3252         capture();
3253         return;
3254     }
3255 
3256     int offset = (static_cast<double>(focusBoxSize->value()) / subBinX) * 1.5;
3257 
3258     QRect starRect;
3259 
3260     bool squareMovedOutside = false;
3261 
3262     if (subFramed == false && useSubFrame->isChecked() && targetChip->canSubframe())
3263     {
3264         int minX, maxX, minY, maxY, minW, maxW, minH, maxH; //, fx,fy,fw,fh;
3265 
3266         targetChip->getFrameMinMax(&minX, &maxX, &minY, &maxY, &minW, &maxW, &minH, &maxH);
3267         //targetChip->getFrame(&fx, &fy, &fw, &fy);
3268 
3269         x     = (x - offset) * subBinX;
3270         y     = (y - offset) * subBinY;
3271         int w = offset * 2 * subBinX;
3272         int h = offset * 2 * subBinY;
3273 
3274         if (x < minX)
3275             x = minX;
3276         if (y < minY)
3277             y = minY;
3278         if ((x + w) > maxW)
3279             w = maxW - x;
3280         if ((y + h) > maxH)
3281             h = maxH - y;
3282 
3283         //fx += x;
3284         //fy += y;
3285         //fw = w;
3286         //fh = h;
3287 
3288         //targetChip->setFocusFrame(fx, fy, fw, fh);
3289         //frameModified=true;
3290 
3291         QVariantMap settings = frameSettings[targetChip];
3292         settings["x"]        = x;
3293         settings["y"]        = y;
3294         settings["w"]        = w;
3295         settings["h"]        = h;
3296         settings["binx"]     = subBinX;
3297         settings["biny"]     = subBinY;
3298 
3299         frameSettings[targetChip] = settings;
3300 
3301         subFramed = true;
3302 
3303         qCDebug(KSTARS_EKOS_FOCUS) << "Frame is subframed. X:" << x << "Y:" << y << "W:" << w << "H:" << h << "binX:" << subBinX <<
3304                                    "binY:" << subBinY;
3305 
3306         focusView->setFirstLoad(true);
3307 
3308         capture();
3309 
3310         //starRect = QRect((w-focusBoxSize->value())/(subBinX*2), (h-focusBoxSize->value())/(subBinY*2), focusBoxSize->value()/subBinX, focusBoxSize->value()/subBinY);
3311         starCenter.setX(w / (2 * subBinX));
3312         starCenter.setY(h / (2 * subBinY));
3313     }
3314     else
3315     {
3316         //starRect = QRect(x-focusBoxSize->value()/(subBinX*2), y-focusBoxSize->value()/(subBinY*2), focusBoxSize->value()/subBinX, focusBoxSize->value()/subBinY);
3317         double dist = sqrt((starCenter.x() - x) * (starCenter.x() - x) + (starCenter.y() - y) * (starCenter.y() - y));
3318 
3319         squareMovedOutside = (dist > (static_cast<double>(focusBoxSize->value()) / subBinX));
3320         starCenter.setX(x);
3321         starCenter.setY(y);
3322         //starRect = QRect( starCenter.x()-focusBoxSize->value()/(2*subBinX), starCenter.y()-focusBoxSize->value()/(2*subBinY), focusBoxSize->value()/subBinX, focusBoxSize->value()/subBinY);
3323         starRect = QRect(starCenter.x() - focusBoxSize->value() / (2 * subBinX),
3324                          starCenter.y() - focusBoxSize->value() / (2 * subBinY), focusBoxSize->value() / subBinX,
3325                          focusBoxSize->value() / subBinY);
3326         focusView->setTrackingBox(starRect);
3327     }
3328 
3329     starsHFR.clear();
3330 
3331     starCenter.setZ(subBinX);
3332 
3333     //starSelected=true;
3334 
3335     defaultScale = static_cast<FITSScale>(filterCombo->currentIndex());
3336 
3337     if (squareMovedOutside && inAutoFocus == false && useAutoStar->isChecked())
3338     {
3339         useAutoStar->blockSignals(true);
3340         useAutoStar->setChecked(false);
3341         useAutoStar->blockSignals(false);
3342         appendLogText(i18n("Disabling Auto Star Selection as star selection box was moved manually."));
3343         starSelected = false;
3344     }
3345     else if (starSelected == false)
3346     {
3347         appendLogText(i18n("Focus star is selected."));
3348         starSelected = true;
3349         capture();
3350     }
3351 
3352     waitStarSelectTimer.stop();
3353     FocusState nextState = inAutoFocus ? FOCUS_PROGRESS : FOCUS_IDLE;
3354     if (nextState != state)
3355     {
3356         state = nextState;
3357         qCDebug(KSTARS_EKOS_FOCUS) << "State:" << Ekos::getFocusStatusString(state);
3358         emit newStatus(state);
3359     }
3360 }
3361 
checkFocus(double requiredHFR)3362 void Focus::checkFocus(double requiredHFR)
3363 {
3364     if (inAutoFocus || inFocusLoop)
3365     {
3366         qCDebug(KSTARS_EKOS_FOCUS) << "Check Focus rejected, focus procedure is already running.";
3367     }
3368     else
3369     {
3370         qCDebug(KSTARS_EKOS_FOCUS) << "Check Focus requested with minimum required HFR" << requiredHFR;
3371         minimumRequiredHFR = requiredHFR;
3372 
3373         appendLogText("Capturing to check HFR...");
3374         capture();
3375     }
3376 }
3377 
toggleSubframe(bool enable)3378 void Focus::toggleSubframe(bool enable)
3379 {
3380     if (enable == false)
3381         resetFrame();
3382 
3383     starSelected = false;
3384     starCenter   = QVector3D();
3385 
3386     if (useFullField->isChecked())
3387         useFullField->setChecked(!enable);
3388 }
3389 
filterChangeWarning(int index)3390 void Focus::filterChangeWarning(int index)
3391 {
3392     Options::setFocusEffect(index);
3393     defaultScale = static_cast<FITSScale>(index);
3394 
3395     // Median filter helps reduce noise, rotation/flip have no dire effect on focus, others degrade procedure
3396     switch (defaultScale)
3397     {
3398         case FITS_NONE:
3399         case FITS_MEDIAN:
3400         case FITS_ROTATE_CW:
3401         case FITS_ROTATE_CCW:
3402         case FITS_FLIP_H:
3403         case FITS_FLIP_V:
3404             break;
3405 
3406         default:
3407             // Warn the end-user, count the no-op filter
3408             appendLogText(i18n("Warning: Only use filter '%1' for preview as it may interfere with autofocus operation.",
3409                                FITSViewer::filterTypes.value(index - 1, "???")));
3410     }
3411 }
3412 
setExposure(double value)3413 void Focus::setExposure(double value)
3414 {
3415     exposureIN->setValue(value);
3416 }
3417 
setBinning(int subBinX,int subBinY)3418 void Focus::setBinning(int subBinX, int subBinY)
3419 {
3420     INDI_UNUSED(subBinY);
3421     binningCombo->setCurrentIndex(subBinX - 1);
3422 }
3423 
setImageFilter(const QString & value)3424 void Focus::setImageFilter(const QString &value)
3425 {
3426     for (int i = 0; i < filterCombo->count(); i++)
3427         if (filterCombo->itemText(i) == value)
3428         {
3429             filterCombo->setCurrentIndex(i);
3430             filterCombo->activated(i);
3431             break;
3432         }
3433 }
3434 
setAutoStarEnabled(bool enable)3435 void Focus::setAutoStarEnabled(bool enable)
3436 {
3437     useAutoStar->setChecked(enable);
3438     Options::setFocusAutoStarEnabled(enable);
3439 }
3440 
setAutoSubFrameEnabled(bool enable)3441 void Focus::setAutoSubFrameEnabled(bool enable)
3442 {
3443     useSubFrame->setChecked(enable);
3444     Options::setFocusSubFrame(enable);
3445 }
3446 
setAutoFocusParameters(int boxSize,int stepSize,int maxTravel,double tolerance)3447 void Focus::setAutoFocusParameters(int boxSize, int stepSize, int maxTravel, double tolerance)
3448 {
3449     focusBoxSize->setValue(boxSize);
3450     stepIN->setValue(stepSize);
3451     maxTravelIN->setValue(maxTravel);
3452     toleranceIN->setValue(tolerance);
3453 }
3454 
checkAutoStarTimeout()3455 void Focus::checkAutoStarTimeout()
3456 {
3457     //if (starSelected == false && inAutoFocus)
3458     if (starCenter.isNull() && (inAutoFocus || minimumRequiredHFR > 0))
3459     {
3460         if (inAutoFocus)
3461         {
3462             if (rememberStarCenter.isNull() == false)
3463             {
3464                 focusStarSelected(rememberStarCenter.x(), rememberStarCenter.y());
3465                 appendLogText(i18n("No star was selected. Using last known position..."));
3466                 return;
3467             }
3468         }
3469 
3470         initialFocuserAbsPosition = -1;
3471         appendLogText(i18n("No star was selected. Aborting..."));
3472         completeFocusProcedure(Ekos::FOCUS_ABORTED);
3473     }
3474     else if (state == FOCUS_WAITING)
3475     {
3476         state = FOCUS_IDLE;
3477         qCDebug(KSTARS_EKOS_FOCUS) << "State:" << Ekos::getFocusStatusString(state);
3478         emit newStatus(state);
3479     }
3480 }
3481 
setAbsoluteFocusTicks()3482 void Focus::setAbsoluteFocusTicks()
3483 {
3484     if (currentFocuser == nullptr)
3485     {
3486         appendLogText(i18n("Error: No Focuser detected."));
3487         checkStopFocus(true);
3488         return;
3489     }
3490 
3491     if (currentFocuser->isConnected() == false)
3492     {
3493         appendLogText(i18n("Error: Lost connection to Focuser."));
3494         checkStopFocus(true);
3495         return;
3496     }
3497 
3498     qCDebug(KSTARS_EKOS_FOCUS) << "Setting focus ticks to " << absTicksSpin->value();
3499 
3500     currentFocuser->moveAbs(absTicksSpin->value());
3501 }
3502 
3503 //void Focus::setActiveBinning(int bin)
3504 //{
3505 //    activeBin = bin + 1;
3506 //    Options::setFocusXBin(activeBin);
3507 //}
3508 
3509 // TODO remove from kstars.kcfg
3510 /*void Focus::setFrames(int value)
3511 {
3512     Options::setFocusFrames(value);
3513 }*/
3514 
syncTrackingBoxPosition()3515 void Focus::syncTrackingBoxPosition()
3516 {
3517     ISD::CCDChip *targetChip = currentCCD->getChip(ISD::CCDChip::PRIMARY_CCD);
3518     Q_ASSERT(targetChip);
3519 
3520     int subBinX = 1, subBinY = 1;
3521     targetChip->getBinning(&subBinX, &subBinY);
3522 
3523     if (starCenter.isNull() == false)
3524     {
3525         double boxSize = focusBoxSize->value();
3526         int x, y, w, h;
3527         targetChip->getFrame(&x, &y, &w, &h);
3528         // If box size is larger than image size, set it to lower index
3529         if (boxSize / subBinX >= w || boxSize / subBinY >= h)
3530         {
3531             focusBoxSize->setValue((boxSize / subBinX >= w) ? w : h);
3532             return;
3533         }
3534 
3535         // If binning changed, update coords accordingly
3536         if (subBinX != starCenter.z())
3537         {
3538             if (starCenter.z() > 0)
3539             {
3540                 starCenter.setX(starCenter.x() * (starCenter.z() / subBinX));
3541                 starCenter.setY(starCenter.y() * (starCenter.z() / subBinY));
3542             }
3543 
3544             starCenter.setZ(subBinX);
3545         }
3546 
3547         QRect starRect = QRect(starCenter.x() - boxSize / (2 * subBinX), starCenter.y() - boxSize / (2 * subBinY),
3548                                boxSize / subBinX, boxSize / subBinY);
3549         focusView->setTrackingBoxEnabled(true);
3550         focusView->setTrackingBox(starRect);
3551     }
3552 }
3553 
showFITSViewer()3554 void Focus::showFITSViewer()
3555 {
3556     static int lastFVTabID = -1;
3557     if (m_ImageData)
3558     {
3559         QUrl url = QUrl::fromLocalFile("focus.fits");
3560         if (fv.isNull())
3561         {
3562             fv = KStars::Instance()->createFITSViewer();
3563             fv->loadData(m_ImageData, url, &lastFVTabID);
3564         }
3565         else if (fv->updateData(m_ImageData, url, lastFVTabID, &lastFVTabID) == false)
3566             fv->loadData(m_ImageData, url, &lastFVTabID);
3567 
3568         fv->show();
3569     }
3570 }
3571 
adjustFocusOffset(int value,bool useAbsoluteOffset)3572 void Focus::adjustFocusOffset(int value, bool useAbsoluteOffset)
3573 {
3574     adjustFocus = true;
3575 
3576     int relativeOffset = 0;
3577 
3578     if (useAbsoluteOffset == false)
3579         relativeOffset = value;
3580     else
3581         relativeOffset = value - currentPosition;
3582 
3583     changeFocus(relativeOffset);
3584 }
3585 
toggleFocusingWidgetFullScreen()3586 void Focus::toggleFocusingWidgetFullScreen()
3587 {
3588     if (focusingWidget->parent() == nullptr)
3589     {
3590         focusingWidget->setParent(this);
3591         rightLayout->insertWidget(0, focusingWidget);
3592         focusingWidget->showNormal();
3593     }
3594     else
3595     {
3596         focusingWidget->setParent(nullptr);
3597         focusingWidget->setWindowTitle(i18nc("@title:window", "Focus Frame"));
3598         focusingWidget->setWindowFlags(Qt::Window | Qt::WindowTitleHint | Qt::CustomizeWindowHint);
3599         focusingWidget->showMaximized();
3600         focusingWidget->show();
3601     }
3602 }
3603 
setMountStatus(ISD::Telescope::Status newState)3604 void Focus::setMountStatus(ISD::Telescope::Status newState)
3605 {
3606     switch (newState)
3607     {
3608         case ISD::Telescope::MOUNT_PARKING:
3609         case ISD::Telescope::MOUNT_SLEWING:
3610         case ISD::Telescope::MOUNT_MOVING:
3611             captureB->setEnabled(false);
3612             startFocusB->setEnabled(false);
3613             startLoopB->setEnabled(false);
3614 
3615             // If mount is moved while we have a star selected and subframed
3616             // let us reset the frame.
3617             if (subFramed)
3618                 resetFrame();
3619 
3620             break;
3621 
3622         default:
3623             resetButtons();
3624             break;
3625     }
3626 }
3627 
setMountCoords(const SkyPoint & position,ISD::Telescope::PierSide pierSide,const dms & ha)3628 void Focus::setMountCoords(const SkyPoint &position, ISD::Telescope::PierSide pierSide, const dms &ha)
3629 {
3630     Q_UNUSED(pierSide);
3631     Q_UNUSED(ha);
3632     mountAlt = position.alt().Degrees();
3633 }
3634 
removeDevice(ISD::GDInterface * deviceRemoved)3635 void Focus::removeDevice(ISD::GDInterface *deviceRemoved)
3636 {
3637     // Check in Focusers
3638     for (ISD::GDInterface *focuser : Focusers)
3639     {
3640         if (focuser->getDeviceName() == deviceRemoved->getDeviceName())
3641         {
3642             Focusers.removeAll(dynamic_cast<ISD::Focuser*>(focuser));
3643             focuserCombo->removeItem(focuserCombo->findText(focuser->getDeviceName()));
3644             QTimer::singleShot(1000, this, [this]()
3645             {
3646                 checkFocuser();
3647                 resetButtons();
3648             });
3649         }
3650     }
3651 
3652     // Check in Temperature Sources.
3653     for (auto &oneSource : TemperatureSources)
3654     {
3655         if (oneSource->getDeviceName() == deviceRemoved->getDeviceName())
3656         {
3657             TemperatureSources.removeAll(oneSource);
3658             temperatureSourceCombo->removeItem(temperatureSourceCombo->findText(oneSource->getDeviceName()));
3659             QTimer::singleShot(1000, this, [this]()
3660             {
3661                 checkTemperatureSource();
3662             });
3663         }
3664     }
3665 
3666     // Check in CCDs
3667     for (ISD::GDInterface *ccd : CCDs)
3668     {
3669         if (ccd->getDeviceName() == deviceRemoved->getDeviceName())
3670         {
3671             CCDs.removeAll(dynamic_cast<ISD::CCD*>(ccd));
3672             CCDCaptureCombo->removeItem(CCDCaptureCombo->findText(ccd->getDeviceName()));
3673             CCDCaptureCombo->removeItem(CCDCaptureCombo->findText(ccd->getDeviceName() + QString(" Guider")));
3674 
3675             if (CCDs.empty())
3676             {
3677                 currentCCD = nullptr;
3678                 CCDCaptureCombo->setCurrentIndex(-1);
3679             }
3680             else
3681             {
3682                 currentCCD = CCDs[0];
3683                 CCDCaptureCombo->setCurrentIndex(0);
3684             }
3685 
3686             QTimer::singleShot(1000, this, [this]()
3687             {
3688                 checkCCD();
3689                 resetButtons();
3690             });
3691         }
3692     }
3693 
3694     // Check in Filters
3695     for (ISD::GDInterface *filter : Filters)
3696     {
3697         if (filter->getDeviceName() == deviceRemoved->getDeviceName())
3698         {
3699             Filters.removeAll(filter);
3700             FilterDevicesCombo->removeItem(FilterDevicesCombo->findText(filter->getDeviceName()));
3701             if (Filters.empty())
3702             {
3703                 currentFilter = nullptr;
3704                 FilterDevicesCombo->setCurrentIndex(-1);
3705             }
3706             else
3707                 FilterDevicesCombo->setCurrentIndex(0);
3708 
3709             QTimer::singleShot(1000, this, [this]()
3710             {
3711                 checkFilter();
3712                 resetButtons();
3713             });
3714         }
3715     }
3716 }
3717 
setFilterManager(const QSharedPointer<FilterManager> & manager)3718 void Focus::setFilterManager(const QSharedPointer<FilterManager> &manager)
3719 {
3720     filterManager = manager;
3721     connect(filterManagerB, &QPushButton::clicked, [this]()
3722     {
3723         filterManager->show();
3724         filterManager->raise();
3725     });
3726 
3727     connect(filterManager.data(), &FilterManager::ready, [this]()
3728     {
3729         if (filterPositionPending)
3730         {
3731             filterPositionPending = false;
3732             capture();
3733         }
3734         else if (fallbackFilterPending)
3735         {
3736             fallbackFilterPending = false;
3737             emit newStatus(state);
3738         }
3739     }
3740            );
3741 
3742     connect(filterManager.data(), &FilterManager::failed, [this]()
3743     {
3744         appendLogText(i18n("Filter operation failed."));
3745         completeFocusProcedure(Ekos::FOCUS_ABORTED);
3746     }
3747            );
3748 
3749     connect(this, &Focus::newStatus, [this](Ekos::FocusState state)
3750     {
3751         if (FilterPosCombo->currentIndex() != -1 && canAbsMove && state == Ekos::FOCUS_COMPLETE)
3752         {
3753             filterManager->setFilterAbsoluteFocusPosition(FilterPosCombo->currentIndex(), currentPosition);
3754         }
3755     });
3756 
3757     // Resume guiding if suspended after focus position is adjusted.
3758     connect(this, &Focus::focusPositionAdjusted, this, [this]()
3759     {
3760         if (m_GuidingSuspended && state != Ekos::FOCUS_PROGRESS)
3761         {
3762             QTimer::singleShot(FocusSettleTime->value() * 1000, this, [this]()
3763             {
3764                 m_GuidingSuspended = false;
3765                 emit resumeGuiding();
3766             });
3767         }
3768     });
3769 
3770     // Suspend guiding if filter offset is change with OAG
3771     connect(filterManager.data(), &FilterManager::newStatus, this, [this](Ekos::FilterState filterState)
3772     {
3773         // If we are changing filter offset while idle, then check if we need to suspend guiding.
3774         const bool isOAG = currentCCD->getTelescopeType() == Options::guideScopeType();
3775         if (isOAG && filterState == FILTER_OFFSET && state != Ekos::FOCUS_PROGRESS)
3776         {
3777             if (m_GuidingSuspended == false && suspendGuideCheck->isChecked())
3778             {
3779                 m_GuidingSuspended = true;
3780                 emit suspendGuiding();
3781             }
3782         }
3783     });
3784 
3785     connect(exposureIN, &QDoubleSpinBox::editingFinished, [this]()
3786     {
3787         if (currentFilter)
3788             filterManager->setFilterExposure(FilterPosCombo->currentIndex(), exposureIN->value());
3789         else
3790             Options::setFocusExposure(exposureIN->value());
3791     });
3792 
3793     connect(filterManager.data(), &FilterManager::labelsChanged, this, [this]()
3794     {
3795         FilterPosCombo->clear();
3796         FilterPosCombo->addItems(filterManager->getFilterLabels());
3797         currentFilterPosition = filterManager->getFilterPosition();
3798         FilterPosCombo->setCurrentIndex(currentFilterPosition - 1);
3799         //Options::setDefaultFocusFilterWheelFilter(FilterPosCombo->currentText());
3800     });
3801     connect(filterManager.data(), &FilterManager::positionChanged, this, [this]()
3802     {
3803         currentFilterPosition = filterManager->getFilterPosition();
3804         FilterPosCombo->setCurrentIndex(currentFilterPosition - 1);
3805         //Options::setDefaultFocusFilterWheelFilter(FilterPosCombo->currentText());
3806     });
3807     connect(filterManager.data(), &FilterManager::exposureChanged, this, [this]()
3808     {
3809         exposureIN->setValue(filterManager->getFilterExposure());
3810     });
3811 
3812     connect(FilterPosCombo, static_cast<void(QComboBox::*)(const QString &)>(&QComboBox::currentIndexChanged),
3813             [ = ](const QString & text)
3814     {
3815         exposureIN->setValue(filterManager->getFilterExposure(text));
3816         //Options::setDefaultFocusFilterWheelFilter(text);
3817     });
3818 }
3819 
toggleVideo(bool enabled)3820 void Focus::toggleVideo(bool enabled)
3821 {
3822     if (currentCCD == nullptr)
3823         return;
3824 
3825     if (currentCCD->isBLOBEnabled() == false)
3826     {
3827 
3828         if (Options::guiderType() != Ekos::Guide::GUIDE_INTERNAL)
3829             currentCCD->setBLOBEnabled(true);
3830         else
3831         {
3832             connect(KSMessageBox::Instance(), &KSMessageBox::accepted, this, [this, enabled]()
3833             {
3834                 //QObject::disconnect(KSMessageBox::Instance(), &KSMessageBox::accepted, this, nullptr);
3835                 KSMessageBox::Instance()->disconnect(this);
3836                 currentCCD->setVideoStreamEnabled(enabled);
3837             });
3838             KSMessageBox::Instance()->questionYesNo(i18n("Image transfer is disabled for this camera. Would you like to enable it?"));
3839         }
3840     }
3841     else
3842         currentCCD->setVideoStreamEnabled(enabled);
3843 }
3844 
3845 //void Focus::setWeatherData(const std::vector<ISD::Weather::WeatherData> &data)
3846 //{
3847 //    auto pos = std::find_if(data.begin(), data.end(), [](ISD::Weather::WeatherData oneEntry)
3848 //    {
3849 //        return (oneEntry.name == "WEATHER_TEMPERATURE");
3850 //    });
3851 
3852 //    if (pos != data.end())
3853 //    {
3854 //        updateTemperature(OBSERVATORY_TEMPERATURE, pos->value);
3855 //    }
3856 //}
3857 
setVideoStreamEnabled(bool enabled)3858 void Focus::setVideoStreamEnabled(bool enabled)
3859 {
3860     if (enabled)
3861     {
3862         liveVideoB->setChecked(true);
3863         liveVideoB->setIcon(QIcon::fromTheme("camera-on"));
3864     }
3865     else
3866     {
3867         liveVideoB->setChecked(false);
3868         liveVideoB->setIcon(QIcon::fromTheme("camera-ready"));
3869     }
3870 }
3871 
processCaptureTimeout()3872 void Focus::processCaptureTimeout()
3873 {
3874     captureTimeoutCounter++;
3875 
3876     if (captureTimeoutCounter >= 3)
3877     {
3878         captureTimeoutCounter = 0;
3879         captureTimeout.stop();
3880         appendLogText(i18n("Exposure timeout. Aborting..."));
3881         completeFocusProcedure(Ekos::FOCUS_ABORTED);
3882     }
3883     else
3884     {
3885         appendLogText(i18n("Exposure timeout. Restarting exposure..."));
3886         ISD::CCDChip *targetChip = currentCCD->getChip(ISD::CCDChip::PRIMARY_CCD);
3887         targetChip->abortExposure();
3888 
3889         prepareCapture(targetChip);
3890 
3891         if (targetChip->capture(exposureIN->value()))
3892         {
3893             // Timeout is exposure duration + timeout threshold in seconds
3894             //long const timeout = lround(ceil(exposureIN->value() * 1000)) + FOCUS_TIMEOUT_THRESHOLD;
3895             captureTimeout.start(Options::focusCaptureTimeout() * 1000);
3896 
3897             if (inFocusLoop == false)
3898                 appendLogText(i18n("Capturing image again..."));
3899 
3900             resetButtons();
3901         }
3902         else if (inAutoFocus)
3903         {
3904             completeFocusProcedure(Ekos::FOCUS_ABORTED);
3905         }
3906     }
3907 }
3908 
processCaptureError(ISD::CCD::ErrorType type)3909 void Focus::processCaptureError(ISD::CCD::ErrorType type)
3910 {
3911     if (type == ISD::CCD::ERROR_SAVE)
3912     {
3913         appendLogText(i18n("Failed to save image. Aborting..."));
3914         completeFocusProcedure(Ekos::FOCUS_ABORTED);
3915         return;
3916     }
3917 
3918     captureFailureCounter++;
3919 
3920     if (captureFailureCounter >= 3)
3921     {
3922         captureFailureCounter = 0;
3923         appendLogText(i18n("Exposure failure. Aborting..."));
3924         completeFocusProcedure(Ekos::FOCUS_ABORTED);
3925         return;
3926     }
3927 
3928     appendLogText(i18n("Exposure failure. Restarting exposure..."));
3929     ISD::CCDChip *targetChip = currentCCD->getChip(ISD::CCDChip::PRIMARY_CCD);
3930     targetChip->abortExposure();
3931     targetChip->capture(exposureIN->value());
3932 }
3933 
syncSettings()3934 void Focus::syncSettings()
3935 {
3936     QDoubleSpinBox *dsb = nullptr;
3937     QSpinBox *sb = nullptr;
3938     QCheckBox *cb = nullptr;
3939     QComboBox *cbox = nullptr;
3940 
3941     if ( (dsb = qobject_cast<QDoubleSpinBox*>(sender())))
3942     {
3943         ///////////////////////////////////////////////////////////////////////////
3944         /// Focuser Group
3945         ///////////////////////////////////////////////////////////////////////////
3946         if (dsb == FocusSettleTime)
3947             Options::setFocusSettleTime(dsb->value());
3948 
3949         ///////////////////////////////////////////////////////////////////////////
3950         /// CCD & Filter Wheel Group
3951         ///////////////////////////////////////////////////////////////////////////
3952         else if (dsb == gainIN)
3953             Options::setFocusGain(dsb->value());
3954 
3955         ///////////////////////////////////////////////////////////////////////////
3956         /// Settings Group
3957         ///////////////////////////////////////////////////////////////////////////
3958         else if (dsb == fullFieldInnerRing)
3959             Options::setFocusFullFieldInnerRadius(dsb->value());
3960         else if (dsb == fullFieldOuterRing)
3961             Options::setFocusFullFieldOuterRadius(dsb->value());
3962         else if (dsb == GuideSettleTime)
3963             Options::setGuideSettleTime(dsb->value());
3964         else if (dsb == maxTravelIN)
3965             Options::setFocusMaxTravel(dsb->value());
3966         else if (dsb == toleranceIN)
3967             Options::setFocusTolerance(dsb->value());
3968         else if (dsb == thresholdSpin)
3969             Options::setFocusThreshold(dsb->value());
3970         else if (dsb == gaussianSigmaSpin)
3971             Options::setFocusGaussianSigma(dsb->value());
3972         else if (dsb == initialFocusOutStepsIN)
3973             Options::setInitialFocusOutSteps(dsb->value());
3974     }
3975     else if ( (sb = qobject_cast<QSpinBox*>(sender())))
3976     {
3977         ///////////////////////////////////////////////////////////////////////////
3978         /// Settings Group
3979         ///////////////////////////////////////////////////////////////////////////
3980         if (sb == focusBoxSize)
3981             Options::setFocusBoxSize(sb->value());
3982         else if (sb == stepIN)
3983             Options::setFocusTicks(sb->value());
3984         else if (sb == maxSingleStepIN)
3985             Options::setFocusMaxSingleStep(sb->value());
3986         else if (sb == focusFramesSpin)
3987             Options::setFocusFramesCount(sb->value());
3988         else if (sb == gaussianKernelSizeSpin)
3989             Options::setFocusGaussianKernelSize(sb->value());
3990         else if (sb == multiRowAverageSpin)
3991             Options::setFocusMultiRowAverage(sb->value());
3992         else if (sb == captureTimeoutSpin)
3993             Options::setFocusCaptureTimeout(sb->value());
3994         else if (sb == motionTimeoutSpin)
3995             Options::setFocusMotionTimeout(sb->value());
3996     }
3997     else if ( (cb = qobject_cast<QCheckBox*>(sender())))
3998     {
3999         ///////////////////////////////////////////////////////////////////////////
4000         /// Settings Group
4001         ///////////////////////////////////////////////////////////////////////////
4002         if (cb == useAutoStar)
4003             Options::setFocusAutoStarEnabled(cb->isChecked());
4004         else if (cb == useSubFrame)
4005             Options::setFocusSubFrame(cb->isChecked());
4006         else if (cb == darkFrameCheck)
4007             Options::setUseFocusDarkFrame(cb->isChecked());
4008         else if (cb == useFullField)
4009             Options::setFocusUseFullField(cb->isChecked());
4010         else if (cb == suspendGuideCheck)
4011             Options::setSuspendGuiding(cb->isChecked());
4012     }
4013     else if ( (cbox = qobject_cast<QComboBox*>(sender())))
4014     {
4015         ///////////////////////////////////////////////////////////////////////////
4016         /// CCD & Filter Wheel Group
4017         ///////////////////////////////////////////////////////////////////////////
4018         if (cbox == focuserCombo)
4019             Options::setDefaultFocusFocuser(cbox->currentText());
4020         else if (cbox == CCDCaptureCombo)
4021             Options::setDefaultFocusCCD(cbox->currentText());
4022         else if (cbox == binningCombo)
4023         {
4024             activeBin = cbox->currentIndex() + 1;
4025             Options::setFocusXBin(activeBin);
4026         }
4027         else if (cbox == FilterDevicesCombo)
4028             Options::setDefaultFocusFilterWheel(cbox->currentText());
4029         else if (cbox == temperatureSourceCombo)
4030             Options::setDefaultFocusTemperatureSource(cbox->currentText());
4031         // Filter Effects already taken care of in filterChangeWarning
4032 
4033         ///////////////////////////////////////////////////////////////////////////
4034         /// Settings Group
4035         ///////////////////////////////////////////////////////////////////////////
4036         else if (cbox == focusAlgorithmCombo)
4037             Options::setFocusAlgorithm(cbox->currentIndex());
4038         else if (cbox == focusDetectionCombo)
4039             Options::setFocusDetection(cbox->currentIndex());
4040     }
4041 
4042     emit settingsUpdated(getSettings());
4043 }
4044 
loadSettings()4045 void Focus::loadSettings()
4046 {
4047     ///////////////////////////////////////////////////////////////////////////
4048     /// Focuser Group
4049     ///////////////////////////////////////////////////////////////////////////
4050     // Focus settle time
4051     FocusSettleTime->setValue(Options::focusSettleTime());
4052 
4053     ///////////////////////////////////////////////////////////////////////////
4054     /// CCD & Filter Wheel Group
4055     ///////////////////////////////////////////////////////////////////////////
4056     // Default Exposure
4057     exposureIN->setValue(Options::focusExposure());
4058     // Binning
4059     activeBin = Options::focusXBin();
4060     binningCombo->setCurrentIndex(activeBin - 1);
4061     // Gain
4062     gainIN->setValue(Options::focusGain());
4063 
4064     ///////////////////////////////////////////////////////////////////////////
4065     /// Settings Group
4066     ///////////////////////////////////////////////////////////////////////////
4067     // Subframe?
4068     useSubFrame->setChecked(Options::focusSubFrame());
4069     // Dark frame?
4070     darkFrameCheck->setChecked(Options::useFocusDarkFrame());
4071     // Use full field?
4072     useFullField->setChecked(Options::focusUseFullField());
4073     // full field inner ring
4074     fullFieldInnerRing->setValue(Options::focusFullFieldInnerRadius());
4075     // full field outer ring
4076     fullFieldOuterRing->setValue(Options::focusFullFieldOuterRadius());
4077     // Suspend guiding?
4078     suspendGuideCheck->setChecked(Options::suspendGuiding());
4079     // Guide Setting time
4080     GuideSettleTime->setValue(Options::guideSettleTime());
4081 
4082     // Box Size
4083     focusBoxSize->setValue(Options::focusBoxSize());
4084     // Max Travel - this will be overriden by the device
4085     maxTravelIN->setMinimum(0.0);
4086     if (Options::focusMaxTravel() > maxTravelIN->maximum())
4087         maxTravelIN->setMaximum(Options::focusMaxTravel());
4088     maxTravelIN->setValue(Options::focusMaxTravel());
4089     // Step
4090     stepIN->setValue(Options::focusTicks());
4091     // Single Max Step
4092     maxSingleStepIN->setValue(Options::focusMaxSingleStep());
4093     // LinearFocus initial outward steps
4094     initialFocusOutStepsIN->setValue(Options::initialFocusOutSteps());
4095     // Tolerance
4096     toleranceIN->setValue(Options::focusTolerance());
4097     // Threshold spin
4098     thresholdSpin->setValue(Options::focusThreshold());
4099     // Focus Algorithm
4100     setFocusAlgorithm(static_cast<FocusAlgorithm>(Options::focusAlgorithm()));
4101     // This must go below the above line (which sets focusAlgorithm from options).
4102     focusAlgorithmCombo->setCurrentIndex(focusAlgorithm);
4103     // Frames Count
4104     focusFramesSpin->setValue(Options::focusFramesCount());
4105     // Focus Detection
4106     focusDetection = static_cast<StarAlgorithm>(Options::focusDetection());
4107     thresholdSpin->setEnabled(focusDetection == ALGORITHM_THRESHOLD);
4108     focusDetectionCombo->setCurrentIndex(focusDetection);
4109     // Gaussian blur
4110     gaussianSigmaSpin->setValue(Options::focusGaussianSigma());
4111     gaussianKernelSizeSpin->setValue(Options::focusGaussianKernelSize());
4112     // Hough algorithm multi row average
4113     multiRowAverageSpin->setValue(Options::focusMultiRowAverage());
4114     multiRowAverageSpin->setEnabled(focusDetection == ALGORITHM_BAHTINOV);
4115     // Timeouts
4116     captureTimeoutSpin->setValue(Options::focusCaptureTimeout());
4117     motionTimeoutSpin->setValue(Options::focusMotionTimeout());
4118 
4119     // Increase focus box size in case of Bahtinov mask focus
4120     // Disable auto star in case of Bahtinov mask focus
4121     if (focusDetection == ALGORITHM_BAHTINOV)
4122     {
4123         Options::setFocusAutoStarEnabled(false);
4124         focusBoxSize->setMaximum(512);
4125     }
4126     else
4127     {
4128         // When not using Bathinov mask, limit box size to 256 and make sure value stays within range.
4129         if (Options::focusBoxSize() > 256)
4130         {
4131             Options::setFocusBoxSize(32);
4132         }
4133         focusBoxSize->setMaximum(256);
4134     }
4135     // Box Size
4136     focusBoxSize->setValue(Options::focusBoxSize());
4137     // Auto Star?
4138     useAutoStar->setChecked(Options::focusAutoStarEnabled());
4139     useAutoStar->setEnabled(focusDetection != ALGORITHM_BAHTINOV);
4140 }
4141 
initSettingsConnections()4142 void Focus::initSettingsConnections()
4143 {
4144     ///////////////////////////////////////////////////////////////////////////
4145     /// Focuser Group
4146     ///////////////////////////////////////////////////////////////////////////
4147     connect(focuserCombo, static_cast<void (QComboBox::*)(const QString &)>(&QComboBox::activated), this,
4148             &Ekos::Focus::syncSettings);
4149     connect(FocusSettleTime, &QDoubleSpinBox::editingFinished, this, &Focus::syncSettings);
4150 
4151     ///////////////////////////////////////////////////////////////////////////
4152     /// CCD & Filter Wheel Group
4153     ///////////////////////////////////////////////////////////////////////////
4154     connect(CCDCaptureCombo, static_cast<void (QComboBox::*)(const QString &)>(&QComboBox::activated), this,
4155             &Ekos::Focus::syncSettings);
4156     connect(binningCombo, static_cast<void (QComboBox::*)(int)>(&QComboBox::activated), this, &Ekos::Focus::syncSettings);
4157     connect(gainIN, &QDoubleSpinBox::editingFinished, this, &Focus::syncSettings);
4158     connect(FilterDevicesCombo, static_cast<void (QComboBox::*)(const QString &)>(&QComboBox::activated), this,
4159             &Ekos::Focus::syncSettings);
4160     connect(FilterPosCombo, static_cast<void(QComboBox::*)(const QString &)>(&QComboBox::activated), this,
4161             &Ekos::Focus::syncSettings);
4162     connect(temperatureSourceCombo, static_cast<void(QComboBox::*)(const QString &)>(&QComboBox::activated), this,
4163             &Ekos::Focus::syncSettings);
4164 
4165     ///////////////////////////////////////////////////////////////////////////
4166     /// Settings Group
4167     ///////////////////////////////////////////////////////////////////////////
4168     connect(useAutoStar, &QCheckBox::toggled, this, &Ekos::Focus::syncSettings);
4169     connect(useSubFrame, &QCheckBox::toggled, this, &Ekos::Focus::syncSettings);
4170     connect(darkFrameCheck, &QCheckBox::toggled, this, &Ekos::Focus::syncSettings);
4171     connect(useFullField, &QCheckBox::toggled, this, &Ekos::Focus::syncSettings);
4172     connect(fullFieldInnerRing, &QDoubleSpinBox::editingFinished, this, &Focus::syncSettings);
4173     connect(fullFieldOuterRing, &QDoubleSpinBox::editingFinished, this, &Focus::syncSettings);
4174     connect(suspendGuideCheck, &QCheckBox::toggled, this, &Ekos::Focus::syncSettings);
4175     connect(GuideSettleTime, &QDoubleSpinBox::editingFinished, this, &Focus::syncSettings);
4176 
4177     connect(focusBoxSize, static_cast<void (QSpinBox::*)(int)>(&QSpinBox::valueChanged), this, &Focus::syncSettings);
4178     connect(maxTravelIN, &QDoubleSpinBox::editingFinished, this, &Focus::syncSettings);
4179     connect(stepIN, &QDoubleSpinBox::editingFinished, this, &Focus::syncSettings);
4180     connect(maxSingleStepIN, &QDoubleSpinBox::editingFinished, this, &Focus::syncSettings);
4181     connect(initialFocusOutStepsIN, &QDoubleSpinBox::editingFinished, this, &Focus::syncSettings);
4182     connect(toleranceIN, &QDoubleSpinBox::editingFinished, this, &Focus::syncSettings);
4183     connect(thresholdSpin, &QDoubleSpinBox::editingFinished, this, &Focus::syncSettings);
4184     connect(gaussianSigmaSpin, &QDoubleSpinBox::editingFinished, this, &Focus::syncSettings);
4185     connect(gaussianKernelSizeSpin, static_cast<void (QSpinBox::*)(int)>(&QSpinBox::valueChanged), this, &Focus::syncSettings);
4186     connect(multiRowAverageSpin, static_cast<void (QSpinBox::*)(int)>(&QSpinBox::valueChanged), this, &Focus::syncSettings);
4187     connect(captureTimeoutSpin, static_cast<void (QSpinBox::*)(int)>(&QSpinBox::valueChanged), this, &Focus::syncSettings);
4188     connect(motionTimeoutSpin, static_cast<void (QSpinBox::*)(int)>(&QSpinBox::valueChanged), this, &Focus::syncSettings);
4189 
4190     connect(focusAlgorithmCombo, static_cast<void(QComboBox::*)(const QString &)>(&QComboBox::activated), this,
4191             &Ekos::Focus::syncSettings);
4192     connect(focusFramesSpin, static_cast<void (QSpinBox::*)(int)>(&QSpinBox::valueChanged), this, &Focus::syncSettings);
4193     connect(focusDetectionCombo, static_cast<void(QComboBox::*)(const QString &)>(&QComboBox::activated), this,
4194             &Ekos::Focus::syncSettings);
4195 }
4196 
initPlots()4197 void Focus::initPlots()
4198 {
4199     connect(clearDataB, &QPushButton::clicked, this, &Ekos::Focus::clearDataPoints);
4200 
4201     profileDialog = new QDialog(this);
4202     profileDialog->setWindowFlags(Qt::Tool | Qt::WindowStaysOnTopHint);
4203     QVBoxLayout *profileLayout = new QVBoxLayout(profileDialog);
4204     profileDialog->setWindowTitle(i18nc("@title:window", "Relative Profile"));
4205     profilePlot = new FocusProfilePlot(profileDialog);
4206 
4207     profileLayout->addWidget(profilePlot);
4208     profileDialog->setLayout(profileLayout);
4209     profileDialog->resize(400, 300);
4210 
4211     connect(relativeProfileB, &QPushButton::clicked, profileDialog, &QDialog::show);
4212     connect(this, &Ekos::Focus::newHFR, [this](double currentHFR, int pos)
4213     {
4214         Q_UNUSED(pos) profilePlot->drawProfilePlot(currentHFR);
4215     });
4216 }
4217 
initConnections()4218 void Focus::initConnections()
4219 {
4220     // How long do we wait until the user select a star?
4221     waitStarSelectTimer.setInterval(AUTO_STAR_TIMEOUT);
4222     connect(&waitStarSelectTimer, &QTimer::timeout, this, &Ekos::Focus::checkAutoStarTimeout);
4223     connect(liveVideoB, &QPushButton::clicked, this, &Ekos::Focus::toggleVideo);
4224 
4225     // Show FITS Image in a new window
4226     showFITSViewerB->setIcon(QIcon::fromTheme("kstars_fitsviewer"));
4227     showFITSViewerB->setAttribute(Qt::WA_LayoutUsesWidgetRect);
4228     connect(showFITSViewerB, &QPushButton::clicked, this, &Ekos::Focus::showFITSViewer);
4229 
4230     // Toggle FITS View to full screen
4231     toggleFullScreenB->setIcon(QIcon::fromTheme("view-fullscreen"));
4232     toggleFullScreenB->setShortcut(Qt::Key_F4);
4233     toggleFullScreenB->setAttribute(Qt::WA_LayoutUsesWidgetRect);
4234     connect(toggleFullScreenB, &QPushButton::clicked, this, &Ekos::Focus::toggleFocusingWidgetFullScreen);
4235 
4236     // delayed capturing for waiting the scope to settle
4237     captureTimer.setSingleShot(true);
4238     connect(&captureTimer, &QTimer::timeout, this, [&]()
4239     {
4240         capture();
4241     });
4242 
4243     // How long do we wait until an exposure times out and needs a retry?
4244     captureTimeout.setSingleShot(true);
4245     connect(&captureTimeout, &QTimer::timeout, this, &Ekos::Focus::processCaptureTimeout);
4246 
4247     // Start/Stop focus
4248     connect(startFocusB, &QPushButton::clicked, this, &Ekos::Focus::start);
4249     connect(stopFocusB, &QPushButton::clicked, this, &Ekos::Focus::abort);
4250 
4251     // Focus IN/OUT
4252     connect(focusOutB, &QPushButton::clicked, [&]()
4253     {
4254         focusOut();
4255     });
4256     connect(focusInB, &QPushButton::clicked, [&]()
4257     {
4258         focusIn();
4259     });
4260 
4261     // Capture a single frame
4262     connect(captureB, &QPushButton::clicked, this, &Ekos::Focus::capture);
4263     // Start continuous capture
4264     connect(startLoopB, &QPushButton::clicked, this, &Ekos::Focus::startFraming);
4265     // Use a subframe when capturing
4266     connect(useSubFrame, &QCheckBox::toggled, this, &Ekos::Focus::toggleSubframe);
4267     // Reset frame dimensions to default
4268     connect(resetFrameB, &QPushButton::clicked, this, &Ekos::Focus::resetFrame);
4269     // Sync setting if full field setting is toggled.
4270     connect(useFullField, &QCheckBox::toggled, [&](bool toggled)
4271     {
4272         fullFieldInnerRing->setEnabled(toggled);
4273         fullFieldOuterRing->setEnabled(toggled);
4274         if (toggled)
4275         {
4276             useSubFrame->setChecked(false);
4277             useAutoStar->setChecked(false);
4278         }
4279         else
4280         {
4281             // Disable the overlay
4282             focusView->setStarFilterRange(0, 1);
4283         }
4284     });
4285 
4286 
4287     // Sync settings if the CCD selection is updated.
4288     connect(CCDCaptureCombo, static_cast<void (QComboBox::*)(int)>(&QComboBox::activated), this, &Ekos::Focus::checkCCD);
4289     // Sync settings if the Focuser selection is updated.
4290     connect(focuserCombo, static_cast<void (QComboBox::*)(int)>(&QComboBox::activated), this, &Ekos::Focus::checkFocuser);
4291     // Sync settings if the filter selection is updated.
4292     connect(FilterDevicesCombo, static_cast<void (QComboBox::*)(int)>(&QComboBox::activated), this, &Ekos::Focus::checkFilter);
4293     // Sync settings if the temperature source selection is updated.
4294     connect(temperatureSourceCombo, static_cast<void (QComboBox::*)(int)>(&QComboBox::activated), this,
4295             &Ekos::Focus::checkTemperatureSource);
4296 
4297     // Set focuser absolute position
4298     connect(startGotoB, &QPushButton::clicked, this, &Ekos::Focus::setAbsoluteFocusTicks);
4299     connect(stopGotoB, &QPushButton::clicked, [this]()
4300     {
4301         if (currentFocuser)
4302             currentFocuser->stop();
4303     });
4304     // Update the focuser box size used to enclose a star
4305     connect(focusBoxSize, static_cast<void (QSpinBox::*)(int)>(&QSpinBox::valueChanged), this, &Ekos::Focus::updateBoxSize);
4306 
4307     // Update the focuser star detection if the detection algorithm selection changes.
4308     connect(focusDetectionCombo, static_cast<void (QComboBox::*)(int)>(&QComboBox::activated), this, [&](int index)
4309     {
4310         focusDetection = static_cast<StarAlgorithm>(index);
4311         thresholdSpin->setEnabled(focusDetection == ALGORITHM_THRESHOLD);
4312         multiRowAverageSpin->setEnabled(focusDetection == ALGORITHM_BAHTINOV);
4313         if (focusDetection == ALGORITHM_BAHTINOV)
4314         {
4315             // In case of Bahtinov mask uncheck auto select star
4316             useAutoStar->setChecked(false);
4317             focusBoxSize->setMaximum(512);
4318         }
4319         else
4320         {
4321             // When not using Bathinov mask, limit box size to 256 and make sure value stays within range.
4322             if (Options::focusBoxSize() > 256)
4323             {
4324                 Options::setFocusBoxSize(32);
4325                 // Focus box size changed, update control
4326                 focusBoxSize->setValue(Options::focusBoxSize());
4327             }
4328             focusBoxSize->setMaximum(256);
4329         }
4330         useAutoStar->setEnabled(focusDetection != ALGORITHM_BAHTINOV);
4331     });
4332 
4333     // Update the focuser solution algorithm if the selection changes.
4334     connect(focusAlgorithmCombo, static_cast<void (QComboBox::*)(int)>(&QComboBox::activated), this, [&](int index)
4335     {
4336         setFocusAlgorithm(static_cast<FocusAlgorithm>(index));
4337     });
4338 
4339     // Reset star center on auto star check toggle
4340     connect(useAutoStar, &QCheckBox::toggled, this, [&](bool enabled)
4341     {
4342         if (enabled)
4343         {
4344             starCenter   = QVector3D();
4345             starSelected = false;
4346             focusView->setTrackingBox(QRect());
4347         }
4348     });
4349 }
4350 
setFocusAlgorithm(FocusAlgorithm algorithm)4351 void Focus::setFocusAlgorithm(FocusAlgorithm algorithm)
4352 {
4353     focusAlgorithm = algorithm;
4354     switch(algorithm)
4355     {
4356         case FOCUS_ITERATIVE:
4357             initialFocusOutStepsIN->setEnabled(false); // Out step multiple
4358             maxTravelIN->setEnabled(true);             // Max Travel
4359             stepIN->setEnabled(true);                  // Initial Step Size
4360             maxSingleStepIN->setEnabled(true);         // Max Step Size
4361             break;
4362 
4363         case FOCUS_POLYNOMIAL:
4364             initialFocusOutStepsIN->setEnabled(false); // Out step multiple
4365             maxTravelIN->setEnabled(true);             // Max Travel
4366             stepIN->setEnabled(true);                  // Initial Step Size
4367             maxSingleStepIN->setEnabled(true);         // Max Step Size
4368             break;
4369 
4370         case FOCUS_LINEAR:
4371             initialFocusOutStepsIN->setEnabled(true);  // Out step multiple
4372             maxTravelIN->setEnabled(true);             // Max Travel
4373             stepIN->setEnabled(true);                  // Initial Step Size
4374             maxSingleStepIN->setEnabled(false);        // Max Step Size
4375             break;
4376     }
4377 }
4378 
initView()4379 void Focus::initView()
4380 {
4381     focusView = new FITSView(focusingWidget, FITS_FOCUS);
4382     focusView->setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Expanding);
4383     focusView->setBaseSize(focusingWidget->size());
4384     focusView->createFloatingToolBar();
4385     QVBoxLayout *vlayout = new QVBoxLayout();
4386     vlayout->addWidget(focusView);
4387     focusingWidget->setLayout(vlayout);
4388     connect(focusView, &FITSView::trackingStarSelected, this, &Ekos::Focus::focusStarSelected, Qt::UniqueConnection);
4389     focusView->setStarsEnabled(true);
4390     focusView->setStarsHFREnabled(true);
4391 }
4392 
4393 ///////////////////////////////////////////////////////////////////////////////////////////
4394 ///
4395 ///////////////////////////////////////////////////////////////////////////////////////////
getSettings() const4396 QJsonObject Focus::getSettings() const
4397 {
4398     QJsonObject settings;
4399 
4400     settings.insert("camera", CCDCaptureCombo->currentText());
4401     settings.insert("focuser", focuserCombo->currentText());
4402     settings.insert("fw", FilterDevicesCombo->currentText());
4403     settings.insert("filter", FilterPosCombo->currentText());
4404     settings.insert("exp", exposureIN->value());
4405     settings.insert("bin", qMax(1, binningCombo->currentIndex() + 1));
4406     settings.insert("gain", gainIN->value());
4407     settings.insert("iso", ISOCombo->currentIndex());
4408     return settings;
4409 }
4410 
4411 ///////////////////////////////////////////////////////////////////////////////////////////
4412 ///
4413 ///////////////////////////////////////////////////////////////////////////////////////////
setSettings(const QJsonObject & settings)4414 void Focus::setSettings(const QJsonObject &settings)
4415 {
4416     static bool init = false;
4417 
4418     // Camera
4419     if (syncControl(settings, "camera", CCDCaptureCombo) || init == false)
4420         checkCCD();
4421     // Focuser
4422     if (syncControl(settings, "focuser", focuserCombo) || init == false)
4423         checkFocuser();
4424     // Filter Wheel
4425     if (syncControl(settings, "fw", FilterDevicesCombo) || init == false)
4426         checkFilter();
4427     // Filter
4428     syncControl(settings, "filter", FilterPosCombo);
4429     Options::setLockAlignFilterIndex(FilterPosCombo->currentIndex());
4430     // Exposure
4431     syncControl(settings, "exp", exposureIN);
4432     // Binning
4433     const int bin = settings["bin"].toInt(binningCombo->currentIndex() + 1) - 1;
4434     if (bin != binningCombo->currentIndex())
4435         binningCombo->setCurrentIndex(bin);
4436 
4437     // Gain
4438     if (currentCCD->hasGain())
4439         syncControl(settings, "gain", gainIN);
4440     // ISO
4441     if (ISOCombo->count() > 1)
4442     {
4443         const int iso = settings["iso"].toInt(ISOCombo->currentIndex());
4444         if (iso != ISOCombo->currentIndex())
4445             ISOCombo->setCurrentIndex(iso);
4446     }
4447 
4448     init = true;
4449 }
4450 
4451 
4452 ///////////////////////////////////////////////////////////////////////////////////////////
4453 ///
4454 ///////////////////////////////////////////////////////////////////////////////////////////
getPrimarySettings() const4455 QJsonObject Focus::getPrimarySettings() const
4456 {
4457     QJsonObject settings;
4458 
4459     settings.insert("autostar", useAutoStar->isChecked());
4460     settings.insert("dark", darkFrameCheck->isChecked());
4461     settings.insert("subframe", useSubFrame->isChecked());
4462     settings.insert("box", focusBoxSize->value());
4463     settings.insert("fullfield", useFullField->isChecked());
4464     settings.insert("inner", fullFieldInnerRing->value());
4465     settings.insert("outer", fullFieldOuterRing->value());
4466     settings.insert("suspend", suspendGuideCheck->isChecked());
4467     settings.insert("guide_settle", FocusSettleTime->value());
4468 
4469     return settings;
4470 }
4471 
4472 ///////////////////////////////////////////////////////////////////////////////////////////
4473 ///
4474 ///////////////////////////////////////////////////////////////////////////////////////////
setPrimarySettings(const QJsonObject & settings)4475 void Focus::setPrimarySettings(const QJsonObject &settings)
4476 {
4477     syncControl(settings, "autostar", useAutoStar);
4478     syncControl(settings, "dark", darkFrameCheck);
4479     syncControl(settings, "subframe", useSubFrame);
4480     syncControl(settings, "box", focusBoxSize);
4481     syncControl(settings, "fullfield", useFullField);
4482     syncControl(settings, "inner", fullFieldInnerRing);
4483     syncControl(settings, "outer", fullFieldOuterRing);
4484     syncControl(settings, "suspend", suspendGuideCheck);
4485     syncControl(settings, "guide_settle", FocusSettleTime);
4486 
4487 }
4488 
4489 ///////////////////////////////////////////////////////////////////////////////////////////
4490 ///
4491 ///////////////////////////////////////////////////////////////////////////////////////////
getProcessSettings() const4492 QJsonObject Focus::getProcessSettings() const
4493 {
4494     QJsonObject settings;
4495 
4496     settings.insert("detection", focusDetectionCombo->currentText());
4497     settings.insert("algorithm", focusAlgorithmCombo->currentText());
4498     settings.insert("sep", focusOptionsProfiles->currentText());
4499     settings.insert("threshold", thresholdSpin->value());
4500     settings.insert("tolerance", toleranceIN->value());
4501     settings.insert("average", focusFramesSpin->value());
4502     settings.insert("rows", multiRowAverageSpin->value());
4503     settings.insert("kernel", gaussianKernelSizeSpin->value());
4504     settings.insert("sigma", gaussianSigmaSpin->value());
4505 
4506     return settings;
4507 }
4508 
4509 ///////////////////////////////////////////////////////////////////////////////////////////
4510 ///
4511 ///////////////////////////////////////////////////////////////////////////////////////////
setProcessSettings(const QJsonObject & settings)4512 void Focus::setProcessSettings(const QJsonObject &settings)
4513 {
4514     syncControl(settings, "detection", focusDetectionCombo);
4515     syncControl(settings, "algorithm", focusAlgorithmCombo);
4516     syncControl(settings, "sep", focusOptionsProfiles);
4517     syncControl(settings, "threshold", thresholdSpin);
4518     syncControl(settings, "tolerance", toleranceIN);
4519     syncControl(settings, "average", focusFramesSpin);
4520     syncControl(settings, "rows", multiRowAverageSpin);
4521     syncControl(settings, "kernel", gaussianKernelSizeSpin);
4522     syncControl(settings, "sigma", gaussianSigmaSpin);
4523 }
4524 
4525 ///////////////////////////////////////////////////////////////////////////////////////////
4526 ///
4527 ///////////////////////////////////////////////////////////////////////////////////////////
getMechanicsSettings() const4528 QJsonObject Focus::getMechanicsSettings() const
4529 {
4530     QJsonObject settings;
4531 
4532     settings.insert("step", stepIN->value());
4533     settings.insert("travel", maxTravelIN->value());
4534     settings.insert("maxstep", maxSingleStepIN->value());
4535     settings.insert("backlash", focusBacklashSpin->value());
4536     settings.insert("settle", FocusSettleTime->value());
4537     settings.insert("out", initialFocusOutStepsIN->value());
4538 
4539     return settings;
4540 }
4541 
4542 ///////////////////////////////////////////////////////////////////////////////////////////
4543 ///
4544 ///////////////////////////////////////////////////////////////////////////////////////////
setMechanicsSettings(const QJsonObject & settings)4545 void Focus::setMechanicsSettings(const QJsonObject &settings)
4546 {
4547     syncControl(settings, "step", stepIN);
4548     syncControl(settings, "travel", maxTravelIN);
4549     syncControl(settings, "maxstep", maxSingleStepIN);
4550     syncControl(settings, "backlash", focusBacklashSpin);
4551     syncControl(settings, "settle", FocusSettleTime);
4552     syncControl(settings, "out", initialFocusOutStepsIN);
4553 }
4554 
4555 ///////////////////////////////////////////////////////////////////////////////////////////
4556 ///
4557 ///////////////////////////////////////////////////////////////////////////////////////////
syncControl(const QJsonObject & settings,const QString & key,QWidget * widget)4558 bool Focus::syncControl(const QJsonObject &settings, const QString &key, QWidget * widget)
4559 {
4560     QSpinBox *pSB = nullptr;
4561     QDoubleSpinBox *pDSB = nullptr;
4562     QCheckBox *pCB = nullptr;
4563     QComboBox *pComboBox = nullptr;
4564 
4565     if ((pSB = qobject_cast<QSpinBox *>(widget)))
4566     {
4567         const int value = settings[key].toInt(pSB->value());
4568         if (value != pSB->value())
4569         {
4570             pSB->setValue(value);
4571             return true;
4572         }
4573     }
4574     else if ((pDSB = qobject_cast<QDoubleSpinBox *>(widget)))
4575     {
4576         const double value = settings[key].toDouble(pDSB->value());
4577         if (value != pDSB->value())
4578         {
4579             pDSB->setValue(value);
4580             return true;
4581         }
4582     }
4583     else if ((pCB = qobject_cast<QCheckBox *>(widget)))
4584     {
4585         const bool value = settings[key].toBool(pCB->isChecked());
4586         if (value != pCB->isChecked())
4587         {
4588             pCB->setChecked(value);
4589             return true;
4590         }
4591     }
4592     // ONLY FOR STRINGS, not INDEX
4593     else if ((pComboBox = qobject_cast<QComboBox *>(widget)))
4594     {
4595         const QString value = settings[key].toString(pComboBox->currentText());
4596         if (value != pComboBox->currentText())
4597         {
4598             pComboBox->setCurrentText(value);
4599             return true;
4600         }
4601     }
4602 
4603     return false;
4604 };
4605 
4606 }
4607