Kstars

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