Kstars

focus.cpp
1/*
2 SPDX-FileCopyrightText: 2012 Jasem Mutlaq <mutlaqja@ikarustech.com>
3
4 SPDX-License-Identifier: GPL-2.0-or-later
5*/
6
7#include "adaptivefocus.h"
8#include "focusadaptor.h"
9#include "focusalgorithms.h"
10#include "focusfwhm.h"
11#include "kstars.h"
12#include "kstarsdata.h"
13#include "Options.h"
14#include "stellarsolver.h"
15
16// Modules
17#include "ekos/guide/guide.h"
18#include "ekos/manager.h"
19
20// KStars Auxiliary
21#include "auxiliary/kspaths.h"
22#include "auxiliary/ksmessagebox.h"
23
24// Ekos Auxiliary
25#include "ekos/auxiliary/darklibrary.h"
26#include "ekos/auxiliary/darkprocessor.h"
27#include "ekos/auxiliary/profilesettings.h"
28#include "ekos/auxiliary/opticaltrainmanager.h"
29#include "ekos/auxiliary/opticaltrainsettings.h"
30#include "ekos/auxiliary/filtermanager.h"
31#include "ekos/auxiliary/stellarsolverprofileeditor.h"
32
33// FITS
34#include "fitsviewer/fitsdata.h"
35#include "fitsviewer/fitsview.h"
36#include "fitsviewer/fitsviewer.h"
37
38// Devices
39#include "indi/indifilterwheel.h"
40#include "ksnotification.h"
41#include "kconfigdialog.h"
42
43#include <basedevice.h>
44#include <gsl/gsl_fit.h>
45#include <gsl/gsl_vector.h>
46#include <gsl/gsl_min.h>
47
48#include <ekos_focus_debug.h>
49#include <config-kstars.h>
50
51#include <cmath>
52
53#ifdef HAVE_DATAVISUALIZATION
54#include "aberrationinspector.h"
55#include "aberrationinspectorutils.h"
56#endif
57
58#define MAXIMUM_ABS_ITERATIONS 51
59#define MAXIMUM_RESET_ITERATIONS 3
60#define MAXIMUM_RESTART_ITERATIONS 3
61#define AUTO_STAR_TIMEOUT 45000
62#define MINIMUM_PULSE_TIMER 32
63#define MAX_RECAPTURE_RETRIES 3
64#define MINIMUM_POLY_SOLUTIONS 2
65
66const QString FOCUSER_SIMULATOR = "Focuser Simulator";
67
68namespace Ekos
69{
70Focus::Focus() : QWidget()
71{
72 // #1 Set the UI
73 setupUi(this);
74
75 // #1a Prepare UI
76 prepareGUI();
77
78 // #2 Register DBus
79 qRegisterMetaType<Ekos::FocusState>("Ekos::FocusState");
81 new FocusAdaptor(this);
82 QDBusConnection::sessionBus().registerObject("/KStars/Ekos/Focus", this);
83
84 // #3 Init connections
85 initConnections();
86
87 // #4 Init Plots
88 initPlots();
89
90 // #5 Init View
91 initView();
92
93 // #6 Reset all buttons to default states
94 resetButtons();
95
96 // #7 Load All settings
97 loadGlobalSettings();
98
99 // #8 Init Setting Connection now
100 connectSyncSettings();
101
102 // #9 Init Adaptive Focus
103 adaptFocus.reset(new AdaptiveFocus(this));
104
105 connect(&m_StarFinderWatcher, &QFutureWatcher<bool>::finished, this, &Focus::starDetectionFinished);
106
107 //Note: This is to prevent a button from being called the default button
108 //and then executing when the user hits the enter key such as when on a Text Box
110 for (auto &button : qButtons)
111 button->setAutoDefault(false);
112
113 appendLogText(i18n("Idle."));
114
115 // Focus motion timeout
116 m_FocusMotionTimer.setInterval(m_OpsFocusMechanics->focusMotionTimeout->value() * 1000);
117 m_FocusMotionTimer.setSingleShot(true);
118 connect(&m_FocusMotionTimer, &QTimer::timeout, this, &Focus::handleFocusMotionTimeout);
119
120 // Create an autofocus CSV file, dated at startup time
121 m_FocusLogFileName = QDir(KSPaths::writableLocation(QStandardPaths::AppLocalDataLocation)).filePath("focuslogs/autofocus-" +
122 QDateTime::currentDateTime().toString("yyyy-MM-ddThh-mm-ss") + ".txt");
123 m_FocusLogFile.setFileName(m_FocusLogFileName);
124
125 m_OpsFocusProcess->editFocusProfile->setIcon(QIcon::fromTheme("document-edit"));
126 m_OpsFocusProcess->editFocusProfile->setAttribute(Qt::WA_LayoutUsesWidgetRect);
127
128 connect(m_OpsFocusProcess->editFocusProfile, &QAbstractButton::clicked, this, [this]()
129 {
130 KConfigDialog *optionsEditor = new KConfigDialog(this, "OptionsProfileEditor", Options::self());
131 optionsProfileEditor = new StellarSolverProfileEditor(this, Ekos::FocusProfiles, optionsEditor);
132#ifdef Q_OS_OSX
133 optionsEditor->setWindowFlags(Qt::Tool | Qt::WindowStaysOnTopHint);
134#endif
135 KPageWidgetItem *mainPage = optionsEditor->addPage(optionsProfileEditor, i18n("Focus Options Profile Editor"));
136 mainPage->setIcon(QIcon::fromTheme("configure"));
137 connect(optionsProfileEditor, &StellarSolverProfileEditor::optionsProfilesUpdated, this, &Focus::loadStellarSolverProfiles);
138 optionsProfileEditor->loadProfile(m_OpsFocusProcess->focusSEPProfile->currentText());
139 optionsEditor->show();
140 });
141
142 loadStellarSolverProfiles();
143
144 // connect HFR plot widget
145 connect(this, &Ekos::Focus::initHFRPlot, HFRPlot, &FocusHFRVPlot::init);
146 connect(this, &Ekos::Focus::redrawHFRPlot, HFRPlot, &FocusHFRVPlot::redraw);
147 connect(this, &Ekos::Focus::newHFRPlotPosition, HFRPlot, &FocusHFRVPlot::addPosition);
148 connect(this, &Ekos::Focus::drawPolynomial, HFRPlot, &FocusHFRVPlot::drawPolynomial);
149 // connect signal/slot for the curve plotting to the V-Curve widget
150 connect(this, &Ekos::Focus::drawCurve, HFRPlot, &FocusHFRVPlot::drawCurve);
151 connect(this, &Ekos::Focus::setTitle, HFRPlot, &FocusHFRVPlot::setTitle);
152 connect(this, &Ekos::Focus::finalUpdates, HFRPlot, &FocusHFRVPlot::finalUpdates);
153 connect(this, &Ekos::Focus::minimumFound, HFRPlot, &FocusHFRVPlot::drawMinimum);
154 connect(this, &Ekos::Focus::drawCFZ, HFRPlot, &FocusHFRVPlot::drawCFZ);
155
156 m_DarkProcessor = new DarkProcessor(this);
157 connect(m_DarkProcessor, &DarkProcessor::newLog, this, &Ekos::Focus::appendLogText);
158 connect(m_DarkProcessor, &DarkProcessor::darkFrameCompleted, this, [this](bool completed)
159 {
160 m_OpsFocusSettings->useFocusDarkFrame->setChecked(completed);
161 m_FocusView->setProperty("suspended", false);
162 if (completed)
163 {
164 m_FocusView->rescale(ZOOM_KEEP_LEVEL);
165 m_FocusView->updateFrame();
166 }
167 setCaptureComplete();
168 resetButtons();
169 });
170
171 setupOpticalTrainManager();
172 // Needs to be done once
173 connectFilterManager();
174}
175
176// Do once only preparation of GUI
177void Focus::prepareGUI()
178{
179 // Parameters are handled by the KConfigDialog invoked by pressing the "Options..." button
180 // on the Focus window. There are 3 pages of options.
181 // Parameters are persisted per Optical Train, so when the user changes OT, the last persisted
182 // parameters for the new OT are loaded. In addition the "current" parameter values are also
183 // persisted locally using kstars.kcfg
184 // KConfigDialog has the ability to persist parameters to kstars.kcfg but this functionality
185 // is not used in Focus
186 KConfigDialog *dialog = new KConfigDialog(this, "focussettings", Options::self());
187 m_OpsFocusSettings = new OpsFocusSettings();
188#ifdef Q_OS_OSX
189 dialog->setWindowFlags(Qt::Tool | Qt::WindowStaysOnTopHint);
190#endif
191
192 KPageWidgetItem *page = dialog->addPage(m_OpsFocusSettings, i18n("Settings"), nullptr, i18n("Focus Settings"), false);
193 page->setIcon(QIcon::fromTheme("configure"));
194
195 m_OpsFocusProcess = new OpsFocusProcess();
196 page = dialog->addPage(m_OpsFocusProcess, i18n("Process"), nullptr, i18n("Focus Process"), false);
197 page->setIcon(QIcon::fromTheme("transform-move"));
198
199 m_OpsFocusMechanics = new OpsFocusMechanics();
200 page = dialog->addPage(m_OpsFocusMechanics, i18n("Mechanics"), nullptr, i18n("Focus Mechanics"), false);
201 page->setIcon(QIcon::fromTheme("tool-measure"));
202
203 // The CFZ is a tool so has its own dialog box.
204 m_CFZDialog = new QDialog(this);
205 m_CFZUI.reset(new Ui::focusCFZDialog());
206 m_CFZUI->setupUi(m_CFZDialog);
207#ifdef Q_OS_OSX
208 m_CFZDialog->setWindowFlags(Qt::Tool | Qt::WindowStaysOnTopHint);
209#endif
210
211 // The Focus Advisor is a tool so has its own dialog box.
212 m_AdvisorDialog = new QDialog(this);
213 m_AdvisorUI.reset(new Ui::focusAdvisorDialog());
214 m_AdvisorUI->setupUi(m_AdvisorDialog);
215#ifdef Q_OS_OSX
216 m_AdvisorDialog->setWindowFlags(Qt::Tool | Qt::WindowStaysOnTopHint);
217#endif
218
219 // Remove all widgets from the temporary bucket. These will then be loaded as required
220 m_OpsFocusProcess->gridLayoutProcessBucket->removeWidget(m_OpsFocusProcess->focusMultiRowAverageLabel);
221 m_OpsFocusProcess->gridLayoutProcessBucket->removeWidget(m_OpsFocusProcess->focusMultiRowAverage);
222 m_OpsFocusProcess->gridLayoutProcessBucket->removeWidget(m_OpsFocusProcess->focusGaussianSigmaLabel);
223 m_OpsFocusProcess->gridLayoutProcessBucket->removeWidget(m_OpsFocusProcess->focusGaussianSigma);
224 m_OpsFocusProcess->gridLayoutProcessBucket->removeWidget(m_OpsFocusProcess->focusThresholdLabel);
225 m_OpsFocusProcess->gridLayoutProcessBucket->removeWidget(m_OpsFocusProcess->focusThreshold);
226 m_OpsFocusProcess->gridLayoutProcessBucket->removeWidget(m_OpsFocusProcess->focusGaussianKernelSizeLabel);
227 m_OpsFocusProcess->gridLayoutProcessBucket->removeWidget(m_OpsFocusProcess->focusGaussianKernelSize);
228 m_OpsFocusProcess->gridLayoutProcessBucket->removeWidget(m_OpsFocusProcess->focusToleranceLabel);
229 m_OpsFocusProcess->gridLayoutProcessBucket->removeWidget(m_OpsFocusProcess->focusTolerance);
230 delete m_OpsFocusProcess->gridLayoutProcessBucket;
231
232 // Setup the Walk fields. OutSteps and NumSteps are either/or widgets so co-locate them
233 m_OpsFocusMechanics->gridLayoutMechanics->replaceWidget(m_OpsFocusMechanics->focusOutStepsLabel,
234 m_OpsFocusMechanics->focusNumStepsLabel);
235 m_OpsFocusMechanics->gridLayoutMechanics->replaceWidget(m_OpsFocusMechanics->focusOutSteps,
236 m_OpsFocusMechanics->focusNumSteps);
237
238 // Some combo-boxes have changeable values depending on other settings so store the full list of options from the .ui
239 // This helps keep some synchronisation with the .ui
240 for (int i = 0; i < m_OpsFocusProcess->focusStarMeasure->count(); i++)
241 m_StarMeasureText.append(m_OpsFocusProcess->focusStarMeasure->itemText(i));
242 for (int i = 0; i < m_OpsFocusProcess->focusCurveFit->count(); i++)
243 m_CurveFitText.append(m_OpsFocusProcess->focusCurveFit->itemText(i));
244 for (int i = 0; i < m_OpsFocusMechanics->focusWalk->count(); i++)
245 m_FocusWalkText.append(m_OpsFocusMechanics->focusWalk->itemText(i));
246}
247
248void Focus::loadStellarSolverProfiles()
249{
250 QString savedOptionsProfiles = QDir(KSPaths::writableLocation(
251 QStandardPaths::AppLocalDataLocation)).filePath("SavedFocusProfiles.ini");
252 if(QFileInfo::exists(savedOptionsProfiles))
253 {
254 m_StellarSolverProfiles = StellarSolver::loadSavedOptionsProfiles(savedOptionsProfiles);
255 addMissingStellarSolverProfile(savedOptionsProfiles, FOCUS_DEFAULT_NAME);
256 addMissingStellarSolverProfile(savedOptionsProfiles, FOCUS_DEFAULT_DONUT_NAME);
257 }
258 else
259 m_StellarSolverProfiles = getDefaultFocusOptionsProfiles();
260 m_OpsFocusProcess->focusSEPProfile->clear();
261 for(auto &param : m_StellarSolverProfiles)
262 m_OpsFocusProcess->focusSEPProfile->addItem(param.listName);
263 auto profile = m_Settings["focusSEPProfile"];
264 if (profile.isValid())
265 m_OpsFocusProcess->focusSEPProfile->setCurrentText(profile.toString());
266}
267
268void Focus::addMissingStellarSolverProfile(const QString profilePath, const QString profile)
269{
270 for(auto params : m_StellarSolverProfiles)
271 {
272 if (params.listName == profile)
273 // Profile already exists so nothing more to do
274 return;
275 }
276
278 SSolver::Parameters params;
279 if (profile == FOCUS_DEFAULT_DONUT_NAME)
280 params = getFocusOptionsProfileDefaultDonut();
281 else if (profile == FOCUS_DEFAULT_NAME)
282 params = getFocusOptionsProfileDefault();
283 else
284 return;
285
286 settings.beginGroup(params.listName);
287 QMap<QString, QVariant> map = SSolver::Parameters::convertToMap(params);
289 while(it.hasNext())
290 {
291 it.next();
292 settings.setValue(it.key(), it.value());
293 }
294 settings.endGroup();
295
296 m_StellarSolverProfiles.append(params);
297}
298
299QStringList Focus::getStellarSolverProfiles()
300{
301 QStringList profiles;
302 for (auto param : m_StellarSolverProfiles)
303 profiles << param.listName;
304
305 return profiles;
306}
307
308Focus::~Focus()
309{
310 if (focusingWidget->parent() == nullptr)
311 toggleFocusingWidgetFullScreen();
312
313 m_FocusLogFile.close();
314}
315
316void Focus::resetFrame()
317{
318 if (m_Camera && m_Camera->isConnected())
319 {
320 ISD::CameraChip *targetChip = m_Camera->getChip(ISD::CameraChip::PRIMARY_CCD);
321
322 if (targetChip)
323 {
324 //fx=fy=fw=fh=0;
325 targetChip->resetFrame();
326
327 int x, y, w, h;
328 targetChip->getFrame(&x, &y, &w, &h);
329
330 qCDebug(KSTARS_EKOS_FOCUS) << "Frame is reset. X:" << x << "Y:" << y << "W:" << w << "H:" << h << "binX:" << 1 << "binY:" <<
331 1;
332
333 QVariantMap settings;
334 settings["x"] = x;
335 settings["y"] = y;
336 settings["w"] = w;
337 settings["h"] = h;
338 settings["binx"] = 1;
339 settings["biny"] = 1;
340 frameSettings[targetChip] = settings;
341
342 starSelected = false;
343 starCenter = QVector3D();
344 subFramed = false;
345
346 m_FocusView->setTrackingBox(QRect());
347 checkMosaicMaskLimits();
348 }
349 }
350}
351
352QString Focus::camera()
353{
354 if (m_Camera)
355 return m_Camera->getDeviceName();
356
357 return QString();
358}
359
360void Focus::checkCamera()
361{
362 if (!m_Camera)
363 return;
364
365 // Do NOT perform checks when the camera is capturing or busy as this may result
366 // in signals/slots getting disconnected.
367 switch (state())
368 {
369 // Idle, can change camera.
370 case FOCUS_IDLE:
371 case FOCUS_COMPLETE:
372 case FOCUS_FAILED:
373 case FOCUS_ABORTED:
374 break;
375
376 // Busy, cannot change camera.
377 case FOCUS_WAITING:
378 case FOCUS_PROGRESS:
379 case FOCUS_FRAMING:
380 case FOCUS_CHANGING_FILTER:
381 return;
382 }
383
384
385 ISD::CameraChip *targetChip = m_Camera->getChip(ISD::CameraChip::PRIMARY_CCD);
386 if (targetChip && targetChip->isCapturing())
387 return;
388
389 if (targetChip)
390 {
391 focusBinning->setEnabled(targetChip->canBin());
392 m_OpsFocusSettings->focusSubFrame->setEnabled(targetChip->canSubframe());
393 if (targetChip->canBin())
394 {
395 int subBinX = 1, subBinY = 1;
396 focusBinning->clear();
397 targetChip->getMaxBin(&subBinX, &subBinY);
398 for (int i = 1; i <= subBinX; i++)
399 focusBinning->addItem(QString("%1x%2").arg(i).arg(i));
400
401 auto binning = m_Settings["focusBinning"];
402 if (binning.isValid())
403 focusBinning->setCurrentText(binning.toString());
404 }
405
406 connect(m_Camera, &ISD::Camera::videoStreamToggled, this, &Ekos::Focus::setVideoStreamEnabled, Qt::UniqueConnection);
407 liveVideoB->setEnabled(m_Camera->hasVideoStream());
408 if (m_Camera->hasVideoStream())
409 setVideoStreamEnabled(m_Camera->isStreamingEnabled());
410 else
411 liveVideoB->setIcon(QIcon::fromTheme("camera-off"));
412
413 }
414
415 syncCCDControls();
416 syncCameraInfo();
417}
418
419void Focus::syncCCDControls()
420{
421 if (m_Camera == nullptr)
422 return;
423
424 auto targetChip = m_Camera->getChip(ISD::CameraChip::PRIMARY_CCD);
425 if (targetChip == nullptr || (targetChip && targetChip->isCapturing()))
426 return;
427
428 auto isoList = targetChip->getISOList();
429 focusISO->clear();
430
431 if (isoList.isEmpty())
432 {
433 focusISO->setEnabled(false);
434 ISOLabel->setEnabled(false);
435 }
436 else
437 {
438 focusISO->setEnabled(true);
439 ISOLabel->setEnabled(true);
440 focusISO->addItems(isoList);
441 focusISO->setCurrentIndex(targetChip->getISOIndex());
442 }
443
444 bool hasGain = m_Camera->hasGain();
445 gainLabel->setEnabled(hasGain);
446 focusGain->setEnabled(hasGain && m_Camera->getGainPermission() != IP_RO);
447 if (hasGain)
448 {
449 double gain = 0, min = 0, max = 0, step = 1;
450 m_Camera->getGainMinMaxStep(&min, &max, &step);
451 if (m_Camera->getGain(&gain))
452 {
453 // Allow the possibility of no gain value at all.
454 GainSpinSpecialValue = min - step;
455 focusGain->setRange(GainSpinSpecialValue, max);
456 focusGain->setSpecialValueText(i18n("--"));
457 if (step > 0)
458 focusGain->setSingleStep(step);
459
460 auto defaultGain = m_Settings["focusGain"];
461 if (defaultGain.isValid())
462 focusGain->setValue(defaultGain.toDouble());
463 else
464 focusGain->setValue(GainSpinSpecialValue);
465 }
466 }
467 else
468 focusGain->clear();
469}
470
471void Focus::syncCameraInfo()
472{
473 if (m_Camera == nullptr)
474 return;
475
476 auto targetChip = m_Camera->getChip(ISD::CameraChip::PRIMARY_CCD);
477 if (targetChip == nullptr || (targetChip && targetChip->isCapturing()))
478 return;
479
480 m_OpsFocusSettings->focusSubFrame->setEnabled(targetChip->canSubframe());
481
482 if (frameSettings.contains(targetChip) == false)
483 {
484 int x, y, w, h;
485 if (targetChip->getFrame(&x, &y, &w, &h))
486 {
487 int binx = 1, biny = 1;
488 targetChip->getBinning(&binx, &biny);
489 if (w > 0 && h > 0)
490 {
491 int minX, maxX, minY, maxY, minW, maxW, minH, maxH;
492 targetChip->getFrameMinMax(&minX, &maxX, &minY, &maxY, &minW, &maxW, &minH, &maxH);
493
494 QVariantMap settings;
495
496 settings["x"] = m_OpsFocusSettings->focusSubFrame->isChecked() ? x : minX;
497 settings["y"] = m_OpsFocusSettings->focusSubFrame->isChecked() ? y : minY;
498 settings["w"] = m_OpsFocusSettings->focusSubFrame->isChecked() ? w : maxW;
499 settings["h"] = m_OpsFocusSettings->focusSubFrame->isChecked() ? h : maxH;
500 settings["binx"] = binx;
501 settings["biny"] = biny;
502
503 frameSettings[targetChip] = settings;
504 }
505 }
506 }
507}
508
509bool Focus::setFilterWheel(ISD::FilterWheel *device)
510{
511 if (m_FilterWheel && m_FilterWheel == device)
512 {
513 checkFilter();
514 return false;
515 }
516
517 if (m_FilterWheel)
518 m_FilterWheel->disconnect(this);
519
520 m_FilterWheel = device;
521
522 if (m_FilterWheel)
523 {
524 connect(m_FilterWheel, &ISD::ConcreteDevice::Connected, this, [this]()
525 {
526 FilterPosLabel->setEnabled(true);
527 focusFilter->setEnabled(true);
528 filterManagerB->setEnabled(true);
529 });
530 connect(m_FilterWheel, &ISD::ConcreteDevice::Disconnected, this, [this]()
531 {
532 FilterPosLabel->setEnabled(false);
533 focusFilter->setEnabled(false);
534 filterManagerB->setEnabled(false);
535 });
536 }
537
538 auto isConnected = m_FilterWheel && m_FilterWheel->isConnected();
539 FilterPosLabel->setEnabled(isConnected);
540 focusFilter->setEnabled(isConnected);
541 filterManagerB->setEnabled(isConnected);
542
543 FilterPosLabel->setEnabled(true);
544 focusFilter->setEnabled(true);
545
546 checkFilter();
547 return true;
548}
549
550bool Focus::addTemperatureSource(const QSharedPointer<ISD::GenericDevice> &device)
551{
552 if (device.isNull())
553 return false;
554
555 for (auto &oneSource : m_TemperatureSources)
556 {
557 if (oneSource->getDeviceName() == device->getDeviceName())
558 return false;
559 }
560
561 m_TemperatureSources.append(device);
562 defaultFocusTemperatureSource->addItem(device->getDeviceName());
563
564 auto targetSource = m_Settings["defaultFocusTemperatureSource"];
565 if (targetSource.isValid())
566 checkTemperatureSource(targetSource.toString());
567 return true;
568}
569
570void Focus::checkTemperatureSource(const QString &name )
571{
572 auto source = name;
573 if (name.isEmpty())
574 {
575 source = defaultFocusTemperatureSource->currentText();
576 if (source.isEmpty())
577 return;
578 }
579
581
582 for (auto &oneSource : m_TemperatureSources)
583 {
584 if (oneSource->getDeviceName() == source)
585 {
586 currentSource = oneSource;
587 break;
588 }
589 }
590
591 m_LastSourceDeviceAutofocusTemperature = currentSource;
592
593 // No valid device found
594 if (!currentSource)
595 return;
596
597 // Disconnect all existing signals
598 for (const auto &oneSource : m_TemperatureSources)
599 disconnect(oneSource.get(), &ISD::GenericDevice::propertyUpdated, this, &Ekos::Focus::processTemperatureSource);
600
601
602 if (findTemperatureElement(currentSource))
603 {
604 m_LastSourceAutofocusTemperature = currentTemperatureSourceElement->value;
605 absoluteTemperatureLabel->setText(QString("%1 °C").arg(currentTemperatureSourceElement->value, 0, 'f', 2));
606 deltaTemperatureLabel->setText(QString("%1 °C").arg(0.0, 0, 'f', 2));
607 }
608 else
609 {
610 m_LastSourceAutofocusTemperature = INVALID_VALUE;
611 }
612
613 connect(currentSource.get(), &ISD::GenericDevice::propertyUpdated, this, &Ekos::Focus::processTemperatureSource,
615}
616
617bool Focus::findTemperatureElement(const QSharedPointer<ISD::GenericDevice> &device)
618{
619 // protect for nullptr
620 if (device.isNull())
621 return false;
622
623 auto temperatureProperty = device->getProperty("FOCUS_TEMPERATURE");
625 temperatureProperty = device->getProperty("CCD_TEMPERATURE");
627 {
628 currentTemperatureSourceElement = temperatureProperty.getNumber()->at(0);
629 return true;
630 }
631
632 temperatureProperty = device->getProperty("WEATHER_PARAMETERS");
634 {
635 for (int i = 0; i < temperatureProperty.getNumber()->count(); i++)
636 {
637 if (strstr(temperatureProperty.getNumber()->at(i)->getName(), "_TEMPERATURE"))
638 {
639 currentTemperatureSourceElement = temperatureProperty.getNumber()->at(i);
640 return true;
641 }
642 }
643 }
644
645 return false;
646}
647
648QString Focus::filterWheel()
649{
650 if (m_FilterWheel)
651 return m_FilterWheel->getDeviceName();
652
653 return QString();
654}
655
656
657bool Focus::setFilter(const QString &filter)
658{
659 if (m_FilterWheel)
660 {
661 focusFilter->setCurrentText(filter);
662 return true;
663 }
664
665 return false;
666}
667
668QString Focus::filter()
669{
670 return focusFilter->currentText();
671}
672
673void Focus::checkFilter()
674{
675 focusFilter->clear();
676
677 if (!m_FilterWheel)
678 {
679 FilterPosLabel->setEnabled(false);
680 focusFilter->setEnabled(false);
681 filterManagerB->setEnabled(false);
682
683 if (m_FilterManager)
684 {
685 m_FilterManager->disconnect(this);
686 disconnect(m_FilterManager.get());
687 m_FilterManager.reset();
688 }
689 return;
690 }
691
692 FilterPosLabel->setEnabled(true);
693 focusFilter->setEnabled(true);
694 filterManagerB->setEnabled(true);
695
696 setupFilterManager();
697
698 focusFilter->addItems(m_FilterManager->getFilterLabels());
699
700 currentFilterPosition = m_FilterManager->getFilterPosition();
701
702 focusFilter->setCurrentIndex(currentFilterPosition - 1);
703
704 focusExposure->setValue(m_FilterManager->getFilterExposure());
705}
706
707bool Focus::setFocuser(ISD::Focuser *device)
708{
709 if (m_Focuser && m_Focuser == device)
710 {
711 checkFocuser();
712 return false;
713 }
714
715 if (m_Focuser)
716 m_Focuser->disconnect(this);
717
718 m_Focuser = device;
719
720 if (m_Focuser)
721 {
722 connect(m_Focuser, &ISD::ConcreteDevice::Connected, this, [this]()
723 {
724 resetButtons();
725 });
726 connect(m_Focuser, &ISD::ConcreteDevice::Disconnected, this, [this]()
727 {
728 resetButtons();
729 });
730 }
731
732 checkFocuser();
733 return true;
734}
735
736QString Focus::focuser()
737{
738 if (m_Focuser)
739 return m_Focuser->getDeviceName();
740
741 return QString();
742}
743
744void Focus::checkFocuser()
745{
746 if (!m_Focuser)
747 {
748 if (m_FilterManager)
749 m_FilterManager->setFocusReady(false);
750 canAbsMove = canRelMove = canTimerMove = false;
751 resetButtons();
752 setFocusAlgorithm(static_cast<Algorithm> (m_OpsFocusProcess->focusAlgorithm->currentIndex()));
753 return;
754 }
755 else
756 focuserLabel->setText(m_Focuser->getDeviceName());
757
758 if (m_FilterManager)
759 m_FilterManager->setFocusReady(m_Focuser->isConnected());
760
761 hasDeviation = m_Focuser->hasDeviation();
762
763 canAbsMove = m_Focuser->canAbsMove();
764
765 if (canAbsMove)
766 {
767 getAbsFocusPosition();
768
769 absTicksSpin->setEnabled(true);
770 absTicksLabel->setEnabled(true);
771 startGotoB->setEnabled(true);
772
773 // Now that Optical Trains are managing settings, no need to set absTicksSpin, except if it has a bad value
774 if (absTicksSpin->value() <= 0)
775 absTicksSpin->setValue(currentPosition);
776 }
777 else
778 {
779 absTicksSpin->setEnabled(false);
780 absTicksLabel->setEnabled(false);
781 startGotoB->setEnabled(false);
782 }
783
784 canRelMove = m_Focuser->canRelMove();
785
786 // In case we have a purely relative focuser, we pretend
787 // it is an absolute focuser with initial point set at 50,000.
788 // This is done we can use the same algorithm used for absolute focuser.
789 if (canAbsMove == false && canRelMove == true)
790 {
791 currentPosition = 50000;
792 absMotionMax = 100000;
793 absMotionMin = 0;
794 }
795
796 canTimerMove = m_Focuser->canTimerMove();
797
798 // In case we have a timer-based focuser and using the linear focus algorithm,
799 // we pretend it is an absolute focuser with initial point set at 50,000.
800 // These variables don't have in impact on timer-based focusers if the algorithm
801 // is not the linear focus algorithm.
802 if (!canAbsMove && !canRelMove && canTimerMove)
803 {
804 currentPosition = 50000;
805 absMotionMax = 100000;
806 absMotionMin = 0;
807 }
808
809 m_FocusType = (canRelMove || canAbsMove || canTimerMove) ? FOCUS_AUTO : FOCUS_MANUAL;
810 profilePlot->setFocusAuto(m_FocusType == FOCUS_AUTO);
811
812 bool hasBacklash = m_Focuser->hasBacklash();
813 m_OpsFocusMechanics->focusBacklash->setEnabled(hasBacklash);
814 m_OpsFocusMechanics->focusBacklash->disconnect(this);
815 if (hasBacklash)
816 {
817 double min = 0, max = 0, step = 0;
818 m_Focuser->getMinMaxStep("FOCUS_BACKLASH_STEPS", "FOCUS_BACKLASH_VALUE", &min, &max, &step);
819 m_OpsFocusMechanics->focusBacklash->setMinimum(min);
820 m_OpsFocusMechanics->focusBacklash->setMaximum(max);
821 m_OpsFocusMechanics->focusBacklash->setSingleStep(step);
822 m_OpsFocusMechanics->focusBacklash->setValue(m_Focuser->getBacklash());
823 connect(m_OpsFocusMechanics->focusBacklash, static_cast<void (QSpinBox::*)(int)>(&QSpinBox::valueChanged),
824 this, [this](int value)
825 {
826 if (m_Focuser)
827 {
828 if (m_Focuser->getBacklash() == value)
829 // Prevent an event storm where a fast update of the backlash field, e.g. changing
830 // the value to "12" results in 2 events; "1", "12". As these events get
831 // processed in the driver and persisted, they in turn update backlash and start
832 // to conflict with this callback, getting stuck forever: "1", "12", "1', "12"
833 return;
834 m_Focuser->setBacklash(value);
835 }
836 });
837 // Re-esablish connection to sync settings. Only need to reconnect if the focuser
838 // has backlash as the value can't be changed if the focuser doesn't have the backlash property.
839 connect(m_OpsFocusMechanics->focusBacklash, QOverload<int>::of(&QSpinBox::valueChanged), this, &Ekos::Focus::syncSettings);
840 }
841 else
842 {
843 m_OpsFocusMechanics->focusBacklash->setValue(0);
844 }
845
846 connect(m_Focuser, &ISD::Focuser::propertyUpdated, this, &Ekos::Focus::updateProperty, Qt::UniqueConnection);
847
848 resetButtons();
849 setFocusAlgorithm(static_cast<Algorithm> (m_OpsFocusProcess->focusAlgorithm->currentIndex()));
850}
851
852bool Focus::setCamera(ISD::Camera *device)
853{
854 if (m_Camera && m_Camera == device)
855 {
856 checkCamera();
857 return false;
858 }
859
860 m_captureInProgress = false;
861
862 if (m_Camera)
863 m_Camera->disconnect(this);
864
865 m_Camera = device;
866
867 if (m_Camera)
868 {
869 connect(m_Camera, &ISD::ConcreteDevice::Connected, this, [this]()
870 {
871 focuserGroup->setEnabled(true);
872 ccdGroup->setEnabled(true);
873 toolsGroup->setEnabled(true);
874 });
875 connect(m_Camera, &ISD::ConcreteDevice::Disconnected, this, [this]()
876 {
877 focuserGroup->setEnabled(false);
878 ccdGroup->setEnabled(false);
879 toolsGroup->setEnabled(false);
880 });
881 }
882
883 auto isConnected = m_Camera && m_Camera->isConnected();
884 focuserGroup->setEnabled(isConnected);
885 ccdGroup->setEnabled(isConnected);
886 toolsGroup->setEnabled(isConnected);
887
888 if (!m_Camera)
889 return false;
890
891 resetFrame();
892
893 checkCamera();
894 checkMosaicMaskLimits();
895 return true;
896}
897
898void Focus::getAbsFocusPosition()
899{
900 if (!canAbsMove)
901 return;
902
903 auto absMove = m_Focuser->getNumber("ABS_FOCUS_POSITION");
904
905 if (absMove)
906 {
907 const auto &it = absMove->at(0);
908 currentPosition = static_cast<int>(it->getValue());
909 absMotionMax = it->getMax();
910 absMotionMin = it->getMin();
911
912 absTicksSpin->setMinimum(it->getMin());
913 absTicksSpin->setMaximum(it->getMax());
914 absTicksSpin->setSingleStep(it->getStep());
915
916 // Restrict the travel if needed
917 double const travel = std::abs(it->getMax() - it->getMin());
918 m_OpsFocusMechanics->focusMaxTravel->setMaximum(travel);;
919
920 absTicksLabel->setText(QString::number(currentPosition));
921
922 m_OpsFocusMechanics->focusTicks->setMaximum(it->getMax() / 2);
923 }
924}
925
926void Focus::processTemperatureSource(INDI::Property prop)
927{
928 if (m_LastSourceAutofocusTemperature == INVALID_VALUE && m_LastSourceDeviceAutofocusTemperature
929 && !currentTemperatureSourceElement )
930 {
931 if( findTemperatureElement( m_LastSourceDeviceAutofocusTemperature ) )
932 {
933 appendLogText(i18n("Finally found temperature source %1", QString(currentTemperatureSourceElement->nvp->name)));
934 m_LastSourceAutofocusTemperature = currentTemperatureSourceElement->value;
935 }
936 }
937
938 double delta = 0;
939 if (currentTemperatureSourceElement && currentTemperatureSourceElement->nvp->name == prop.getName())
940 {
941 if (m_LastSourceAutofocusTemperature != INVALID_VALUE)
942 {
943 delta = currentTemperatureSourceElement->value - m_LastSourceAutofocusTemperature;
944 emit newFocusTemperatureDelta(abs(delta), currentTemperatureSourceElement->value);
945 }
946 else
947 {
948 emit newFocusTemperatureDelta(0, currentTemperatureSourceElement->value);
949 }
950
951 absoluteTemperatureLabel->setText(QString("%1 °C").arg(currentTemperatureSourceElement->value, 0, 'f', 2));
952 deltaTemperatureLabel->setText(QString("%1%2 °C").arg((delta > 0.0 ? "+" : "")).arg(delta, 0, 'f', 2));
953 if (delta == 0)
954 deltaTemperatureLabel->setStyleSheet("color: lightgreen");
955 else if (delta > 0)
956 deltaTemperatureLabel->setStyleSheet("color: lightcoral");
957 else
958 deltaTemperatureLabel->setStyleSheet("color: lightblue");
959 }
960}
961
962void Focus::setLastFocusTemperature()
963{
964 m_LastSourceAutofocusTemperature = currentTemperatureSourceElement ? currentTemperatureSourceElement->value : INVALID_VALUE;
965
966 // Reset delta to zero now that we're just done with autofocus
967 deltaTemperatureLabel->setText(QString("0 °C"));
968 deltaTemperatureLabel->setStyleSheet("color: lightgreen");
969
970 emit newFocusTemperatureDelta(0, -1e6);
971}
972
973void Focus::setLastFocusAlt()
974{
976 m_LastSourceAutofocusAlt = INVALID_VALUE;
977 else
978 m_LastSourceAutofocusAlt = mountAlt;
979}
980
981// Reset Adaptive Focus parameters.
982void Focus::resetAdaptiveFocus(bool enabled)
983{
984 // Set the focusAdaptive switch so other modules such as Capture can access it
985 Options::setFocusAdaptive(enabled);
986
987 if (enabled)
988 adaptFocus.reset(new AdaptiveFocus(this));
989 adaptFocus->resetAdaptiveFocusCounters();
990}
991
992// Capture has signalled an Adaptive Focus run
993void Focus::adaptiveFocus()
994{
995 // Invoke runAdaptiveFocus
996 adaptFocus->runAdaptiveFocus(currentPosition, filter());
997}
998
999// Run Aberration Inspector
1000void Focus::startAbIns()
1001{
1002 m_abInsOn = canAbInsStart();
1003 runAutoFocus(AutofocusReason::FOCUS_ABERRATION_INSPECTOR, "");
1004}
1005
1006// User pressed the Autofocus button
1007void Focus::manualStart()
1008{
1009 runAutoFocus(AutofocusReason::FOCUS_MANUAL, "");
1010}
1011
1012// An Autofocus start request over DBUS (most likely from the Scheduler)
1013void Focus::start()
1014{
1015 runAutoFocus(AutofocusReason::FOCUS_SCHEDULER, "");
1016}
1017
1018// Start an Autofocus run. This is called from Build Offsets, FilterManger and Capture
1019void Focus::runAutoFocus(AutofocusReason autofocusReason, const QString &reasonInfo)
1020{
1021 m_AutofocusReason = autofocusReason;
1022 m_AutofocusReasonInfo = reasonInfo;
1023 if (m_Focuser == nullptr)
1024 {
1025 appendLogText(i18n("No Focuser connected."));
1026 completeFocusProcedure(Ekos::FOCUS_ABORTED, Ekos::FOCUS_FAIL_NO_FOCUSER);
1027 return;
1028 }
1029
1030 if (m_Camera == nullptr)
1031 {
1032 appendLogText(i18n("No CCD connected."));
1033 completeFocusProcedure(Ekos::FOCUS_ABORTED, Ekos::FOCUS_FAIL_NO_CAMERA);
1034 return;
1035 }
1036
1037 if (!canAbsMove && !canRelMove && m_OpsFocusMechanics->focusTicks->value() <= MINIMUM_PULSE_TIMER)
1038 {
1039 appendLogText(i18n("Starting pulse step is too low. Increase the step size to %1 or higher...",
1040 MINIMUM_PULSE_TIMER * 5));
1041 completeFocusProcedure(Ekos::FOCUS_ABORTED, Ekos::FOCUS_FAIL_LOW_PULSE);
1042 return;
1043 }
1044
1045 if (inAutoFocus)
1046 {
1047 // If Autofocus is already running, just ignore this request. This condition should not happen
1048 // There is no point signalling Autofocus failure as that will trigger actions based on the
1049 // currently running Autofocus failing (whilst it is still running).
1050 appendLogText(i18n("Autofocus is already running, discarding start request."));
1051 return;
1052 }
1053
1054 if (inBuildOffsets)
1055 {
1056 // If Build Offsets is running, reject the Autofocus request.
1057 appendLogText(i18n("Build Offset is already running, Autofocus rejected."));
1058 completeFocusProcedure(Ekos::FOCUS_ABORTED, Ekos::FOCUS_FAIL_INTERNAL);
1059 return;
1060 }
1061
1062 if (inFocusLoop)
1063 {
1064 // If inFocusLoop is already running, reject this Autofocus request
1065 appendLogText(i18n("In Focus Loop, Autofocus rejected."));
1066 completeFocusProcedure(Ekos::FOCUS_ABORTED, Ekos::FOCUS_FAIL_INTERNAL);
1067 return;
1068 }
1069
1070 if (inAdjustFocus || adaptFocus->inAdaptiveFocus())
1071 {
1072 QString str = inAdjustFocus ? i18n("Adjust Focus") : i18n("Adaptive Focus");
1073 if (++m_StartRetries < MAXIMUM_RESTART_ITERATIONS)
1074 {
1075 appendLogText(i18n("Autofocus start request - Waiting 10sec for %1 to complete.", str));
1076 QTimer::singleShot(10 * 1000, this, [this]()
1077 {
1078 runAutoFocus(m_AutofocusReason, m_AutofocusReasonInfo);
1079 });
1080 return;
1081 }
1082
1083 appendLogText(i18n("Discarding Autofocus start request - %1 in progress.", str));
1084 completeFocusProcedure(Ekos::FOCUS_ABORTED, Ekos::FOCUS_FAIL_INTERNAL);
1085 return;
1086 }
1087
1088 inBuildOffsets = (autofocusReason == AutofocusReason::FOCUS_FILTER_OFFSETS);
1089 if (autofocusReason == AutofocusReason::FOCUS_USER_REQUEST)
1090 {
1091 forceInSeqAF->setChecked(false);
1092 Options::setFocusForceInSeqAF(false);
1093 }
1094 inAutoFocus = true;
1095 m_AFRun++;
1096 m_StartRetries = 0;
1097 m_LastFocusDirection = FOCUS_NONE;
1098
1099 waitStarSelectTimer.stop();
1100
1101 // Reset the focus motion timer
1102 m_FocusMotionTimerCounter = 0;
1103 m_FocusMotionTimer.stop();
1104 m_FocusMotionTimer.setInterval(m_OpsFocusMechanics->focusMotionTimeout->value() * 1000);
1105
1106 // Reset focuser reconnect counter
1107 m_FocuserReconnectCounter = 0;
1108
1109 // Reset focuser reconnect counter
1110 m_FocuserReconnectCounter = 0;
1111 m_DebugFocuserCounter = 0;
1112
1113 starsHFR.clear();
1114
1115 lastHFR = 0;
1116
1117 // Reset state variable that deals with retrying and aborting aurofocus
1118 m_RestartState = RESTART_NONE;
1119
1120 // Keep the last focus temperature, it can still be useful in case the autofocus fails
1121 // lastFocusTemperature
1122
1123 if (canAbsMove)
1124 {
1125 absIterations = 0;
1126 getAbsFocusPosition();
1127 pulseDuration = m_OpsFocusMechanics->focusTicks->value();
1128 }
1129 else if (canRelMove)
1130 {
1131 //appendLogText(i18n("Setting dummy central position to 50000"));
1132 absIterations = 0;
1133 pulseDuration = m_OpsFocusMechanics->focusTicks->value();
1134 //currentPosition = 50000;
1135 absMotionMax = 100000;
1136 absMotionMin = 0;
1137 }
1138 else
1139 {
1140 pulseDuration = m_OpsFocusMechanics->focusTicks->value();
1141 absIterations = 0;
1142 absMotionMax = 100000;
1143 absMotionMin = 0;
1144 }
1145
1146 focuserAdditionalMovement = 0;
1147 starMeasureFrames.clear();
1148
1149 resetButtons();
1150
1151 reverseDir = false;
1152
1153 /*if (fw > 0 && fh > 0)
1154 starSelected= true;
1155 else
1156 starSelected= false;*/
1157
1158 clearDataPoints();
1159 profilePlot->clear();
1160 FWHMOut->setText("");
1161
1162 qCInfo(KSTARS_EKOS_FOCUS) << "Starting Autofocus " << m_AFRun
1163 << " on" << focuserLabel->text()
1164 << " Reason: " << AutofocusReasonStr[m_AutofocusReason]
1165 << " Reason Info: " << m_AutofocusReasonInfo
1166 << " CanAbsMove: " << (canAbsMove ? "yes" : "no" )
1167 << " CanRelMove: " << (canRelMove ? "yes" : "no" )
1168 << " CanTimerMove: " << (canTimerMove ? "yes" : "no" )
1169 << " Position:" << currentPosition
1170 << " Filter:" << filter()
1171 << " Exp:" << focusExposure->value()
1172 << " Bin:" << focusBinning->currentText()
1173 << " Gain:" << focusGain->value()
1174 << " ISO:" << focusISO->currentText();
1175 qCInfo(KSTARS_EKOS_FOCUS) << "Settings Tab."
1176 << " Auto Select Star:" << ( m_OpsFocusSettings->focusAutoStarEnabled->isChecked() ? "yes" : "no" )
1177 << " Dark Frame:" << ( m_OpsFocusSettings->useFocusDarkFrame->isChecked() ? "yes" : "no" )
1178 << " Sub Frame:" << ( m_OpsFocusSettings->focusSubFrame->isChecked() ? "yes" : "no" )
1179 << " Box:" << m_OpsFocusSettings->focusBoxSize->value()
1180 << " Full frame:" << ( m_OpsFocusSettings->focusUseFullField->isChecked() ? "yes" : "no " )
1181 << " Focus Mask: " << (m_OpsFocusSettings->focusNoMaskRB->isChecked() ? "Use all stars" :
1182 (m_OpsFocusSettings->focusRingMaskRB->isChecked() ? QString("Ring Mask: [%1%, %2%]").
1183 arg(m_OpsFocusSettings->focusFullFieldInnerRadius->value()).arg(m_OpsFocusSettings->focusFullFieldOuterRadius->value()) :
1184 QString("Mosaic Mask: [%1%, space=%2px]").
1185 arg(m_OpsFocusSettings->focusMosaicTileWidth->value()).arg(m_OpsFocusSettings->focusMosaicSpace->value())))
1186 << " Suspend Guiding:" << ( m_OpsFocusSettings->focusSuspendGuiding->isChecked() ? "yes" : "no " )
1187 << " Guide Settle:" << m_OpsFocusSettings->focusGuideSettleTime->value()
1188 << " Display Units:" << m_OpsFocusSettings->focusUnits->currentText()
1189 << " Adaptive Focus:" << ( m_OpsFocusSettings->focusAdaptive->isChecked() ? "yes" : "no" )
1190 << " Min Move:" << m_OpsFocusSettings->focusAdaptiveMinMove->value()
1191 << " Adapt Start:" << ( m_OpsFocusSettings->focusAdaptStart->isChecked() ? "yes" : "no" )
1192 << " Max Total Move:" << m_OpsFocusSettings->focusAdaptiveMaxMove->value();
1193 qCInfo(KSTARS_EKOS_FOCUS) << "Process Tab."
1194 << " Detection:" << m_OpsFocusProcess->focusDetection->currentText()
1195 << " SEP Profile:" << m_OpsFocusProcess->focusSEPProfile->currentText()
1196 << " Algorithm:" << m_OpsFocusProcess->focusAlgorithm->currentText()
1197 << " Curve Fit:" << m_OpsFocusProcess->focusCurveFit->currentText()
1198 << " Measure:" << m_OpsFocusProcess->focusStarMeasure->currentText()
1199 << " PSF:" << m_OpsFocusProcess->focusStarPSF->currentText()
1200 << " Use Weights:" << ( m_OpsFocusProcess->focusUseWeights->isChecked() ? "yes" : "no" )
1201 << " R2 Limit:" << m_OpsFocusProcess->focusR2Limit->value()
1202 << " Refine Curve Fit:" << ( m_OpsFocusProcess->focusRefineCurveFit->isChecked() ? "yes" : "no" )
1203 << " Average Over:" << m_OpsFocusProcess->focusFramesCount->value()
1204 << " Average HFR Check Over:" << m_OpsFocusProcess->focusHFRFramesCount->value()
1205 << " Num.of Rows:" << m_OpsFocusProcess->focusMultiRowAverage->value()
1206 << " Sigma:" << m_OpsFocusProcess->focusGaussianSigma->value()
1207 << " Threshold:" << m_OpsFocusProcess->focusThreshold->value()
1208 << " Kernel size:" << m_OpsFocusProcess->focusGaussianKernelSize->value()
1209 << " Tolerance:" << m_OpsFocusProcess->focusTolerance->value()
1210 << " Donut Buster:" << ( m_OpsFocusProcess->focusDonut->isChecked() ? "yes" : "no" )
1211 << " Donut Time Dilation:" << m_OpsFocusProcess->focusTimeDilation->value()
1212 << " Outlier Rejection:" << m_OpsFocusProcess->focusOutlierRejection->value();
1213 qCInfo(KSTARS_EKOS_FOCUS) << "Mechanics Tab."
1214 << " Initial Step Size:" << m_OpsFocusMechanics->focusTicks->value()
1215 << " Out Step Multiple:" << m_OpsFocusMechanics->focusOutSteps->value()
1216 << " Number Steps:" << m_OpsFocusMechanics->focusNumSteps->value()
1217 << " Max Travel:" << m_OpsFocusMechanics->focusMaxTravel->value()
1218 << " Max Step Size:" << m_OpsFocusMechanics->focusMaxSingleStep->value()
1219 << " Driver Backlash:" << m_OpsFocusMechanics->focusBacklash->value()
1220 << " AF Overscan:" << m_OpsFocusMechanics->focusAFOverscan->value()
1221 << " Overscan Delay:" << m_OpsFocusMechanics->focusOverscanDelay->value()
1222 << " Focuser Settle:" << m_OpsFocusMechanics->focusSettleTime->value()
1223 << " Walk:" << m_OpsFocusMechanics->focusWalk->currentText()
1224 << " Capture Timeout:" << m_OpsFocusMechanics->focusCaptureTimeout->value()
1225 << " Motion Timeout:" << m_OpsFocusMechanics->focusMotionTimeout->value();
1226 qCInfo(KSTARS_EKOS_FOCUS) << "CFZ Tab."
1227 << " Algorithm:" << m_CFZUI->focusCFZAlgorithm->currentText()
1228 << " Tolerance:" << m_CFZUI->focusCFZTolerance->value()
1229 << " Tolerance (τ):" << m_CFZUI->focusCFZTau->value()
1230 << " Display:" << ( m_CFZUI->focusCFZDisplayVCurve->isChecked() ? "yes" : "no" )
1231 << " Wavelength (λ):" << m_CFZUI->focusCFZWavelength->value()
1232 << " Aperture (A):" << m_CFZUI->focusCFZAperture->value()
1233 << " Focal Ratio (f):" << m_CFZUI->focusCFZFNumber->value()
1234 << " Step Size:" << m_CFZUI->focusCFZStepSize->value()
1235 << " FWHM (θ):" << m_CFZUI->focusCFZSeeing->value();
1236
1237 const double temperature = (currentTemperatureSourceElement) ? currentTemperatureSourceElement->value : INVALID_VALUE;
1238 emit autofocusStarting(temperature, filter(), m_AutofocusReason, m_AutofocusReasonInfo);
1239
1240 if (m_OpsFocusSettings->focusAutoStarEnabled->isChecked())
1241 appendLogText(i18n("Autofocus in progress..."));
1242 else if (!inAutoFocus)
1243 appendLogText(i18n("Please wait until image capture is complete..."));
1244
1245 // Only suspend when we have Off-Axis Guider
1246 // If the guide camera is operating on a different OTA
1247 // then no need to suspend.
1248 if (m_GuidingSuspended == false && m_OpsFocusSettings->focusSuspendGuiding->isChecked())
1249 {
1250 m_GuidingSuspended = true;
1251 emit suspendGuiding();
1252 }
1253
1254 //emit statusUpdated(true);
1255 setState(Ekos::FOCUS_PROGRESS);
1256
1257 KSNotification::event(QLatin1String("FocusStarted"), i18n("Autofocus operation started"), KSNotification::Focus);
1258
1259 // Used for all the focuser types.
1260 if (m_FocusAlgorithm == FOCUS_LINEAR || m_FocusAlgorithm == FOCUS_LINEAR1PASS)
1261 {
1262 m_AFfilter = filter();
1263 int position = adaptFocus->adaptStartPosition(currentPosition, m_AFfilter);
1264
1265 curveFitting.reset(new CurveFitting());
1266
1267 if (m_FocusAlgorithm == FOCUS_LINEAR1PASS)
1268 {
1269 // Curve fitting for stars and FWHM processing
1270 starFitting.reset(new CurveFitting());
1271 focusFWHM.reset(new FocusFWHM(m_ScaleCalc));
1272 focusFourierPower.reset(new FocusFourierPower(m_ScaleCalc));
1273 // Donut Buster
1274 if (initDonutProcessing())
1275 return;
1276 }
1277
1278 // Setup the focuser
1279 setupLinearFocuser(position);
1280 if (!changeFocus(linearRequestedPosition - currentPosition))
1281 completeFocusProcedure(Ekos::FOCUS_ABORTED, Ekos::FOCUS_FAIL_FOCUSER_NO_MOVE);
1282
1283 // Avoid the capture below.
1284 return;
1285 }
1286 capture();
1287}
1288
1289void Focus::setupLinearFocuser(int initialPosition)
1290{
1291 FocusAlgorithmInterface::FocusParams params(curveFitting.get(),
1292 m_OpsFocusMechanics->focusMaxTravel->value(), m_OpsFocusMechanics->focusTicks->value(), initialPosition, absMotionMin,
1293 absMotionMax, MAXIMUM_ABS_ITERATIONS, m_OpsFocusProcess->focusTolerance->value() / 100.0, m_AFfilter,
1294 currentTemperatureSourceElement ? currentTemperatureSourceElement->value : INVALID_VALUE,
1295 m_OpsFocusMechanics->focusOutSteps->value(), m_OpsFocusMechanics->focusNumSteps->value(),
1296 m_FocusAlgorithm, m_OpsFocusMechanics->focusBacklash->value(), m_CurveFit, m_OpsFocusProcess->focusUseWeights->isChecked(),
1297 m_StarMeasure, m_StarPSF, m_OpsFocusProcess->focusRefineCurveFit->isChecked(), m_FocusWalk,
1298 m_OpsFocusProcess->focusDonut->isChecked(), m_OpsFocusProcess->focusOutlierRejection->value(), m_OptDir, m_ScaleCalc);
1299
1300 if (canAbsMove)
1301 initialFocuserAbsPosition = initialPosition;
1302 linearFocuser.reset(MakeLinearFocuser(params));
1303 linearRequestedPosition = linearFocuser->initialPosition();
1304}
1305
1306// Initialise donut buster
1307bool Focus::initDonutProcessing()
1308{
1309 if (!m_OpsFocusProcess->focusDonut->isChecked())
1310 return false;
1311
1312 m_donutOrigExposure = focusExposure->value();
1313
1314 if (m_OpsFocusProcess->focusScanStartPos->isChecked())
1315 {
1316 inScanStartPos = true;
1317 initialFocuserAbsPosition = currentPosition;
1318 m_scanMeasure.clear();
1319 m_scanPosition.clear();
1320
1321 appendLogText(i18n("Starting scan for initial focuser position."));
1322 emit setTitle(QString(i18n("Scanning for starting position...")), true);
1323 capture();
1324 return true;
1325 }
1326 return false;
1327}
1328
1329// Reset donut buster
1330void Focus::resetDonutProcessing()
1331{
1332 // If donut busting variable focus exposures have been used, reset to starting value
1333 if (m_OpsFocusProcess->focusDonut->isChecked() && inAutoFocus)
1334 focusExposure->setValue(m_donutOrigExposure);
1335}
1336
1337int Focus::adjustLinearPosition(int position, int newPosition, int overscan, bool updateDir)
1338{
1339 if (overscan > 0 && newPosition > position)
1340 {
1341 // If user has set an overscan value then use it
1342 int adjustment = overscan;
1343 qCDebug(KSTARS_EKOS_FOCUS) << QString("Linear: extending outward movement by overscan %1").arg(adjustment);
1344
1345 if (newPosition + adjustment > absMotionMax)
1346 adjustment = static_cast<int>(absMotionMax) - newPosition;
1347
1348 focuserAdditionalMovement = adjustment;
1349 focuserAdditionalMovementUpdateDir = updateDir;
1350
1351 return newPosition + adjustment;
1352 }
1353 return newPosition;
1354}
1355
1356// This function checks whether capture is in progress or HFR processing is in progress. If so, it waits until complete before
1357// stopping processing and signalling to Capture and Scheduler. This prevents Capture / Scheduler from starting the next action
1358// before Focus is done.
1359void Focus::checkStopFocus(bool abort)
1360{
1361 m_abortInProgress = true;
1362 // if abort, avoid try to restart
1363 if (abort)
1364 resetFocusIteration = MAXIMUM_RESET_ITERATIONS + 1;
1365
1366 if (captureTimer.isActive())
1367 // Capture is waiting for the settling period before starting, so just kill the timer which will prevent capture starting
1368 captureTimer.stop();
1369
1370 if (m_captureInProgress)
1371 {
1372 if (inAutoFocus == false && inFocusLoop == false)
1373 {
1374 captureB->setEnabled(true);
1375 stopFocusB->setEnabled(false);
1376
1377 appendLogText(i18n("Capture aborted."));
1378 }
1379 else if (inAutoFocus)
1380 {
1381 stopFocusB->setEnabled(false);
1382 appendLogText(i18n("Capture in progress, retrying in 1s..."));
1383 QTimer::singleShot(1000, this, [ &, abort]()
1384 {
1385 checkStopFocus(abort);
1386 });
1387 return;
1388 }
1389 }
1390
1391 if (m_starDetectInProgress)
1392 {
1393 stopFocusB->setEnabled(false);
1394 appendLogText(i18n("Star detection in progress, retrying in 1s..."));
1395 QTimer::singleShot(1000, this, [ &, abort]()
1396 {
1397 checkStopFocus(abort);
1398 });
1399 return;
1400 }
1401
1402 completeFocusProcedure(abort ? Ekos::FOCUS_ABORTED : Ekos::FOCUS_FAILED, Ekos::FOCUS_FAIL_ABORT);
1403}
1404
1405void Focus::meridianFlipStarted()
1406{
1407 // if focusing is not running, do nothing
1408 if (state() == FOCUS_IDLE || state() == FOCUS_COMPLETE || state() == FOCUS_FAILED || state() == FOCUS_ABORTED)
1409 return;
1410
1411 // store current focus iteration counter since abort() sets it to the maximal value to avoid restarting
1412 int old = resetFocusIteration;
1413 // abort focusing
1414 abort();
1415 // restore iteration counter
1416 resetFocusIteration = old;
1417}
1418
1419void Focus::abort()
1420{
1421 // No need to "abort" if not already in progress.
1422 if (state() <= FOCUS_ABORTED)
1423 return;
1424
1425 bool focusLoop = inFocusLoop;
1426 checkStopFocus(true);
1427 appendLogText(i18n("Autofocus aborted."));
1428 if (!focusLoop)
1429 // For looping leave focuser where it is
1430 // Otherwise try to shift the focuser back to its initial position
1431 resetFocuser();
1432}
1433
1434void Focus::stop(Ekos::FocusState completionState)
1435{
1436 qCDebug(KSTARS_EKOS_FOCUS) << "Stopping Focus";
1437
1438 captureTimeout.stop();
1439 m_FocusMotionTimer.stop();
1440 m_FocusMotionTimerCounter = 0;
1441 m_FocuserReconnectCounter = 0;
1442
1443 opticalTrainCombo->setEnabled(true);
1444 resetDonutProcessing();
1445 inAutoFocus = false;
1446 inAdjustFocus = false;
1447 adaptFocus->setInAdaptiveFocus(false);
1448 inBuildOffsets = false;
1449 inScanStartPos = false;
1450 m_AutofocusReason = AutofocusReason::FOCUS_NONE;
1451 m_AutofocusReasonInfo = "";
1452 focuserAdditionalMovement = 0;
1453 focuserAdditionalMovementUpdateDir = true;
1454 inFocusLoop = false;
1455 m_captureInProgress = false;
1456 m_abortInProgress = false;
1457 m_starDetectInProgress = false;
1458 isVShapeSolution = false;
1459 captureFailureCounter = 0;
1460 minimumRequiredHFR = INVALID_STAR_MEASURE;
1461 noStarCount = 0;
1462 starMeasureFrames.clear();
1463 m_abInsOn = false;
1464 m_StartRetries = 0;
1465
1466 // Check if CCD was not removed due to crash or other reasons.
1467 if (m_Camera)
1468 {
1469 disconnect(m_Camera, &ISD::Camera::newImage, this, &Ekos::Focus::processData);
1470 disconnect(m_Camera, &ISD::Camera::error, this, &Ekos::Focus::processCaptureError);
1471
1472 if (rememberUploadMode != m_Camera->getUploadMode())
1473 m_Camera->setUploadMode(rememberUploadMode);
1474
1475 // Remember to reset fast exposure if it was enabled before.
1476 if (m_RememberCameraFastExposure)
1477 {
1478 m_RememberCameraFastExposure = false;
1479 m_Camera->setFastExposureEnabled(true);
1480 }
1481
1482 ISD::CameraChip *targetChip = m_Camera->getChip(ISD::CameraChip::PRIMARY_CCD);
1483 targetChip->abortExposure();
1484 }
1485
1486 resetButtons();
1487
1488 absIterations = 0;
1489 HFRInc = 0;
1490 reverseDir = false;
1491
1492 if (m_GuidingSuspended)
1493 {
1494 emit resumeGuiding();
1495 m_GuidingSuspended = false;
1496 }
1497
1498 if (completionState == Ekos::FOCUS_ABORTED || completionState == Ekos::FOCUS_FAILED)
1499 setState(completionState);
1500}
1501
1502void Focus::capture(double settleTime)
1503{
1504 if (m_abortInProgress)
1505 {
1506 // We are trying to abort Autofocus so do no further processing
1507 m_captureInProgress = false;
1508 qCDebug(KSTARS_EKOS_FOCUS) << "Abort AF: discarding capture request.";
1509 return;
1510 }
1511
1512 // If capturing should be delayed by a given settling time, we start the capture timer.
1513 // This is intentionally designed re-entrant, i.e. multiple calls with settle time > 0 takes the last delay
1514 if (settleTime > 0 && m_captureInProgress == false)
1515 {
1516 captureTimer.start(static_cast<int>(settleTime * 1000));
1517 return;
1518 }
1519
1520 if (m_captureInProgress)
1521 {
1522 qCWarning(KSTARS_EKOS_FOCUS) << "Capture called while already in progress. Capture is ignored.";
1523 return;
1524 }
1525
1526 if (m_Camera == nullptr)
1527 {
1528 appendLogText(i18n("Error: No Camera detected."));
1529 checkStopFocus(true);
1530 return;
1531 }
1532
1533 if (m_Camera->isConnected() == false)
1534 {
1535 appendLogText(i18n("Error: Lost connection to Camera."));
1536 checkStopFocus(true);
1537 return;
1538 }
1539
1540 // reset timeout for receiving an image
1541 captureTimeout.stop();
1542 // reset timeout for focus star selection
1543 waitStarSelectTimer.stop();
1544
1545 ISD::CameraChip *targetChip = m_Camera->getChip(ISD::CameraChip::PRIMARY_CCD);
1546
1547 if (m_Camera->isBLOBEnabled() == false)
1548 {
1549 m_Camera->setBLOBEnabled(true);
1550 }
1551
1552 if (focusFilter->currentIndex() != -1)
1553 {
1554 if (m_FilterWheel == nullptr)
1555 {
1556 appendLogText(i18n("Error: No Filter Wheel detected."));
1557 checkStopFocus(true);
1558 return;
1559 }
1560 if (m_FilterWheel->isConnected() == false)
1561 {
1562 appendLogText(i18n("Error: Lost connection to Filter Wheel."));
1563 checkStopFocus(true);
1564 return;
1565 }
1566
1567 // In "regular" autofocus mode if the chosen filter has an associated lock filter then we need
1568 // to swap to the lock filter before running autofocus. However, AF is also called from build
1569 // offsets to run AF. In this case we need to ignore the lock filter and use the chosen filter
1570 int targetPosition = focusFilter->currentIndex() + 1;
1571 if (!inBuildOffsets)
1572 {
1573 QString lockedFilter = m_FilterManager->getFilterLock(filter());
1574
1575 // We change filter if:
1576 // 1. Target position is not equal to current position.
1577 // 2. Locked filter of CURRENT filter is a different filter.
1578 if (lockedFilter != "--" && lockedFilter != filter())
1579 {
1580 int lockedFilterIndex = focusFilter->findText(lockedFilter);
1581 if (lockedFilterIndex >= 0)
1582 {
1583 // Go back to this filter once we are done
1584 fallbackFilterPending = true;
1585 fallbackFilterPosition = targetPosition;
1586 targetPosition = lockedFilterIndex + 1;
1587 }
1588 }
1589 }
1590
1591 filterPositionPending = (targetPosition != currentFilterPosition);
1592 if (filterPositionPending)
1593 {
1594 // Change the filter. When done this will signal to update the focusFilter combo
1595 // Apply filter change policy if in Autofocus; otherwise apply change and offsets
1596 // Note that Autofocus doesn't need the change policy as Adapt Start Pos takes care of this
1597 FilterManager::FilterPolicy policy = (inAutoFocus) ?
1598 static_cast<FilterManager::FilterPolicy>(FilterManager::CHANGE_POLICY) :
1599 static_cast<FilterManager::FilterPolicy>(FilterManager::CHANGE_POLICY | FilterManager::OFFSET_POLICY);
1600 m_FilterManager->setFilterPosition(targetPosition, policy);
1601 return;
1602 }
1603 else if (targetPosition != focusFilter->currentIndex() + 1)
1604 focusFilter->setCurrentIndex(targetPosition - 1);
1605 }
1606
1607 m_FocusView->setProperty("suspended", m_OpsFocusSettings->useFocusDarkFrame->isChecked());
1608 prepareCapture(targetChip);
1609
1610 connect(m_Camera, &ISD::Camera::newImage, this, &Ekos::Focus::processData, Qt::UniqueConnection);
1611 connect(m_Camera, &ISD::Camera::error, this, &Ekos::Focus::processCaptureError, Qt::UniqueConnection);
1612
1613 if (frameSettings.contains(targetChip))
1614 {
1615 QVariantMap settings = frameSettings[targetChip];
1616 targetChip->setFrame(settings["x"].toInt(), settings["y"].toInt(), settings["w"].toInt(),
1617 settings["h"].toInt());
1618 settings["binx"] = (focusBinning->currentIndex() + 1);
1619 settings["biny"] = (focusBinning->currentIndex() + 1);
1620 frameSettings[targetChip] = settings;
1621 }
1622
1623 m_captureInProgress = true;
1624 if (state() != FOCUS_PROGRESS)
1625 setState(FOCUS_PROGRESS);
1626
1627 m_FocusView->setBaseSize(focusingWidget->size());
1628
1629 if (targetChip->capture(focusExposure->value()))
1630 {
1631 // Timeout is exposure duration + timeout threshold in seconds
1632 //long const timeout = lround(ceil(focusExposure->value() * 1000)) + FOCUS_TIMEOUT_THRESHOLD;
1633 captureTimeout.start( (focusExposure->value() + m_OpsFocusMechanics->focusCaptureTimeout->value()) * 1000);
1634
1635 if (inFocusLoop == false)
1636 appendLogText(i18n("Capturing image..."));
1637
1638 resetButtons();
1639 }
1640 else if (inAutoFocus)
1641 {
1642 completeFocusProcedure(Ekos::FOCUS_ABORTED, Ekos::FOCUS_FAIL_CAPTURE_FAILED);
1643 }
1644}
1645
1646void Focus::prepareCapture(ISD::CameraChip *targetChip)
1647{
1648 if (m_Camera->getUploadMode() == ISD::Camera::UPLOAD_LOCAL)
1649 {
1650 rememberUploadMode = ISD::Camera::UPLOAD_LOCAL;
1651 m_Camera->setUploadMode(ISD::Camera::UPLOAD_CLIENT);
1652 }
1653
1654 // We cannot use fast exposure in focus.
1655 if (m_Camera->isFastExposureEnabled())
1656 {
1657 m_RememberCameraFastExposure = true;
1658 m_Camera->setFastExposureEnabled(false);
1659 }
1660
1661 m_Camera->setEncodingFormat("FITS");
1662 targetChip->setBatchMode(false);
1663 targetChip->setBinning((focusBinning->currentIndex() + 1), (focusBinning->currentIndex() + 1));
1664 targetChip->setCaptureMode(FITS_FOCUS);
1665 targetChip->setFrameType(FRAME_LIGHT);
1666 targetChip->setCaptureFilter(FITS_NONE);
1667
1668 if (isFocusISOEnabled() && focusISO->currentIndex() != -1 &&
1669 targetChip->getISOIndex() != focusISO->currentIndex())
1670 targetChip->setISOIndex(focusISO->currentIndex());
1671
1672 if (isFocusGainEnabled() && focusGain->value() != GainSpinSpecialValue)
1673 m_Camera->setGain(focusGain->value());
1674}
1675
1676bool Focus::focusIn(int ms)
1677{
1678 if (currentPosition == absMotionMin)
1679 {
1680 appendLogText(i18n("At minimum focus position %1...", absMotionMin));
1681 return false;
1682 }
1683 focusInB->setEnabled(false);
1684 focusOutB->setEnabled(false);
1685 startGotoB->setEnabled(false);
1686 if (ms <= 0)
1687 ms = m_OpsFocusMechanics->focusTicks->value();
1688 if (currentPosition - ms <= absMotionMin)
1689 {
1690 ms = currentPosition - absMotionMin;
1691 appendLogText(i18n("Moving to minimum focus position %1...", absMotionMin));
1692 }
1693 return changeFocus(-ms);
1694}
1695
1696bool Focus::focusOut(int ms)
1697{
1698 if (currentPosition == absMotionMax)
1699 {
1700 appendLogText(i18n("At maximum focus position %1...", absMotionMax));
1701 return false;
1702 }
1703 focusInB->setEnabled(false);
1704 focusOutB->setEnabled(false);
1705 startGotoB->setEnabled(false);
1706 if (ms <= 0)
1707 ms = m_OpsFocusMechanics->focusTicks->value();
1708 if (currentPosition + ms >= absMotionMax)
1709 {
1710 ms = absMotionMax - currentPosition;
1711 appendLogText(i18n("Moving to maximum focus position %1...", absMotionMax));
1712 }
1713 return changeFocus(ms);
1714}
1715
1716// Routine to manage focus movements. All moves are now subject to overscan
1717// + amount indicates a movement out; - amount indictaes a movement in
1718bool Focus::changeFocus(int amount, bool updateDir)
1719{
1720 // Retry capture if we stay at the same position
1721 // Allow 1 step of tolerance--Have seen stalls with amount==1.
1722 if (inAutoFocus && abs(amount) <= 1)
1723 {
1724 if (m_RestartState == RESTART_NOW)
1725 // Restart in progress which is normally triggered by the completion of the focuser
1726 // reset to its starting position but in this case the focuser is already at its starting
1727 // position... so create a small movement. As above comment amount=1 can cause a stall, so use 2.
1728 amount = -2;
1729 else
1730 {
1731 capture(m_OpsFocusMechanics->focusSettleTime->value());
1732 return true;
1733 }
1734 }
1735
1736 if (m_Focuser == nullptr)
1737 {
1738 appendLogText(i18n("Error: No Focuser detected."));
1739 checkStopFocus(true);
1740 return false;
1741 }
1742
1743 if (m_Focuser->isConnected() == false)
1744 {
1745 appendLogText(i18n("Error: Lost connection to Focuser."));
1746 checkStopFocus(true);
1747 return false;
1748 }
1749
1750 const int newPosition = adjustLinearPosition(currentPosition, currentPosition + amount,
1751 m_OpsFocusMechanics->focusAFOverscan->value(),
1752 updateDir);
1753 if (newPosition == currentPosition)
1754 return true;
1755
1756 const int newAmount = newPosition - currentPosition;
1757 const int absNewAmount = abs(newAmount);
1758 const bool focusingOut = newAmount > 0;
1759 const QString dirStr = focusingOut ? i18n("outward") : i18n("inward");
1760 // update the m_LastFocusDirection unless in Iterative / Polynomial which controls this variable itself.
1761 if (updateDir)
1762 m_LastFocusDirection = (focusingOut) ? FOCUS_OUT : FOCUS_IN;
1763
1764 if (focusingOut)
1765 m_Focuser->focusOut();
1766 else
1767 m_Focuser->focusIn();
1768
1769 // Keep track of motion in case it gets stuck.
1770 m_FocusMotionTimer.start();
1771
1772 if (canAbsMove)
1773 {
1774 m_LastFocusSteps = newPosition;
1775 m_Focuser->moveAbs(newPosition);
1776 appendLogText(i18n("Focusing %2 by %1 steps...", abs(absNewAmount), dirStr));
1777 }
1778 else if (canRelMove)
1779 {
1780 m_LastFocusSteps = absNewAmount;
1781 m_Focuser->moveRel(absNewAmount);
1782 appendLogText(i18np("Focusing %2 by %1 step...", "Focusing %2 by %1 steps...", absNewAmount, dirStr));
1783 }
1784 else
1785 {
1786 m_LastFocusSteps = absNewAmount;
1787 m_Focuser->moveByTimer(absNewAmount);
1788 appendLogText(i18n("Focusing %2 by %1 ms...", absNewAmount, dirStr));
1789 }
1790
1791 return true;
1792}
1793
1794void Focus::handleFocusMotionTimeout()
1795{
1796 // handleFocusMotionTimeout is called when the focus motion timer times out. This is only
1797 // relevant to AutoFocus runs which could be unattended so make an attempt to recover. Other
1798 // types of focuser movement issues are logged and the user is expected to take action.
1799 // If set correctly, say 30 secs, this should only occur when there are comms issues
1800 // with the focuser.
1801 // Step 1: Just retry the last requested move. If the issue is a transient one-off issue
1802 // this should resolve it. Try this twice.
1803 // Step 2: Step 1 didn't resolve the issue so try to restart the focus driver. In this case
1804 // abort the inflight autofocus run and let it retry from the start. It will try to
1805 // return the focuser to the start (last successful autofocus) position. Try twice.
1806 // Step 3: Step 2 didn't work either because the driver restart wasn't successful or we are
1807 // still getting timeouts. In this case just stop the autoFocus process and return
1808 // control to either the Scheduer or GUI. Note that here we cannot reset the focuser
1809 // to the previous good position so if the focuser miraculously fixes itself the
1810 // next autofocus run won't start from the best place.
1811
1812 if (!inAutoFocus)
1813 {
1814 qCDebug(KSTARS_EKOS_FOCUS) << "handleFocusMotionTimeout() called while not in AutoFocus";
1815 return;
1816 }
1817
1818 m_FocusMotionTimerCounter++;
1819
1820 if (m_FocusMotionTimerCounter > 4)
1821 {
1822 // We've tried everything but still get timeouts so abort...
1823 appendLogText(i18n("Focuser is still timing out. Aborting..."));
1824 stop(Ekos::FOCUS_ABORTED);
1825 return;
1826 }
1827 else if (m_FocusMotionTimerCounter > 2)
1828 {
1829 QString focuser = m_Focuser->getDeviceName();
1830 appendLogText(i18n("Focus motion timed out (%1). Restarting focus driver %2", m_FocusMotionTimerCounter, focuser));
1831 emit focuserTimedout(focuser);
1832
1833 QTimer::singleShot(5000, this, [ &, focuser]()
1834 {
1835 Focus::reconnectFocuser(focuser);
1836 });
1837 return;
1838 }
1839
1840 if (!changeFocus(m_LastFocusSteps - currentPosition))
1841 appendLogText(i18n("Focus motion timed out (%1). Focusing to %2 steps...", m_FocusMotionTimerCounter, m_LastFocusSteps));
1842}
1843
1844void Focus::selectImageMask()
1845{
1846 const bool useFullField = m_OpsFocusSettings->focusUseFullField->isChecked();
1847 ImageMaskType masktype;
1848 if (m_OpsFocusSettings->focusRingMaskRB->isChecked())
1849 masktype = FOCUS_MASK_RING;
1850 else if (m_OpsFocusSettings->focusMosaicMaskRB->isChecked())
1851 masktype = FOCUS_MASK_MOSAIC;
1852 else
1853 masktype = FOCUS_MASK_NONE;
1854
1855 // mask selection only enabled if full field should be used for focusing
1856 m_OpsFocusSettings->focusRingMaskRB->setEnabled(useFullField);
1857 m_OpsFocusSettings->focusMosaicMaskRB->setEnabled(useFullField);
1858 // ring mask
1859 m_OpsFocusSettings->focusFullFieldInnerRadius->setEnabled(useFullField && masktype == FOCUS_MASK_RING);
1860 m_OpsFocusSettings->focusFullFieldOuterRadius->setEnabled(useFullField && masktype == FOCUS_MASK_RING);
1861 // aberration inspector mosaic
1862 m_OpsFocusSettings->focusMosaicTileWidth->setEnabled(useFullField && masktype == FOCUS_MASK_MOSAIC);
1863 m_OpsFocusSettings->focusSpacerLabel->setEnabled(useFullField && masktype == FOCUS_MASK_MOSAIC);
1864 m_OpsFocusSettings->focusMosaicSpace->setEnabled(useFullField && masktype == FOCUS_MASK_MOSAIC);
1865
1866 // create the type specific mask
1867 if (masktype == FOCUS_MASK_RING)
1868 m_FocusView->setImageMask(new ImageRingMask(m_OpsFocusSettings->focusFullFieldInnerRadius->value() / 100.0,
1869 m_OpsFocusSettings->focusFullFieldOuterRadius->value() / 100.0));
1870 else if (masktype == FOCUS_MASK_MOSAIC)
1871 m_FocusView->setImageMask(new ImageMosaicMask(m_OpsFocusSettings->focusMosaicTileWidth->value(),
1872 m_OpsFocusSettings->focusMosaicSpace->value()));
1873 else
1874 m_FocusView->setImageMask(nullptr);
1875
1876 checkMosaicMaskLimits();
1877 m_currentImageMask = masktype;
1878 startAbInsB->setEnabled(canAbInsStart());
1879}
1880
1881void Focus::reconnectFocuser(const QString &focuser)
1882{
1883 m_FocuserReconnectCounter++;
1884
1885 if (m_Focuser && m_Focuser->getDeviceName() == focuser)
1886 {
1887 appendLogText(i18n("Attempting to reconnect focuser: %1", focuser));
1888 refreshOpticalTrain();
1889 completeFocusProcedure(Ekos::FOCUS_ABORTED, Ekos::FOCUS_FAIL_FOCUSER_ERROR);
1890 return;
1891 }
1892
1893 if (m_FocuserReconnectCounter > 12)
1894 {
1895 // We've waited a minute and can't reconnect the focuser so abort...
1896 appendLogText(i18n("Cannot reconnect focuser: %1. Aborting...", focuser));
1897 stop(Ekos::FOCUS_ABORTED);
1898 return;
1899 }
1900
1901 QTimer::singleShot(5000, this, [ &, focuser]()
1902 {
1903 reconnectFocuser(focuser);
1904 });
1905}
1906
1907void Focus::processData(const QSharedPointer<FITSData> &data)
1908{
1909 // Ignore guide head if there is any.
1910 if (data->property("chip").toInt() == ISD::CameraChip::GUIDE_CCD)
1911 return;
1912
1913 if (data)
1914 {
1915 m_FocusView->loadData(data);
1916 m_ImageData = data;
1917 }
1918 else
1919 m_ImageData.reset();
1920
1921 captureTimeout.stop();
1922 captureTimeoutCounter = 0;
1923 m_MissingCameraCounter = 0;
1924
1925 ISD::CameraChip *targetChip = m_Camera->getChip(ISD::CameraChip::PRIMARY_CCD);
1926 disconnect(m_Camera, &ISD::Camera::newImage, this, &Ekos::Focus::processData);
1927 disconnect(m_Camera, &ISD::Camera::error, this, &Ekos::Focus::processCaptureError);
1928
1929 if (m_ImageData && m_OpsFocusSettings->useFocusDarkFrame->isChecked())
1930 {
1931 QVariantMap settings = frameSettings[targetChip];
1932 uint16_t offsetX = settings["x"].toInt() / settings["binx"].toInt();
1933 uint16_t offsetY = settings["y"].toInt() / settings["biny"].toInt();
1934
1935 m_DarkProcessor->denoise(OpticalTrainManager::Instance()->id(opticalTrainCombo->currentText()),
1936 targetChip, m_ImageData, focusExposure->value(), offsetX, offsetY);
1937 return;
1938 }
1939
1940 setCaptureComplete();
1941 resetButtons();
1942}
1943
1944void Focus::starDetectionFinished()
1945{
1946 m_starDetectInProgress = false;
1947 if (m_abortInProgress)
1948 {
1949 // We are trying to abort Autofocus so do no further processing
1950 qCDebug(KSTARS_EKOS_FOCUS) << "Abort AF: discarding results from " << __FUNCTION__;
1951 return;
1952 }
1953
1954 appendLogText(i18n("Detection complete."));
1955
1956 // Beware as this HFR value is then treated specifically by the graph renderer
1957 double hfr = INVALID_STAR_MEASURE;
1958
1959 if (m_StarFinderWatcher.result() == false)
1960 {
1961 qCWarning(KSTARS_EKOS_FOCUS) << "Failed to extract any stars.";
1962 }
1963 else
1964 {
1965 if (m_OpsFocusSettings->focusUseFullField->isChecked())
1966 {
1967 m_FocusView->filterStars();
1968
1969 // Get the average HFR of the whole frame
1970 hfr = m_ImageData->getHFR(m_StarMeasure == FOCUS_STAR_HFR_ADJ ? HFR_ADJ_AVERAGE : HFR_AVERAGE);
1971 }
1972 else
1973 {
1974 m_FocusView->setTrackingBoxEnabled(true);
1975
1976 // JM 2020-10-08: Try to get first the same HFR star already selected before
1977 // so that it doesn't keep jumping around
1978
1979 if (starCenter.isNull() == false)
1980 hfr = m_ImageData->getHFR(starCenter.x(), starCenter.y());
1981
1982 // If not found, then get the MAX or MEDIAN depending on the selected algorithm.
1983 if (hfr < 0)
1984 hfr = m_ImageData->getHFR(m_FocusDetection == ALGORITHM_SEP ? HFR_HIGH : HFR_MAX);
1985 }
1986 }
1987
1988 currentHFR = hfr;
1989 currentNumStars = m_ImageData->getDetectedStars();
1990
1991 // Setup with measure we are using (HFR, FWHM, etc)
1992 if (!inAutoFocus)
1993 currentMeasure = currentHFR;
1994 else
1995 {
1996 if (m_StarMeasure == FOCUS_STAR_NUM_STARS)
1997 {
1998 currentMeasure = currentNumStars;
1999 currentWeight = 1.0;
2000 }
2001 else if (m_StarMeasure == FOCUS_STAR_FWHM)
2002 {
2003 getFWHM(m_ImageData->getStarCenters(), &currentFWHM, &currentWeight);
2004 currentMeasure = currentFWHM;
2005 }
2006 else if (m_StarMeasure == FOCUS_STAR_FOURIER_POWER)
2007 {
2008 getFourierPower(&currentFourierPower, &currentWeight);
2009 currentMeasure = currentFourierPower;
2010 }
2011 else
2012 {
2013 currentMeasure = currentHFR;
2014 QList<Edge*> stars = m_ImageData->getStarCenters();
2015 std::vector<double> hfrs(stars.size());
2016 std::transform(stars.constBegin(), stars.constEnd(), hfrs.begin(), [](Edge * edge)
2017 {
2018 return edge->HFR;
2019 });
2020 currentWeight = calculateStarWeight(m_OpsFocusProcess->focusUseWeights->isChecked(), hfrs);
2021 }
2022 }
2023 setCurrentMeasure();
2024}
2025
2026// The image has been processed for star centroids and HFRs so now process it for star FWHMs
2027void Focus::getFWHM(const QList<Edge *> &stars, double *FWHM, double *weight)
2028{
2029 *FWHM = INVALID_STAR_MEASURE;
2030 *weight = 0.0;
2031
2032 auto imageBuffer = m_ImageData->getImageBuffer();
2033
2034 switch (m_ImageData->getStatistics().dataType)
2035 {
2036 case TBYTE:
2037 focusFWHM->processFWHM(reinterpret_cast<uint8_t const *>(imageBuffer), stars, m_ImageData, starFitting, FWHM, weight);
2038 break;
2039
2040 case TSHORT: // Don't think short is used as its recorded as unsigned short
2041 focusFWHM->processFWHM(reinterpret_cast<short const *>(imageBuffer), stars, m_ImageData, starFitting, FWHM, weight);
2042 break;
2043
2044 case TUSHORT:
2045 focusFWHM->processFWHM(reinterpret_cast<unsigned short const *>(imageBuffer), stars, m_ImageData, starFitting, FWHM,
2046 weight);
2047 break;
2048
2049 case TLONG: // Don't think long is used as its recorded as unsigned long
2050 focusFWHM->processFWHM(reinterpret_cast<long const *>(imageBuffer), stars, m_ImageData, starFitting, FWHM, weight);
2051 break;
2052
2053 case TULONG:
2054 focusFWHM->processFWHM(reinterpret_cast<unsigned long const *>(imageBuffer), stars, m_ImageData, starFitting, FWHM, weight);
2055 break;
2056
2057 case TFLOAT:
2058 focusFWHM->processFWHM(reinterpret_cast<float const *>(imageBuffer), stars, m_ImageData, starFitting, FWHM, weight);
2059 break;
2060
2061 case TLONGLONG:
2062 focusFWHM->processFWHM(reinterpret_cast<long long const *>(imageBuffer), stars, m_ImageData, starFitting, FWHM, weight);
2063 break;
2064
2065 case TDOUBLE:
2066 focusFWHM->processFWHM(reinterpret_cast<double const *>(imageBuffer), stars, m_ImageData, starFitting, FWHM, weight);
2067 break;
2068
2069 default:
2070 qCDebug(KSTARS_EKOS_FOCUS) << "Unknown image buffer datatype " << m_ImageData->getStatistics().dataType <<
2071 " Cannot calc FWHM";
2072 break;
2073 }
2074}
2075
2076// The image has been processed for star centroids and HFRs so now process it for star FWHMs
2077void Focus::getFourierPower(double *fourierPower, double *weight, const int mosaicTile)
2078{
2079 *fourierPower = INVALID_STAR_MEASURE;
2080 *weight = 1.0;
2081
2082 auto imageBuffer = m_ImageData->getImageBuffer();
2083
2084 switch (m_ImageData->getStatistics().dataType)
2085 {
2086 case TBYTE:
2087 focusFourierPower->processFourierPower(reinterpret_cast<uint8_t const *>(imageBuffer), m_ImageData,
2088 m_FocusView->imageMask(), mosaicTile, fourierPower, weight);
2089 break;
2090
2091 case TSHORT: // Don't think short is used as its recorded as unsigned short
2092 focusFourierPower->processFourierPower(reinterpret_cast<short const *>(imageBuffer), m_ImageData,
2093 m_FocusView->imageMask(), mosaicTile, fourierPower, weight);
2094 break;
2095
2096 case TUSHORT:
2097 focusFourierPower->processFourierPower(reinterpret_cast<unsigned short const *>(imageBuffer), m_ImageData,
2098 m_FocusView->imageMask(), mosaicTile, fourierPower, weight);
2099 break;
2100
2101 case TLONG: // Don't think long is used as its recorded as unsigned long
2102 focusFourierPower->processFourierPower(reinterpret_cast<long const *>(imageBuffer), m_ImageData,
2103 m_FocusView->imageMask(), mosaicTile, fourierPower, weight);
2104 break;
2105
2106 case TULONG:
2107 focusFourierPower->processFourierPower(reinterpret_cast<unsigned long const *>(imageBuffer), m_ImageData,
2108 m_FocusView->imageMask(), mosaicTile, fourierPower, weight);
2109 break;
2110
2111 case TFLOAT:
2112 focusFourierPower->processFourierPower(reinterpret_cast<float const *>(imageBuffer), m_ImageData,
2113 m_FocusView->imageMask(), mosaicTile, fourierPower, weight);
2114 break;
2115
2116 case TLONGLONG:
2117 focusFourierPower->processFourierPower(reinterpret_cast<long long const *>(imageBuffer), m_ImageData,
2118 m_FocusView->imageMask(), mosaicTile, fourierPower, weight);
2119 break;
2120
2121 case TDOUBLE:
2122 focusFourierPower->processFourierPower(reinterpret_cast<double const *>(imageBuffer), m_ImageData,
2123 m_FocusView->imageMask(), mosaicTile, fourierPower, weight);
2124 break;
2125
2126 default:
2127 qCDebug(KSTARS_EKOS_FOCUS) << "Unknown image buffer datatype " << m_ImageData->getStatistics().dataType <<
2128 " Cannot calc Fourier Power";
2129 break;
2130 }
2131}
2132
2133double Focus::calculateStarWeight(const bool useWeights, const std::vector<double> values)
2134{
2135 if (!useWeights || values.size() <= 0)
2136 // If we can't calculate weight set to 1 = equal weights
2137 // Also if we are using numStars as the measure - don't use weights
2138 return 1.0f;
2139
2140 return Mathematics::RobustStatistics::ComputeWeight(m_ScaleCalc, values);
2141}
2142
2143void Focus::analyzeSources()
2144{
2145 appendLogText(i18n("Detecting sources..."));
2146 m_starDetectInProgress = true;
2147
2148 QVariantMap extractionSettings;
2149 extractionSettings["optionsProfileIndex"] = m_OpsFocusProcess->focusSEPProfile->currentIndex();
2150 extractionSettings["optionsProfileGroup"] = static_cast<int>(Ekos::FocusProfiles);
2151 m_ImageData->setSourceExtractorSettings(extractionSettings);
2152 // When we're using FULL field view, we always use either CENTROID algorithm which is the default
2153 // standard algorithm in KStars, or SEP. The other algorithms are too inefficient to run on full frames and require
2154 // a bounding box for them to be effective in near real-time application.
2155 if (m_OpsFocusSettings->focusUseFullField->isChecked())
2156 {
2157 m_FocusView->setTrackingBoxEnabled(false);
2158
2159 if (m_FocusDetection != ALGORITHM_CENTROID && m_FocusDetection != ALGORITHM_SEP)
2160 m_StarFinderWatcher.setFuture(m_ImageData->findStars(ALGORITHM_CENTROID));
2161 else
2162 m_StarFinderWatcher.setFuture(m_ImageData->findStars(m_FocusDetection));
2163 }
2164 else
2165 {
2166 QRect searchBox = m_FocusView->isTrackingBoxEnabled() ? m_FocusView->getTrackingBox() : QRect();
2167 // If star is already selected then use whatever algorithm currently selected.
2168 if (starSelected)
2169 {
2170 m_StarFinderWatcher.setFuture(m_ImageData->findStars(m_FocusDetection, searchBox));
2171 }
2172 else
2173 {
2174 // Disable tracking box
2175 m_FocusView->setTrackingBoxEnabled(false);
2176
2177 // If algorithm is set something other than Centeroid or SEP, then force Centroid
2178 // Since it is the most reliable detector when nothing was selected before.
2179 if (m_FocusDetection != ALGORITHM_CENTROID && m_FocusDetection != ALGORITHM_SEP)
2180 m_StarFinderWatcher.setFuture(m_ImageData->findStars(ALGORITHM_CENTROID));
2181 else
2182 // Otherwise, continue to find use using the selected algorithm
2183 m_StarFinderWatcher.setFuture(m_ImageData->findStars(m_FocusDetection, searchBox));
2184 }
2185 }
2186}
2187
2188bool Focus::appendMeasure(double newMeasure)
2189{
2190 // Add new star measure (e.g. HFR, FWHM, etc) to existing values, even if invalid
2191 starMeasureFrames.append(newMeasure);
2192
2193 // Prepare a work vector with valid HFR values
2194 QVector <double> samples(starMeasureFrames);
2195 samples.erase(std::remove_if(samples.begin(), samples.end(), [](const double measure)
2196 {
2197 return measure == INVALID_STAR_MEASURE;
2198 }), samples.end());
2199
2200 // Consolidate the average star measure. Sigma clips outliers and averages remainder.
2201 if (!samples.isEmpty())
2202 {
2203 currentMeasure = Mathematics::RobustStatistics::ComputeLocation(
2204 Mathematics::RobustStatistics::LOCATION_SIGMACLIPPING, std::vector<double>(samples.begin(), samples.end()));
2205
2206 switch(m_StarMeasure)
2207 {
2208 case FOCUS_STAR_HFR:
2209 case FOCUS_STAR_HFR_ADJ:
2210 currentHFR = currentMeasure;
2211 break;
2212 case FOCUS_STAR_FWHM:
2213 currentFWHM = currentMeasure;
2214 break;
2215 case FOCUS_STAR_NUM_STARS:
2216 currentNumStars = currentMeasure;
2217 break;
2218 case FOCUS_STAR_FOURIER_POWER:
2219 currentFourierPower = currentMeasure;
2220 break;
2221 default:
2222 break;
2223 }
2224 }
2225
2226 // Save the focus frame
2227 saveFocusFrame();
2228
2229 // Return whether we need more frame based on user requirement
2230 int framesCount = m_OpsFocusProcess->focusFramesCount->value();
2231 if ((minimumRequiredHFR > 0) || (inAutoFocus && !inBuildOffsets && !inScanStartPos && m_FocusAlgorithm == FOCUS_LINEAR1PASS
2232 && linearFocuser && !linearFocuser->isInFirstPass()))
2233 // If in-sequence HFR Check or L1P autofocus and doing the last (in focus) datapoint use focusHFRFramesCount
2234 framesCount = m_OpsFocusProcess->focusHFRFramesCount->value();
2235
2236 return starMeasureFrames.count() < framesCount;
2237}
2238
2239void Focus::settle(const FocusState completionState, const bool autoFocusUsed, const bool buildOffsetsUsed,
2240 const AutofocusFailReason failCode, const QString failCodeInfo)
2241{
2242 // TODO: check if the completion state can be emitted in all cases (sterne-jaeger 2023-09-12)
2243 m_state = completionState;
2244 if (autoFocusUsed)
2245 {
2246 if (completionState == Ekos::FOCUS_COMPLETE)
2247 {
2248 KSNotification::event(QLatin1String("FocusSuccessful"), i18n("Autofocus operation completed successfully"),
2249 KSNotification::Focus);
2250 // Pass consistent Autofocus temperature to analyze
2251 if (m_FocusAlgorithm == FOCUS_LINEAR1PASS && curveFitting != nullptr)
2252 emit autofocusComplete(m_LastSourceAutofocusTemperature, filter(), getAnalyzeData(),
2253 m_OpsFocusProcess->focusUseWeights->isChecked(),
2254 curveFitting->serialize(), linearFocuser->getTextStatus(R2));
2255 else
2256 emit autofocusComplete(m_LastSourceAutofocusTemperature, filter(), getAnalyzeData(),
2257 m_OpsFocusProcess->focusUseWeights->isChecked());
2258 }
2259 else
2260 {
2261 KSNotification::event(QLatin1String("FocusFailed"), i18n("Autofocus operation failed"),
2262 KSNotification::Focus, KSNotification::Alert);
2263 emit autofocusAborted(filter(), getAnalyzeData(), m_OpsFocusProcess->focusUseWeights->isChecked(), failCode, failCodeInfo);
2264 }
2265 }
2266
2267 if (completionState == Ekos::FOCUS_COMPLETE)
2268 {
2269 if (autoFocusUsed && fallbackFilterPending)
2270 {
2271 // Save the solution details for the filter used for the AF run before changing
2272 // filers to the fallback filter. Details for the fallback filter will be saved once that
2273 // filter has been processed and the offset applied
2274 m_FilterManager->setFilterAbsoluteFocusDetails(focusFilter->currentIndex(), currentPosition,
2275 m_LastSourceAutofocusTemperature, m_LastSourceAutofocusAlt);
2276 }
2277 }
2278
2279 qCDebug(KSTARS_EKOS_FOCUS) << "Settled. State:" << Ekos::getFocusStatusString(state());
2280
2281 // Delay state notification if we have a locked filter pending return to original filter
2282 if (fallbackFilterPending)
2283 {
2284 FilterManager::FilterPolicy policy = (autoFocusUsed) ?
2285 static_cast<FilterManager::FilterPolicy>(FilterManager::CHANGE_POLICY) :
2286 static_cast<FilterManager::FilterPolicy>(FilterManager::CHANGE_POLICY | FilterManager::OFFSET_POLICY);
2287 m_FilterManager->setFilterPosition(fallbackFilterPosition, policy);
2288 }
2289 else
2290 emit newStatus(state());
2291
2293 // If we are building filter offsets signal AF run is complete
2294 m_FilterManager->autoFocusComplete(state(), currentPosition, m_LastSourceAutofocusTemperature, m_LastSourceAutofocusAlt);
2295
2296 resetButtons();
2297}
2298
2299QString Focus::getAnalyzeData()
2300{
2302
2303 for (int i = 0; i < plot_position.size(); ++i)
2304 {
2305 analysis_results.append(QString("%1%2|%3|%4|%5")
2306 .arg(i == 0 ? "" : "|" )
2307 .arg(QString::number(plot_position[i], 'f', 0))
2308 .arg(QString::number(plot_value[i], 'f', 3))
2309 .arg(QString::number(plot_weight[i], 'f', 3))
2310 .arg(QString::number(plot_outlier[i])));
2311 }
2312 return analysis_results;
2313}
2314
2315void Focus::completeFocusProcedure(FocusState completionState, AutofocusFailReason failCode, QString failCodeInfo,
2316 bool plot)
2317{
2318 if (inAutoFocus)
2319 {
2320 // Update the plot vectors (used by Analyze)
2321 updatePlotPosition();
2322
2323 if (completionState == Ekos::FOCUS_COMPLETE)
2324 {
2325 if (plot)
2326 emit redrawHFRPlot(polynomialFit.get(), currentPosition, currentHFR);
2327
2328 appendLogText(i18np("Focus procedure completed after %1 iteration.",
2329 "Focus procedure completed after %1 iterations.", plot_position.count()));
2330
2331 setLastFocusTemperature();
2332 setLastFocusAlt();
2333 resetAdaptiveFocus(m_OpsFocusSettings->focusAdaptive->isChecked());
2334
2335 // CR add auto focus position, temperature and filter to log in CSV format
2336 // this will help with setting up focus offsets and temperature compensation
2337 qCInfo(KSTARS_EKOS_FOCUS) << "Autofocus values: position," << currentPosition << ", temperature,"
2338 << m_LastSourceAutofocusTemperature << ", filter," << filter()
2339 << ", HFR," << currentHFR << ", altitude," << m_LastSourceAutofocusAlt;
2340
2341 if (m_FocusAlgorithm == FOCUS_POLYNOMIAL)
2342 {
2343 // Add the final polynomial values to the signal sent to Analyze.
2344 plot_position.append(currentPosition);
2345 plot_value.append(currentHFR);
2346 plot_weight.append(1.0);
2347 plot_outlier.append(false);
2348 }
2349
2350 appendFocusLogText(QString("%1, %2, %3, %4, %5\n")
2351 .arg(QString::number(currentPosition))
2352 .arg(QString::number(m_LastSourceAutofocusTemperature, 'f', 1))
2353 .arg(filter())
2354 .arg(QString::number(currentHFR, 'f', 3))
2355 .arg(QString::number(m_LastSourceAutofocusAlt, 'f', 1)));
2356
2357 // Replace user position with optimal position
2358 absTicksSpin->setValue(currentPosition);
2359 }
2360 // In case of failure, go back to last position if the focuser is absolute
2361 else if (canAbsMove && initialFocuserAbsPosition >= 0 && resetFocusIteration <= MAXIMUM_RESET_ITERATIONS
2362 && m_RestartState != RESTART_ABORT)
2363 {
2364 // If we're doing in-sequence focusing using an absolute focuser, retry focusing once, starting from last known good position
2365 bool const retry_focusing = m_RestartState == RESTART_NONE && ++resetFocusIteration < MAXIMUM_RESET_ITERATIONS;
2366
2367 // If retrying, before moving, reset focus frame in case the star in subframe was lost
2368 if (retry_focusing)
2369 {
2370 m_RestartState = RESTART_NOW;
2371 resetFrame();
2372 }
2373
2374 resetFocuser();
2375
2376 // Bypass the rest of the function if we retry - we will fail if we could not move the focuser
2377 if (retry_focusing)
2378 {
2379 emit autofocusAborted(filter(), getAnalyzeData(), m_OpsFocusProcess->focusUseWeights->isChecked(), failCode, failCodeInfo);
2380 return;
2381 }
2382 else
2383 {
2384 // We're in Autofocus and we've hit our max retry limit, so...
2385 // resetFocuser will have initiated a focuser reset back to its starting position
2386 // so we need to wait for that move to complete before returning control.
2387 // This is important if the scheduler is running autofocus as it will likely
2388 // immediately retry. The startup process will take the current focuser position
2389 // as the start position and so the focuser needs to have arrived at its starting
2390 // position before this. So set m_RestartState to log this.
2391 m_RestartState = RESTART_ABORT;
2392 return;
2393 }
2394 }
2395
2396 // Reset the retry count on success or maximum count
2397 resetFocusIteration = 0;
2398 }
2399
2400 const bool autoFocusUsed = inAutoFocus;
2401 const bool inBuildOffsetsUsed = inBuildOffsets;
2402
2403 // Refresh display if needed
2404 if (m_FocusAlgorithm == FOCUS_POLYNOMIAL && plot)
2405 emit drawPolynomial(polynomialFit.get(), isVShapeSolution, true);
2406
2407 // Enforce settling duration. Note stop resets m_GuidingSuspended
2408 int const settleTime = m_GuidingSuspended ? m_OpsFocusSettings->focusGuideSettleTime->value() : 0;
2409
2410 // Reset the autofocus flags
2412
2413 if (settleTime > 0)
2414 appendLogText(i18n("Settling for %1s...", settleTime));
2415
2417 failCodeInfo]()
2418 {
2419 settle(completionState, autoFocusUsed, inBuildOffsetsUsed, failCode, failCodeInfo);
2420
2421 if (settleTime > 0)
2422 appendLogText(i18n("Settling complete."));
2423 });
2424}
2425
2426void Focus::resetFocuser()
2427{
2428 // If we are able to and need to, move the focuser back to the initial position and let the procedure restart from its termination
2429 if (m_Focuser && m_Focuser->isConnected() && initialFocuserAbsPosition >= 0)
2430 {
2431 // HACK: If the focuser will not move, cheat a little to get the notification - see processNumber
2432 if (currentPosition == initialFocuserAbsPosition)
2433 currentPosition--;
2434
2435 appendLogText(i18n("Autofocus failed, moving back to initial focus position %1.", initialFocuserAbsPosition));
2436 changeFocus(initialFocuserAbsPosition - currentPosition);
2437 /* Restart will be executed by the end-of-move notification from the device if needed by resetFocus */
2438 }
2439}
2440
2441void Focus::setCurrentMeasure()
2442{
2443 // Let's now report the current HFR
2444 qCDebug(KSTARS_EKOS_FOCUS) << "Focus newFITS #" << starMeasureFrames.count() + 1 << ": Current HFR " << currentHFR <<
2445 " Num stars "
2446 << (starSelected ? 1 : currentNumStars);
2447
2448 // Take the new HFR into account, eventually continue to stack samples
2449 if (appendMeasure(currentMeasure))
2450 {
2451 capture();
2452 return;
2453 }
2454 else starMeasureFrames.clear();
2455
2456 // Let signal the current HFR now depending on whether the focuser is absolute or relative
2457 // Outside of Focus we continue to rely on HFR and independent of which measure the user selected we always calculate HFR
2458 if (canAbsMove)
2459 emit newHFR(currentHFR, currentPosition, inAutoFocus);
2460 else
2461 emit newHFR(currentHFR, -1, inAutoFocus);
2462
2463 // Format the labels under the V-curve
2464 HFROut->setText(QString("%1").arg(currentHFR * getStarUnits(m_StarMeasure, m_StarUnits), 0, 'f', 2));
2465 if (m_StarMeasure == FOCUS_STAR_FWHM)
2466 FWHMOut->setText(QString("%1").arg(currentFWHM * getStarUnits(m_StarMeasure, m_StarUnits), 0, 'f', 2));
2467 starsOut->setText(QString("%1").arg(m_ImageData->getDetectedStars()));
2468 iterOut->setText(QString("%1").arg(absIterations + 1));
2469
2470 // Display message in case _last_ HFR was invalid
2471 if (lastHFR == INVALID_STAR_MEASURE)
2472 appendLogText(i18n("FITS received. No stars detected."));
2473
2474 // If we have a valid HFR value
2475 if (currentHFR > 0)
2476 {
2477 // Check if we're done from polynomial fitting algorithm
2478 if (m_FocusAlgorithm == FOCUS_POLYNOMIAL && isVShapeSolution)
2479 {
2480 completeFocusProcedure(Ekos::FOCUS_COMPLETE, Ekos::FOCUS_FAIL_NONE);
2481 return;
2482 }
2483
2484 Edge selectedHFRStarHFR = m_ImageData->getSelectedHFRStar();
2485
2486 // Center tracking box around selected star (if it valid) either in:
2487 // 1. Autofocus
2488 // 2. CheckFocus (minimumHFRCheck)
2489 // The starCenter _must_ already be defined, otherwise, we proceed until
2490 // the latter half of the function searches for a star and define it.
2491 if (starCenter.isNull() == false && (inAutoFocus || minimumRequiredHFR >= 0))
2492 {
2493 // Now we have star selected in the frame
2494 starSelected = true;
2495 starCenter.setX(qMax(0, static_cast<int>(selectedHFRStarHFR.x)));
2496 starCenter.setY(qMax(0, static_cast<int>(selectedHFRStarHFR.y)));
2497
2498 syncTrackingBoxPosition();
2499
2500 // Record the star information (X, Y, currentHFR)
2501 QVector3D oneStar = starCenter;
2502 oneStar.setZ(currentHFR);
2503 starsHFR.append(oneStar);
2504 }
2505 else
2506 {
2507 // Record the star information (X, Y, currentHFR)
2508 QVector3D oneStar(starCenter.x(), starCenter.y(), currentHFR);
2509 starsHFR.append(oneStar);
2510 }
2511
2512 if (currentHFR > maxHFR)
2513 maxHFR = currentHFR;
2514
2515 // Append point to the #Iterations vs #HFR chart in case of looping or in case in autofocus with a focus
2516 // that does not support position feedback.
2517
2518 // If inAutoFocus is true without canAbsMove and without canRelMove, canTimerMove must be true.
2519 // We'd only want to execute this if the focus linear algorithm is not being used, as that
2520 // algorithm simulates a position-based system even for timer-based focusers.
2521 if (inFocusLoop || (inAutoFocus && ! isPositionBased()))
2522 {
2523 int pos = plot_position.empty() ? 1 : plot_position.last() + 1;
2524 addPlotPosition(pos, currentHFR);
2525 }
2526 }
2527 else
2528 {
2529 // Let's record an invalid star result
2530 QVector3D oneStar(starCenter.x(), starCenter.y(), INVALID_STAR_MEASURE);
2531 starsHFR.append(oneStar);
2532 }
2533
2534 // First check that we haven't already search for stars
2535 // Since star-searching algorithm are time-consuming, we should only search when necessary
2536 m_FocusView->updateFrame();
2537
2538 setHFRComplete();
2539
2540 if (m_abInsOn && !inScanStartPos)
2541 calculateAbInsData();
2542}
2543
2544// Save off focus frame during Autofocus for later debugging
2545void Focus::saveFocusFrame()
2546{
2547 if (inAutoFocus && Options::focusLogging() && Options::saveFocusImages())
2548 {
2549 QDir dir;
2550 QDateTime now = KStarsData::Instance()->lt();
2551 QString path = QDir(KSPaths::writableLocation(QStandardPaths::AppLocalDataLocation)).filePath("autofocus/" +
2552 now.toString("yyyy-MM-dd"));
2553 dir.mkpath(path);
2554
2555 // To help identify focus frames add run number, step and frame (for multiple frames at each step)
2556 QString detail;
2557 if (m_FocusAlgorithm == FOCUS_LINEAR1PASS && !inScanStartPos && linearFocuser)
2558 {
2559 const int currentStep = linearFocuser->currentStep() + 1;
2560 detail = QString("_%1_%2_%3").arg(m_AFRun).arg(currentStep).arg(starMeasureFrames.count());
2561 }
2562
2563 // IS8601 contains colons but they are illegal under Windows OS, so replacing them with '-'
2564 // The timestamp is no longer ISO8601 but it should solve interoperality issues between different OS hosts
2565 QString name = "autofocus_frame_" + now.toString("HH-mm-ss") + detail + ".fits";
2566 QString filename = path + QStringLiteral("/") + name;
2567 m_ImageData->saveImage(filename);
2568 }
2569}
2570
2571void Focus::calculateAbInsData()
2572{
2573 ImageMosaicMask *mosaicmask = dynamic_cast<ImageMosaicMask *>(m_FocusView->imageMask().get());
2574 const QVector<QRect> tiles = mosaicmask->tiles();
2575 auto stars = m_ImageData->getStarCenters();
2576 QVector<QList<Edge *>> tileStars(NUM_TILES);
2577
2578 for (int star = 0; star < stars.count(); star++)
2579 {
2580 const int x = stars[star]->x;
2581 const int y = stars[star]->y;
2582 for (int tile = 0; tile < NUM_TILES; tile++)
2583 {
2584 QRect thisTile = tiles[tile];
2585 if (thisTile.contains(x, y))
2586 {
2587 tileStars[tile].append(stars[star]);
2588 break;
2589 }
2590 }
2591 }
2592
2593 // Get the measure for each tile
2594 for (int tile = 0; tile < tileStars.count(); tile++)
2595 {
2596 double measure, weight;
2597
2598 if (m_StarMeasure == FOCUS_STAR_NUM_STARS)
2599 {
2600 measure = tileStars[tile].count();
2601 weight = 1.0;
2602 }
2603 else if (m_StarMeasure == FOCUS_STAR_FWHM)
2604 {
2605 getFWHM(tileStars[tile], &measure, &weight);
2606 }
2607 else if (m_StarMeasure == FOCUS_STAR_FOURIER_POWER)
2608 {
2609 getFourierPower(&measure, &weight, tile);
2610 }
2611 else
2612 {
2613 // HFR or HFR_adj
2614 std::vector<double> HFRs;
2615
2616 for (int star = 0; star < tileStars[tile].count(); star++)
2617 {
2618 HFRs.push_back(tileStars[tile][star]->HFR);
2619 }
2620 measure = Mathematics::RobustStatistics::ComputeLocation(Mathematics::RobustStatistics::LOCATION_SIGMACLIPPING, HFRs, 2);
2621 weight = calculateStarWeight(m_OpsFocusProcess->focusUseWeights->isChecked(), HFRs);
2622 }
2623
2624 m_abInsMeasure[tile].append(measure);
2625 m_abInsWeight[tile].append(weight);
2626 m_abInsNumStars[tile].append(tileStars[tile].count());
2627
2628 if (!linearFocuser->isInFirstPass())
2629 {
2630 // This is the last datapoint so calculate average star position in the tile from the tile center
2631 // FOCUS_STAR_FOURIER_POWER doesn't use stars directly so no need to calculate offset. The other
2632 // measures all use stars: FOCUS_STAR_NUM_STARS, FOCUS_STAR_FWHM, FOCUS_STAR_HFR, FOCUS_STAR_HFR_ADJ
2633 int xAv = 0, yAv = 0;
2634 if (m_StarMeasure != FOCUS_STAR_FOURIER_POWER)
2635 {
2636 QPoint tileCenter = tiles[tile].center();
2637 int xSum = 0.0, ySum = 0.0;
2638 for (int star = 0; star < tileStars[tile].count(); star++)
2639 {
2640 xSum += tileStars[tile][star]->x - tileCenter.x();
2641 ySum += tileStars[tile][star]->y - tileCenter.y();
2642 }
2643
2644 xAv = (tileStars[tile].count() <= 0) ? 0 : xSum / tileStars[tile].count();
2645 yAv = (tileStars[tile].count() <= 0) ? 0 : ySum / tileStars[tile].count();
2646 }
2647 m_abInsTileCenterOffset.append(QPoint(xAv, yAv));
2648 }
2649 }
2650 m_abInsPosition.append(currentPosition);
2651}
2652
2653void Focus::setCaptureComplete()
2654{
2655 DarkLibrary::Instance()->disconnect(this);
2656
2657 // If we have a box, sync the bounding box to its position.
2658 syncTrackingBoxPosition();
2659
2660 // Notify user if we're not looping
2661 if (inFocusLoop == false)
2662 appendLogText(i18n("Image received."));
2663
2664 if (m_captureInProgress && inFocusLoop == false && inAutoFocus == false)
2665 m_Camera->setUploadMode(rememberUploadMode);
2666
2667 if (m_RememberCameraFastExposure && inFocusLoop == false && inAutoFocus == false)
2668 {
2669 m_RememberCameraFastExposure = false;
2670 m_Camera->setFastExposureEnabled(true);
2671 }
2672
2673 m_captureInProgress = false;
2674 if (m_abortInProgress)
2675 {
2676 // We are trying to abort autofocus so do no further processing.
2677 // Note this will cover both the original capture and any dark frame processing
2678 qCDebug(KSTARS_EKOS_FOCUS) << "Abort AF: discarding frame results in " << __FUNCTION__;
2679 return;
2680 }
2681
2682 // update the limits from the real values
2683 checkMosaicMaskLimits();
2684
2685 // Emit the whole image
2686 emit newImage(m_FocusView);
2687 // Emit the tracking (bounding) box view. Used in Summary View
2688 emit newStarPixmap(m_FocusView->getTrackingBoxPixmap(10));
2689
2690 // If we are not looping; OR
2691 // If we are looping but we already have tracking box enabled; OR
2692 // If we are asked to analyze _all_ the stars within the field
2693 // THEN let's find stars in the image and get current HFR
2694 if (inFocusLoop == false || (inFocusLoop && (m_FocusView->isTrackingBoxEnabled()
2695 || m_OpsFocusSettings->focusUseFullField->isChecked())))
2696 analyzeSources();
2697 else
2698 setHFRComplete();
2699}
2700
2701void Focus::setHFRComplete()
2702{
2703 // If we are just framing, let's capture again
2704 if (inFocusLoop)
2705 {
2706 capture();
2707 return;
2708 }
2709
2710 // Get target chip
2711 ISD::CameraChip *targetChip = m_Camera->getChip(ISD::CameraChip::PRIMARY_CCD);
2712
2713 // Get target chip binning
2714 int subBinX = 1, subBinY = 1;
2715 if (!targetChip->getBinning(&subBinX, &subBinY))
2716 qCDebug(KSTARS_EKOS_FOCUS) << "Warning: target chip is reporting no binning property, using 1x1.";
2717
2718 // If star is NOT yet selected in a non-full-frame situation
2719 // then let's now try to find the star. This step is skipped for full frames
2720 // since there isn't a single star to select as we are only interested in the overall average HFR.
2721 // We need to check if we can find the star right away, or if we need to _subframe_ around the
2722 // selected star.
2723 if (m_OpsFocusSettings->focusUseFullField->isChecked() == false && starCenter.isNull())
2724 {
2725 int x = 0, y = 0, w = 0, h = 0;
2726
2727 // Let's get the stored frame settings for this particular chip
2728 if (frameSettings.contains(targetChip))
2729 {
2730 QVariantMap settings = frameSettings[targetChip];
2731 x = settings["x"].toInt();
2732 y = settings["y"].toInt();
2733 w = settings["w"].toInt();
2734 h = settings["h"].toInt();
2735 }
2736 else
2737 // Otherwise let's get the target chip frame coordinates.
2738 targetChip->getFrame(&x, &y, &w, &h);
2739
2740 // In case auto star is selected.
2741 if (m_OpsFocusSettings->focusAutoStarEnabled->isChecked())
2742 {
2743 // Do we have a valid star detected?
2744 const Edge selectedHFRStar = m_ImageData->getSelectedHFRStar();
2745
2746 if (selectedHFRStar.x == -1)
2747 {
2748 appendLogText(i18n("Failed to automatically select a star. Please select a star manually."));
2749
2750 // Center the tracking box in the frame and display it
2751 m_FocusView->setTrackingBox(QRect(w - m_OpsFocusSettings->focusBoxSize->value() / (subBinX * 2),
2752 h - m_OpsFocusSettings->focusBoxSize->value() / (subBinY * 2),
2753 m_OpsFocusSettings->focusBoxSize->value() / subBinX, m_OpsFocusSettings->focusBoxSize->value() / subBinY));
2754 m_FocusView->setTrackingBoxEnabled(true);
2755
2756 // Use can now move it to select the desired star
2757 setState(Ekos::FOCUS_WAITING);
2758
2759 // Start the wait timer so we abort after a timeout if the user does not make a choice
2760 waitStarSelectTimer.start();
2761
2762 return;
2763 }
2764
2765 // set the tracking box on selectedHFRStar
2766 starCenter.setX(selectedHFRStar.x);
2767 starCenter.setY(selectedHFRStar.y);
2768 starCenter.setZ(subBinX);
2769 starSelected = true;
2770 syncTrackingBoxPosition();
2771
2772 // Do we need to subframe?
2773 if (subFramed == false && isFocusSubFrameEnabled() && m_OpsFocusSettings->focusSubFrame->isChecked())
2774 {
2775 int offset = (static_cast<double>(m_OpsFocusSettings->focusBoxSize->value()) / subBinX) * 1.5;
2776 int subX = (selectedHFRStar.x - offset) * subBinX;
2777 int subY = (selectedHFRStar.y - offset) * subBinY;
2778 int subW = offset * 2 * subBinX;
2779 int subH = offset * 2 * subBinY;
2780
2781 int minX, maxX, minY, maxY, minW, maxW, minH, maxH;
2782 targetChip->getFrameMinMax(&minX, &maxX, &minY, &maxY, &minW, &maxW, &minH, &maxH);
2783
2784 // Try to limit the subframed selection
2785 if (subX < minX)
2786 subX = minX;
2787 if (subY < minY)
2788 subY = minY;
2789 if ((subW + subX) > maxW)
2790 subW = maxW - subX;
2791 if ((subH + subY) > maxH)
2792 subH = maxH - subY;
2793
2794 // Now we store the subframe coordinates in the target chip frame settings so we
2795 // reuse it later when we capture again.
2796 QVariantMap settings = frameSettings[targetChip];
2797 settings["x"] = subX;
2798 settings["y"] = subY;
2799 settings["w"] = subW;
2800 settings["h"] = subH;
2801 settings["binx"] = subBinX;
2802 settings["biny"] = subBinY;
2803
2804 qCDebug(KSTARS_EKOS_FOCUS) << "Frame is subframed. X:" << subX << "Y:" << subY << "W:" << subW << "H:" << subH << "binX:" <<
2805 subBinX << "binY:" << subBinY;
2806
2807 starsHFR.clear();
2808
2809 frameSettings[targetChip] = settings;
2810
2811 // Set the star center in the center of the subframed coordinates
2812 starCenter.setX(subW / (2 * subBinX));
2813 starCenter.setY(subH / (2 * subBinY));
2814 starCenter.setZ(subBinX);
2815
2816 subFramed = true;
2817
2818 m_FocusView->setFirstLoad(true);
2819
2820 // Now let's capture again for the actual requested subframed image.
2821 capture();
2822 return;
2823 }
2824 // If we're subframed or don't need subframe, let's record the max star coordinates
2825 else
2826 {
2827 starCenter.setX(selectedHFRStar.x);
2828 starCenter.setY(selectedHFRStar.y);
2829 starCenter.setZ(subBinX);
2830
2831 // Let's now capture again if we're autofocusing
2832 if (inAutoFocus)
2833 {
2834 capture();
2835 return;
2836 }
2837 }
2838 }
2839 // If manual selection is enabled then let's ask the user to select the focus star
2840 else
2841 {
2842 appendLogText(i18n("Capture complete. Select a star to focus."));
2843
2844 starSelected = false;
2845
2846 // Let's now display and set the tracking box in the center of the frame
2847 // so that the user moves it around to select the desired star.
2848 int subBinX = 1, subBinY = 1;
2849 targetChip->getBinning(&subBinX, &subBinY);
2850
2851 m_FocusView->setTrackingBox(QRect((w - m_OpsFocusSettings->focusBoxSize->value()) / (subBinX * 2),
2852 (h - m_OpsFocusSettings->focusBoxSize->value()) / (2 * subBinY),
2853 m_OpsFocusSettings->focusBoxSize->value() / subBinX, m_OpsFocusSettings->focusBoxSize->value() / subBinY));
2854 m_FocusView->setTrackingBoxEnabled(true);
2855
2856 // Now we wait
2857 setState(Ekos::FOCUS_WAITING);
2858
2859 // If the user does not select for a timeout period, we abort.
2860 waitStarSelectTimer.start();
2861 return;
2862 }
2863 }
2864
2865 // Check if the focus module is requested to verify if the minimum HFR value is met.
2866 if (minimumRequiredHFR >= 0)
2867 {
2868 // In case we failed to detected, we capture again.
2869 if (currentHFR == INVALID_STAR_MEASURE)
2870 {
2871 if (noStarCount++ < MAX_RECAPTURE_RETRIES)
2872 {
2873 appendLogText(i18n("No stars detected while testing HFR, capturing again..."));
2874 // On Last Attempt reset focus frame to capture full frame and recapture star if possible
2875 if (noStarCount == MAX_RECAPTURE_RETRIES)
2876 resetFrame();
2877 capture();
2878 return;
2879 }
2880 // If we exceeded maximum tries we abort
2881 else
2882 {
2883 noStarCount = 0;
2884 completeFocusProcedure(Ekos::FOCUS_ABORTED, Ekos::FOCUS_FAIL_NO_STARS);
2885 }
2886 }
2887 // If the detect current HFR is more than the minimum required HFR
2888 // then we should start the autofocus process now to bring it down.
2889 else if (currentHFR > minimumRequiredHFR)
2890 {
2891 qCDebug(KSTARS_EKOS_FOCUS) << "Current HFR:" << currentHFR << "is above required minimum HFR:" << minimumRequiredHFR <<
2892 ". Starting AutoFocus...";
2893 QString reasonInfo = i18n("HFR %1 > Limit %2", QString::number(currentHFR, 'f', 2), QString::number(minimumRequiredHFR, 'f',
2894 2));
2895 minimumRequiredHFR = INVALID_STAR_MEASURE;
2896 runAutoFocus(AutofocusReason::FOCUS_HFR_CHECK, reasonInfo);
2897 }
2898 // Otherwise, the current HFR is fine and lower than the required minimum HFR so we announce success.
2899 else
2900 {
2901 qCDebug(KSTARS_EKOS_FOCUS) << "Current HFR:" << currentHFR << "is below required minimum HFR:" << minimumRequiredHFR <<
2902 ". Autofocus successful.";
2903 completeFocusProcedure(Ekos::FOCUS_COMPLETE, Ekos::FOCUS_FAIL_NONE);
2904 }
2905
2906 // Nothing more for now
2907 return;
2908 }
2909
2910 // If we are not in autofocus process, we're done.
2911 if (inAutoFocus == false)
2912 {
2913 // If we are done and there is no further autofocus,
2914 // we reset state to IDLE
2915 if (state() != Ekos::FOCUS_IDLE)
2916 setState(Ekos::FOCUS_IDLE);
2917
2918 resetButtons();
2919 return;
2920 }
2921
2922 // Set state to progress
2923 if (state() != Ekos::FOCUS_PROGRESS)
2924 setState(Ekos::FOCUS_PROGRESS);
2925
2926 // Now let's kick in the algorithms
2927 if (inScanStartPos)
2928 scanStartPos();
2929 else if (m_FocusAlgorithm == FOCUS_LINEAR || m_FocusAlgorithm == FOCUS_LINEAR1PASS)
2930 autoFocusLinear();
2931 else if (canAbsMove || canRelMove)
2932 // Position-based algorithms
2933 autoFocusAbs();
2934 else
2935 // Time open-looped algorithms
2936 autoFocusRel();
2937}
2938
2939QString Focus::getyAxisLabel(StarMeasure starMeasure)
2940{
2941 QString str = "HFR";
2942 m_StarUnits == FOCUS_UNITS_ARCSEC ? str += " (\")" : str += " (pix)";
2943
2944 if (inAutoFocus)
2945 {
2946 switch (starMeasure)
2947 {
2948 case FOCUS_STAR_HFR:
2949 break;
2950 case FOCUS_STAR_HFR_ADJ:
2951 str = "HFR Adj";
2952 m_StarUnits == FOCUS_UNITS_ARCSEC ? str += " (\")" : str += " (pix)";
2953 break;
2954 case FOCUS_STAR_FWHM:
2955 str = "FWHM";
2956 m_StarUnits == FOCUS_UNITS_ARCSEC ? str += " (\")" : str += " (pix)";
2957 break;
2958 case FOCUS_STAR_NUM_STARS:
2959 str = "# Stars";
2960 break;
2961 case FOCUS_STAR_FOURIER_POWER:
2962 str = "Fourier Power";
2963 break;
2964 default:
2965 break;
2966 }
2967 }
2968 return str;
2969}
2970
2971void Focus::clearDataPoints()
2972{
2973 maxHFR = 1;
2974 polynomialFit.reset();
2975 plot_position.clear();
2976 plot_value.clear();
2977 plot_weight.clear();
2978 plot_outlier.clear();
2979 isVShapeSolution = false;
2980 m_abInsPosition.clear();
2981 m_abInsTileCenterOffset.clear();
2982 if (m_abInsMeasure.count() != NUM_TILES)
2983 {
2984 m_abInsMeasure.resize(NUM_TILES);
2985 m_abInsWeight.resize(NUM_TILES);
2986 m_abInsNumStars.resize(NUM_TILES);
2987 }
2988 else
2989 {
2990 for (int i = 0; i < m_abInsMeasure.count(); i++)
2991 {
2992 m_abInsMeasure[i].clear();
2993 m_abInsWeight[i].clear();
2994 m_abInsNumStars[i].clear();
2995 }
2996 }
2997
2998 emit initHFRPlot(getyAxisLabel(m_StarMeasure), getStarUnits(m_StarMeasure, m_StarUnits),
2999 m_OptDir == CurveFitting::OPTIMISATION_MINIMISE, m_OpsFocusProcess->focusUseWeights->isChecked(),
3000 inFocusLoop == false && isPositionBased());
3001}
3002
3003bool Focus::autoFocusChecks()
3004{
3005 if (++absIterations > MAXIMUM_ABS_ITERATIONS)
3006 {
3007 appendLogText(i18n("Autofocus failed: exceeded max iterations %1", MAXIMUM_ABS_ITERATIONS));
3008 completeFocusProcedure(Ekos::FOCUS_ABORTED, Ekos::FOCUS_FAIL_MAX_ITERS);
3009 return false;
3010 }
3011
3012 // No stars detected, try to capture again
3013 if (currentHFR == INVALID_STAR_MEASURE)
3014 {
3015 if (noStarCount < MAX_RECAPTURE_RETRIES)
3016 {
3017 noStarCount++;
3018 appendLogText(i18n("No stars detected, capturing again..."));
3019 capture();
3020 return false;
3021 }
3022 else if (m_FocusAlgorithm == FOCUS_LINEAR)
3023 {
3024 appendLogText(i18n("Failed to detect any stars at position %1. Continuing...", currentPosition));
3025 noStarCount = 0;
3026 }
3027 else if(!m_OpsFocusProcess->focusDonut->isChecked())
3028 {
3029 // Carry on for donut detection
3030 appendLogText(i18n("Failed to detect any stars. Reset frame and try again."));
3031 completeFocusProcedure(Ekos::FOCUS_ABORTED, Ekos::FOCUS_FAIL_NO_STARS);
3032 return false;
3033 }
3034 }
3035 else
3036 noStarCount = 0;
3037
3038 return true;
3039}
3040
3041void Focus::plotLinearFocus()
3042{
3043 // I was hoping to avoid intermediate plotting, just set everything up then plot,
3044 // but this isn't working. For now, with plt=true, plot on every intermediate update.
3045 bool plt = true;
3046
3047 // Get the data to plot.
3048 QVector<double> values, weights;
3049 QVector<int> positions;
3050 linearFocuser->getMeasurements(&positions, &values, &weights);
3051 const FocusAlgorithmInterface::FocusParams &params = linearFocuser->getParams();
3052
3053 // As an optimization for slower machines, e.g. RPi4s, if the points are the same except for
3054 // the last point, just emit the last point instead of redrawing everything.
3057
3058 bool incrementalChange = false;
3059 if (positions.size() > 1 && positions.size() == lastPositions.size() + 1)
3060 {
3061 bool ok = true;
3062 for (int i = 0; i < positions.size() - 1; ++i)
3063 if (positions[i] != lastPositions[i] || values[i] != lastValues[i])
3064 {
3065 ok = false;
3066 break;
3067 }
3069 }
3070 lastPositions = positions;
3071 lastValues = values;
3072
3073 const bool outlier = false;
3075 emit newHFRPlotPosition(static_cast<double>(positions.last()), values.last(), (pow(weights.last(), -0.5)),
3076 outlier, params.initialStepSize, plt);
3077 else
3078 {
3079 emit initHFRPlot(getyAxisLabel(m_StarMeasure), getStarUnits(m_StarMeasure, m_StarUnits),
3080 m_OptDir == CurveFitting::OPTIMISATION_MINIMISE, params.useWeights, plt);
3081 for (int i = 0; i < positions.size(); ++i)
3082 emit newHFRPlotPosition(static_cast<double>(positions[i]), values[i], (pow(weights.last(), -0.5)),
3083 outlier, params.initialStepSize, plt);
3084 }
3085
3086 // Plot the polynomial, if there are enough points.
3087 if (values.size() > 3)
3088 {
3089 // The polynomial should only reflect 1st-pass samples.
3090 QVector<double> pass1Values, pass1Weights;
3091 QVector<int> pass1Positions;
3092 QVector<bool> pass1Outliers;
3093 double minPosition, minValue;
3094 double searchMin = std::max(params.minPositionAllowed, params.startPosition - params.maxTravel);
3095 double searchMax = std::min(params.maxPositionAllowed, params.startPosition + params.maxTravel);
3096
3097 linearFocuser->getPass1Measurements(&pass1Positions, &pass1Values, &pass1Weights, &pass1Outliers);
3098 if (m_FocusAlgorithm == FOCUS_LINEAR || m_CurveFit == CurveFitting::FOCUS_QUADRATIC)
3099 {
3100 // TODO: Need to determine whether to change LINEAR over to the LM solver in CurveFitting
3101 // This will be determined after L1P's first release has been deemed successful.
3102 polynomialFit.reset(new PolynomialFit(2, pass1Positions, pass1Values));
3103
3104 if (polynomialFit->findMinimum(params.startPosition, searchMin, searchMax, &minPosition, &minValue))
3105 {
3106 emit drawPolynomial(polynomialFit.get(), true, true, plt);
3107
3108 // Only plot the first pass' min position if we're not done.
3109 // Once we have a result, we don't want to display an intermediate minimum.
3110 if (linearFocuser->isDone())
3111 emit minimumFound(-1, -1, plt);
3112 else
3113 emit minimumFound(minPosition, minValue, plt);
3114 }
3115 else
3116 {
3117 // Didn't get a good polynomial fit.
3118 emit drawPolynomial(polynomialFit.get(), false, false, plt);
3119 emit minimumFound(-1, -1, plt);
3120 }
3121
3122 }
3123 else // Linear 1 Pass
3124 {
3125 if (curveFitting->findMinMax(params.startPosition, searchMin, searchMax, &minPosition, &minValue, params.curveFit,
3126 params.optimisationDirection))
3127 {
3128 R2 = curveFitting->calculateR2(static_cast<CurveFitting::CurveFit>(params.curveFit));
3129 emit drawCurve(curveFitting.get(), true, true, plt);
3130
3131 // For Linear 1 Pass always display the minimum on the graph
3132 emit minimumFound(minPosition, minValue, plt);
3133 }
3134 else
3135 {
3136 // Didn't get a good fit.
3137 emit drawCurve(curveFitting.get(), false, false, plt);
3138 emit minimumFound(-1, -1, plt);
3139 }
3140 }
3141 }
3142
3143 // Linear focuser might change the latest hfr with its relativeHFR scheme.
3144 HFROut->setText(QString("%1").arg(currentHFR * getStarUnits(m_StarMeasure, m_StarUnits), 0, 'f', 2));
3145
3146 emit setTitle(linearFocuser->getTextStatus(R2));
3147
3148 if (!plt) HFRPlot->replot();
3149}
3150
3151// Get the curve fitting goal
3152CurveFitting::FittingGoal Focus::getGoal(int numSteps)
3153{
3154 // The classic walk needs the STANDARD curve fitting
3155 if (m_FocusWalk == FOCUS_WALK_CLASSIC)
3156 return CurveFitting::FittingGoal::STANDARD;
3157
3158 // Fixed step walks will use C, except for the last step which should be BEST
3159 return (numSteps >= m_OpsFocusMechanics->focusNumSteps->value()) ? CurveFitting::FittingGoal::BEST :
3160 CurveFitting::FittingGoal::STANDARD;
3161}
3162
3163// Called after the first pass is complete and we're moving to the final focus position
3164// Calculate R2 for the curve and update the graph.
3165// Add the CFZ to the graph
3166void Focus::plotLinearFinalUpdates()
3167{
3168 bool plt = true;
3169 if (!m_OpsFocusProcess->focusRefineCurveFit->isChecked())
3170 {
3171 // Display the CFZ on the graph
3172 emit drawCFZ(linearFocuser->solution(), linearFocuser->solutionValue(), m_cfzSteps,
3173 m_CFZUI->focusCFZDisplayVCurve->isChecked());
3174 // Final updates to the graph title
3175 emit finalUpdates(linearFocuser->getTextStatus(R2), plt);
3176 }
3177 else
3178 {
3179 // v-curve needs to be redrawn to reflect the data from the refining process
3180 // Get the data to plot.
3181 QVector<double> pass1Values, pass1Weights;
3182 QVector<int> pass1Positions;
3183 QVector<bool> pass1Outliers;
3184
3185 linearFocuser->getPass1Measurements(&pass1Positions, &pass1Values, &pass1Weights, &pass1Outliers);
3186 const FocusAlgorithmInterface::FocusParams &params = linearFocuser->getParams();
3187
3188 emit initHFRPlot(getyAxisLabel(m_StarMeasure), getStarUnits(m_StarMeasure, m_StarUnits),
3189 m_OptDir == CurveFitting::OPTIMISATION_MINIMISE, m_OpsFocusProcess->focusUseWeights->isChecked(), plt);
3190
3191 for (int i = 0; i < pass1Positions.size(); ++i)
3192 emit newHFRPlotPosition(static_cast<double>(pass1Positions[i]), pass1Values[i], (pow(pass1Weights[i], -0.5)),
3193 pass1Outliers[i], params.initialStepSize, plt);
3194
3195 R2 = curveFitting->calculateR2(static_cast<CurveFitting::CurveFit>(params.curveFit));
3196 emit drawCurve(curveFitting.get(), true, true, false);
3197
3198 // For Linear 1 Pass always display the minimum on the graph
3199 emit minimumFound(linearFocuser->solution(), linearFocuser->solutionValue(), plt);
3200 // Display the CFZ on the graph
3201 emit drawCFZ(linearFocuser->solution(), linearFocuser->solutionValue(), m_cfzSteps,
3202 m_CFZUI->focusCFZDisplayVCurve->isChecked());
3203 // Update the graph title
3204 emit setTitle(linearFocuser->getTextStatus(R2), plt);
3205 }
3206}
3207
3208void Focus::startAberrationInspector()
3209{
3210#ifdef HAVE_DATAVISUALIZATION
3211 // Fill in the data structure to be passed to the Aberration Inspector
3212 AberrationInspector::abInsData data;
3213
3214 ImageMosaicMask *mosaicmask = dynamic_cast<ImageMosaicMask *>(m_FocusView->imageMask().get());
3215 if (!mosaicmask)
3216 {
3217 appendLogText(i18n("Unable to launch Aberration Inspector run %1...", m_abInsRun));
3218 return;
3219 }
3220
3221
3222 data.run = ++m_abInsRun;
3223 data.curveFit = m_CurveFit;
3224 data.useWeights = m_OpsFocusProcess->focusUseWeights->isChecked();
3225 data.optDir = m_OptDir;
3226 data.sensorWidth = m_CcdWidth;
3227 data.sensorHeight = m_CcdHeight;
3228 data.pixelSize = m_CcdPixelSizeX;
3229 data.tileWidth = mosaicmask->tiles()[0].width();
3230 data.focuserStepMicrons = m_CFZUI->focusCFZStepSize->value();
3231 data.yAxisLabel = getyAxisLabel(m_StarMeasure);
3232 data.starUnits = getStarUnits(m_StarMeasure, m_StarUnits);
3233 data.cfzSteps = m_cfzSteps;
3234 data.isPositionBased = isPositionBased();
3235
3236 // Launch the Aberration Inspector.
3237 appendLogText(i18n("Launching Aberration Inspector run %1...", m_abInsRun));
3238 QPointer<AberrationInspector> abIns(new AberrationInspector(data, m_abInsPosition, m_abInsMeasure, m_abInsWeight,
3239 m_abInsNumStars, m_abInsTileCenterOffset));
3240 abIns->setAttribute(Qt::WA_DeleteOnClose);
3241 abIns->show();
3242#endif
3243}
3244
3245// Algorithm to scan for an optimum start position for Autofocus
3246void Focus::scanStartPos()
3247{
3248 // Firstly do we have any stars?
3249 if (currentHFR == INVALID_STAR_MEASURE)
3250 {
3251 if (noStarCount < MAX_RECAPTURE_RETRIES)
3252 {
3253 noStarCount++;
3254 appendLogText(i18n("No stars detected, capturing again..."));
3255 capture();
3256 return;
3257 }
3258 else
3259 {
3260 // Carry on for donut detection
3261 appendLogText(i18n("Failed to detect any stars. Aborting..."));
3262 completeFocusProcedure(Ekos::FOCUS_ABORTED, Ekos::FOCUS_FAIL_NO_STARS);
3263 return;
3264 }
3265 }
3266
3267 // Cap the maximum number of iterations before failing
3268 if (++absIterations > MAXIMUM_ABS_ITERATIONS)
3269 {
3270 appendLogText(i18n("Scan Start Pos: exceeded max iterations %1", MAXIMUM_ABS_ITERATIONS));
3271 completeFocusProcedure(Ekos::FOCUS_ABORTED, Ekos::FOCUS_FAIL_MAX_ITERS);
3272 return;
3273 }
3274
3275 noStarCount = 0;
3276 m_scanPosition.push_back(currentPosition);
3277 m_scanMeasure.push_back(currentMeasure);
3278
3279 int deltaPos;
3280 const int step = m_scanPosition.size();
3281 const int maxSteps = m_OpsFocusProcess->focusScanDatapoints->value();
3282 const int stepSize = m_OpsFocusMechanics->focusTicks->value() * m_OpsFocusProcess->focusScanStepSizeFactor->value();
3283 emit newHFRPlotPosition(static_cast<double>(currentPosition), currentMeasure, pow(currentWeight, -0.5), false, stepSize,
3284 true);
3285 if (step < maxSteps)
3286 {
3287 // Collect more data
3288 emit setTitle(QString(i18n("Scanning for starting position %1/%2", step, maxSteps)), true);
3289 deltaPos = step * stepSize * std::pow(-1, step + 1);
3290 }
3291 else
3292 {
3293 // We have enough data
3294 auto it = std::min_element(std::begin(m_scanMeasure), std::end(m_scanMeasure));
3295 auto min = std::distance(std::begin(m_scanMeasure), it);
3296 if (min >= m_OpsFocusProcess->focusScanDatapoints->value() - 2)
3297 {
3298 // No minimum so we need to move and rerun the scan
3299 deltaPos = m_scanPosition[min] - currentPosition;
3300 m_scanPosition.clear();
3301 m_scanMeasure.clear();
3302 emit setTitle(QString(i18n("No scan minimum - widening search...", step, maxSteps)), true);
3303 }
3304 else
3305 {
3306 // We have a minimum so kick in Autofocus
3307 const int initialPosition = m_scanPosition[min];
3308 setupLinearFocuser(initialPosition);
3309 inScanStartPos = false;
3310 absIterations = 0;
3311 deltaPos = linearRequestedPosition - currentPosition;
3312 emit setTitle(QString(i18n("Scan Start Position %1 Found", initialPosition)), true);
3313 appendLogText(i18n("Scan Start Position %1 found", initialPosition));
3314 }
3315 }
3316 if (!changeFocus(deltaPos))
3317 completeFocusProcedure(Ekos::FOCUS_ABORTED, Ekos::FOCUS_FAIL_FOCUSER_NO_MOVE);
3318}
3319
3320void Focus::autoFocusLinear()
3321{
3322 if (!autoFocusChecks())
3323 return;
3324
3325 if (!canAbsMove && !canRelMove && canTimerMove)
3326 {
3327 //const bool kFixPosition = true;
3328 if (linearRequestedPosition != currentPosition)
3329 //if (kFixPosition && (linearRequestedPosition != currentPosition))
3330 {
3331 qCDebug(KSTARS_EKOS_FOCUS) << "Linear: warning, changing position " << currentPosition << " to "
3332 << linearRequestedPosition;
3333
3334 currentPosition = linearRequestedPosition;
3335 }
3336 }
3337
3338 // Only use the relativeHFR algorithm if full field is enabled with one capture/measurement.
3339 bool useFocusStarsHFR = m_OpsFocusSettings->focusUseFullField->isChecked()
3340 && m_OpsFocusProcess->focusFramesCount->value() == 1;
3341 auto focusStars = useFocusStarsHFR || (m_FocusAlgorithm == FOCUS_LINEAR1PASS) ? &(m_ImageData->getStarCenters()) : nullptr;
3342
3343 linearRequestedPosition = linearFocuser->newMeasurement(currentPosition, currentMeasure, currentWeight, focusStars);
3344 if (m_FocusAlgorithm == FOCUS_LINEAR1PASS && linearFocuser->isDone() && linearFocuser->solution() != -1)
3345 {
3346 // Linear 1 Pass is done, graph is drawn, so just move to the focus position, and update the graph.
3347 plotLinearFinalUpdates();
3348 if (m_abInsOn)
3349 startAberrationInspector();
3350 }
3351 else
3352 // Update the graph with the next datapoint, draw the curve, etc.
3353 plotLinearFocus();
3354
3355 if (linearFocuser->isDone())
3356 {
3357 if (linearFocuser->solution() != -1)
3358 {
3359 // Now test that the curve fit was acceptable. If not retry the focus process using standard retry process
3360 // R2 check is only available for Linear 1 Pass for Hyperbola and Parabola
3361 if (m_CurveFit == CurveFitting::FOCUS_QUADRATIC)
3362 // Linear only uses Quadratic so no need to do the R2 check, just complete
3363 completeFocusProcedure(Ekos::FOCUS_COMPLETE, Ekos::FOCUS_FAIL_NONE, "", false);
3364 else if (R2 >= m_OpsFocusProcess->focusR2Limit->value())
3365 {
3366 qCDebug(KSTARS_EKOS_FOCUS) << QString("Linear Curve Fit check passed R2=%1 focusR2Limit=%2").arg(R2).arg(
3367 m_OpsFocusProcess->focusR2Limit->value());
3368 completeFocusProcedure(Ekos::FOCUS_COMPLETE, FOCUS_FAIL_NONE, "", false);
3369 R2Retries = 0;
3370 }
3371 else if (R2Retries == 0)
3372 {
3373 // Failed the R2 check for the first time so retry...
3374 appendLogText(i18n("Curve Fit check failed R2=%1 focusR2Limit=%2 retrying...", R2,
3375 m_OpsFocusProcess->focusR2Limit->value()));
3376 QString failCodeInfo = i18n("R2=%1 < Limit=%2", QString::number(R2, 'f', 2),
3377 QString::number(m_OpsFocusProcess->focusR2Limit->value(), 'f', 2));
3378 completeFocusProcedure(Ekos::FOCUS_ABORTED, Ekos::FOCUS_FAIL_R2, failCodeInfo, false);
3379 R2Retries++;
3380 }
3381 else
3382 {
3383 // Retried after an R2 check fail but failed again so... log msg and continue
3384 appendLogText(i18n("Curve Fit check failed again R2=%1 focusR2Limit=%2 but continuing...", R2,
3385 m_OpsFocusProcess->focusR2Limit->value()));
3386 completeFocusProcedure(Ekos::FOCUS_COMPLETE, Ekos::FOCUS_FAIL_NONE, "", false);
3387 R2Retries = 0;
3388 }
3389 }
3390 else
3391 {
3392 qCDebug(KSTARS_EKOS_FOCUS) << linearFocuser->doneReason();
3393 AutofocusFailReason failCode = linearFocuser->getFailCode();
3394 appendLogText(i18n("Linear autofocus algorithm aborted: %1", AutofocusFailReasonStr[failCode]));
3395 completeFocusProcedure(Ekos::FOCUS_ABORTED, failCode, "", false);
3396 }
3397 return;
3398 }
3399 else
3400 {
3401 const int delta = linearRequestedPosition - currentPosition;
3402
3403 if (!changeFocus(delta))
3404 completeFocusProcedure(Ekos::FOCUS_ABORTED, Ekos::FOCUS_FAIL_FOCUSER_NO_MOVE, "", false);
3405
3406 return;
3407 }
3408}
3409
3410void Focus::autoFocusAbs()
3411{
3412 // Q_ASSERT_X(canAbsMove || canRelMove, __FUNCTION__, "Prerequisite: only absolute and relative focusers");
3413
3414 static int minHFRPos = 0, focusOutLimit = 0, focusInLimit = 0, lastHFRPos = 0, fluctuations = 0;
3415 static double minHFR = 0, lastDelta = 0;
3416 double targetPosition = 0;
3417 bool ignoreLimitedDelta = false;
3418
3419 QString deltaTxt = QString("%1").arg(fabs(currentHFR - minHFR) * 100.0, 0, 'g', 3);
3420 QString HFRText = QString("%1").arg(currentHFR, 0, 'g', 3);
3421
3422 qCDebug(KSTARS_EKOS_FOCUS) << "===============================";
3423 qCDebug(KSTARS_EKOS_FOCUS) << "Current HFR: " << currentHFR << " Current Position: " << currentPosition;
3424 qCDebug(KSTARS_EKOS_FOCUS) << "Last minHFR: " << minHFR << " Last MinHFR Pos: " << minHFRPos;
3425 qCDebug(KSTARS_EKOS_FOCUS) << "Delta: " << deltaTxt << "%";
3426 qCDebug(KSTARS_EKOS_FOCUS) << "========================================";
3427
3428 if (minHFR)
3429 appendLogText(i18n("FITS received. HFR %1 @ %2. Delta (%3%)", HFRText, currentPosition, deltaTxt));
3430 else
3431 appendLogText(i18n("FITS received. HFR %1 @ %2.", HFRText, currentPosition));
3432
3433 if (!autoFocusChecks())
3434 return;
3435
3436 addPlotPosition(currentPosition, currentHFR);
3437
3438 switch (m_LastFocusDirection)
3439 {
3440 case FOCUS_NONE:
3441 lastHFR = currentHFR;
3442 initialFocuserAbsPosition = currentPosition;
3443 minHFR = currentHFR;
3444 minHFRPos = currentPosition;
3445 HFRDec = 0;
3446 HFRInc = 0;
3447 focusOutLimit = 0;
3448 focusInLimit = 0;
3449 lastDelta = 0;
3450 fluctuations = 0;
3451
3452 // This is the first step, so clamp the initial target position to the device limits
3453 // If the focuser cannot move because it is at one end of the interval, try the opposite direction next
3454 if (absMotionMax < currentPosition + pulseDuration)
3455 {
3456 if (currentPosition < absMotionMax)
3457 {
3458 pulseDuration = absMotionMax - currentPosition;
3459 }
3460 else
3461 {
3462 pulseDuration = 0;
3463 m_LastFocusDirection = FOCUS_IN;
3464 }
3465 }
3466 else if (currentPosition + pulseDuration < absMotionMin)
3467 {
3468 if (absMotionMin < currentPosition)
3469 {
3470 pulseDuration = currentPosition - absMotionMin;
3471 }
3472 else
3473 {
3474 pulseDuration = 0;
3475 m_LastFocusDirection = FOCUS_OUT;
3476 }
3477 }
3478
3479 m_LastFocusDirection = (pulseDuration > 0) ? FOCUS_OUT : FOCUS_IN;
3480 if (!changeFocus(pulseDuration, false))
3481 completeFocusProcedure(Ekos::FOCUS_ABORTED, Ekos::FOCUS_FAIL_FOCUSER_NO_MOVE);
3482
3483 break;
3484
3485 case FOCUS_IN:
3486 case FOCUS_OUT:
3487 if (reverseDir && focusInLimit && focusOutLimit &&
3488 fabs(currentHFR - minHFR) < (m_OpsFocusProcess->focusTolerance->value() / 100.0) && HFRInc == 0)
3489 {
3490 if (absIterations <= 2)
3491 {
3492 QString message = i18n("Change in HFR is too small. Try increasing the step size or decreasing the tolerance.");
3493 appendLogText(message);
3494 KSNotification::event(QLatin1String("FocusFailed"), message, KSNotification::Focus, KSNotification::Alert);
3495 completeFocusProcedure(Ekos::FOCUS_ABORTED, Ekos::FOCUS_FAIL_SMALL_HFR);
3496 }
3497 else if (noStarCount > 0)
3498 {
3499 QString message = i18n("Failed to detect focus star in frame. Capture and select a focus star.");
3500 appendLogText(message);
3501 KSNotification::event(QLatin1String("FocusFailed"), message, KSNotification::Focus, KSNotification::Alert);
3502 completeFocusProcedure(Ekos::FOCUS_ABORTED, Ekos::FOCUS_FAIL_NO_STARS);
3503 }
3504 else
3505 {
3506 completeFocusProcedure(Ekos::FOCUS_COMPLETE, Ekos::FOCUS_FAIL_NONE);
3507 }
3508 break;
3509 }
3510 else if (currentHFR < lastHFR)
3511 {
3512 // Let's now limit the travel distance of the focuser
3513 if (HFRInc >= 1 && m_LastFocusDirection == FOCUS_OUT && lastHFRPos < focusInLimit && fabs(currentHFR - lastHFR) > 0.1)
3514 {
3516 qCDebug(KSTARS_EKOS_FOCUS) << "New FocusInLimit " << focusInLimit;
3517 }
3518 else if (HFRInc >= 1 && m_LastFocusDirection == FOCUS_IN && lastHFRPos > focusOutLimit &&
3519 fabs(currentHFR - lastHFR) > 0.1)
3520 {
3522 qCDebug(KSTARS_EKOS_FOCUS) << "New FocusOutLimit " << focusOutLimit;
3523 }
3524
3525 double factor = std::max(1.0, HFRDec / 2.0);
3526 if (m_LastFocusDirection == FOCUS_IN)
3527 targetPosition = currentPosition - (pulseDuration * factor);
3528 else
3529 targetPosition = currentPosition + (pulseDuration * factor);
3530
3531 qCDebug(KSTARS_EKOS_FOCUS) << "current Position" << currentPosition << " targetPosition " << targetPosition;
3532
3533 lastHFR = currentHFR;
3534
3535 // Let's keep track of the minimum HFR
3536 if (lastHFR < minHFR)
3537 {
3538 minHFR = lastHFR;
3539 minHFRPos = currentPosition;
3540 qCDebug(KSTARS_EKOS_FOCUS) << "new minHFR " << minHFR << " @ position " << minHFRPos;
3541 }
3542
3543 lastHFRPos = currentPosition;
3544
3545 // HFR is decreasing, we are on the right direction
3546 HFRDec++;
3547 if (HFRInc > 0)
3548 {
3549 // Remove bad data point and mark fluctuation
3550 if (plot_position.count() >= 2)
3551 {
3552 plot_position.remove(plot_position.count() - 2);
3553 plot_value.remove(plot_value.count() - 2);
3554 }
3555 fluctuations++;
3556 }
3557 HFRInc = 0;
3558 }
3559 else
3560 {
3561 // HFR increased, let's deal with it.
3562 HFRInc++;
3563 if (HFRDec > 0)
3564 fluctuations++;
3565 HFRDec = 0;
3566
3567 lastHFR = currentHFR;
3568 lastHFRPos = currentPosition;
3569
3570 // Keep moving in same direction (even if bad) for one more iteration to gather data points.
3571 if (HFRInc > 1)
3572 {
3573 // Looks like we're going away from optimal HFR
3574 reverseDir = true;
3575 HFRInc = 0;
3576
3577 qCDebug(KSTARS_EKOS_FOCUS) << "Focus is moving away from optimal HFR.";
3578
3579 // Let's set new limits
3580 if (m_LastFocusDirection == FOCUS_IN)
3581 {
3582 focusInLimit = currentPosition;
3583 qCDebug(KSTARS_EKOS_FOCUS) << "Setting focus IN limit to " << focusInLimit;
3584 }
3585 else
3586 {
3587 focusOutLimit = currentPosition;
3588 qCDebug(KSTARS_EKOS_FOCUS) << "Setting focus OUT limit to " << focusOutLimit;
3589 }
3590
3591 if (m_FocusAlgorithm == FOCUS_POLYNOMIAL && plot_position.count() > 4)
3592 {
3593 polynomialFit.reset(new PolynomialFit(2, 5, plot_position, plot_value));
3594 double a = *std::min_element(plot_position.constBegin(), plot_position.constEnd());
3595 double b = *std::max_element(plot_position.constBegin(), plot_position.constEnd());
3596 double min_position = 0, min_hfr = 0;
3597 isVShapeSolution = polynomialFit->findMinimum(minHFRPos, a, b, &min_position, &min_hfr);
3598 qCDebug(KSTARS_EKOS_FOCUS) << "Found Minimum?" << (isVShapeSolution ? "Yes" : "No");
3599 if (isVShapeSolution)
3600 {
3601 ignoreLimitedDelta = true;
3602 qCDebug(KSTARS_EKOS_FOCUS) << "Minimum Solution:" << min_hfr << "@" << min_position;
3603 targetPosition = round(min_position);
3604 appendLogText(i18n("Found polynomial solution @ %1", QString::number(min_position, 'f', 0)));
3605
3606 emit drawPolynomial(polynomialFit.get(), isVShapeSolution, true);
3607 emit minimumFound(min_position, min_hfr);
3608 }
3609 else
3610 {
3611 emit drawPolynomial(polynomialFit.get(), isVShapeSolution, false);
3612 }
3613 }
3614 }
3615
3616 if (HFRInc == 1)
3617 {
3618 // Keep going at same stride even if we are going away from CFZ
3619 // This is done to gather data points are the trough.
3620 if (std::abs(lastDelta) > 0)
3621 targetPosition = currentPosition + lastDelta;
3622 else
3623 targetPosition = currentPosition + pulseDuration;
3624 }
3625 else if (isVShapeSolution == false)
3626 {
3627 ignoreLimitedDelta = true;
3628 // Let's get close to the minimum HFR position so far detected
3629 if (m_LastFocusDirection == FOCUS_OUT)
3630 targetPosition = minHFRPos - pulseDuration / 2;
3631 else
3632 targetPosition = minHFRPos + pulseDuration / 2;
3633 }
3634
3635 qCDebug(KSTARS_EKOS_FOCUS) << "new targetPosition " << targetPosition;
3636 }
3637
3638 // Limit target Pulse to algorithm limits
3639 if (focusInLimit != 0 && m_LastFocusDirection == FOCUS_IN && targetPosition < focusInLimit)
3640 {
3641 targetPosition = focusInLimit;
3642 qCDebug(KSTARS_EKOS_FOCUS) << "Limiting target pulse to focus in limit " << targetPosition;
3643 }
3644 else if (focusOutLimit != 0 && m_LastFocusDirection == FOCUS_OUT && targetPosition > focusOutLimit)
3645 {
3646 targetPosition = focusOutLimit;
3647 qCDebug(KSTARS_EKOS_FOCUS) << "Limiting target pulse to focus out limit " << targetPosition;
3648 }
3649
3650 // Limit target pulse to focuser limits
3651 if (targetPosition < absMotionMin)
3652 targetPosition = absMotionMin;
3653 else if (targetPosition > absMotionMax)
3654 targetPosition = absMotionMax;
3655
3656 // We cannot go any further because of the device limits, this is a failure
3657 if (targetPosition == currentPosition)
3658 {
3659 // If case target position happens to be the minimal historical
3660 // HFR position, accept this value instead of bailing out.
3661 if (targetPosition == minHFRPos || isVShapeSolution)
3662 {
3663 appendLogText("Stopping at minimum recorded HFR position.");
3664 completeFocusProcedure(Ekos::FOCUS_COMPLETE, Ekos::FOCUS_FAIL_NONE);
3665 }
3666 else
3667 {
3668 QString message = i18n("Focuser cannot move further, device limits reached. Autofocus aborted.");
3669 appendLogText(message);
3670 KSNotification::event(QLatin1String("FocusFailed"), message, KSNotification::Focus, KSNotification::Alert);
3671 completeFocusProcedure(Ekos::FOCUS_ABORTED, Ekos::FOCUS_FAIL_FOCUSER_OOB);
3672 }
3673 return;
3674 }
3675
3676 // Too many fluctuatoins
3677 if (fluctuations >= MAXIMUM_FLUCTUATIONS)
3678 {
3679 QString message = i18n("Unstable fluctuations. Try increasing initial step size or exposure time.");
3680 appendLogText(message);
3681 KSNotification::event(QLatin1String("FocusFailed"), message, KSNotification::Focus, KSNotification::Alert);
3682 completeFocusProcedure(Ekos::FOCUS_ABORTED, Ekos::FOCUS_FAIL_FLUCTUATIONS);
3683 return;
3684 }
3685
3686 // Ops, deadlock
3688 {
3689 QString message = i18n("Deadlock reached. Please try again with different settings.");
3690 appendLogText(message);
3691 KSNotification::event(QLatin1String("FocusFailed"), message, KSNotification::Focus, KSNotification::Alert);
3692 completeFocusProcedure(Ekos::FOCUS_ABORTED, Ekos::FOCUS_FAIL_DEADLOCK);
3693 return;
3694 }
3695
3696 // Restrict the target position even more with the maximum travel option
3697 if (fabs(targetPosition - initialFocuserAbsPosition) > m_OpsFocusMechanics->focusMaxTravel->value())
3698 {
3699 int minTravelLimit = qMax(0.0, initialFocuserAbsPosition - m_OpsFocusMechanics->focusMaxTravel->value());
3700 int maxTravelLimit = qMin(absMotionMax, initialFocuserAbsPosition + m_OpsFocusMechanics->focusMaxTravel->value());
3701
3702 // In case we are asked to go below travel limit, but we are not there yet
3703 // let us go there and see the result before aborting
3704 if (fabs(currentPosition - minTravelLimit) > 10 && targetPosition < minTravelLimit)
3705 {
3706 targetPosition = minTravelLimit;
3707 }
3708 // Same for max travel
3709 else if (fabs(currentPosition - maxTravelLimit) > 10 && targetPosition > maxTravelLimit)
3710 {
3711 targetPosition = maxTravelLimit;
3712 }
3713 else
3714 {
3715 qCDebug(KSTARS_EKOS_FOCUS) << "targetPosition (" << targetPosition << ") - initHFRAbsPos ("
3716 << initialFocuserAbsPosition << ") exceeds maxTravel distance of " << m_OpsFocusMechanics->focusMaxTravel->value();
3717
3718 QString message = i18n("Maximum travel limit reached. Autofocus aborted.");
3719 appendLogText(message);
3720 KSNotification::event(QLatin1String("FocusFailed"), message, KSNotification::Focus, KSNotification::Alert);
3721 completeFocusProcedure(Ekos::FOCUS_ABORTED, Ekos::FOCUS_FAIL_FOCUSER_OOB);
3722 break;
3723 }
3724 }
3725
3726 // Get delta for next move
3727 lastDelta = (targetPosition - currentPosition);
3728
3729 qCDebug(KSTARS_EKOS_FOCUS) << "delta (targetPosition - currentPosition) " << lastDelta;
3730
3731 // Limit to Maximum permitted delta (Max Single Step Size)
3732 if (ignoreLimitedDelta == false)
3733 {
3734 double limitedDelta = qMax(-1.0 * m_OpsFocusMechanics->focusMaxSingleStep->value(),
3735 qMin(1.0 * m_OpsFocusMechanics->focusMaxSingleStep->value(), lastDelta));
3736 if (std::fabs(limitedDelta - lastDelta) > 0)
3737 {
3738 qCDebug(KSTARS_EKOS_FOCUS) << "Limited delta to maximum permitted single step " <<
3739 m_OpsFocusMechanics->focusMaxSingleStep->value();
3741 }
3742 }
3743
3744 m_LastFocusDirection = (lastDelta > 0) ? FOCUS_OUT : FOCUS_IN;
3745 if (!changeFocus(lastDelta, false))
3746 completeFocusProcedure(Ekos::FOCUS_ABORTED, Ekos::FOCUS_FAIL_FOCUSER_NO_MOVE);
3747
3748 break;
3749 }
3750}
3751
3752void Focus::addPlotPosition(int pos, double value, bool plot)
3753{
3754 plot_position.append(pos);
3755 plot_value.append(value);
3756 if (plot)
3757 emit newHFRPlotPosition(static_cast<double>(pos), value, 1.0, false, pulseDuration);
3758}
3759
3760// Synchronises the plot_position and plot_value vectors with the data used by linearFocuser
3761// This keeps the 2 sets of data in sync for the Linear and Linear1Pass algorithms.
3762// For Iterative and Polynomial these vectors are built during the focusing cycle so nothing to do here
3763void Focus::updatePlotPosition()
3764{
3765 if (m_FocusAlgorithm == FOCUS_LINEAR1PASS)
3766 {
3767 QVector<int> positions;
3768 if (inScanStartPos)
3769 {
3770 // If we are inScanStartPos then there is no focus data
3771 plot_position.clear();
3772 plot_value.clear();
3773 plot_weight.clear();
3774 plot_outlier.clear();
3775 }
3776 else
3777 {
3778 linearFocuser->getPass1Measurements(&positions, &plot_value, &plot_weight, &plot_outlier);
3779 plot_position.clear();
3780 for (int i = 0; i < positions.count(); i++)
3781 plot_position.append(positions[i]);
3782
3783 // For L1P add in the solution datapoint (if there is one). Linear already has this included.
3784 if (linearFocuser && linearFocuser->isDone() && linearFocuser->solution() != -1)
3785 {
3786 plot_position.append(linearFocuser->solution());
3787 plot_value.append(linearFocuser->solutionValue());
3788 plot_weight.append(linearFocuser->solutionWeight());
3789 plot_outlier.append(false);
3790 }
3791 }
3792 }
3793 else if (m_FocusAlgorithm == FOCUS_LINEAR)
3794 {
3795 QVector<int> positions;
3796 linearFocuser->getMeasurements(&positions, &plot_value, &plot_weight);
3797 plot_position.clear();
3798 plot_outlier.clear();
3799 for (int i = 0; i < positions.count(); i++)
3800 {
3801 plot_position.append(positions[i]);
3802 // Outlier functionality isn't supported for Linear so make sure no outliers in the data
3803 plot_outlier.append(false);
3804 }
3805 }
3806 else
3807 {
3808 // Polynomial and Iterative don't support weights or outliers so fill in this data.
3809 for (int i = 0; i < plot_position.count(); i++)
3810 {
3811 plot_weight.append(1.0);
3812 plot_outlier.append(false);
3813 }
3814 }
3815}
3816
3817void Focus::autoFocusRel()
3818{
3819 static int noStarCount = 0;
3820 static double minHFR = 1e6;
3821 QString deltaTxt = QString("%1").arg(fabs(currentHFR - minHFR) * 100.0, 0, 'g', 2);
3822 QString minHFRText = QString("%1").arg(minHFR, 0, 'g', 3);
3823 QString HFRText = QString("%1").arg(currentHFR, 0, 'g', 3);
3824
3825 appendLogText(i18n("FITS received. HFR %1. Delta (%2%) Min HFR (%3)", HFRText, deltaTxt, minHFRText));
3826
3827 if (pulseDuration <= MINIMUM_PULSE_TIMER)
3828 {
3829 appendLogText(i18n("Autofocus failed to reach proper focus. Try adjusting the tolerance value."));
3830 completeFocusProcedure(Ekos::FOCUS_ABORTED, Ekos::FOCUS_FAIL_TOLERANCE);
3831 return;
3832 }
3833
3834 // No stars detected, try to capture again
3835 if (currentHFR == INVALID_STAR_MEASURE)
3836 {
3837 if (noStarCount < MAX_RECAPTURE_RETRIES)
3838 {
3839 noStarCount++;
3840 appendLogText(i18n("No stars detected, capturing again..."));
3841 capture();
3842 return;
3843 }
3844 else if (m_FocusAlgorithm == FOCUS_LINEAR || m_FocusAlgorithm == FOCUS_LINEAR1PASS)
3845 {
3846 appendLogText(i18n("Failed to detect any stars at position %1. Continuing...", currentPosition));
3847 noStarCount = 0;
3848 }
3849 else if(!m_OpsFocusProcess->focusDonut->isChecked())
3850 {
3851 // Carry on for donut detection
3852 appendLogText(i18n("Failed to detect any stars. Reset frame and try again."));
3853 completeFocusProcedure(Ekos::FOCUS_ABORTED, Ekos::FOCUS_FAIL_NO_STARS);
3854 return;
3855 }
3856 }
3857 else
3858 noStarCount = 0;
3859
3860 switch (m_LastFocusDirection)
3861 {
3862 case FOCUS_NONE:
3863 lastHFR = currentHFR;
3864 minHFR = 1e6;
3865 m_LastFocusDirection = FOCUS_IN;
3866 changeFocus(-pulseDuration, false);
3867 break;
3868
3869 case FOCUS_IN:
3870 case FOCUS_OUT:
3871 if (fabs(currentHFR - minHFR) < (m_OpsFocusProcess->focusTolerance->value() / 100.0) && HFRInc == 0)
3872 {
3873 completeFocusProcedure(Ekos::FOCUS_COMPLETE, Ekos::FOCUS_FAIL_NONE);
3874 }
3875 else if (currentHFR < lastHFR)
3876 {
3877 if (currentHFR < minHFR)
3878 minHFR = currentHFR;
3879
3880 lastHFR = currentHFR;
3881 changeFocus(m_LastFocusDirection == FOCUS_IN ? -pulseDuration : pulseDuration, false);
3882 HFRInc = 0;
3883 }
3884 else
3885 {
3886 //HFRInc++;
3887
3888 lastHFR = currentHFR;
3889
3890 HFRInc = 0;
3891
3892 pulseDuration *= 0.75;
3893
3894 if (!changeFocus(m_LastFocusDirection == FOCUS_IN ? pulseDuration : -pulseDuration, false))
3895 completeFocusProcedure(Ekos::FOCUS_ABORTED, Ekos::FOCUS_FAIL_FOCUSER_NO_MOVE);
3896
3897 // HFR getting worse so reverse direction
3898 m_LastFocusDirection = (m_LastFocusDirection == FOCUS_IN) ? FOCUS_OUT : FOCUS_IN;
3899 }
3900 break;
3901 }
3902}
3903
3904void Focus::autoFocusProcessPositionChange(IPState state)
3905{
3906 if (state == IPS_OK)
3907 {
3908 // Normally, if we are auto-focusing, after we move the focuser we capture an image.
3909 // However, the Linear algorithm, at the start of its passes, requires two
3910 // consecutive focuser moves--the first out further than we want, and a second
3911 // move back in, so that we eliminate backlash and are always moving in before a capture.
3912 if (focuserAdditionalMovement > 0)
3913 {
3914 int temp = focuserAdditionalMovement;
3915 focuserAdditionalMovement = 0;
3916
3917 qCDebug(KSTARS_EKOS_FOCUS) << QString("Undoing overscan extension. Moving back in by %1 ticks in %2s")
3918 .arg(temp).arg(m_OpsFocusMechanics->focusOverscanDelay->value());
3919
3920 QTimer::singleShot(m_OpsFocusMechanics->focusOverscanDelay->value() * 1000, this, [this, temp]()
3921 {
3922 if (!changeFocus(-temp, focuserAdditionalMovementUpdateDir))
3923 {
3924 appendLogText(i18n("Focuser error, check INDI panel."));
3925 completeFocusProcedure(Ekos::FOCUS_ABORTED, Ekos::FOCUS_FAIL_FOCUSER_NO_MOVE);
3926 }
3927 });
3928 }
3929 else if (inAutoFocus)
3930 {
3931 // Add a check that the current position matches the requested position (within a tolerance)
3932 if (m_FocusAlgorithm == FOCUS_LINEAR || m_FocusAlgorithm == FOCUS_LINEAR1PASS)
3933 if (abs(linearRequestedPosition - currentPosition) > m_OpsFocusMechanics->focusTicks->value())
3934 qCDebug(KSTARS_EKOS_FOCUS) << QString("Focus positioning error: requested position %1, current position %2")
3935 .arg(linearRequestedPosition).arg(currentPosition);
3936
3937 qCDebug(KSTARS_EKOS_FOCUS) << QString("Focus position reached at %1, starting capture in %2 seconds.").arg(
3938 currentPosition).arg(m_OpsFocusMechanics->focusSettleTime->value());
3939 // Adjust exposure if Donut Buster activated
3940 if (m_OpsFocusProcess->focusDonut->isChecked())
3941 donutTimeDilation();
3942 capture(m_OpsFocusMechanics->focusSettleTime->value());
3943 }
3944 }
3945 else if (state == IPS_ALERT)
3946 {
3947 appendLogText(i18n("Focuser error, check INDI panel."));
3948 completeFocusProcedure(Ekos::FOCUS_ABORTED, Ekos::FOCUS_FAIL_FOCUSER_ERROR);
3949 }
3950 else
3951 qCDebug(KSTARS_EKOS_FOCUS) <<
3952 QString("autoFocusProcessPositionChange called with state %1 (%2), focuserAdditionalMovement=%3, inAutoFocus=%4, m_captureInProgress=%5, currentPosition=%6")
3953 .arg(state).arg(pstateStr(state)).arg(focuserAdditionalMovement).arg(inAutoFocus).arg(m_captureInProgress)
3954 .arg(currentPosition);
3955}
3956
3957// This routine adjusts capture exposure during Autofocus depending on donut parameter settings
3958void Focus::donutTimeDilation()
3959{
3960 if (m_OpsFocusProcess->focusTimeDilation->value() == 1.0 || inScanStartPos)
3961 return;
3962
3963 // Get the max distance from focus to outer points
3964 const double centre = (m_OpsFocusMechanics->focusNumSteps->value() + 1.0) / 2.0;
3965 // Get the current step - treat the final
3966 const int currentStep = linearFocuser->currentStep() + 1;
3967 double distance;
3969 distance = std::abs(centre - currentStep);
3970 else
3971 // Last step is always back to focus
3972 distance = 0.0;
3973 double multiplier = distance / (centre - 1.0) * m_OpsFocusProcess->focusTimeDilation->value();
3974 multiplier = std::max(multiplier, 1.0);
3975 const double exposure = multiplier * m_donutOrigExposure;
3976 focusExposure->setValue(exposure);
3977 qCDebug(KSTARS_EKOS_FOCUS) << "Donut time dilation for point " << currentStep << " from " << m_donutOrigExposure << " to "
3978 << exposure;
3979
3980}
3981
3982void Focus::updateProperty(INDI::Property prop)
3983{
3984 if (m_Focuser == nullptr || prop.getType() != INDI_NUMBER || prop.getDeviceName() != m_Focuser->getDeviceName())
3985 return;
3986
3987 auto nvp = prop.getNumber();
3988
3989 // Only process focus properties
3990 if (QString(nvp->getName()).contains("focus", Qt::CaseInsensitive) == false)
3991 return;
3992
3993 if (nvp->isNameMatch("FOCUS_BACKLASH_STEPS"))
3994 {
3995 m_OpsFocusMechanics->focusBacklash->setValue(nvp->np[0].value);
3996 return;
3997 }
3998
3999 if (nvp->isNameMatch("ABS_FOCUS_POSITION"))
4000 {
4001 if (m_DebugFocuser)
4002 {
4003 // Simulate focuser comms issues every 5 moves
4004 if (m_DebugFocuserCounter++ >= 10 && m_DebugFocuserCounter <= 14)
4005 {
4006 if (m_DebugFocuserCounter == 14)
4007 m_DebugFocuserCounter = 0;
4008 appendLogText(i18n("Simulate focuser comms failure..."));
4009 return;
4010 }
4011 }
4012
4013 m_FocusMotionTimer.stop();
4014 INumber *pos = IUFindNumber(nvp, "FOCUS_ABSOLUTE_POSITION");
4015 IPState newState = nvp->s;
4016
4017 // FIXME: We should check state validity, but some focusers do not care - make ignore an option!
4018 if (pos)
4019 {
4020 int newPosition = static_cast<int>(pos->value);
4021
4022 // Some absolute focuser constantly report the position without a state change.
4023 // Therefore we ignore it if both value and state are the same as last time.
4024 // HACK: This would shortcut the autofocus procedure reset, see completeFocusProcedure for the small hack
4025 if (currentPosition == newPosition && currentPositionState == newState)
4026 {
4027 qCDebug(KSTARS_EKOS_FOCUS) << "Focuser position " << currentPosition << " and state:"
4028 << pstateStr(currentPositionState) << " unchanged";
4029 return;
4030 }
4031
4032 currentPositionState = newState;
4033
4034 if (currentPosition != newPosition)
4035 {
4036 currentPosition = newPosition;
4037 qCDebug(KSTARS_EKOS_FOCUS) << "Abs Focuser position changed to " << currentPosition << "State:" << pstateStr(
4038 currentPositionState);
4039 absTicksLabel->setText(QString::number(currentPosition));
4040 emit absolutePositionChanged(currentPosition);
4041 }
4042 }
4043 else
4044 qCDebug(KSTARS_EKOS_FOCUS) << "Can't access FOCUS_ABSOLUTE_POSITION. Current state:"
4045 << pstateStr(currentPositionState) << " New state:" << pstateStr(newState);
4046
4047 if (newState != IPS_OK)
4048 {
4049 if (inAutoFocus || inAdjustFocus || adaptFocus->inAdaptiveFocus())
4050 {
4051 // We had something back from the focuser but we're not done yet, so
4052 // restart motion timer in case focuser gets stuck.
4053 qCDebug(KSTARS_EKOS_FOCUS) << "Restarting focus motion timer, state " << pstateStr(newState);
4054 m_FocusMotionTimer.start();
4055 }
4056 }
4057 else
4058 {
4059 // Systematically reset UI when focuser finishes moving
4060 if (focuserAdditionalMovement == 0)
4061 resetButtons();
4062
4063 if (inAdjustFocus)
4064 {
4065 if (focuserAdditionalMovement == 0)
4066 {
4067 inAdjustFocus = false;
4068 emit focusPositionAdjusted();
4069 return;
4070 }
4071 }
4072
4073 if (adaptFocus->inAdaptiveFocus())
4074 {
4075 if (focuserAdditionalMovement == 0)
4076 {
4077 adaptFocus->adaptiveFocusAdmin(currentPosition, true, true);
4078 return;
4079 }
4080 }
4081
4082 if (m_RestartState == RESTART_NOW && status() != Ekos::FOCUS_ABORTED)
4083 {
4084 if (focuserAdditionalMovement == 0)
4085 {
4086 m_RestartState = RESTART_NONE;
4087 inAutoFocus = inBuildOffsets = inAdjustFocus = inScanStartPos = false;
4088 adaptFocus->setInAdaptiveFocus(false);
4089 appendLogText(i18n("Restarting autofocus process..."));
4090 runAutoFocus(m_AutofocusReason, m_AutofocusReasonInfo);
4091 return;
4092 }
4093 }
4094 else if (m_RestartState == RESTART_ABORT && focuserAdditionalMovement == 0)
4095 {
4096 // We are trying to abort an autofocus run
4097 // This event means that the focuser has been reset and arrived at its starting point
4098 // so we can finish processing the abort. Set inAutoFocus to avoid repeating
4099 // processing already done in completeFocusProcedure
4100 completeFocusProcedure(Ekos::FOCUS_ABORTED, Ekos::FOCUS_FAIL_FORCE_ABORT);
4101 m_RestartState = RESTART_NONE;
4102 inAutoFocus = inBuildOffsets = inAdjustFocus = inScanStartPos = false;
4103 adaptFocus->setInAdaptiveFocus(false);
4104 return;
4105 }
4106 }
4107
4108 if (canAbsMove)
4109 autoFocusProcessPositionChange(newState);
4110 else if (newState == IPS_ALERT)
4111 appendLogText(i18n("Focuser error, check INDI panel."));
4112 return;
4113 }
4114
4115 if (canAbsMove)
4116 return;
4117
4118 if (nvp->isNameMatch("manualfocusdrive"))
4119 {
4120 if (m_DebugFocuser)
4121 {
4122 // Simulate focuser comms issues every 5 moves
4123 if (m_DebugFocuserCounter++ >= 10 && m_DebugFocuserCounter <= 14)
4124 {
4125 if (m_DebugFocuserCounter == 14)
4126 m_DebugFocuserCounter = 0;
4127 appendLogText(i18n("Simulate focuser comms failure..."));
4128 return;
4129 }
4130 }
4131
4132 m_FocusMotionTimer.stop();
4133
4134 INumber *pos = IUFindNumber(nvp, "manualfocusdrive");
4135 IPState newState = nvp->s;
4136 if (pos && newState == IPS_OK)
4137 {
4138 if (focuserAdditionalMovement == 0)
4139 resetButtons();
4140
4141 currentPosition += pos->value;
4142 absTicksLabel->setText(QString::number(static_cast<int>(currentPosition)));
4143 emit absolutePositionChanged(currentPosition);
4144 }
4145
4146 if (inAdjustFocus && newState == IPS_OK)
4147 {
4148 if (focuserAdditionalMovement == 0)
4149 {
4150 inAdjustFocus = false;
4151 emit focusPositionAdjusted();
4152 return;
4153 }
4154 }
4155
4156 if (adaptFocus->inAdaptiveFocus() && newState == IPS_OK)
4157 {
4158 if (focuserAdditionalMovement == 0)
4159 {
4160 adaptFocus->adaptiveFocusAdmin(currentPosition, true, true);
4161 return;
4162 }
4163 }
4164
4165 // restart if focus movement has finished
4166 if (m_RestartState == RESTART_NOW && newState == IPS_OK && status() != Ekos::FOCUS_ABORTED)
4167 {
4168 if (focuserAdditionalMovement == 0)
4169 {
4170 m_RestartState = RESTART_NONE;
4171 inAutoFocus = inBuildOffsets = inAdjustFocus = inScanStartPos = false;
4172 adaptFocus->setInAdaptiveFocus(false);
4173 appendLogText(i18n("Restarting autofocus process..."));
4174 runAutoFocus(m_AutofocusReason, m_AutofocusReasonInfo);
4175 return;
4176 }
4177 }
4178 else if (m_RestartState == RESTART_ABORT && newState == IPS_OK)
4179 {
4180 // Abort the autofocus run now the focuser has finished moving to its start position
4181 completeFocusProcedure(Ekos::FOCUS_ABORTED, Ekos::FOCUS_FAIL_FORCE_ABORT);
4182 m_RestartState = RESTART_NONE;
4183 inAutoFocus = inBuildOffsets = inAdjustFocus = inScanStartPos = false;
4184 adaptFocus->setInAdaptiveFocus(false);
4185 return;
4186 }
4187
4188 if (canRelMove)
4189 autoFocusProcessPositionChange(newState);
4190 else if (newState == IPS_ALERT)
4191 appendLogText(i18n("Focuser error, check INDI panel."));
4192
4193 return;
4194 }
4195
4196 if (nvp->isNameMatch("REL_FOCUS_POSITION"))
4197 {
4198 m_FocusMotionTimer.stop();
4199
4200 INumber *pos = IUFindNumber(nvp, "FOCUS_RELATIVE_POSITION");
4201 IPState newState = nvp->s;
4202 if (pos && newState == IPS_OK)
4203 {
4204 if (focuserAdditionalMovement == 0)
4205 resetButtons();
4206
4207 currentPosition += pos->value * (m_LastFocusDirection == FOCUS_IN ? -1 : 1);
4208 qCDebug(KSTARS_EKOS_FOCUS)
4209 << QString("Rel Focuser position moved %1 by %2 to %3")
4210 .arg((m_LastFocusDirection == FOCUS_IN) ? "in" : "out").arg(pos->value).arg(currentPosition);
4211 absTicksLabel->setText(QString::number(static_cast<int>(currentPosition)));
4212 emit absolutePositionChanged(currentPosition);
4213 }
4214
4215 if (inAdjustFocus && newState == IPS_OK)
4216 {
4217 if (focuserAdditionalMovement == 0)
4218 {
4219 inAdjustFocus = false;
4220 emit focusPositionAdjusted();
4221 return;
4222 }
4223 }
4224
4225 if (adaptFocus->inAdaptiveFocus() && newState == IPS_OK)
4226 {
4227 if (focuserAdditionalMovement == 0)
4228 {
4229 adaptFocus->adaptiveFocusAdmin(currentPosition, true, true);
4230 return;
4231 }
4232 }
4233
4234 // restart if focus movement has finished
4235 if (m_RestartState == RESTART_NOW && newState == IPS_OK && status() != Ekos::FOCUS_ABORTED)
4236 {
4237 if (focuserAdditionalMovement == 0)
4238 {
4239 m_RestartState = RESTART_NONE;
4240 inAutoFocus = inBuildOffsets = inAdjustFocus = inScanStartPos = false;
4241 adaptFocus->setInAdaptiveFocus(false);
4242 appendLogText(i18n("Restarting autofocus process..."));
4243 runAutoFocus(m_AutofocusReason, m_AutofocusReasonInfo);
4244 return;
4245 }
4246 }
4247 else if (m_RestartState == RESTART_ABORT && newState == IPS_OK)
4248 {
4249 // Abort the autofocus run now the focuser has finished moving to its start position
4250 completeFocusProcedure(Ekos::FOCUS_ABORTED, Ekos::FOCUS_FAIL_FORCE_ABORT);
4251 m_RestartState = RESTART_NONE;
4252 inAutoFocus = inBuildOffsets = inAdjustFocus = inScanStartPos = false;
4253 adaptFocus->setInAdaptiveFocus(false);
4254 return;
4255 }
4256
4257 if (canRelMove)
4258 autoFocusProcessPositionChange(newState);
4259 else if (newState == IPS_ALERT)
4260 appendLogText(i18n("Focuser error, check INDI panel."));
4261
4262 return;
4263 }
4264
4265 if (canRelMove)
4266 return;
4267
4268 if (nvp->isNameMatch("FOCUS_TIMER"))
4269 {
4270 IPState newState = nvp->s;
4271 m_FocusMotionTimer.stop();
4272 // restart if focus movement has finished
4273 if (m_RestartState == RESTART_NOW && newState == IPS_OK && status() != Ekos::FOCUS_ABORTED)
4274 {
4275 if (focuserAdditionalMovement == 0)
4276 {
4277 m_RestartState = RESTART_NONE;
4278 inAutoFocus = inBuildOffsets = inAdjustFocus = inScanStartPos = false;
4279 adaptFocus->setInAdaptiveFocus(false);
4280 appendLogText(i18n("Restarting autofocus process..."));
4281 runAutoFocus(m_AutofocusReason, m_AutofocusReasonInfo);
4282 return;
4283 }
4284 }
4285 else if (m_RestartState == RESTART_ABORT && newState == IPS_OK)
4286 {
4287 // Abort the autofocus run now the focuser has finished moving to its start position
4288 completeFocusProcedure(Ekos::FOCUS_ABORTED, Ekos::FOCUS_FAIL_FORCE_ABORT);
4289 m_RestartState = RESTART_NONE;
4290 inAutoFocus = inBuildOffsets = inAdjustFocus = inScanStartPos = false;
4291 adaptFocus->setInAdaptiveFocus(false);
4292 return;
4293 }
4294
4295 if (canAbsMove == false && canRelMove == false)
4296 {
4297 // Used by the linear focus algorithm. Ignored if that's not in use for the timer-focuser.
4298 INumber *pos = IUFindNumber(nvp, "FOCUS_TIMER_VALUE");
4299 if (pos)
4300 {
4301 currentPosition += pos->value * (m_LastFocusDirection == FOCUS_IN ? -1 : 1);
4302 qCDebug(KSTARS_EKOS_FOCUS)
4303 << QString("Timer Focuser position moved %1 by %2 to %3")
4304 .arg((m_LastFocusDirection == FOCUS_IN) ? "in" : "out").arg(pos->value).arg(currentPosition);
4305 }
4306
4307 if (pos && newState == IPS_OK && focuserAdditionalMovement == 0)
4308 resetButtons();
4309
4310 if (inAdjustFocus && newState == IPS_OK)
4311 {
4312 if (focuserAdditionalMovement == 0)
4313 {
4314 inAdjustFocus = false;
4315 emit focusPositionAdjusted();
4316 return;
4317 }
4318 }
4319
4320 if (adaptFocus->inAdaptiveFocus() && newState == IPS_OK)
4321 {
4322 if (focuserAdditionalMovement == 0)
4323 {
4324 adaptFocus->adaptiveFocusAdmin(true, true, true);
4325 return;
4326 }
4327 }
4328
4329 autoFocusProcessPositionChange(newState);
4330 }
4331 else if (newState == IPS_ALERT)
4332 appendLogText(i18n("Focuser error, check INDI panel."));
4333
4334 return;
4335 }
4336}
4337
4338void Focus::appendLogText(const QString &text)
4339{
4340 m_LogText.insert(0, i18nc("log entry; %1 is the date, %2 is the text", "%1 %2",
4341 KStarsData::Instance()->lt().toString("yyyy-MM-ddThh:mm:ss"), text));
4342
4343 qCInfo(KSTARS_EKOS_FOCUS) << text;
4344
4345 emit newLog(text);
4346}
4347
4348void Focus::clearLog()
4349{
4350 m_LogText.clear();
4351 emit newLog(QString());
4352}
4353
4354void Focus::appendFocusLogText(const QString &lines)
4355{
4356 if (Options::focusLogging())
4357 {
4358
4359 if (!m_FocusLogFile.exists())
4360 {
4361 // Create focus-specific log file and write the header record
4362 QDir dir(KSPaths::writableLocation(QStandardPaths::AppLocalDataLocation));
4363 dir.mkpath("focuslogs");
4364 m_FocusLogEnabled = m_FocusLogFile.open(QIODevice::WriteOnly | QIODevice::Text);
4365 if (m_FocusLogEnabled)
4366 {
4367 QTextStream header(&m_FocusLogFile);
4368 header << "date, time, position, temperature, filter, HFR, altitude\n";
4369 header.flush();
4370 }
4371 else
4372 qCWarning(KSTARS_EKOS_FOCUS) << "Failed to open focus log file: " << m_FocusLogFileName;
4373 }
4374
4375 if (m_FocusLogEnabled)
4376 {
4377 QTextStream out(&m_FocusLogFile);
4378 out << QDateTime::currentDateTime().toString("yyyy-MM-dd, hh:mm:ss, ") << lines;
4379 out.flush();
4380 }
4381 }
4382}
4383
4384void Focus::startFraming()
4385{
4386 if (m_Camera == nullptr)
4387 {
4388 appendLogText(i18n("No CCD connected."));
4389 return;
4390 }
4391
4392 waitStarSelectTimer.stop();
4393
4394 inFocusLoop = true;
4395 starMeasureFrames.clear();
4396
4397 clearDataPoints();
4398
4399 //emit statusUpdated(true);
4400 setState(Ekos::FOCUS_FRAMING);
4401
4402 resetButtons();
4403
4404 appendLogText(i18n("Starting continuous exposure..."));
4405
4406 capture();
4407}
4408
4409void Focus::resetButtons()
4410{
4411 if (inFocusLoop)
4412 {
4413 startFocusB->setEnabled(false);
4414 startAbInsB->setEnabled(false);
4415 startLoopB->setEnabled(false);
4416 focusOutB->setEnabled(true);
4417 focusInB->setEnabled(true);
4418 startGotoB->setEnabled(canAbsMove);
4419 stopFocusB->setEnabled(true);
4420 captureB->setEnabled(false);
4421 opticalTrainCombo->setEnabled(false);
4422 trainB->setEnabled(false);
4423 return;
4424 }
4425
4426 if (inAutoFocus)
4427 {
4428 // During an Autofocus run we need to disable input widgets to stop the user changing them
4429 // We need to disable the widgets currently enabled and save a QVector of these to be
4430 // reinstated once the AF run completes.
4431 // Certain widgets (highlighted below) have the isEnabled() property used in the code to
4432 // determine functionality. So the isEnabled state for these is saved off before the
4433 // interface is disabled and these saved states used to control the code path and preserve
4434 // functionality.
4435 // Since this function can be called several times only load up widgets once
4436 if (disabledWidgets.empty())
4437 {
4438 AFDisable(trainLabel, false);
4439 AFDisable(opticalTrainCombo, false);
4440 AFDisable(trainB, false);
4441 AFDisable(focuserGroup, true);
4442 AFDisable(clearDataB, false);
4443
4444 // In the ccdGroup save the enabled state of Gain and ISO
4445 m_FocusGainAFEnabled = focusGain->isEnabled();
4446 m_FocusISOAFEnabled = focusISO->isEnabled();
4447 AFDisable(ccdGroup, false);
4448
4449 AFDisable(toolsGroup, false);
4450
4451 // Save the enabled state of SubFrame
4452 m_FocusSubFrameAFEnabled = m_OpsFocusSettings->focusSubFrame->isEnabled();
4453
4454 // Disable parameters and tools to prevent changes while Autofocus is running
4455 AFDisable(m_OpsFocusSettings, false);
4456 AFDisable(m_OpsFocusProcess, false);
4457 AFDisable(m_OpsFocusMechanics, false);
4458 AFDisable(m_AdvisorDialog, false);
4459 AFDisable(m_CFZDialog, false);
4460
4461 // Enable the "stop" button so the user can abort an AF run
4462 stopFocusB->setEnabled(true);
4463 }
4464 return;
4465 }
4466
4467 // Restore widgets that were disabled when Autofocus started
4468 for(int i = 0 ; i < disabledWidgets.size() ; i++)
4469 disabledWidgets[i]->setEnabled(true);
4470 disabledWidgets.clear();
4471
4472 auto enableCaptureButtons = (m_captureInProgress == false && m_starDetectInProgress == false);
4473
4474 captureB->setEnabled(enableCaptureButtons);
4475 resetFrameB->setEnabled(enableCaptureButtons);
4476 startLoopB->setEnabled(enableCaptureButtons);
4477 m_OpsFocusSettings->focusAutoStarEnabled->setEnabled(enableCaptureButtons
4478 && m_OpsFocusSettings->focusUseFullField->isChecked() == false);
4479
4480 if (m_Focuser && m_Focuser->isConnected())
4481 {
4482 focusOutB->setEnabled(true);
4483 focusInB->setEnabled(true);
4484
4485 startFocusB->setEnabled(m_FocusType == FOCUS_AUTO);
4486 startAbInsB->setEnabled(canAbInsStart());
4487 stopFocusB->setEnabled(!enableCaptureButtons);
4488 startGotoB->setEnabled(canAbsMove);
4489 stopGotoB->setEnabled(true);
4490 }
4491 else
4492 {
4493 focusOutB->setEnabled(false);
4494 focusInB->setEnabled(false);
4495
4496 startFocusB->setEnabled(false);
4497 startAbInsB->setEnabled(false);
4498 stopFocusB->setEnabled(false);
4499 startGotoB->setEnabled(false);
4500 stopGotoB->setEnabled(false);
4501 }
4502}
4503
4504// Return whether the Aberration Inspector Start button should be enabled. The pre-requisties are:
4505// - Absolute position focuser
4506// - Mosaic Mask is on
4507// - Algorithm is LINEAR 1 PASS
4508bool Focus::canAbInsStart()
4509{
4510 return canAbsMove && m_FocusAlgorithm == FOCUS_LINEAR1PASS && m_currentImageMask == FOCUS_MASK_MOSAIC;
4511}
4512
4513// Disable input widgets during an Autofocus run. Keep a record so after the AF run, widgets can be re-enabled
4514void Focus::AFDisable(QWidget * widget, const bool children)
4515{
4516 if (children)
4517 {
4518 // The parent widget has been passed in so disable any enabled child widgets
4519 for(auto *wid : widget->findChildren<QWidget *>())
4520 {
4521 if (wid->isEnabled())
4522 {
4523 wid->setEnabled(false);
4524 disabledWidgets.push_back(wid);
4525 }
4526 }
4527
4528 }
4529 else if (widget->isEnabled())
4530 {
4531 // Base level widget or group of widgets, so just disable the passed what was passed in
4532 widget->setEnabled(false);
4533 disabledWidgets.push_back(widget);
4534 }
4535}
4536
4537bool Focus::isFocusGainEnabled()
4538{
4539 return (inAutoFocus) ? m_FocusGainAFEnabled : focusGain->isEnabled();
4540}
4541
4542bool Focus::isFocusISOEnabled()
4543{
4544 return (inAutoFocus) ? m_FocusISOAFEnabled : focusISO->isEnabled();
4545}
4546
4547bool Focus::isFocusSubFrameEnabled()
4548{
4549 return (inAutoFocus) ? m_FocusSubFrameAFEnabled : m_OpsFocusSettings->focusSubFrame->isEnabled();
4550}
4551
4552void Focus::updateBoxSize(int value)
4553{
4554 if (m_Camera == nullptr)
4555 return;
4556
4557 ISD::CameraChip *targetChip = m_Camera->getChip(ISD::CameraChip::PRIMARY_CCD);
4558
4559 if (targetChip == nullptr)
4560 return;
4561
4562 int subBinX, subBinY;
4563 targetChip->getBinning(&subBinX, &subBinY);
4564
4565 QRect trackBox = m_FocusView->getTrackingBox();
4566 QPoint center(trackBox.x() + (trackBox.width() / 2), trackBox.y() + (trackBox.height() / 2));
4567
4568 trackBox =
4569 QRect(center.x() - value / (2 * subBinX), center.y() - value / (2 * subBinY), value / subBinX, value / subBinY);
4570
4571 m_FocusView->setTrackingBox(trackBox);
4572}
4573
4574void Focus::selectFocusStarFraction(double x, double y)
4575{
4576 if (m_ImageData.isNull())
4577 return;
4578
4579 focusStarSelected(x * m_ImageData->width(), y * m_ImageData->height());
4580 // Focus view timer takes 50ms second to update, so let's emit afterwards.
4581 QTimer::singleShot(250, this, [this]()
4582 {
4583 emit newImage(m_FocusView);
4584 });
4585}
4586
4587void Focus::focusStarSelected(int x, int y)
4588{
4589 if (state() == Ekos::FOCUS_PROGRESS)
4590 return;
4591
4592 if (subFramed == false)
4593 {
4594 rememberStarCenter.setX(x);
4595 rememberStarCenter.setY(y);
4596 }
4597
4598 ISD::CameraChip *targetChip = m_Camera->getChip(ISD::CameraChip::PRIMARY_CCD);
4599
4600 int subBinX, subBinY;
4601 targetChip->getBinning(&subBinX, &subBinY);
4602
4603 // If binning was changed outside of the focus module, recapture
4604 if (subBinX != (focusBinning->currentIndex() + 1))
4605 {
4606 capture();
4607 return;
4608 }
4609
4610 int offset = (static_cast<double>(m_OpsFocusSettings->focusBoxSize->value()) / subBinX) * 1.5;
4611
4613
4614 bool squareMovedOutside = false;
4615
4616 if (subFramed == false && m_OpsFocusSettings->focusSubFrame->isChecked() && targetChip->canSubframe())
4617 {
4618 int minX, maxX, minY, maxY, minW, maxW, minH, maxH; //, fx,fy,fw,fh;
4619
4620 targetChip->getFrameMinMax(&minX, &maxX, &minY, &maxY, &minW, &maxW, &minH, &maxH);
4621 //targetChip->getFrame(&fx, &fy, &fw, &fy);
4622
4623 x = (x - offset) * subBinX;
4624 y = (y - offset) * subBinY;
4625 int w = offset * 2 * subBinX;
4626 int h = offset * 2 * subBinY;
4627
4628 if (x < minX)
4629 x = minX;
4630 if (y < minY)
4631 y = minY;
4632 if ((x + w) > maxW)
4633 w = maxW - x;
4634 if ((y + h) > maxH)
4635 h = maxH - y;
4636
4637 //fx += x;
4638 //fy += y;
4639 //fw = w;
4640 //fh = h;
4641
4642 //targetChip->setFocusFrame(fx, fy, fw, fh);
4643 //frameModified=true;
4644
4645 QVariantMap settings = frameSettings[targetChip];
4646 settings["x"] = x;
4647 settings["y"] = y;
4648 settings["w"] = w;
4649 settings["h"] = h;
4650 settings["binx"] = subBinX;
4651 settings["biny"] = subBinY;
4652
4653 frameSettings[targetChip] = settings;
4654
4655 subFramed = true;
4656
4657 qCDebug(KSTARS_EKOS_FOCUS) << "Frame is subframed. X:" << x << "Y:" << y << "W:" << w << "H:" << h << "binX:" << subBinX <<
4658 "binY:" << subBinY;
4659
4660 m_FocusView->setFirstLoad(true);
4661
4662 capture();
4663
4664 //starRect = QRect((w-focusBoxSize->value())/(subBinX*2), (h-focusBoxSize->value())/(subBinY*2), focusBoxSize->value()/subBinX, focusBoxSize->value()/subBinY);
4665 starCenter.setX(w / (2 * subBinX));
4666 starCenter.setY(h / (2 * subBinY));
4667 }
4668 else
4669 {
4670 //starRect = QRect(x-focusBoxSize->value()/(subBinX*2), y-focusBoxSize->value()/(subBinY*2), focusBoxSize->value()/subBinX, focusBoxSize->value()/subBinY);
4671 double dist = sqrt((starCenter.x() - x) * (starCenter.x() - x) + (starCenter.y() - y) * (starCenter.y() - y));
4672
4673 squareMovedOutside = (dist > (static_cast<double>(m_OpsFocusSettings->focusBoxSize->value()) / subBinX));
4674 starCenter.setX(x);
4675 starCenter.setY(y);
4676 //starRect = QRect( starCenter.x()-focusBoxSize->value()/(2*subBinX), starCenter.y()-focusBoxSize->value()/(2*subBinY), focusBoxSize->value()/subBinX, focusBoxSize->value()/subBinY);
4677 starRect = QRect(starCenter.x() - m_OpsFocusSettings->focusBoxSize->value() / (2 * subBinX),
4678 starCenter.y() - m_OpsFocusSettings->focusBoxSize->value() / (2 * subBinY),
4679 m_OpsFocusSettings->focusBoxSize->value() / subBinX,
4680 m_OpsFocusSettings->focusBoxSize->value() / subBinY);
4681 m_FocusView->setTrackingBox(starRect);
4682 }
4683
4684 starsHFR.clear();
4685
4686 starCenter.setZ(subBinX);
4687
4688 if (squareMovedOutside && inAutoFocus == false && m_OpsFocusSettings->focusAutoStarEnabled->isChecked())
4689 {
4690 m_OpsFocusSettings->focusAutoStarEnabled->blockSignals(true);
4691 m_OpsFocusSettings->focusAutoStarEnabled->setChecked(false);
4692 m_OpsFocusSettings->focusAutoStarEnabled->blockSignals(false);
4693 appendLogText(i18n("Disabling Auto Star Selection as star selection box was moved manually."));
4694 starSelected = false;
4695 }
4696 else if (starSelected == false)
4697 {
4698 appendLogText(i18n("Focus star is selected."));
4699 starSelected = true;
4700 capture();
4701 }
4702
4703 waitStarSelectTimer.stop();
4704 FocusState nextState = inAutoFocus ? FOCUS_PROGRESS : FOCUS_IDLE;
4705 if (nextState != state())
4706 {
4707 setState(nextState);
4708 }
4709}
4710
4711void Focus::checkFocus(double requiredHFR)
4712{
4713 if (inAutoFocus || inAdjustFocus || adaptFocus->inAdaptiveFocus())
4714 {
4715 QString str;
4716 if (inAutoFocus)
4717 str = i18n("Autofocus");
4718 else if (inAdjustFocus)
4719 str = i18n("Adjust Focus");
4720 else
4721 str = i18n("Adaptive Focus");
4722
4723 if (++m_StartRetries < MAXIMUM_RESTART_ITERATIONS)
4724 {
4725 appendLogText(i18n("Check focus request - Waiting 10sec for %1 to complete.", str));
4726 QTimer::singleShot(10 * 1000, this, [this, requiredHFR]()
4727 {
4728 checkFocus(requiredHFR);
4729 });
4730 return;
4731 }
4732
4733 appendLogText(i18n("Discarding Check Focus request - %1 in progress.", str));
4734 completeFocusProcedure(Ekos::FOCUS_ABORTED, Ekos::FOCUS_FAIL_INTERNAL);
4735 return;
4736 }
4737
4738 m_StartRetries = 0;
4739
4740 if (inFocusLoop || inBuildOffsets)
4741 {
4742 QString str = inFocusLoop ? "Focus Looping" : " Build Offsets";
4743 qCDebug(KSTARS_EKOS_FOCUS) << QString("Check Focus rejected, %1 is already running.").arg(str);
4744 }
4745 else
4746 {
4747 qCDebug(KSTARS_EKOS_FOCUS) << "Check Focus requested with minimum required HFR" << requiredHFR;
4748 minimumRequiredHFR = requiredHFR;
4749
4750 appendLogText("Capturing to check HFR...");
4751 capture();
4752 }
4753}
4754
4755void Focus::toggleSubframe(bool enable)
4756{
4757 if (enable == false)
4758 resetFrame();
4759
4760 starSelected = false;
4761 starCenter = QVector3D();
4762
4763 if (enable)
4764 {
4765 // sub frame focusing
4766 m_OpsFocusSettings->focusAutoStarEnabled->setEnabled(true);
4767 // disable focus image mask
4768 m_OpsFocusSettings->focusNoMaskRB->setChecked(true);
4769 }
4770 else
4771 {
4772 // full frame focusing
4773 m_OpsFocusSettings->focusAutoStarEnabled->setChecked(false);
4774 m_OpsFocusSettings->focusAutoStarEnabled->setEnabled(false);
4775 }
4776 // update image mask controls
4777 selectImageMask();
4778 // enable focus mask selection if full field is selected
4779 m_OpsFocusSettings->focusRingMaskRB->setEnabled(!enable);
4780 m_OpsFocusSettings->focusMosaicMaskRB->setEnabled(!enable);
4781
4782 setUseWeights();
4783}
4784
4785// Set the useWeights widget based on various other user selected parameters
4786// 1. weights are only available with the LM solver used by Hyperbola and Parabola
4787// 2. weights are only used for multiple stars so only if full frame is selected
4788// 3. weights are only used for star measures involving multiple star measures: HFR, HFR_ADJ and FWHM
4789void Focus::setUseWeights()
4790{
4791 if (m_CurveFit == CurveFitting::FOCUS_QUADRATIC || !m_OpsFocusSettings->focusUseFullField->isChecked()
4792 || m_StarMeasure == FOCUS_STAR_NUM_STARS
4793 || m_StarMeasure == FOCUS_STAR_FOURIER_POWER)
4794 {
4795 m_OpsFocusProcess->focusUseWeights->setEnabled(false);
4796 m_OpsFocusProcess->focusUseWeights->setChecked(false);
4797 }
4798 else
4799 m_OpsFocusProcess->focusUseWeights->setEnabled(true);
4800
4801}
4802
4803// Set the setDonutBuster widget based on various other user selected parameters
4804// 1. Donut Buster is only available for algorithm: Linear 1 Pass
4805// 2. Donut Buster is available for measures: HFR, HFR Adj and FWHM
4806// 3. Donut Buster is available for walks: Fixed and CFZ Shuffle
4807// 4. Donut Buster is available for curves fits: Hyperbola and Parabola
4808void Focus::setDonutBuster()
4809{
4810 if (m_FocusAlgorithm != FOCUS_LINEAR1PASS)
4811 {
4812 m_OpsFocusProcess->focusDonut->hide();
4813 m_OpsFocusProcess->focusDonut->setEnabled(false);
4814 m_OpsFocusProcess->focusDonut->setChecked(false);
4815 }
4816 else
4817 {
4818 m_OpsFocusProcess->focusDonut->show();
4819 if ((m_StarMeasure == FOCUS_STAR_HFR || m_StarMeasure == FOCUS_STAR_HFR_ADJ || m_StarMeasure == FOCUS_STAR_FWHM) &&
4820 (m_FocusWalk == FOCUS_WALK_FIXED_STEPS || m_FocusWalk == FOCUS_WALK_CFZ_SHUFFLE) &&
4821 (m_CurveFit != CurveFitting::FOCUS_QUADRATIC))
4822 m_OpsFocusProcess->focusDonut->setEnabled(true);
4823 else
4824 {
4825 m_OpsFocusProcess->focusDonut->setEnabled(false);
4826 m_OpsFocusProcess->focusDonut->setChecked(false);
4827 }
4828 }
4829}
4830
4831void Focus::setExposure(double value)
4832{
4833 focusExposure->setValue(value);
4834}
4835
4836void Focus::setBinning(int subBinX, int subBinY)
4837{
4838 INDI_UNUSED(subBinY);
4839 focusBinning->setCurrentIndex(subBinX - 1);
4840}
4841
4842void Focus::setAutoStarEnabled(bool enable)
4843{
4844 m_OpsFocusSettings->focusAutoStarEnabled->setChecked(enable);
4845}
4846
4847void Focus::setAutoSubFrameEnabled(bool enable)
4848{
4849 m_OpsFocusSettings->focusSubFrame->setChecked(enable);
4850}
4851
4852void Focus::setAutoFocusParameters(int boxSize, int stepSize, int maxTravel, double tolerance)
4853{
4854 m_OpsFocusSettings->focusBoxSize->setValue(boxSize);
4855 m_OpsFocusMechanics->focusTicks->setValue(stepSize);
4856 m_OpsFocusMechanics->focusMaxTravel->setValue(maxTravel);
4857 m_OpsFocusProcess->focusTolerance->setValue(tolerance);
4858}
4859
4860void Focus::checkAutoStarTimeout()
4861{
4862 //if (starSelected == false && inAutoFocus)
4863 if (starCenter.isNull() && (inAutoFocus || minimumRequiredHFR > 0))
4864 {
4865 if (inAutoFocus)
4866 {
4867 if (rememberStarCenter.isNull() == false)
4868 {
4869 focusStarSelected(rememberStarCenter.x(), rememberStarCenter.y());
4870 appendLogText(i18n("No star was selected. Using last known position..."));
4871 return;
4872 }
4873 }
4874
4875 initialFocuserAbsPosition = -1;
4876 appendLogText(i18n("No star was selected. Aborting..."));
4877 completeFocusProcedure(Ekos::FOCUS_ABORTED, Ekos::FOCUS_FAIL_NO_STARS);
4878 }
4879 else if (state() == FOCUS_WAITING)
4880 setState(FOCUS_IDLE);
4881}
4882
4883void Focus::setAbsoluteFocusTicks()
4884{
4885 if (absTicksSpin->value() == currentPosition)
4886 {
4887 appendLogText(i18n("Focuser already at %1...", currentPosition));
4888 return;
4889 }
4890 focusInB->setEnabled(false);
4891 focusOutB->setEnabled(false);
4892 startGotoB->setEnabled(false);
4893 if (!changeFocus(absTicksSpin->value() - currentPosition))
4894 qCDebug(KSTARS_EKOS_FOCUS) << "setAbsoluteFocusTicks unable to move focuser.";
4895}
4896
4897void Focus::syncTrackingBoxPosition()
4898{
4899 ISD::CameraChip *targetChip = m_Camera->getChip(ISD::CameraChip::PRIMARY_CCD);
4900 Q_ASSERT(targetChip);
4901
4902 int subBinX = 1, subBinY = 1;
4903 targetChip->getBinning(&subBinX, &subBinY);
4904
4905 if (starCenter.isNull() == false)
4906 {
4907 double boxSize = m_OpsFocusSettings->focusBoxSize->value();
4908 int x, y, w, h;
4909 targetChip->getFrame(&x, &y, &w, &h);
4910 // If box size is larger than image size, set it to lower index
4911 if (boxSize / subBinX >= w || boxSize / subBinY >= h)
4912 {
4913 m_OpsFocusSettings->focusBoxSize->setValue((boxSize / subBinX >= w) ? w : h);
4914 return;
4915 }
4916
4917 // If binning changed, update coords accordingly
4918 if (subBinX != starCenter.z())
4919 {
4920 if (starCenter.z() > 0)
4921 {
4922 starCenter.setX(starCenter.x() * (starCenter.z() / subBinX));
4923 starCenter.setY(starCenter.y() * (starCenter.z() / subBinY));
4924 }
4925
4926 starCenter.setZ(subBinX);
4927 }
4928
4929 QRect starRect = QRect(starCenter.x() - boxSize / (2 * subBinX), starCenter.y() - boxSize / (2 * subBinY),
4930 boxSize / subBinX, boxSize / subBinY);
4931 m_FocusView->setTrackingBoxEnabled(true);
4932 m_FocusView->setTrackingBox(starRect);
4933 }
4934}
4935
4936void Focus::showFITSViewer()
4937{
4938 static int lastFVTabID = -1;
4939 if (m_ImageData)
4940 {
4941 QUrl url = QUrl::fromLocalFile("focus.fits");
4942 if (fv.isNull())
4943 {
4944 fv = KStars::Instance()->createFITSViewer();
4945 fv->loadData(m_ImageData, url, &lastFVTabID);
4946 connect(fv.get(), &FITSViewer::terminated, this, [this]()
4947 {
4948 fv.clear();
4949 });
4950 }
4951 else if (fv->updateData(m_ImageData, url, lastFVTabID, &lastFVTabID) == false)
4952 fv->loadData(m_ImageData, url, &lastFVTabID);
4953
4954 fv->show();
4955 }
4956}
4957
4958void Focus::adjustFocusOffset(int value, bool useAbsoluteOffset)
4959{
4960 // Allow focuser adjustments during looping & autofocus to honour Filter Offsets
4961 if (inAdjustFocus || adaptFocus->inAdaptiveFocus())
4962 {
4963 QString str = inAdjustFocus ? i18n("Adjust Focus") : i18n("Adaptive Focus");
4964 if (++m_StartRetries < MAXIMUM_RESTART_ITERATIONS)
4965 {
4966 appendLogText(i18n("Adjust focus request - Waiting 10sec for %1 to complete.", str));
4967 QTimer::singleShot(10 * 1000, this, [this, value, useAbsoluteOffset] ()
4968 {
4969 adjustFocusOffset(value, useAbsoluteOffset);
4970 });
4971 return;
4972 }
4973
4974 qCDebug(KSTARS_EKOS_FOCUS) << "adjustFocusOffset called whilst" << str << "in progress. Ignoring...";
4975 emit focusPositionAdjusted();
4976 return;
4977 }
4978
4979 m_StartRetries = 0;
4980 inAdjustFocus = true;
4981
4982 // Get the new position
4983 int newPosition = (useAbsoluteOffset) ? value : value + currentPosition;
4984
4985 if (!changeFocus(newPosition - currentPosition))
4986 qCDebug(KSTARS_EKOS_FOCUS) << "adjustFocusOffset unable to move focuser";
4987}
4988
4989void Focus::toggleFocusingWidgetFullScreen()
4990{
4991 if (focusingWidget->parent() == nullptr)
4992 {
4993 focusingWidget->setParent(this);
4994 rightLayout->insertWidget(0, focusingWidget);
4995 focusingWidget->showNormal();
4996 }
4997 else
4998 {
4999 focusingWidget->setParent(nullptr);
5000 focusingWidget->setWindowTitle(i18nc("@title:window", "Focus Frame"));
5002 focusingWidget->showMaximized();
5003 focusingWidget->show();
5004 }
5005}
5006
5007void Focus::setMountStatus(ISD::Mount::Status newState)
5008{
5009 switch (newState)
5010 {
5011 case ISD::Mount::MOUNT_PARKING:
5012 case ISD::Mount::MOUNT_SLEWING:
5013 case ISD::Mount::MOUNT_MOVING:
5014 captureB->setEnabled(false);
5015 startFocusB->setEnabled(false);
5016 startAbInsB->setEnabled(false);
5017 startLoopB->setEnabled(false);
5018
5019 // If mount is moved while we have a star selected and subframed
5020 // let us reset the frame.
5021 if (subFramed)
5022 resetFrame();
5023
5024 break;
5025
5026 default:
5027 resetButtons();
5028 break;
5029 }
5030}
5031
5032void Focus::setMountCoords(const SkyPoint &position, ISD::Mount::PierSide pierSide, const dms &ha)
5033{
5034 Q_UNUSED(pierSide)
5035 Q_UNUSED(ha)
5036 mountAlt = position.alt().Degrees();
5037}
5038
5039void Focus::removeDevice(const QSharedPointer<ISD::GenericDevice> &deviceRemoved)
5040{
5041 auto name = deviceRemoved->getDeviceName();
5042
5043 // Check in Focusers
5044
5045 if (m_Focuser && m_Focuser->getDeviceName() == name)
5046 {
5047 m_Focuser->disconnect(this);
5048 m_Focuser = nullptr;
5049 QTimer::singleShot(1000, this, [this]()
5050 {
5051 checkFocuser();
5052 resetButtons();
5053 });
5054 }
5055
5056 // Check in Temperature Sources.
5057 for (auto &oneSource : m_TemperatureSources)
5058 {
5059 if (oneSource->getDeviceName() == name)
5060 {
5061 // clear reference to avoid runtime exception in checkTemperatureSource()
5062 if (m_LastSourceDeviceAutofocusTemperature && m_LastSourceDeviceAutofocusTemperature->getDeviceName() == name)
5063 m_LastSourceDeviceAutofocusTemperature.reset(nullptr);
5064
5065 m_TemperatureSources.removeAll(oneSource);
5066 QTimer::singleShot(1000, this, [this, name]()
5067 {
5069 });
5070 break;
5071 }
5072 }
5073
5074 // Check camera
5075 if (m_Camera && m_Camera->getDeviceName() == name)
5076 {
5077 m_Camera->disconnect(this);
5078 m_Camera = nullptr;
5079
5080 QTimer::singleShot(1000, this, [this]()
5081 {
5082 checkCamera();
5083 resetButtons();
5084 });
5085 }
5086
5087 // Check Filter
5088 if (m_FilterWheel && m_FilterWheel->getDeviceName() == name)
5089 {
5090 m_FilterWheel->disconnect(this);
5091 m_FilterWheel = nullptr;
5092
5093 QTimer::singleShot(1000, this, [this]()
5094 {
5095 checkFilter();
5096 resetButtons();
5097 });
5098 }
5099}
5100
5101void Focus::setupFilterManager()
5102{
5103 // Do we have an existing filter manager?
5104 if (m_FilterManager)
5105 m_FilterManager->disconnect(this);
5106
5107 // Create new or refresh device
5108 Ekos::Manager::Instance()->createFilterManager(m_FilterWheel);
5109
5110 // Return global filter manager for this filter wheel.
5111 Ekos::Manager::Instance()->getFilterManager(m_FilterWheel->getDeviceName(), m_FilterManager);
5112
5113 // Focus Module ----> Filter Manager connections
5114
5115 // Update focuser absolute position.
5116 connect(this, &Focus::absolutePositionChanged, m_FilterManager.get(), &FilterManager::setFocusAbsolutePosition);
5117
5118 // Update Filter Manager state
5119 connect(this, &Focus::newStatus, this, [this](Ekos::FocusState state)
5120 {
5121 if (m_FilterManager)
5122 {
5123 m_FilterManager->setFocusStatus(state);
5124 if (focusFilter->currentIndex() != -1 && canAbsMove && state == Ekos::FOCUS_COMPLETE)
5125 {
5126 m_FilterManager->setFilterAbsoluteFocusDetails(focusFilter->currentIndex(), currentPosition,
5127 m_LastSourceAutofocusTemperature, m_LastSourceAutofocusAlt);
5128 }
5129 }
5130 });
5131
5132 // Filter Manager ----> Focus Module connections
5133
5134 // Suspend guiding if filter offset is change with OAG
5135 connect(m_FilterManager.get(), &FilterManager::newStatus, this, [this](Ekos::FilterState filterState)
5136 {
5137 // If we are changing filter offset while idle, then check if we need to suspend guiding.
5138 if (filterState == FILTER_OFFSET && state() != Ekos::FOCUS_PROGRESS)
5139 {
5140 if (m_GuidingSuspended == false && m_OpsFocusSettings->focusSuspendGuiding->isChecked())
5141 {
5142 m_GuidingSuspended = true;
5143 emit suspendGuiding();
5144 }
5145 }
5146 });
5147
5148 // Take action once filter manager completes filter position
5149 connect(m_FilterManager.get(), &FilterManager::ready, this, [this]()
5150 {
5151 // Keep the focusFilter widget consistent with the filter wheel
5152 if (focusFilter->currentIndex() != currentFilterPosition - 1)
5153 focusFilter->setCurrentIndex(currentFilterPosition - 1);
5154
5155 if (filterPositionPending)
5156 {
5157 filterPositionPending = false;
5158 capture();
5159 }
5160 else if (fallbackFilterPending)
5161 {
5162 fallbackFilterPending = false;
5163 emit newStatus(state());
5164 }
5165 });
5166
5167 // Take action when filter operation fails
5168 connect(m_FilterManager.get(), &FilterManager::failed, this, [this]()
5169 {
5170 appendLogText(i18n("Filter operation failed."));
5171 completeFocusProcedure(Ekos::FOCUS_ABORTED, Ekos::FOCUS_FAIL_FILTER_MANAGER);
5172 });
5173
5174 // Run Autofocus if required by filter manager
5175 connect(m_FilterManager.get(), &FilterManager::runAutoFocus, this, &Focus::runAutoFocus);
5176
5177 // Abort Autofocus if required by filter manager
5178 connect(m_FilterManager.get(), &FilterManager::abortAutoFocus, this, &Focus::abort);
5179
5180 // Adjust focus offset
5181 connect(m_FilterManager.get(), &FilterManager::newFocusOffset, this, &Focus::adjustFocusOffset);
5182
5183 // Update labels
5184 connect(m_FilterManager.get(), &FilterManager::labelsChanged, this, [this]()
5185 {
5186 focusFilter->clear();
5187 focusFilter->addItems(m_FilterManager->getFilterLabels());
5188 currentFilterPosition = m_FilterManager->getFilterPosition();
5189 focusFilter->setCurrentIndex(currentFilterPosition - 1);
5190 });
5191
5192 // Position changed
5193 connect(m_FilterManager.get(), &FilterManager::positionChanged, this, [this]()
5194 {
5195 currentFilterPosition = m_FilterManager->getFilterPosition();
5196 focusFilter->setCurrentIndex(currentFilterPosition - 1);
5197 });
5198
5199 // Exposure Changed
5200 connect(m_FilterManager.get(), &FilterManager::exposureChanged, this, [this]()
5201 {
5202 focusExposure->setValue(m_FilterManager->getFilterExposure());
5203 });
5204
5205 // Wavelength Changed
5206 connect(m_FilterManager.get(), &FilterManager::wavelengthChanged, this, [this]()
5207 {
5208 wavelengthChanged();
5209 });
5210}
5211
5212void Focus::connectFilterManager()
5213{
5214 // Show filter manager if toggled.
5216 {
5217 if (m_FilterManager)
5218 {
5219 m_FilterManager->refreshFilterModel();
5220 m_FilterManager->show();
5221 m_FilterManager->raise();
5222 }
5223 });
5224
5225 // Resume guiding if suspended after focus position is adjusted.
5226 connect(this, &Focus::focusPositionAdjusted, this, [this]()
5227 {
5228 if (m_FilterManager)
5229 m_FilterManager->setFocusOffsetComplete();
5230 if (m_GuidingSuspended && state() != Ekos::FOCUS_PROGRESS)
5231 {
5232 QTimer::singleShot(m_OpsFocusMechanics->focusSettleTime->value() * 1000, this, [this]()
5233 {
5234 m_GuidingSuspended = false;
5235 emit resumeGuiding();
5236 });
5237 }
5238 });
5239
5240 // Save focus exposure for a particular filter
5242 {
5243 // Don't save if donut processing is changing this field
5244 if (inAutoFocus && m_OpsFocusProcess->focusDonut->isEnabled())
5245 return;
5246
5247 if (m_FilterManager)
5248 m_FilterManager->setFilterExposure(focusFilter->currentIndex(), focusExposure->value());
5249 });
5250
5251 // Load exposure if filter is changed.
5252 connect(focusFilter, &QComboBox::currentTextChanged, this, [this](const QString & text)
5253 {
5254 if (m_FilterManager)
5255 {
5256 focusExposure->setValue(m_FilterManager->getFilterExposure(text));
5257 // Update the CFZ for the new filter - force all data back to OT & filter values
5258 resetCFZToOT();
5259 }
5260 });
5261
5262}
5263
5264void Focus::toggleVideo(bool enabled)
5265{
5266 if (m_Camera == nullptr)
5267 return;
5268
5269 if (m_Camera->isBLOBEnabled() == false)
5270 {
5271
5272 if (Options::guiderType() != Ekos::Guide::GUIDE_INTERNAL)
5273 m_Camera->setBLOBEnabled(true);
5274 else
5275 {
5276 connect(KSMessageBox::Instance(), &KSMessageBox::accepted, this, [this, enabled]()
5277 {
5278 //QObject::disconnect(KSMessageBox::Instance(), &KSMessageBox::accepted, this, nullptr);
5279 KSMessageBox::Instance()->disconnect(this);
5280 m_Camera->setVideoStreamEnabled(enabled);
5281 });
5282 KSMessageBox::Instance()->questionYesNo(i18n("Image transfer is disabled for this camera. Would you like to enable it?"));
5283 }
5284 }
5285 else
5286 m_Camera->setVideoStreamEnabled(enabled);
5287}
5288
5289//void Focus::setWeatherData(const std::vector<ISD::Weather::WeatherData> &data)
5290//{
5291// auto pos = std::find_if(data.begin(), data.end(), [](ISD::Weather::WeatherData oneEntry)
5292// {
5293// return (oneEntry.name == "WEATHER_TEMPERATURE");
5294// });
5295
5296// if (pos != data.end())
5297// {
5298// updateTemperature(OBSERVATORY_TEMPERATURE, pos->value);
5299// }
5300//}
5301
5302void Focus::setVideoStreamEnabled(bool enabled)
5303{
5304 if (enabled)
5305 {
5306 liveVideoB->setChecked(true);
5307 liveVideoB->setIcon(QIcon::fromTheme("camera-on"));
5308 }
5309 else
5310 {
5311 liveVideoB->setChecked(false);
5312 liveVideoB->setIcon(QIcon::fromTheme("camera-ready"));
5313 }
5314}
5315
5316void Focus::processCaptureTimeout()
5317{
5318 m_captureInProgress = false;
5319
5320 if (m_abortInProgress)
5321 {
5322 // Timeout occured whilst trying to abort AF. So do no further processing - stop function will call abortExposure()
5323 qCDebug(KSTARS_EKOS_FOCUS) << "Abort AF: discarding frame in " << __FUNCTION__;
5324 return;
5325 }
5326
5327 captureTimeoutCounter++;
5328
5329 if (captureTimeoutCounter >= 3)
5330 {
5331 captureTimeoutCounter = 0;
5332 m_MissingCameraCounter = 0;
5333 captureTimeout.stop();
5334 appendLogText(i18n("Exposure timeout. Aborting..."));
5335 completeFocusProcedure(Ekos::FOCUS_ABORTED, Ekos::FOCUS_FAIL_CAPTURE_TIMEOUT);
5336 }
5337 else if (m_Camera)
5338 {
5339 appendLogText(i18n("Exposure timeout. Restarting exposure..."));
5340 ISD::CameraChip *targetChip = m_Camera->getChip(ISD::CameraChip::PRIMARY_CCD);
5341 targetChip->abortExposure();
5342
5343 capture(focusExposure->value());
5344 }
5345 // Don't allow this to happen all night. We will repeat checking (most likely for activeCamera()
5346 // another 200s = 40 * 5s, but after that abort capture.
5347 else if (m_MissingCameraCounter < 40)
5348 {
5349 m_MissingCameraCounter++;
5350 qCDebug(KSTARS_EKOS_FOCUS) << "Unable to restart focus exposure as camera is missing, trying again in 5 seconds...";
5351 QTimer::singleShot(5000, this, &Focus::processCaptureTimeout);
5352 }
5353 else
5354 {
5355 m_MissingCameraCounter = 0;
5356 captureTimeoutCounter = 0;
5357 captureTimeout.stop();
5358 appendLogText(i18n("Exposure timeout. Aborting..."));
5359 completeFocusProcedure(Ekos::FOCUS_ABORTED, Ekos::FOCUS_FAIL_CAPTURE_TIMEOUT);
5360 }
5361
5362}
5363
5364void Focus::processCaptureErrorDefault()
5365{
5366 processCaptureError(ISD::Camera::ERROR_CAPTURE);
5367}
5368
5369void Focus::processCaptureError(ISD::Camera::ErrorType type)
5370{
5371 m_captureInProgress = false;
5372 captureTimeout.stop();
5373
5374 if (m_abortInProgress)
5375 {
5376 qCDebug(KSTARS_EKOS_FOCUS) << "Abort AF: discarding frame in " << __FUNCTION__;
5377 return;
5378 }
5379
5380 if (type == ISD::Camera::ERROR_SAVE)
5381 {
5382 appendLogText(i18n("Failed to save image. Aborting..."));
5383 completeFocusProcedure(Ekos::FOCUS_ABORTED, Ekos::FOCUS_FAIL_CAPTURE_FAILED);
5384 return;
5385 }
5386
5387
5388 if (m_Camera)
5389 {
5390 captureFailureCounter++;
5391
5392 if (captureFailureCounter >= 3)
5393 {
5394 captureFailureCounter = 0;
5395 captureTimeoutCounter = 0;
5396 m_MissingCameraCounter = 0;
5397 appendLogText(i18n("Exposure failure. Aborting..."));
5398 completeFocusProcedure(Ekos::FOCUS_ABORTED, Ekos::FOCUS_FAIL_CAPTURE_FAILED);
5399 return;
5400 }
5401
5402 ISD::CameraChip *targetChip = m_Camera->getChip(ISD::CameraChip::PRIMARY_CCD);
5403 targetChip->abortExposure();
5404 capture(focusExposure->value());
5405 }
5406 // Don't allow this to happen all night. We will repeat checking (most likely for activeCamera()
5407 // another 200s = 40 * 5s, but after that abort capture.
5408 else if (m_MissingCameraCounter < 40)
5409 {
5410 m_MissingCameraCounter++;
5411 qCDebug(KSTARS_EKOS_FOCUS) << "Unable to restart focus exposure as camera is missing, trying again in 5 seconds...";
5412 // We already know type is not ERROR_SAVE, so the value of type isn't used
5413 // and we can let it default to ERROR_CAPTURE.
5414 QTimer::singleShot(5000, this, &Focus::processCaptureErrorDefault);
5415 }
5416 else
5417 {
5418 m_MissingCameraCounter = 0;
5419 captureFailureCounter = 0;
5420 captureTimeoutCounter = 0;
5421 captureTimeout.stop();
5422 appendLogText(i18n("Exposure failure. Aborting..."));
5423 completeFocusProcedure(Ekos::FOCUS_ABORTED, Ekos::FOCUS_FAIL_CAPTURE_FAILED);
5424 }
5425}
5426
5427void Focus::syncSettings()
5428{
5429 QDoubleSpinBox *dsb = nullptr;
5430 QSpinBox *sb = nullptr;
5431 QCheckBox *cb = nullptr;
5432 QGroupBox *gb = nullptr;
5433 QRadioButton *rb = nullptr;
5434 QComboBox *cbox = nullptr;
5435 QSplitter *s = nullptr;
5436
5437 QString key;
5438 QVariant value;
5439
5440 if ( (dsb = qobject_cast<QDoubleSpinBox*>(sender())))
5441 {
5442 key = dsb->objectName();
5443 value = dsb->value();
5444
5445 }
5446 else if ( (sb = qobject_cast<QSpinBox*>(sender())))
5447 {
5448 key = sb->objectName();
5449 value = sb->value();
5450 }
5451 else if ( (cb = qobject_cast<QCheckBox*>(sender())))
5452 {
5453 key = cb->objectName();
5454 value = cb->isChecked();
5455 }
5456 else if ( (gb = qobject_cast<QGroupBox*>(sender())))
5457 {
5458 key = gb->objectName();
5459 value = gb->isChecked();
5460 }
5461 else if ( (rb = qobject_cast<QRadioButton*>(sender())))
5462 {
5463 key = rb->objectName();
5464 // Discard false requests
5465 if (rb->isChecked() == false)
5466 {
5467 m_Settings.remove(key);
5468 return;
5469 }
5470 value = true;
5471 }
5472 else if ( (cbox = qobject_cast<QComboBox*>(sender())))
5473 {
5474 key = cbox->objectName();
5475 value = cbox->currentText();
5476 }
5477 else if ( (s = qobject_cast<QSplitter*>(sender())))
5478 {
5479 key = s->objectName();
5480 // Convert from the QByteArray to QString using Base64
5481 value = QString::fromUtf8(s->saveState().toBase64());
5482 }
5483
5484 // Save immediately
5485 Options::self()->setProperty(key.toLatin1(), value);
5486
5487 m_Settings[key] = value;
5488 m_GlobalSettings[key] = value;
5489 // propagate image mask attributes
5490 selectImageMask();
5491
5492 m_DebounceTimer.start();
5493}
5494
5495///////////////////////////////////////////////////////////////////////////////////////////
5496///
5497///////////////////////////////////////////////////////////////////////////////////////////
5498void Focus::settleSettings()
5499{
5500 emit settingsUpdated(getAllSettings());
5501 // Save to optical train specific settings as well
5502 OpticalTrainSettings::Instance()->setOpticalTrainID(OpticalTrainManager::Instance()->id(opticalTrainCombo->currentText()));
5503 OpticalTrainSettings::Instance()->setOneSetting(OpticalTrainSettings::Focus, m_Settings);
5504}
5505
5506void Focus::loadGlobalSettings()
5507{
5508 QString key;
5509 QVariant value;
5510
5511 QVariantMap settings;
5512 // All Combo Boxes
5513 for (auto &oneWidget : findChildren<QComboBox*>())
5514 {
5515 if (oneWidget->objectName() == "opticalTrainCombo")
5516 continue;
5517
5518 key = oneWidget->objectName();
5519 value = Options::self()->property(key.toLatin1());
5520 if (value.isValid() && oneWidget->count() > 0)
5521 {
5522 oneWidget->setCurrentText(value.toString());
5523 settings[key] = value;
5524 }
5525 else
5526 qCDebug(KSTARS_EKOS_FOCUS) << "Option" << key << "not found!";
5527 }
5528
5529 // All Double Spin Boxes
5530 for (auto &oneWidget : findChildren<QDoubleSpinBox*>())
5531 {
5532 key = oneWidget->objectName();
5533 value = Options::self()->property(key.toLatin1());
5534 if (value.isValid())
5535 {
5536 oneWidget->setValue(value.toDouble());
5537 settings[key] = value;
5538 }
5539 else
5540 qCDebug(KSTARS_EKOS_FOCUS) << "Option" << key << "not found!";
5541 }
5542
5543 // All Spin Boxes
5544 for (auto &oneWidget : findChildren<QSpinBox*>())
5545 {
5546 key = oneWidget->objectName();
5547 value = Options::self()->property(key.toLatin1());
5548 if (value.isValid())
5549 {
5550 oneWidget->setValue(value.toInt());
5551 settings[key] = value;
5552 }
5553 else
5554 qCDebug(KSTARS_EKOS_FOCUS) << "Option" << key << "not found!";
5555 }
5556
5557 // All Checkboxes
5558 for (auto &oneWidget : findChildren<QCheckBox*>())
5559 {
5560 key = oneWidget->objectName();
5561 value = Options::self()->property(key.toLatin1());
5562 if (value.isValid())
5563 {
5564 oneWidget->setChecked(value.toBool());
5565 settings[key] = value;
5566 }
5567 else if (key != forceInSeqAF->objectName())
5568 qCDebug(KSTARS_EKOS_FOCUS) << "Option" << key << "not found!";
5569 }
5570
5571 // All Checkable Groupboxes
5572 for (auto &oneWidget : findChildren<QGroupBox*>())
5573 {
5574 if (oneWidget->isCheckable())
5575 {
5576 key = oneWidget->objectName();
5577 value = Options::self()->property(key.toLatin1());
5578 if (value.isValid())
5579 {
5580 oneWidget->setChecked(value.toBool());
5581 settings[key] = value;
5582 }
5583 else
5584 qCDebug(KSTARS_EKOS_FOCUS) << "Option" << key << "not found!";
5585 }
5586 }
5587
5588 // All Splitters
5589 for (auto &oneWidget : findChildren<QSplitter*>())
5590 {
5591 key = oneWidget->objectName();
5592 value = Options::self()->property(key.toLatin1());
5593 if (value.isValid())
5594 {
5595 // Convert the saved QString to a QByteArray using Base64
5597 oneWidget->restoreState(valueBA);
5598 settings[key] = valueBA;
5599 }
5600 else
5601 qCDebug(KSTARS_EKOS_FOCUS) << "Option" << key << "not found!";
5602 }
5603
5604 // All Radio buttons
5605 for (auto &oneWidget : findChildren<QRadioButton*>())
5606 {
5607 key = oneWidget->objectName();
5608 value = Options::self()->property(key.toLatin1());
5609 if (value.isValid())
5610 {
5611 oneWidget->setChecked(value.toBool());
5612 settings[key] = value;
5613 }
5614 }
5615 // propagate image mask attributes
5616 selectImageMask();
5617 m_GlobalSettings = m_Settings = settings;
5618}
5619
5620void Focus::checkMosaicMaskLimits()
5621{
5622 if (m_Camera == nullptr || m_Camera->isConnected() == false)
5623 return;
5624 auto targetChip = m_Camera->getChip(ISD::CameraChip::PRIMARY_CCD);
5625 if (targetChip == nullptr || frameSettings.contains(targetChip) == false)
5626 return;
5627 auto settings = frameSettings[targetChip];
5628
5629 // Watch out for invalid values.
5630 auto width = settings["w"].toInt();
5631 auto height = settings["h"].toInt();
5632 if (width == 0 || height == 0)
5633 return;
5634
5635 // determine maximal square size
5636 auto min = std::min(width, height);
5637 // now check if the tile size is below this limit
5638 m_OpsFocusSettings->focusMosaicTileWidth->setMaximum(100 * min / (3 * width));
5639}
5640
5641void Focus::connectSyncSettings()
5642{
5643 // All Combo Boxes
5644 for (auto &oneWidget : findChildren<QComboBox*>())
5645 // Don't sync Optical Train combo
5647 connect(oneWidget, QOverload<int>::of(&QComboBox::activated), this, &Ekos::Focus::syncSettings);
5648
5649 // All Double Spin Boxes
5650 for (auto &oneWidget : findChildren<QDoubleSpinBox*>())
5651 connect(oneWidget, QOverload<double>::of(&QDoubleSpinBox::valueChanged), this, &Ekos::Focus::syncSettings);
5652
5653 // All Spin Boxes
5654 for (auto &oneWidget : findChildren<QSpinBox*>())
5655 connect(oneWidget, QOverload<int>::of(&QSpinBox::valueChanged), this, &Ekos::Focus::syncSettings);
5656
5657 // All Checkboxes
5658 for (auto &oneWidget : findChildren<QCheckBox*>())
5659 connect(oneWidget, &QCheckBox::toggled, this, &Ekos::Focus::syncSettings);
5660
5661 // All Checkable Groupboxes
5662 for (auto &oneWidget : findChildren<QGroupBox*>())
5663 if (oneWidget->isCheckable())
5664 connect(oneWidget, &QGroupBox::toggled, this, &Ekos::Focus::syncSettings);
5665
5666 // All Splitters
5667 for (auto &oneWidget : findChildren<QSplitter*>())
5668 connect(oneWidget, &QSplitter::splitterMoved, this, &Ekos::Focus::syncSettings);
5669
5670 // All Radio Buttons
5671 for (auto &oneWidget : findChildren<QRadioButton*>())
5672 connect(oneWidget, &QRadioButton::toggled, this, &Ekos::Focus::syncSettings);
5673}
5674
5675void Focus::disconnectSyncSettings()
5676{
5677 // All Combo Boxes
5678 for (auto &oneWidget : findChildren<QComboBox*>())
5679 disconnect(oneWidget, QOverload<int>::of(&QComboBox::activated), this, &Ekos::Focus::syncSettings);
5680
5681 // All Double Spin Boxes
5682 for (auto &oneWidget : findChildren<QDoubleSpinBox*>())
5683 disconnect(oneWidget, QOverload<double>::of(&QDoubleSpinBox::valueChanged), this, &Ekos::Focus::syncSettings);
5684
5685 // All Spin Boxes
5686 for (auto &oneWidget : findChildren<QSpinBox*>())
5687 disconnect(oneWidget, QOverload<int>::of(&QSpinBox::valueChanged), this, &Ekos::Focus::syncSettings);
5688
5689 // All Checkboxes
5690 for (auto &oneWidget : findChildren<QCheckBox*>())
5691 disconnect(oneWidget, &QCheckBox::toggled, this, &Ekos::Focus::syncSettings);