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

KDE's Doxygen guidelines are available online.