diff --git a/Tests/kstars_ui/test_ekos_focus.cpp b/Tests/kstars_ui/test_ekos_focus.cpp index f386c546ab5601d8e0cda0085d82df3d8705e6d6..673ee65f8b3b96f82a240a4f0ac69189f2ffdc4b 100644 --- a/Tests/kstars_ui/test_ekos_focus.cpp +++ b/Tests/kstars_ui/test_ekos_focus.cpp @@ -49,19 +49,109 @@ void TestEkosFocus::cleanup() } +void TestEkosFocus::testCaptureStates() +{ + // Wait for Focus to come up, switch to Focus tab + KTRY_FOCUS_SHOW(); + + // Sync high on meridian to avoid issues with CCD Simulator + KTRY_FOCUS_SYNC(60.0); + + // Pre-configure steps + KTRY_FOCUS_MOVETO(40000); + + // Prepare to detect state change + QList state_list; + auto state_handler = connect(Ekos::Manager::Instance()->focusModule(), &Ekos::Focus::newStatus, this, [&](Ekos::FocusState s) { + state_list.append(s); + }); + QVERIFY(state_handler); + + // Configure some fields, capture, check states + KTRY_FOCUS_CONFIGURE("SEP", "Iterative", 0.0, 100.0, 3.0); + KTRY_FOCUS_DETECT(2, 1, 99); + QTRY_COMPARE_WITH_TIMEOUT(state_list.count(), 2, 5000); + QCOMPARE(state_list[0], Ekos::FocusState::FOCUS_PROGRESS); + QCOMPARE(state_list[1], Ekos::FocusState::FOCUS_IDLE); + state_list.clear(); + + // Move step value, expect no capture + KTRY_FOCUS_CONFIGURE("SEP", "Iterative", 0.0, 100.0, 3.0); + KTRY_FOCUS_MOVETO(43210); + QTest::qWait(1000); + QCOMPARE(state_list.count(), 0); + + KTRY_FOCUS_GADGET(QPushButton, startLoopB); + KTRY_FOCUS_GADGET(QPushButton, stopFocusB); + + // Loop captures, abort, check states + KTRY_FOCUS_CONFIGURE("SEP", "Iterative", 0.0, 100.0, 3.0); + KTRY_FOCUS_CLICK(startLoopB); + QTRY_VERIFY_WITH_TIMEOUT(state_list.count() >= 1, 5000); + KTRY_FOCUS_CLICK(stopFocusB); + QTRY_VERIFY_WITH_TIMEOUT(state_list.count() >= 4, 5000); + QCOMPARE(state_list[0], Ekos::FocusState::FOCUS_FRAMING); + QCOMPARE(state_list[1], Ekos::FocusState::FOCUS_PROGRESS); + QCOMPARE(state_list[2], Ekos::FocusState::FOCUS_ABORTED); + QCOMPARE(state_list[3], Ekos::FocusState::FOCUS_IDLE); + state_list.clear(); + + KTRY_FOCUS_GADGET(QCheckBox, useAutoStar); + KTRY_FOCUS_GADGET(QPushButton, resetFrameB); + + QWARN("This test does not wait for the hardcoded timeout to select a star."); + + // Use successful automatic star selection (not full-field), capture, check states + KTRY_FOCUS_CONFIGURE("SEP", "Iterative", 0.0, 0.0, 3.0); + useAutoStar->setCheckState(Qt::CheckState::Checked); + KTRY_FOCUS_DETECT(2, 1, 99); + QTRY_VERIFY_WITH_TIMEOUT(state_list.count() >= 2, 5000); + QTRY_VERIFY_WITH_TIMEOUT(!stopFocusB->isEnabled(), 1000); + KTRY_FOCUS_CLICK(resetFrameB); + QCOMPARE(state_list[0], Ekos::FocusState::FOCUS_PROGRESS); + QCOMPARE(state_list[1], Ekos::FocusState::FOCUS_IDLE); + useAutoStar->setCheckState(Qt::CheckState::Unchecked); + state_list.clear(); + + // Use unsuccessful automatic star selection, capture, check states + KTRY_FOCUS_CONFIGURE("SEP", "Iterative", 0.0, 0.0, 3.0); + useAutoStar->setCheckState(Qt::CheckState::Checked); + KTRY_FOCUS_DETECT(0.01, 1, 1); + QTRY_VERIFY_WITH_TIMEOUT(state_list.count() >= 2, 5000); + QTRY_VERIFY_WITH_TIMEOUT(!stopFocusB->isEnabled(), 1000); + KTRY_FOCUS_CLICK(resetFrameB); + QCOMPARE(state_list[0], Ekos::FocusState::FOCUS_PROGRESS); + QCOMPARE(state_list[1], Ekos::FocusState::FOCUS_WAITING); + useAutoStar->setCheckState(Qt::CheckState::Unchecked); + state_list.clear(); + + disconnect(state_handler); +} + void TestEkosFocus::testDuplicateFocusRequest() { + // Wait for Focus to come up, switch to Focus tab + KTRY_FOCUS_SHOW(); + + // Sync high on meridian to avoid issues with CCD Simulator + KTRY_FOCUS_SYNC(60.0); + + // Configure a fast autofocus, pre-set to near-optimal 38500 steps + KTRY_FOCUS_MOVETO(40000); + KTRY_FOCUS_CONFIGURE("SEP", "Iterative", 0.0, 0.0, 3.0); + KTRY_FOCUS_EXPOSURE(1, 99); + KTRY_FOCUS_GADGET(QPushButton, startFocusB); KTRY_FOCUS_GADGET(QPushButton, stopFocusB); QTRY_VERIFY_WITH_TIMEOUT(startFocusB->isEnabled(), 1000); QTRY_VERIFY_WITH_TIMEOUT(!stopFocusB->isEnabled(), 1000); - QCOMPARE(Ekos::Manager::Instance()->focusModule()->status(), Ekos::FOCUS_IDLE); - - KTRY_FOCUS_DETECT(2, 1); // Prepare to detect the beginning of the autofocus_procedure volatile bool autofocus_started = false; - connect(Ekos::Manager::Instance()->focusModule(), &Ekos::Focus::autofocusStarting, this, [&]() { autofocus_started = true; }); + auto startup_handler = connect(Ekos::Manager::Instance()->focusModule(), &Ekos::Focus::autofocusStarting, this, [&]() { + autofocus_started = true; + }); + QVERIFY(startup_handler); // If we click the autofocus button, we receive a signal that the procedure starts, the state change and the button is disabled KTRY_FOCUS_CLICK(startFocusB); @@ -78,7 +168,303 @@ void TestEkosFocus::testDuplicateFocusRequest() // Stop the running autofocus KTRY_FOCUS_CLICK(stopFocusB); - QTRY_COMPARE_WITH_TIMEOUT(Ekos::Manager::Instance()->focusModule()->status(), Ekos::FOCUS_ABORTED, 5000); + QTest::qWait(500); + + disconnect(startup_handler); +} + +void TestEkosFocus::testAutofocusSignalEmission() +{ + // Wait for Focus to come up, switch to Focus tab + KTRY_FOCUS_SHOW(); + + // Sync high on meridian to avoid issues with CCD Simulator + KTRY_FOCUS_SYNC(60.0); + + // Configure a fast autofocus, pre-set to near-optimal 38500 steps + KTRY_FOCUS_MOVETO(40000); + KTRY_FOCUS_CONFIGURE("SEP", "Iterative", 0.0, 100.0, 3.0); + KTRY_FOCUS_EXPOSURE(1, 99); + + KTRY_FOCUS_GADGET(QPushButton, startFocusB); + KTRY_FOCUS_GADGET(QPushButton, stopFocusB); + QTRY_VERIFY_WITH_TIMEOUT(startFocusB->isEnabled(), 1000); + QTRY_VERIFY_WITH_TIMEOUT(!stopFocusB->isEnabled(), 1000); + + // Prepare to detect the beginning of the autofocus_procedure + volatile bool autofocus_started = false; + auto startup_handler = connect(Ekos::Manager::Instance()->focusModule(), &Ekos::Focus::autofocusStarting, this, [&]() { + autofocus_started = true; + }); + QVERIFY(startup_handler); + + // Prepare to restart the autofocus procedure immediately when it finishes + volatile bool ran_once = false; + volatile bool autofocus_complete = false; + auto completion_handler = connect(Ekos::Manager::Instance()->focusModule(), &Ekos::Focus::autofocusComplete, this, [&]() { + autofocus_complete = true; + autofocus_started = false; + if (!ran_once) + { + Ekos::Manager::Instance()->focusModule()->start(); + ran_once = true; + } + }); + QVERIFY(completion_handler); + + // Run the autofocus, wait for the completion signal and restart a second one immediately + QVERIFY(!autofocus_started); + QVERIFY(!autofocus_complete); + KTRY_FOCUS_CLICK(startFocusB); + QTRY_VERIFY_WITH_TIMEOUT(autofocus_started, 500); + QTRY_VERIFY_WITH_TIMEOUT(autofocus_complete, 30000); + + // Wait for the second run to finish + autofocus_complete = false; + QTRY_VERIFY_WITH_TIMEOUT(autofocus_complete, 30000); + + // No other autofocus started after that + QVERIFY(!autofocus_started); + + // Disconnect signals + disconnect(startup_handler); + disconnect(completion_handler); +} + +void TestEkosFocus::testFocusAbort() +{ + // Wait for Focus to come up, switch to Focus tab + KTRY_FOCUS_SHOW(); + + // Sync high on meridian to avoid issues with CCD Simulator + KTRY_FOCUS_SYNC(60.0); + + // Configure a fast autofocus, pre-set to near-optimal 38500 steps + KTRY_FOCUS_MOVETO(40000); + KTRY_FOCUS_CONFIGURE("SEP", "Iterative", 0.0, 100.0, 3.0); + KTRY_FOCUS_EXPOSURE(1, 99); + + KTRY_FOCUS_GADGET(QPushButton, startFocusB); + KTRY_FOCUS_GADGET(QPushButton, stopFocusB); + QTRY_VERIFY_WITH_TIMEOUT(startFocusB->isEnabled(), 1000); + QTRY_VERIFY_WITH_TIMEOUT(!stopFocusB->isEnabled(), 1000); + + // Prepare to detect the beginning of the autofocus_procedure + volatile bool autofocus_started = false; + auto startup_handler = connect(Ekos::Manager::Instance()->focusModule(), &Ekos::Focus::autofocusStarting, this, [&]() { + autofocus_started = true; + }); + QVERIFY(startup_handler); + + // Prepare to detect the end of the autofocus_procedure + volatile bool autofocus_completed = false; + auto completion_handler = connect(Ekos::Manager::Instance()->focusModule(), &Ekos::Focus::autofocusComplete, this, [&]() { + autofocus_completed = true; + autofocus_started = false; + }); + QVERIFY(completion_handler); + + // Prepare to restart the autofocus procedure immediately when it finishes + volatile bool ran_once = false; + volatile bool autofocus_aborted = false; + auto abort_handler = connect(Ekos::Manager::Instance()->focusModule(), &Ekos::Focus::autofocusAborted, this, [&]() { + autofocus_aborted = true; + autofocus_started = false; + if (!ran_once) + { + Ekos::Manager::Instance()->focusModule()->start(); + ran_once = true; + } + }); + QVERIFY(abort_handler); + + // Run the autofocus, don't wait for the completion signal, abort it and restart a second one immediately + QVERIFY(!autofocus_started); + QVERIFY(!autofocus_aborted); + QVERIFY(!autofocus_completed); + KTRY_FOCUS_CLICK(startFocusB); + QTRY_VERIFY_WITH_TIMEOUT(autofocus_started, 500); + KTRY_FOCUS_CLICK(stopFocusB); + QTRY_VERIFY_WITH_TIMEOUT(autofocus_aborted, 1000); + + // Wait for the second run to finish + QTRY_VERIFY_WITH_TIMEOUT(autofocus_started, 500); + QTRY_VERIFY_WITH_TIMEOUT(autofocus_completed, 30000); + + // No other autofocus started after that + QVERIFY(!autofocus_started); + + // Disconnect signals + disconnect(startup_handler); + disconnect(completion_handler); + disconnect(abort_handler); +} + +void TestEkosFocus::testGuidingSuspend() +{ + // Wait for Focus to come up, switch to Focus tab + KTRY_FOCUS_SHOW(); + + // Sync high on meridian to avoid issues with CCD Simulator + KTRY_FOCUS_SYNC(60.0); + + // Configure a fast autofocus + KTRY_FOCUS_MOVETO(40000); + KTRY_FOCUS_CONFIGURE("SEP", "Iterative", 0.0, 100.0, 3); + KTRY_FOCUS_EXPOSURE(1, 99); + + KTRY_FOCUS_GADGET(QPushButton, startFocusB); + KTRY_FOCUS_GADGET(QPushButton, stopFocusB); + QTRY_VERIFY_WITH_TIMEOUT(startFocusB->isEnabled(), 1000); + QTRY_VERIFY_WITH_TIMEOUT(!stopFocusB->isEnabled(), 1000); + + // Prepare to detect the beginning of the autofocus_procedure + volatile bool autofocus_started = false; + auto startup_handler = connect(Ekos::Manager::Instance()->focusModule(), &Ekos::Focus::autofocusStarting, this, [&]() { + autofocus_started = true; + }); + QVERIFY(startup_handler); + + // Prepare to detect the failure of the autofocus procedure + volatile bool autofocus_aborted = false; + auto abort_handler = connect(Ekos::Manager::Instance()->focusModule(), &Ekos::Focus::autofocusAborted, this, [&]() { + autofocus_aborted = true; + autofocus_started = false; + }); + QVERIFY(abort_handler); + + // Prepare to detect the end of the autofocus_procedure + volatile bool autofocus_completed = false; + auto completion_handler = connect(Ekos::Manager::Instance()->focusModule(), &Ekos::Focus::autofocusComplete, this, [&]() { + autofocus_completed = true; + autofocus_started = false; + }); + QVERIFY(completion_handler); + + // Prepare to detect guiding suspending + volatile bool guiding_suspended = false; + auto suspend_handler = connect(Ekos::Manager::Instance()->focusModule(), &Ekos::Focus::suspendGuiding, this, [&]() { + guiding_suspended = true; + }); + QVERIFY(suspend_handler); + + // Prepare to detect guiding suspending + auto resume_handler = connect(Ekos::Manager::Instance()->focusModule(), &Ekos::Focus::resumeGuiding, this, [&]() { + guiding_suspended = false; + }); + QVERIFY(resume_handler); + + KTRY_FOCUS_GADGET(QCheckBox, suspendGuideCheck); + + // Abort the autofocus with guiding set to suspend, guiding will be required to suspend, then required to resume + suspendGuideCheck->setCheckState(Qt::CheckState::Checked); + QVERIFY(!autofocus_started); + QVERIFY(!autofocus_aborted); + QVERIFY(!autofocus_completed); + QVERIFY(!guiding_suspended); + KTRY_FOCUS_CLICK(startFocusB); + QTRY_VERIFY_WITH_TIMEOUT(autofocus_started, 500); + QVERIFY(guiding_suspended); + Ekos::Manager::Instance()->focusModule()->abort(); + QTRY_VERIFY_WITH_TIMEOUT(autofocus_aborted, 5000); + QVERIFY(!guiding_suspended); + + // Run the autofocus to completion with guiding set to suspend, guiding will be required to suspend, then required to resume + autofocus_started = autofocus_aborted = false; + KTRY_FOCUS_CLICK(startFocusB); + QTRY_VERIFY_WITH_TIMEOUT(autofocus_started, 500); + QVERIFY(guiding_suspended); + QTRY_VERIFY_WITH_TIMEOUT(autofocus_completed, 30000); + QVERIFY(!guiding_suspended); + + // No other autofocus started after that + QVERIFY(!autofocus_started); + + // Abort the autofocus with guiding set to continue, no guiding signal will be emitted + suspendGuideCheck->setCheckState(Qt::CheckState::Unchecked); + autofocus_started = autofocus_aborted = autofocus_completed = guiding_suspended = false; + KTRY_FOCUS_CLICK(startFocusB); + QTRY_VERIFY_WITH_TIMEOUT(autofocus_started, 500); + QVERIFY(!guiding_suspended); + Ekos::Manager::Instance()->focusModule()->abort(); + QTRY_VERIFY_WITH_TIMEOUT(autofocus_aborted, 5000); + QVERIFY(!guiding_suspended); + + // Run the autofocus to completion with guiding set to continue, no guiding signal will be emitted + autofocus_started = autofocus_aborted = false; + KTRY_FOCUS_CLICK(startFocusB); + QTRY_VERIFY_WITH_TIMEOUT(autofocus_started, 500); + QVERIFY(!guiding_suspended); + QTRY_VERIFY_WITH_TIMEOUT(autofocus_completed, 30000); + QVERIFY(!guiding_suspended); + + // No other autofocus started after that + QVERIFY(!autofocus_started); + + // Disconnect signals + disconnect(startup_handler); + disconnect(completion_handler); + disconnect(abort_handler); + disconnect(suspend_handler); + disconnect(resume_handler); +} + +void TestEkosFocus::testFocusFailure() +{ + // Wait for Focus to come up, switch to Focus tab + KTRY_FOCUS_SHOW(); + + // Sync high on meridian to avoid issues with CCD Simulator + KTRY_FOCUS_SYNC(60.0); + + // Configure a failing autofocus - small gain, small exposure, small frame filter + KTRY_FOCUS_MOVETO(10000); + KTRY_FOCUS_CONFIGURE("SEP", "Iterative", 0.0, 1.0, 0.1); + KTRY_FOCUS_EXPOSURE(0.01, 1); + + KTRY_FOCUS_GADGET(QPushButton, startFocusB); + KTRY_FOCUS_GADGET(QPushButton, stopFocusB); + QTRY_VERIFY_WITH_TIMEOUT(startFocusB->isEnabled(), 1000); + QTRY_VERIFY_WITH_TIMEOUT(!stopFocusB->isEnabled(), 1000); + + // Prepare to detect the beginning of the autofocus_procedure + volatile bool autofocus_started = false; + auto startup_handler = connect(Ekos::Manager::Instance()->focusModule(), &Ekos::Focus::autofocusStarting, this, [&]() { + autofocus_started = true; + }); + QVERIFY(startup_handler); + + // Prepare to detect the end of the autofocus_procedure + volatile bool autofocus_completed = false; + auto completion_handler = connect(Ekos::Manager::Instance()->focusModule(), &Ekos::Focus::autofocusComplete, this, [&]() { + autofocus_completed = true; + autofocus_started = false; + }); + QVERIFY(completion_handler); + + // Prepare to detect the failure of the autofocus procedure + volatile bool autofocus_aborted = false; + auto abort_handler = connect(Ekos::Manager::Instance()->focusModule(), &Ekos::Focus::autofocusAborted, this, [&]() { + autofocus_aborted = true; + autofocus_started = false; + }); + QVERIFY(abort_handler); + + // Run the autofocus, wait for the completion signal + QVERIFY(!autofocus_started); + QVERIFY(!autofocus_aborted); + QVERIFY(!autofocus_completed); + KTRY_FOCUS_CLICK(startFocusB); + QTRY_VERIFY_WITH_TIMEOUT(autofocus_started, 500); + QTRY_VERIFY_WITH_TIMEOUT(autofocus_aborted, 30000); + + // No other autofocus started after that + QVERIFY(!autofocus_started); + + // Disconnect signals + disconnect(startup_handler); + disconnect(completion_handler); + disconnect(abort_handler); } void TestEkosFocus::testStarDetection_data() @@ -145,39 +531,41 @@ void TestEkosFocus::testStarDetection() QVERIFY(ekos->mountModule()->sync(RA, DEC)); // Wait for Focus to come up, switch to Focus tab - QTRY_VERIFY_WITH_TIMEOUT(ekos->focusModule() != nullptr, 5000); - KTRY_EKOS_GADGET(QTabWidget, toolsWidget); - toolsWidget->setCurrentWidget(ekos->focusModule()); - QTRY_COMPARE_WITH_TIMEOUT(toolsWidget->currentWidget(), ekos->focusModule(), 1000); + KTRY_FOCUS_SHOW(); - QWARN("The Focus capture button toggles after Ekos is started, leave a bit of time for it to settle."); - QTest::qWait(500); + KTRY_FOCUS_GADGET(QPushButton, startFocusB); + KTRY_FOCUS_GADGET(QPushButton, stopFocusB); + QTRY_VERIFY_WITH_TIMEOUT(startFocusB->isEnabled(), 1000); + QTRY_VERIFY_WITH_TIMEOUT(!stopFocusB->isEnabled(), 1000); KTRY_FOCUS_GADGET(QLineEdit, starsOut); + // Locate somewhere we do see stars with the CCD Simulator + KTRY_FOCUS_MOVETO(60000); + // Run the focus procedure for SEP - KTRY_FOCUS_CONFIGURE("SEP", "Iterative", 0.0, 100.0); - KTRY_FOCUS_DETECT(1, 3); + KTRY_FOCUS_CONFIGURE("SEP", "Iterative", 0.0, 100.0, 3.0); + KTRY_FOCUS_DETECT(1, 3, 99); QTRY_VERIFY_WITH_TIMEOUT(starsOut->text().toInt() >= 1, 5000); // Run the focus procedure for Centroid - KTRY_FOCUS_CONFIGURE("Centroid", "Iterative", 0.0, 100.0); - KTRY_FOCUS_DETECT(1, 3); + KTRY_FOCUS_CONFIGURE("Centroid", "Iterative", 0.0, 100.0, 3.0); + KTRY_FOCUS_DETECT(1, 3, 99); QTRY_VERIFY_WITH_TIMEOUT(starsOut->text().toInt() >= 1, 5000); // Run the focus procedure for Threshold - disable full-field - KTRY_FOCUS_CONFIGURE("Threshold", "Iterative", 0.0, 0.0); - KTRY_FOCUS_DETECT(1, 3); + KTRY_FOCUS_CONFIGURE("Threshold", "Iterative", 0.0, 0.0, 3.0); + KTRY_FOCUS_DETECT(1, 3, 99); QTRY_VERIFY_WITH_TIMEOUT(starsOut->text().toInt() >= 1, 5000); // Run the focus procedure for Gradient - disable full-field - KTRY_FOCUS_CONFIGURE("Gradient", "Iterative", 0.0, 0.0); - KTRY_FOCUS_DETECT(1, 3); + KTRY_FOCUS_CONFIGURE("Gradient", "Iterative", 0.0, 0.0, 3.0); + KTRY_FOCUS_DETECT(1, 3, 99); QTRY_VERIFY_WITH_TIMEOUT(starsOut->text().toInt() >= 1, 5000); // Longer exposure - KTRY_FOCUS_CONFIGURE("SEP", "Iterative", 0.0, 100.0); - KTRY_FOCUS_DETECT(8, 1); + KTRY_FOCUS_CONFIGURE("SEP", "Iterative", 0.0, 100.0, 3.0); + KTRY_FOCUS_DETECT(8, 1, 99); QTRY_VERIFY_WITH_TIMEOUT(starsOut->text().toInt() >= 1, 5000); // Run the focus procedure again to cover more code @@ -187,18 +575,18 @@ void TestEkosFocus::testStarDetection() { for (double outer = 100.0; inner < outer; outer -= 42.0) { - KTRY_FOCUS_CONFIGURE("SEP", "Iterative", inner, outer); - KTRY_FOCUS_DETECT(0.1, 2); + KTRY_FOCUS_CONFIGURE("SEP", "Iterative", inner, outer, 3.0); + KTRY_FOCUS_DETECT(0.1, 2, 99); } } // Test threshold - disable full-field - for (double threshold = 80.0; threshold < 200.0; threshold += 13.3) + for (double threshold = 80.0; threshold < 99.0; threshold += 13.3) { KTRY_FOCUS_GADGET(QDoubleSpinBox, thresholdSpin); thresholdSpin->setValue(threshold); - KTRY_FOCUS_CONFIGURE("Threshold", "Iterative", 0, 0.0); - KTRY_FOCUS_DETECT(0.1, 1); + KTRY_FOCUS_CONFIGURE("Threshold", "Iterative", 0, 0.0, 3.0); + KTRY_FOCUS_DETECT(0.1, 1, 99); } #endif } diff --git a/Tests/kstars_ui/test_ekos_focus.h b/Tests/kstars_ui/test_ekos_focus.h index 622c0639f50e9b6ec744fde4b7e35512a91c0d3e..2544667028bcaa78e8777b9ec8451cfe0ada643e 100644 --- a/Tests/kstars_ui/test_ekos_focus.h +++ b/Tests/kstars_ui/test_ekos_focus.h @@ -23,6 +23,14 @@ #include #include +/** @brief Helper to show the Focus tab + */ +#define KTRY_FOCUS_SHOW() do { \ + QTRY_VERIFY_WITH_TIMEOUT(Ekos::Manager::Instance()->focusModule() != nullptr, 5000); \ + KTRY_EKOS_GADGET(QTabWidget, toolsWidget); \ + toolsWidget->setCurrentWidget(Ekos::Manager::Instance()->focusModule()); \ + QTRY_COMPARE_WITH_TIMEOUT(toolsWidget->currentWidget(), Ekos::Manager::Instance()->focusModule(), 1000); } while (false) + /** @brief Helper to retrieve a gadget in the Focus tab specifically. * @param klass is the class of the gadget to look for. * @param name is the gadget name to look for in the UI configuration. @@ -64,9 +72,14 @@ * disabled or does not toggle during exposure or if the stop button is not the opposite of the capture button. */ /** @{ */ -#define KTRY_FOCUS_DETECT(exposure, averaged) do { \ +#define KTRY_FOCUS_EXPOSURE(exposure, gain) do { \ + KTRY_FOCUS_GADGET(QDoubleSpinBox, gainIN); \ + gainIN->setValue(gain); \ KTRY_FOCUS_GADGET(QDoubleSpinBox, exposureIN); \ - exposureIN->setValue(exposure); \ + exposureIN->setValue(exposure); } while (false) + +#define KTRY_FOCUS_DETECT(exposure, averaged, gain) do { \ + KTRY_FOCUS_EXPOSURE(exposure, gain); \ KTRY_FOCUS_GADGET(QSpinBox, focusFramesSpin); \ focusFramesSpin->setValue(averaged); \ KTRY_FOCUS_GADGET(QPushButton, captureB); \ @@ -77,7 +90,7 @@ QTRY_VERIFY_WITH_TIMEOUT(!captureB->isEnabled(), 1000); \ QVERIFY(stopFocusB->isEnabled()); \ QTest::qWait(exposure*averaged*1000); \ - QTRY_VERIFY_WITH_TIMEOUT(captureB->isEnabled(), 5000); \ + QTRY_VERIFY_WITH_TIMEOUT(captureB->isEnabled(), 5000 + exposure*averaged*1000/5); \ QVERIFY(!stopFocusB->isEnabled()); } while (false) /** @} */ @@ -88,16 +101,42 @@ * @param fieldout is the upper radius of the annulus filtering stars. * @warning Fails the test if detection, algorithm, full-field checkbox or annulus fields cannot be used. */ -#define KTRY_FOCUS_CONFIGURE(detection, algorithm, fieldin, fieldout) do { \ +#define KTRY_FOCUS_CONFIGURE(detection, algorithm, fieldin, fieldout, tolerance) do { \ KTRY_FOCUS_GADGET(QCheckBox, useFullField); \ useFullField->setCheckState(fieldin < fieldout ? Qt::CheckState::Checked : Qt::CheckState::Unchecked); \ KTRY_FOCUS_GADGET(QDoubleSpinBox, fullFieldInnerRing); \ fullFieldInnerRing->setValue(fieldin); \ KTRY_FOCUS_GADGET(QDoubleSpinBox, fullFieldOuterRing); \ fullFieldOuterRing->setValue(fieldout); \ + KTRY_FOCUS_GADGET(QDoubleSpinBox, toleranceIN); \ + toleranceIN->setValue(tolerance); \ KTRY_FOCUS_COMBO_SET(focusDetectionCombo, detection); \ KTRY_FOCUS_COMBO_SET(focusAlgorithmCombo, algorithm); } while (false) +/** @brief Helper to move the focuser. + * @param steps is the absolute step value to set. + */ +#define KTRY_FOCUS_MOVETO(steps) do { \ + KTRY_FOCUS_GADGET(QSpinBox, absTicksSpin); \ + absTicksSpin->setValue(steps); \ + KTRY_FOCUS_GADGET(QPushButton, startGotoB); \ + KTRY_FOCUS_CLICK(startGotoB); \ + KTRY_FOCUS_GADGET(QLineEdit, absTicksLabel); \ + QTRY_COMPARE_WITH_TIMEOUT(absTicksLabel->text().toInt(), steps, 5000); } while (false) + +/** @brief Helper to sync the mount at the meridian for focus tests. + * @warning This is needed because the CCD Simulator has much rotation jitter at the celestial pole. + * @param alt is the altitude to sync to, use 60.0 as degrees for instance. + */ +#define KTRY_FOCUS_SYNC(alt) do { \ + QWARN("Syncing mount at an altitude on the meridian to avoid celestial pole jitter on the simulator."); \ + GeoLocation * const geo = KStarsData::Instance()->geo(); \ + KStarsDateTime const now(KStarsData::Instance()->lt()); \ + KSNumbers const numbers(now.djd()); \ + CachingDms const LST = geo->GSTtoLST(geo->LTtoUT(now).gst()); \ + QTRY_VERIFY_WITH_TIMEOUT(Ekos::Manager::Instance()->mountModule() != nullptr, 5000); \ + QVERIFY(Ekos::Manager::Instance()->mountModule()->sync(LST.Hours(), (alt))); } while (false) + class TestEkosFocus : public QObject { Q_OBJECT @@ -112,7 +151,12 @@ private slots: void init(); void cleanup(); + void testCaptureStates(); void testDuplicateFocusRequest(); + void testAutofocusSignalEmission(); + void testFocusAbort(); + void testGuidingSuspend(); + void testFocusFailure(); void testStarDetection_data(); void testStarDetection(); diff --git a/kstars/ekos/focus/focus.cpp b/kstars/ekos/focus/focus.cpp index 61c37b75dcaf44e52d38ff904270387f0dded169..6045043cb80a9fce60b730727e746af1c95f58e2 100644 --- a/kstars/ekos/focus/focus.cpp +++ b/kstars/ekos/focus/focus.cpp @@ -848,8 +848,7 @@ void Focus::start() { if (!changeFocus(newPosition - position)) { - abort(); - setAutoFocusResult(false); + completeFocusProcedure(false); } // Avoid the capture below. return; @@ -895,7 +894,7 @@ void Focus::checkStopFocus() { appendLogText(i18n("Detection in progress, please wait.")); } - else abort(); + else completeFocusProcedure(false); } void Focus::abort() @@ -957,7 +956,12 @@ void Focus::stop(bool aborted) HFRInc = 0; reverseDir = false; - //emit statusUpdated(false); + if (m_GuidingSuspended) + { + emit resumeGuiding(); + m_GuidingSuspended = false; + } + if (aborted) { state = Ekos::FOCUS_ABORTED; @@ -1370,18 +1374,77 @@ bool Focus::appendHFR(double newHFR) return HFRFrames.count() < focusFramesSpin->value(); } -void Focus::emitComplete() +void Focus::completeFocusProcedure(bool success) { - QString str = ""; + appendLogText(i18np("Focus procedure completed after %1 iteration.", + "Focus procedure completed after %1 iterations.", hfr_position.count())); + + // Prepare the message for Analyze + QString analysis_results = ""; const int size = hfr_position.size(); for (int i = 0; i < size; ++i) { - str.append(QString("%1%2|%3") + analysis_results.append(QString("%1%2|%3") .arg(i == 0 ? "" : "|" ) .arg(QString::number(hfr_position[i], 'f', 0)) .arg(QString::number(hfr_value[i], 'f', 3))); } - emit autofocusComplete(filter(), str); + + bool const _inAutoFocus = inAutoFocus; + + // Reset the autofocus flags + stop(!success); + + // Refresh display if needed + if (focusAlgorithm == FOCUS_POLYNOMIAL) + graphPolynomialFunction(); + + // Enforce settling duration + int const settleTime = m_GuidingSuspended ? GuideSettleTime->value() : 0; + + if (settleTime > 0) + appendLogText(i18n("Settling for %1s...", settleTime)); + + QTimer::singleShot(settleTime * 1000, this, [&, settleTime, success, _inAutoFocus, analysis_results]() + { + if (settleTime > 0) + appendLogText(i18n("Settling complete.")); + + if (_inAutoFocus) + { + if (success) + { + state = Ekos::FOCUS_COMPLETE; + KSNotification::event(QLatin1String("FocusSuccessful"), i18n("Autofocus operation completed successfully")); + } + else + { + state = Ekos::FOCUS_FAILED; + KSNotification::event(QLatin1String("FocusFailed"), i18n("Autofocus operation failed"), + KSNotification::EVENT_ALERT); + } + + // Set the procedure result + setAutoFocusResult(success); + + // Send the completion message to other modules + if (success) + emit autofocusComplete(filter(), analysis_results); + else + emit autofocusAborted(filter(), analysis_results); + } + else state = Ekos::FOCUS_IDLE; + + qCDebug(KSTARS_EKOS_FOCUS) << "Settled. State:" << Ekos::getFocusStatusString(state); + + // Delay state notification if we have a locked filter pending return to original filter + if (fallbackFilterPending) + { + filterManager->setFilterPosition(fallbackFilterPosition, + static_cast(FilterManager::CHANGE_POLICY | FilterManager::OFFSET_POLICY)); + } + else emit newStatus(state); + }); } void Focus::setCurrentHFR(double value) @@ -1425,13 +1488,10 @@ void Focus::setCurrentHFR(double value) if (focusAlgorithm == FOCUS_POLYNOMIAL && polySolutionFound == MINIMUM_POLY_SOLUTIONS) { polySolutionFound = 0; - emitComplete(); - appendLogText(i18n("Autofocus complete after %1 iterations.", hfr_position.count())); - stop(); - setAutoFocusResult(true); - graphPolynomialFunction(); + completeFocusProcedure(true); return; } + Edge *selectedHFRStarHFR = nullptr; // Center tracking box around selected star (if it valid) either in: @@ -1516,8 +1576,7 @@ void Focus::setCurrentHFR(double value) if (currentHFR == -1 && (stddev > focusBoxSize->value() / 10.0 || noHFR / starsHFR.count() > 0.75)) { appendLogText(i18n("No reliable star is detected. Aborting...")); - abort(); - setAutoFocusResult(false); + completeFocusProcedure(false); return; } } @@ -1958,8 +2017,7 @@ bool Focus::autoFocusChecks() if (++absIterations > MAXIMUM_ABS_ITERATIONS) { appendLogText(i18n("Autofocus failed to reach proper focus. Try increasing tolerance value.")); - abort(); - setAutoFocusResult(false); + completeFocusProcedure(false); return false; } @@ -1981,8 +2039,7 @@ bool Focus::autoFocusChecks() else { appendLogText(i18n("Failed to detect any stars. Reset frame and try again.")); - abort(); - setAutoFocusResult(false); + completeFocusProcedure(false); return false; } } @@ -2054,29 +2111,23 @@ void Focus::autoFocusLinear() { if (linearFocuser->isDone() && linearFocuser->solution() != -1) { - emitComplete(); - appendLogText(i18np("Autofocus complete after %1 iteration.", - "Autofocus complete after %1 iterations.", hfr_position.count())); - stop(); - setAutoFocusResult(true); + completeFocusProcedure(true); } else { qCDebug(KSTARS_EKOS_FOCUS) << linearFocuser->doneReason(); appendLogText("Linear autofocus algorithm aborted."); - abort(); - setAutoFocusResult(false); + completeFocusProcedure(false); } return; } else { const int delta = nextPosition - currentPosition; + if (!changeFocus(delta)) - { - abort(); - setAutoFocusResult(false); - } + completeFocusProcedure(false); + return; } } @@ -2120,11 +2171,10 @@ void Focus::autoFocusAbs() HFRInc = 0; focusOutLimit = 0; focusInLimit = 0; + if (!changeFocus(pulseDuration)) - { - abort(); - setAutoFocusResult(false); - } + completeFocusProcedure(false); + break; case FOCUS_IN: @@ -2139,24 +2189,16 @@ void Focus::autoFocusAbs() { appendLogText( i18n("Change in HFR is too small. Try increasing the step size or decreasing the tolerance.")); - abort(); - setAutoFocusResult(false); + completeFocusProcedure(false); } else if (noStarCount > 0) { appendLogText(i18n("Failed to detect focus star in frame. Capture and select a focus star.")); - abort(); - setAutoFocusResult(false); + completeFocusProcedure(false); } else { - emitComplete(); - appendLogText(i18n("Autofocus complete after %1 iterations.", hfr_position.count())); - stop(); - setAutoFocusResult(true); - - if (focusAlgorithm == FOCUS_POLYNOMIAL) - graphPolynomialFunction(); + completeFocusProcedure(true); } break; } @@ -2340,12 +2382,7 @@ void Focus::autoFocusAbs() // Ops, we can't go any further, we're done. if (targetPosition == currentPosition) { - emitComplete(); - appendLogText(i18n("Autofocus complete after %1 iterations.", hfr_position.count())); - stop(); - setAutoFocusResult(true); - if (focusAlgorithm == FOCUS_POLYNOMIAL) - graphPolynomialFunction(); + completeFocusProcedure(true); return; } @@ -2353,8 +2390,7 @@ void Focus::autoFocusAbs() if (focusOutLimit && focusOutLimit == focusInLimit) { appendLogText(i18n("Deadlock reached. Please try again with different settings.")); - abort(); - setAutoFocusResult(false); + completeFocusProcedure(false); return; } @@ -2380,8 +2416,7 @@ void Focus::autoFocusAbs() << initialFocuserAbsPosition << ") exceeds maxTravel distance of " << maxTravelIN->value(); appendLogText("Maximum travel limit reached. Autofocus aborted."); - abort(); - setAutoFocusResult(false); + completeFocusProcedure(false); break; } } @@ -2401,10 +2436,8 @@ void Focus::autoFocusAbs() // Now cross your fingers and wait if (!changeFocus(delta)) - { - abort(); - setAutoFocusResult(false); - } + completeFocusProcedure(false); + break; } } @@ -2431,8 +2464,7 @@ void Focus::autoFocusRel() if (pulseDuration <= MINIMUM_PULSE_TIMER) { appendLogText(i18n("Autofocus failed to reach proper focus. Try adjusting the tolerance value.")); - abort(); - setAutoFocusResult(false); + completeFocusProcedure(false); return; } @@ -2463,13 +2495,7 @@ void Focus::autoFocusRel() case FOCUS_OUT: if (fabs(currentHFR - minHFR) < (toleranceIN->value() / 100.0) && HFRInc == 0) { - emitComplete(); - appendLogText(i18n("Autofocus complete after %1 iterations.", hfr_position.count())); - stop(); - setAutoFocusResult(true); - if (focusAlgorithm == FOCUS_POLYNOMIAL) - graphPolynomialFunction(); - break; + completeFocusProcedure(true); } else if (currentHFR < lastHFR) { @@ -2491,10 +2517,7 @@ void Focus::autoFocusRel() pulseDuration *= 0.75; if (!changeFocus(lastFocusDirection == FOCUS_IN ? pulseDuration : -pulseDuration)) - { - abort(); - setAutoFocusResult(false); - } + completeFocusProcedure(false); } break; } @@ -2547,8 +2570,7 @@ void Focus::autoFocusProcessPositionChange(IPState state) if (!focusIn(temp)) { appendLogText(i18n("Focuser error, check INDI panel.")); - abort(); - setAutoFocusResult(false); + completeFocusProcedure(false); } } else @@ -2561,8 +2583,7 @@ void Focus::autoFocusProcessPositionChange(IPState state) else if (state == IPS_ALERT) { appendLogText(i18n("Focuser error, check INDI panel.")); - abort(); - setAutoFocusResult(false); + completeFocusProcedure(false); } } @@ -3111,10 +3132,9 @@ void Focus::setAutoFocusResult(bool status) // CR add auto focus position, temperature and filter to log in CSV format // this will help with setting up focus offsets and temperature compensation - qCInfo(KSTARS_EKOS_FOCUS) << "Autofocus values: position, " << currentPosition << ", temperature, " - << lastFocusTemperature << ", filter, " << filter() - << ", HFR, " << currentHFR << ", altitude, " << mountAlt; - + qCInfo(KSTARS_EKOS_FOCUS) << "Autofocus values: position," << currentPosition << ", temperature," + << lastFocusTemperature << ", filter," << filter() + << ", HFR," << currentHFR << ", altitude," << mountAlt; appendFocusLogText(QString("%1, %2, %3, %4, %5\n") .arg(QString::number(currentPosition)) @@ -3123,10 +3143,8 @@ void Focus::setAutoFocusResult(bool status) .arg(QString::number(currentHFR, 'f', 3)) .arg(QString::number(mountAlt, 'f', 1))); } - // In case of failure, go back to last position if the focuser is absolute - if (status == false && canAbsMove && currentFocuser && currentFocuser->isConnected() && - initialFocuserAbsPosition >= 0) + else if (canAbsMove && currentFocuser && currentFocuser->isConnected() && initialFocuserAbsPosition >= 0) { currentFocuser->moveAbs(initialFocuserAbsPosition); appendLogText(i18n("Autofocus failed, moving back to initial focus position %1.", initialFocuserAbsPosition)); @@ -3137,53 +3155,10 @@ void Focus::setAutoFocusResult(bool status) resetFocus = true; // Reset focus frame in case the star in subframe was lost resetFrame(); - return; } } - int settleTime = m_GuidingSuspended ? GuideSettleTime->value() : 0; - - // Always resume guiding if we suspended it before - if (m_GuidingSuspended) - { - emit resumeGuiding(); - m_GuidingSuspended = false; - } - resetFocusIteration = 0; - - if (settleTime > 0) - appendLogText(i18n("Settling...")); - - QTimer::singleShot(settleTime * 1000, this, [ &, status, settleTime]() - { - if (settleTime > 0) - appendLogText(i18n("Settling complete.")); - - if (status) - { - KSNotification::event(QLatin1String("FocusSuccessful"), i18n("Autofocus operation completed successfully")); - state = Ekos::FOCUS_COMPLETE; - } - else - { - KSNotification::event(QLatin1String("FocusFailed"), i18n("Autofocus operation failed"), - KSNotification::EVENT_ALERT); - state = Ekos::FOCUS_FAILED; - } - - qCDebug(KSTARS_EKOS_FOCUS) << "State:" << Ekos::getFocusStatusString(state); - - // Do not emit result back yet if we have a locked filter pending return to original filter - if (fallbackFilterPending) - { - filterManager->setFilterPosition(fallbackFilterPosition, - static_cast(FilterManager::CHANGE_POLICY | FilterManager::OFFSET_POLICY)); - return; - } - emit newStatus(state); - - }); } void Focus::checkAutoStarTimeout() @@ -3201,10 +3176,9 @@ void Focus::checkAutoStarTimeout() } } - appendLogText(i18n("No star was selected. Aborting...")); initialFocuserAbsPosition = -1; - abort(); - setAutoFocusResult(false); + appendLogText(i18n("No star was selected. Aborting...")); + completeFocusProcedure(false); } else if (state == FOCUS_WAITING) { @@ -3462,7 +3436,7 @@ void Focus::setFilterManager(const QSharedPointer &manager) connect(filterManager.data(), &FilterManager::failed, [this]() { appendLogText(i18n("Filter operation failed.")); - abort(); + completeFocusProcedure(false); } ); @@ -3568,23 +3542,19 @@ void Focus::processCaptureTimeout() if (captureTimeoutCounter >= 3) { captureTimeoutCounter = 0; + appendLogText(i18n("Exposure timeout. Aborting...")); - abort(); - if (inAutoFocus) - setAutoFocusResult(false); - else if (m_GuidingSuspended) - { - emit resumeGuiding(); - m_GuidingSuspended = false; - } - return; - } + completeFocusProcedure(false); - appendLogText(i18n("Exposure timeout. Restarting exposure...")); - ISD::CCDChip *targetChip = currentCCD->getChip(ISD::CCDChip::PRIMARY_CCD); - targetChip->abortExposure(); - targetChip->capture(exposureIN->value()); - captureTimeout.start(exposureIN->value() * 1000 + FOCUS_TIMEOUT_THRESHOLD); + } + else + { + appendLogText(i18n("Exposure timeout. Restarting exposure...")); + ISD::CCDChip *targetChip = currentCCD->getChip(ISD::CCDChip::PRIMARY_CCD); + targetChip->abortExposure(); + targetChip->capture(exposureIN->value()); + captureTimeout.start(exposureIN->value() * 1000 + FOCUS_TIMEOUT_THRESHOLD); + } } void Focus::processCaptureFailure() @@ -3595,14 +3565,7 @@ void Focus::processCaptureFailure() { captureFailureCounter = 0; appendLogText(i18n("Exposure failure. Aborting...")); - abort(); - if (inAutoFocus) - setAutoFocusResult(false); - else if (m_GuidingSuspended) - { - emit resumeGuiding(); - m_GuidingSuspended = false; - } + completeFocusProcedure(false); return; } diff --git a/kstars/ekos/focus/focus.h b/kstars/ekos/focus/focus.h index a9d0d0a13c7b426e0e5da7e651578f34a9aba46f..d671378c6ac8dd788ce3efd98e41ea7d3941e379 100644 --- a/kstars/ekos/focus/focus.h +++ b/kstars/ekos/focus/focus.h @@ -515,9 +515,9 @@ class Focus : public QWidget, public Ui::Focus /** - * @brief emitComplete emits the message needed for Analyze when focus completes. + * @brief completeAutofocusProcedure finishes off autofocus and emits a message for other modules. */ - void emitComplete(); + void completeFocusProcedure(bool success); void initializeFocuserTemperature(); void setLastFocusTemperature();