Kstars

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