File indexing completed on 2024-04-14 14:12:03

0001 /*
0002     KStars UI tests for alignment
0003 
0004     SPDX-FileCopyrightText: 2021 Wolfgang Reissenberger <sterne-jaeger@openfuture.de>
0005 
0006     SPDX-License-Identifier: GPL-2.0-or-later
0007 */
0008 
0009 #include "test_ekos_align.h"
0010 
0011 #include "kstars_ui_tests.h"
0012 #include "test_ekos.h"
0013 #include "test_ekos_scheduler_helper.h"
0014 #include "mountmodel.h"
0015 #include "Options.h"
0016 #include "indi/guimanager.h"
0017 #include "ekos/align/align.h"
0018 
0019 #include "skymapcomposite.h"
0020 
0021 TestEkosAlign::TestEkosAlign(QObject *parent) : QObject(parent)
0022 {
0023     m_CaptureHelper = new TestEkosCaptureHelper();
0024 }
0025 
0026 
0027 void TestEkosAlign::testSlewAlign()
0028 {
0029     // slew to Kocab as target
0030     SkyObject *target = findTargetByName("Kocab");
0031     QVERIFY(m_CaptureHelper->slewTo(target->ra().Hours(), target->dec().Degrees(), true));
0032 
0033     // execute an alignment and verify the result
0034     QVERIFY(executeAlignment(target));
0035 }
0036 
0037 void TestEkosAlign::testEmptySkyAlign()
0038 {
0039     SkyObject *emptySky = new SkyObject(SkyObject::TYPE_UNKNOWN, 15.254722222222222, 72.031388888888889);
0040     // update J2000 coordinates
0041     updateJ2000Coordinates(emptySky);
0042 
0043     QVERIFY(m_CaptureHelper->slewTo(emptySky->ra().Hours(), emptySky->dec().Degrees(), true));
0044 
0045     // execute an alignment and verify the result
0046     QVERIFY(executeAlignment(emptySky));
0047 }
0048 
0049 void TestEkosAlign::testSlewDriftAlign()
0050 {
0051     // slew to Kocab as target
0052     SkyObject *target = findTargetByName("Kocab");
0053     QVERIFY(m_CaptureHelper->slewTo(target->ra().Hours(), target->dec().Degrees(), true));
0054     // execute an alignment and verify the result
0055     QVERIFY(executeAlignment(target));
0056     // now send motion north and west to create a guiding deviation
0057     Ekos::Mount *mount = Ekos::Manager::Instance()->mountModule();
0058     mount->doPulse(RA_INC_DIR, 5000, DEC_INC_DIR, 5000);
0059     // wait until the pulse ends
0060     QTest::qWait(6000);
0061     // execute an alignment and verify if the target is the same
0062     QVERIFY(executeAlignment(target));
0063 }
0064 
0065 void TestEkosAlign::testFitsAlign()
0066 {
0067     // check the FITS file
0068     QString const fits_file = "../Tests/kstars_ui/Kocab.fits";
0069     QFileInfo target_fits(fits_file);
0070     QVERIFY(target_fits.exists());
0071 
0072     // define expected target (center of the FITS file, J2000)
0073     SkyObject targetObject(SkyObject::STAR, 14.844809110652733, 74.1587773591246, 1.8f, "Kocab");
0074     targetObject.apparentCoord(static_cast<long double>(J2000), KStars::Instance()->data()->ut().djd());
0075 
0076     // slew somewhere close to the target
0077     SkyObject *close_to_target = findTargetByName("Pherkab");
0078     QVERIFY(m_CaptureHelper->slewTo(close_to_target->ra().Hours(), close_to_target->dec().Degrees(), true));
0079     // wait until the slew has finished
0080     QTRY_VERIFY_WITH_TIMEOUT(m_TelescopeStatus == ISD::Mount::MOUNT_TRACKING, 60000);
0081 
0082     // ensure that we wait until alignment completion
0083     expectedAlignStates.append(Ekos::ALIGN_PROGRESS);
0084     expectedAlignStates.append(Ekos::ALIGN_COMPLETE);
0085 
0086     // execute the FITS alignment and verify the result
0087     Ekos::Manager::Instance()->alignModule()->loadAndSlew(target_fits.filePath());
0088 
0089     // wait until the alignment succeeds
0090     KVERIFY_EMPTY_QUEUE_WITH_TIMEOUT(expectedAlignStates, 20000);
0091 
0092     // check if the alignment target matches
0093     verifyAlignmentTarget(&targetObject);
0094 }
0095 
0096 void TestEkosAlign::testAlignTargetScheduledJob()
0097 {
0098     // test scheduler with target
0099     alignWithScheduler(findTargetByName("Pherkab"));
0100 }
0101 
0102 void TestEkosAlign::testFitsAlignTargetScheduledJob()
0103 {
0104     // check the FITS file
0105     QString const fits_file = "../Tests/kstars_ui/Kocab.fits";
0106     QFileInfo target_fits(fits_file);
0107     QVERIFY(target_fits.exists());
0108 
0109     // define expected target (center of the FITS file, J2000)
0110     SkyObject targetObject(SkyObject::STAR, 14.844809110652733, 74.1587773591246, 1.8f, "Kocab");
0111     targetObject.apparentCoord(static_cast<long double>(J2000), KStars::Instance()->data()->ut().djd());
0112 
0113     // test scheduler with target
0114     alignWithScheduler(&targetObject, target_fits.absoluteFilePath());
0115 }
0116 
0117 void TestEkosAlign::testSyncOnlyAlign()
0118 {
0119     QSKIP("Align feature of sync only not implemented yet.");
0120     Ekos::Manager *ekos = Ekos::Manager::Instance();
0121     // select to Pherkab as target
0122     SkyObject *pherkab = findTargetByName("Pherkab");
0123 
0124     KTRY_SWITCH_TO_MODULE_WITH_TIMEOUT(ekos->mountModule(), 1000);
0125     // ensure that the mount is tracking
0126     KTRY_CLICK(ekos->mountModule(), trackOnB);
0127     // sync to Pherkab (use JNow for sync)
0128     ekos->mountModule()->sync(pherkab->ra().Hours(), pherkab->dec().Degrees());
0129 
0130     // execute an alignment and verify the result
0131     // Align::getTargetCoords() returns J2000 values
0132     KTRY_SWITCH_TO_MODULE_WITH_TIMEOUT(ekos->alignModule(), 1000);
0133     QVERIFY(executeAlignment(pherkab));
0134 }
0135 
0136 void TestEkosAlign::testAlignRecoverFromSlewing()
0137 {
0138     // slew to Kocab
0139     m_CaptureHelper->slewTo(14.845, 74.106, true);
0140     // expect align events: suspend --> progress --> complete
0141     expectedAlignStates.append(Ekos::ALIGN_SUSPENDED);
0142     expectedAlignStates.append(Ekos::ALIGN_PROGRESS);
0143     expectedAlignStates.append(Ekos::ALIGN_COMPLETE);
0144     // start alignment
0145     QVERIFY(m_CaptureHelper->startAligning(5));
0146     // ensure that capturing has started
0147     QTest::qWait(2000);
0148     // move the mount to interrupt alignment
0149     qCInfo(KSTARS_EKOS_TEST) << "Moving the mount to interrupt alignment...";
0150     Ekos::Mount *mount = Ekos::Manager::Instance()->mountModule();
0151     mount->slew(14.86, 74.106);
0152     mount->slew(14.845, 74.106);
0153     // check if event sequence has been completed
0154     KVERIFY_EMPTY_QUEUE_WITH_TIMEOUT(expectedAlignStates, 30000);
0155     // check the number of images and plate solves
0156     KTRY_CHECK_ALIGNMENTS(1);
0157 }
0158 
0159 void TestEkosAlign::testMountModel()
0160 {
0161     // test two runs
0162     for (int points = 2; points < 4; points++)
0163     {
0164         init();
0165         // prepare the mount model
0166         QVERIFY(prepareMountModel(points));
0167         // execute the mount model tool
0168         QVERIFY(runMountModelTool(points, false));
0169     }
0170 }
0171 
0172 void TestEkosAlign::testMountModelToolRecoverFromSlewing()
0173 {
0174     int points = 3;
0175     // prepare the mount model
0176     QVERIFY(prepareMountModel(points));
0177     // execute the mount model tool
0178     QVERIFY(runMountModelTool(points, true));
0179 }
0180 
0181 /* *********************************************************************************
0182  *
0183  * Test infrastructure
0184  *
0185  * ********************************************************************************* */
0186 
0187 void TestEkosAlign::initTestCase()
0188 {
0189     KVERIFY_EKOS_IS_HIDDEN();
0190     KTRY_OPEN_EKOS();
0191     KVERIFY_EKOS_IS_OPENED();
0192     QStandardPaths::setTestModeEnabled(true);
0193     // create temporary directory
0194     testDir = new QTemporaryDir(QStandardPaths::writableLocation(QStandardPaths::HomeLocation) + "/test-XXXXXX");
0195 
0196     // use the helper to start the profile
0197     m_CaptureHelper->startEkosProfile();
0198 
0199     prepareTestCase();
0200 }
0201 
0202 void TestEkosAlign::cleanupTestCase()
0203 {
0204     // connect to the align process to count solving results
0205     disconnect(Ekos::Manager::Instance()->alignModule(), &Ekos::Align::newSolverResults, this,
0206                &TestEkosAlign::solverResultReceived);
0207     // disconnect to the capture process to receive capture status changes
0208     disconnect(Ekos::Manager::Instance()->alignModule(), &Ekos::Align::newImage, this,
0209                &TestEkosAlign::imageReceived);
0210     // disconnect to the alignment process to receive align status changes
0211     disconnect(Ekos::Manager::Instance()->alignModule(), &Ekos::Align::newStatus, this,
0212                &TestEkosAlign::alignStatusChanged);
0213 
0214     m_CaptureHelper->cleanup();
0215     QVERIFY(m_CaptureHelper->shutdownEkosProfile());
0216     KTRY_CLOSE_EKOS();
0217     KVERIFY_EKOS_IS_HIDDEN();
0218 }
0219 
0220 void TestEkosAlign::prepareTestCase()
0221 {
0222     Ekos::Manager * const ekos = Ekos::Manager::Instance();
0223 
0224     // set logging defaults for alignment
0225     Options::setVerboseLogging(false);
0226     Options::setAlignmentLogging(false);
0227     Options::setLogToFile(false);
0228 
0229     // prepare optical trains for testing
0230     m_CaptureHelper->prepareOpticalTrains();
0231     // prepare the mount module for testing
0232     m_CaptureHelper->prepareMountModule();
0233     // prepare for focusing tests for those that use the focuser
0234     if (m_CaptureHelper->m_FocuserDevice != "")
0235         m_CaptureHelper->prepareFocusModule();
0236     // prepare for alignment tests
0237     m_CaptureHelper->prepareAlignmentModule();
0238 
0239     m_CaptureHelper->init();
0240     // close INDI window
0241     GUIManager::Instance()->close();
0242 
0243     // connect to the alignment process to receive align status changes
0244     connect(Ekos::Manager::Instance()->alignModule(), &Ekos::Align::newStatus, this,
0245             &TestEkosAlign::alignStatusChanged, Qt::UniqueConnection);
0246 
0247     // connect to the mount process to receive status changes
0248     connect(Ekos::Manager::Instance()->mountModule(), &Ekos::Mount::newStatus, this,
0249             &TestEkosAlign::telescopeStatusChanged, Qt::UniqueConnection);
0250 
0251     // connect to the align process to receive captured images
0252     connect(ekos->alignModule(), &Ekos::Align::newImage, this, &TestEkosAlign::imageReceived,
0253             Qt::UniqueConnection);
0254 
0255     // connect to the align process to count solving results
0256     connect(ekos->alignModule(), &Ekos::Align::newSolverResults, this, &TestEkosAlign::solverResultReceived,
0257             Qt::UniqueConnection);
0258 }
0259 
0260 void TestEkosAlign::init()
0261 {
0262     // reset counters
0263     solver_count = 0;
0264     image_count  = 0;
0265 }
0266 
0267 void TestEkosAlign::cleanup()
0268 {
0269     Ekos::Manager *ekos = Ekos::Manager::Instance();
0270     Ekos::Scheduler *scheduler = ekos->schedulerModule();
0271     // press stop button if running
0272     if (scheduler->status() == Ekos::SCHEDULER_STARTUP || scheduler->status() == Ekos::SCHEDULER_RUNNING)
0273         KTRY_CLICK(scheduler, startB);
0274     // in all cases, stop the scheduler and remove all jobs
0275     scheduler->stop();
0276     scheduler->removeAllJobs();
0277 }
0278 
0279 bool TestEkosAlign::prepareMountModel(int points)
0280 {
0281     KWRAP_SUB(KTRY_SWITCH_TO_MODULE_WITH_TIMEOUT(Ekos::Manager::Instance()->alignModule(), 1000));
0282     // open the mount model tool
0283     KTRY_CLICK_SUB(Ekos::Manager::Instance()->alignModule(), mountModelB);
0284 
0285     // wait until the mount model is instantiated
0286     QTest::qWait(1000);
0287     Ekos::MountModel * mountModel = Ekos::Manager::Instance()->alignModule()->mountModel();
0288     KVERIFY_SUB(mountModel != nullptr);
0289 
0290     // clear alignment points
0291     if (mountModel->alignTable->rowCount() > 0)
0292     {
0293         QTimer::singleShot(2000, [&]()
0294         {
0295             QDialog * const dialog = qobject_cast <QDialog*> (QApplication::activeModalWidget());
0296             if (dialog != nullptr)
0297                 QTest::mouseClick(dialog->findChild<QDialogButtonBox*>()->button(QDialogButtonBox::Yes), Qt::LeftButton);
0298         });
0299         mountModel->clearAllAlignB->click();
0300     }
0301     KVERIFY_SUB(mountModel->alignTable->rowCount() == 0);
0302     // set number of alignment points
0303     mountModel->alignPtNum->setValue(points);
0304     KVERIFY_SUB(mountModel->alignPtNum->value() == points);
0305     // use named stars for alignment (this hopefully improves plate soving speed)
0306     mountModel->alignTypeBox->setCurrentIndex(Ekos::MountModel::OBJECT_NAMED_STAR);
0307     KVERIFY_SUB(mountModel->alignTypeBox->currentIndex() == Ekos::MountModel::OBJECT_NAMED_STAR);
0308     // create the alignment points
0309     KVERIFY_SUB(mountModel->wizardAlignB->isEnabled());
0310     mountModel->wizardAlignB->click();
0311 
0312     // success
0313     return true;
0314 }
0315 
0316 bool TestEkosAlign::runMountModelTool(int points, bool moveMount)
0317 {
0318     Ekos::MountModel * mountModel = Ekos::Manager::Instance()->alignModule()->mountModel();
0319 
0320     // start model creation
0321     KVERIFY_SUB(mountModel->startAlignB->isEnabled());
0322     mountModel->startAlignB->click();
0323 
0324     // check alignment
0325     for (int i = 1; i <= points; i++)
0326     {
0327         KVERIFY_SUB(trackSingleAlignment(i == points, moveMount));
0328         KWRAP_SUB(KTRY_CHECK_ALIGNMENTS(i));
0329     }
0330 
0331     // success
0332     return true;
0333 }
0334 
0335 
0336 bool TestEkosAlign::trackSingleAlignment(bool lastPoint, bool moveMount)
0337 {
0338     expectedTelescopeStates.append(ISD::Mount::MOUNT_TRACKING);
0339     KVERIFY_EMPTY_QUEUE_WITH_TIMEOUT_SUB(expectedTelescopeStates, 60000);
0340 
0341     // wait one second ensuring that capturing starts
0342     QTest::qWait(1000);
0343     // determine current target position
0344     Ekos::Mount *mount = Ekos::Manager::Instance()->mountModule();
0345     SkyPoint target = mount->currentTarget();
0346     double ra = target.ra().Hours();
0347     double dec = target.dec().Degrees();
0348     // expect a suspend event if mount is moved
0349     if (moveMount)
0350         expectedAlignStates.append(Ekos::ALIGN_SUSPENDED);
0351     // expect a progress signal for start capturing and solving
0352     expectedAlignStates.append(Ekos::ALIGN_PROGRESS);
0353     // if last point is reached, expect a complete signal, else expect a slew
0354     if (lastPoint == true)
0355         expectedAlignStates.append(Ekos::ALIGN_COMPLETE);
0356     else
0357         expectedAlignStates.append(Ekos::ALIGN_SLEWING);
0358     if (moveMount)
0359     {
0360         // move the mount to interrupt alignment
0361         qCInfo(KSTARS_EKOS_TEST) << "Moving the mount to interrupt alignment...";
0362         mount->slew(ra + 0.05, dec + 0.1);
0363         mount->slew(ra, dec);
0364     }
0365     // check if event sequence has been completed
0366     KVERIFY_EMPTY_QUEUE_WITH_TIMEOUT_SUB(expectedAlignStates, 180000);
0367 
0368     // everything succeeded
0369     return true;
0370 }
0371 
0372 bool TestEkosAlign::executeAlignment(SkyObject *targetObject)
0373 {
0374     // start alignment
0375     KVERIFY_SUB(m_CaptureHelper->startAligning(5.0));
0376     // verify if alignment completes
0377     KTRY_VERIFY_WITH_TIMEOUT_SUB(m_CaptureHelper->getAlignStatus() == Ekos::ALIGN_COMPLETE, 60000);
0378     // check if the alignment target matches
0379     KVERIFY_SUB(verifyAlignmentTarget(targetObject));
0380     // success
0381     return true;
0382 }
0383 
0384 bool TestEkosAlign::verifyAlignmentTarget(SkyObject *targetObject)
0385 {
0386     QList<double> alignmentTarget = Ekos::Manager::Instance()->alignModule()->getTargetCoords();
0387     KVERIFY2_SUB(std::abs(alignmentTarget[0] - targetObject->ra0().Hours()) <
0388                  0.005, // difference small enough to capture JNow/J2000 errors
0389                  QString("RA target J2000 deviation too big: %1 received, %2 expected.").arg(alignmentTarget[0]).arg(
0390                      targetObject->ra0().Hours()).toLocal8Bit());
0391     KVERIFY2_SUB(std::abs(alignmentTarget[1] - targetObject->dec0().Degrees()) < 0.005,
0392                  QString("DEC target J2000 deviation too big: %1 received, %2 expected.").arg(alignmentTarget[1]).arg(
0393                      targetObject->dec0().Degrees()).toLocal8Bit());
0394     // success
0395     return true;
0396 }
0397 
0398 bool TestEkosAlign::alignWithScheduler(SkyObject *targetObject, QString fitsTarget)
0399 {
0400     Ekos::Manager *ekos = Ekos::Manager::Instance();
0401     Ekos::Scheduler *scheduler = ekos->schedulerModule();
0402     // start ASAP
0403     TestEkosSchedulerHelper::StartupCondition startupCondition;
0404     startupCondition.type = Ekos::START_ASAP;
0405     TestEkosSchedulerHelper::CompletionCondition completionCondition;
0406     completionCondition.type = Ekos::FINISH_SEQUENCE;
0407 
0408     // create the capture sequence file
0409     const QString sequenceFile = TestEkosSchedulerHelper::getDefaultEsqContent();
0410     const QString esqFile = testDir->filePath(QString("test.esq"));
0411     // create the scheduler file
0412     const QString schedulerFile = TestEkosSchedulerHelper::getSchedulerFile(targetObject, startupCondition,
0413                                   completionCondition, {true, false, true, false}, false, false, 30, fitsTarget);
0414     const QString eslFile = testDir->filePath(QString("test.esl"));
0415     // write both files to the test directory
0416     KVERIFY_SUB(TestEkosSchedulerHelper::writeSimpleSequenceFiles(schedulerFile, eslFile, sequenceFile, esqFile));
0417     // load the scheduler file
0418     KWRAP_SUB(KTRY_SWITCH_TO_MODULE_WITH_TIMEOUT(scheduler, 1000));
0419     scheduler->loadScheduler(eslFile);
0420 
0421     // start
0422     KTRY_CLICK_SUB(scheduler, startB);
0423 
0424     // expect a slew
0425     expectedTelescopeStates.append(ISD::Mount::MOUNT_SLEWING);
0426     expectedTelescopeStates.append(ISD::Mount::MOUNT_TRACKING);
0427     // ensure that we wait until alignment completion
0428     expectedAlignStates.append(Ekos::ALIGN_PROGRESS);
0429     expectedAlignStates.append(Ekos::ALIGN_COMPLETE);
0430 
0431     // wait until the slew has finished
0432     KVERIFY_EMPTY_QUEUE_WITH_TIMEOUT_SUB(expectedTelescopeStates, 60000);
0433     // wait until the alignment succeeds
0434     KVERIFY_EMPTY_QUEUE_WITH_TIMEOUT_SUB(expectedAlignStates, 20000);
0435 
0436     // check if the alignment target matches
0437     return verifyAlignmentTarget(targetObject);
0438 }
0439 
0440 SkyObject *TestEkosAlign::findTargetByName(QString name)
0441 {
0442     SkyObject *targetObject = KStarsData::Instance()->skyComposite()->findByName(name);
0443     // update J2000 coordinates
0444     updateJ2000Coordinates(targetObject);
0445 
0446     return targetObject;
0447 }
0448 
0449 void TestEkosAlign::updateJ2000Coordinates(SkyPoint *target)
0450 {
0451     SkyPoint J2000Coord(target->ra(), target->dec());
0452     J2000Coord.catalogueCoord(KStars::Instance()->data()->ut().djd());
0453     target->setRA0(J2000Coord.ra());
0454     target->setDec0(J2000Coord.dec());
0455 }
0456 
0457 
0458 
0459 /* *********************************************************************************
0460  *
0461  * Slots for catching state changes
0462  *
0463  * ********************************************************************************* */
0464 
0465 void TestEkosAlign::alignStatusChanged(Ekos::AlignState status)
0466 {
0467     m_AlignStatus = status;
0468     // check if the new state is the next one expected, then remove it from the stack
0469     if (!expectedAlignStates.isEmpty() && expectedAlignStates.head() == status)
0470         expectedAlignStates.dequeue();
0471 }
0472 
0473 void TestEkosAlign::telescopeStatusChanged(ISD::Mount::Status status)
0474 {
0475     m_TelescopeStatus = status;
0476     // check if the new state is the next one expected, then remove it from the stack
0477     if (!expectedTelescopeStates.isEmpty() && expectedTelescopeStates.head() == status)
0478         expectedTelescopeStates.dequeue();
0479 }
0480 
0481 void TestEkosAlign::captureStatusChanged(Ekos::CaptureState status)
0482 {
0483     m_CaptureStatus = status;
0484     // check if the new state is the next one expected, then remove it from the stack
0485     if (!expectedCaptureStates.isEmpty() && expectedCaptureStates.head() == status)
0486         expectedCaptureStates.dequeue();
0487 }
0488 
0489 void TestEkosAlign::imageReceived(const QSharedPointer<FITSView> &view)
0490 {
0491     Q_UNUSED(view);
0492     captureStatusChanged(Ekos::CAPTURE_COMPLETE);
0493     image_count++;
0494 }
0495 
0496 void TestEkosAlign::solverResultReceived(double orientation, double ra, double dec, double pixscale)
0497 {
0498     Q_UNUSED(orientation);
0499     Q_UNUSED(ra);
0500     Q_UNUSED(dec);
0501     Q_UNUSED(pixscale);
0502     solver_count++;
0503 }
0504 
0505 /* *********************************************************************************
0506  *
0507  * Main function
0508  *
0509  * ********************************************************************************* */
0510 
0511 QTEST_KSTARS_MAIN(TestEkosAlign)