File indexing completed on 2024-03-24 15:18:37

0001 /*  KStars UI tests
0002     SPDX-FileCopyrightText: 2020 Eric Dejouhanet <eric.dejouhanet@gmail.com>
0003 
0004     SPDX-License-Identifier: GPL-2.0-or-later
0005 */
0006 
0007 
0008 #include "test_ekos_guide.h"
0009 
0010 #if defined(HAVE_INDI) && HAVE_INDI
0011 
0012 #include "kstars_ui_tests.h"
0013 #include "test_ekos.h"
0014 #include "test_ekos_simulator.h"
0015 #include "test_ekos_mount.h" // KTRY_MOUNT_SYNC
0016 #include "test_ekos_focus.h" // KTRY_FOCUS_MOVETO
0017 
0018 #include "ekos/manager.h"
0019 #include "ekos/profileeditor.h"
0020 #include "Options.h"
0021 
0022 TestEkosGuide::TestEkosGuide(QObject *parent) : QObject(parent)
0023 {
0024 }
0025 
0026 void TestEkosGuide::initTestCase()
0027 {
0028     KVERIFY_EKOS_IS_HIDDEN();
0029     KTRY_OPEN_EKOS();
0030     KVERIFY_EKOS_IS_OPENED();
0031 
0032     // Update simulator profile to use PHD2 as guider
0033     {
0034         bool isDone = false;
0035         Ekos::Manager * const ekos = Ekos::Manager::Instance();
0036         QTimer::singleShot(1000, ekos, [&]
0037         {
0038             ProfileEditor* profileEditor = ekos->findChild<ProfileEditor*>("profileEditorDialog");
0039 
0040             // Change guider to PHD2
0041             QString const gt("PHD2");
0042             KTRY_PROFILEEDITOR_GADGET(QComboBox, guideTypeCombo);
0043             guideTypeCombo->setCurrentText(gt);
0044             QTRY_COMPARE(guideTypeCombo->currentText(), gt);
0045 
0046             // Create a test profile
0047             KTRY_PROFILEEDITOR_GADGET(QLineEdit, profileIN);
0048             profileIN->setText(testProfileName);
0049             QCOMPARE(profileIN->text(), testProfileName);
0050 
0051             // Disable Port Selector
0052             KTRY_PROFILEEDITOR_GADGET(QCheckBox, portSelectorCheck);
0053             portSelectorCheck->setChecked(false);
0054             QCOMPARE(portSelectorCheck->isChecked(), false);
0055 
0056             // Write PHD2 server specs
0057             KTRY_PROFILEEDITOR_GADGET(QLineEdit, externalGuideHost);
0058             externalGuideHost->setText(guider_host);
0059             QCOMPARE(externalGuideHost->text(), guider_host);
0060             KTRY_PROFILEEDITOR_GADGET(QLineEdit, externalGuidePort);
0061             externalGuidePort->setText(guider_port);
0062             QCOMPARE(externalGuidePort->text(), guider_port);
0063 
0064             // Setting an item programmatically in a treeview combobox...
0065             KTRY_PROFILEEDITOR_TREE_COMBOBOX(mountCombo, "Telescope Simulator");
0066             KTRY_PROFILEEDITOR_TREE_COMBOBOX(ccdCombo, "CCD Simulator");
0067             KTRY_PROFILEEDITOR_TREE_COMBOBOX(focuserCombo, "Focuser Simulator");
0068 
0069             // Save the profile using the "Save" button
0070             QDialogButtonBox* buttons = profileEditor->findChild<QDialogButtonBox*>("dialogButtons");
0071             QVERIFY(nullptr != buttons);
0072             QTest::mouseClick(buttons->button(QDialogButtonBox::Save), Qt::LeftButton);
0073 
0074             isDone = true;
0075         });
0076 
0077         // Cancel the profile editor if test fails
0078         QTimer * closeDialog = new QTimer(this);
0079         closeDialog->setSingleShot(true);
0080         closeDialog->setInterval(5000);
0081         ekos->connect(closeDialog, &QTimer::timeout, [&]
0082         {
0083             ProfileEditor* profileEditor = ekos->findChild<ProfileEditor*>("profileEditorDialog");
0084             if (profileEditor != nullptr)
0085                 profileEditor->reject();
0086         });
0087 
0088         // Click on "New profile" button, and let the async tests run on the modal dialog
0089         KTRY_EKOS_GADGET(QPushButton, addProfileB);
0090         QTRY_VERIFY_WITH_TIMEOUT(addProfileB->isEnabled(), 1000);
0091         QTest::mouseClick(addProfileB, Qt::LeftButton);
0092 
0093         // Click handler returned, stop the timer closing the dialog on failure
0094         closeDialog->stop();
0095         delete closeDialog;
0096 
0097         // Verification of the first test step
0098         QVERIFY(isDone);
0099     }
0100 }
0101 
0102 void TestEkosGuide::cleanupTestCase()
0103 {
0104     KTRY_CLOSE_EKOS();
0105     KVERIFY_EKOS_IS_HIDDEN();
0106 }
0107 
0108 void TestEkosGuide::init()
0109 {
0110     // In case the previous test failed
0111     stopPHD2();
0112 
0113     // Start a parallel PHD2 instance
0114     phd2 = new QProcess(this);
0115 
0116     // No success using that strange --load option, it loads and exits, but does not save anywhere
0117 #if 0
0118     QStringList args = { "--load=phd2_Simulators.phd" };
0119     phd2.start(QString("phd2"), args);
0120     if (!phd2.waitForStarted(1000))
0121     {
0122         QString text = QString("Bypassing PHD2 tests, phd2 executable failing or not found. ") + phd2.errorString();
0123         QWARN(text.toStdString().c_str());
0124         return;
0125     }
0126     // PHD2 will exit after loading the configuration
0127     QTRY_VERIFY_WITH_TIMEOUT(phd2.state() == QProcess::NotRunning, 500);
0128 #else
0129     QString const phd2_config = ".PHDGuidingV2";
0130     QStandardPaths::enableTestMode(false);
0131     QFileInfo phd2_config_home(QStandardPaths::writableLocation(QStandardPaths::HomeLocation), phd2_config);
0132     QStandardPaths::enableTestMode(true);
0133     QWARN(QString("Writing PHD configuration file to '%1'").arg(phd2_config_home.filePath()).toStdString().c_str());
0134     if (phd2_config_home.exists())
0135         QVERIFY(QFile::remove(phd2_config_home.filePath()));
0136     QVERIFY(QFile::copy(phd2_config, phd2_config_home.filePath()));
0137 #endif
0138 
0139     // Start PHD2 with the proper configuration
0140     phd2->start(QString("phd2"));
0141     QVERIFY(phd2->waitForStarted(3000));
0142     QTest::qWait(2000);
0143     QTRY_VERIFY_WITH_TIMEOUT(phd2->state() == QProcess::Running, 1000);
0144 
0145     // Try to connect to the PHD2 server
0146     QTcpSocket phd2_server(this);
0147     //phd2_server.connectToHost("localhost", guider_port.toUInt());
0148     phd2_server.connectToHost(guider_host, guider_port.toUInt(), QIODevice::ReadOnly, QAbstractSocket::IPv4Protocol);
0149     if(!phd2_server.waitForConnected(30000))
0150     {
0151         QSKIP(QString("Cannot continue, PHD2 server is unavailable (%1)").arg(phd2_server.errorString()).toStdString().c_str());
0152     }
0153     else
0154     {
0155         phd2_server.disconnectFromHost();
0156         if (phd2_server.state() == QTcpSocket::ConnectedState)
0157             QVERIFY(phd2_server.waitForDisconnected(1000));
0158         QWARN(qPrintable(QString("Connection test OK, phd2 found listening on %1:%2").arg(guider_host).arg(guider_port)));
0159     }
0160 
0161     // Start Ekos
0162     KTRY_EKOS_START_PROFILE(testProfileName);
0163 
0164     // Do not reset clock, we are using the Telescope Simulator which does not receive simulated time
0165     // HACK: Reset clock to initial conditions
0166     // KHACK_RESET_EKOS_TIME();
0167 
0168     // Wait for Focus to come up, adjust focus
0169     KTRY_FOCUS_SHOW();
0170     //KTRY_FOCUS_MOVETO(35000);
0171     // HACK HACK HACK adjust ccd gain
0172     KTRY_FOCUS_EXPOSURE(1, 99);
0173 
0174     // Wait for Guide to come up, switch to guide module
0175     KTRY_GUIDE_SHOW();
0176 
0177     // Verify the phd2 server was connected successfully - by default, PHD2 has a 16-second timeout to camera connection
0178     KTRY_GUIDE_GADGET(QPushButton, externalConnectB);
0179     QTRY_VERIFY_WITH_TIMEOUT(!externalConnectB->isEnabled(), 30000);
0180     KTRY_GUIDE_GADGET(QPushButton, externalDisconnectB);
0181     QTRY_VERIFY_WITH_TIMEOUT(externalDisconnectB->isEnabled(), 10000);
0182 }
0183 
0184 void TestEkosGuide::cleanup()
0185 {
0186     KTRY_EKOS_STOP_SIMULATORS();
0187     stopPHD2();
0188 }
0189 
0190 void TestEkosGuide::stopPHD2()
0191 {
0192     // Stop PHD2 if needed
0193     if (phd2)
0194     {
0195         phd2->terminate();
0196         phd2->waitForFinished(5000);
0197         delete phd2;
0198         phd2 = nullptr;
0199     }
0200 }
0201 
0202 void TestEkosGuide::testPHD2ConnectionStability()
0203 {
0204     KTRY_GUIDE_GADGET(QPushButton, guideB);
0205     KTRY_GUIDE_GADGET(QPushButton, stopB);
0206     KTRY_GUIDE_GADGET(QPushButton, captureB);
0207     KTRY_GUIDE_GADGET(QPushButton, loopB);
0208     KTRY_GUIDE_GADGET(QPushButton, externalConnectB);
0209     KTRY_GUIDE_GADGET(QPushButton, externalDisconnectB);
0210 
0211     // Run a few connect/disconnect cycles
0212     for (int count = 0; count < 10; count++)
0213     {
0214         QVERIFY(guideB->isEnabled());
0215         QVERIFY(captureB->isEnabled());
0216         QVERIFY(loopB->isEnabled());
0217         QVERIFY(!stopB->isEnabled());
0218         KTRY_GUIDE_CLICK(externalDisconnectB);
0219         QTRY_VERIFY_WITH_TIMEOUT(externalConnectB->isEnabled(), 20000);
0220         QVERIFY(!guideB->isEnabled());
0221         QVERIFY(!captureB->isEnabled());
0222         QVERIFY(!loopB->isEnabled());
0223         QVERIFY(!stopB->isEnabled());
0224         KTRY_GUIDE_CLICK(externalConnectB);
0225         QTRY_VERIFY_WITH_TIMEOUT(externalDisconnectB->isEnabled(), 60000);
0226     }
0227 
0228     // Disconnect
0229     KTRY_GUIDE_CLICK(externalDisconnectB);
0230     QTRY_VERIFY_WITH_TIMEOUT(externalConnectB->isEnabled(), 10000);
0231 }
0232 
0233 void TestEkosGuide::testPHD2CaptureStability()
0234 {
0235     KTRY_GUIDE_GADGET(QPushButton, stopB);
0236     KTRY_GUIDE_GADGET(QPushButton, captureB);
0237     KTRY_GUIDE_GADGET(QPushButton, loopB);
0238     KTRY_GUIDE_GADGET(QWidget, idlingStateLed);
0239     KTRY_GUIDE_GADGET(QWidget, preparingStateLed);
0240     KTRY_GUIDE_GADGET(QWidget, runningStateLed);
0241     KTRY_GUIDE_GADGET(QPushButton, externalConnectB);
0242     KTRY_GUIDE_GADGET(QPushButton, externalDisconnectB);
0243 
0244     // When connected, capture, loop and guide are enabled
0245     // Run a few guide/stop cycles
0246     QWARN("Because Guide is changing its state without waiting for PHD2 to reply, we insert a forced delay after each click");
0247     for (int count = 0; count < 10; count++)
0248     {
0249         KTRY_GUIDE_CLICK(loopB);
0250         QTest::qWait(1000);
0251         QTRY_VERIFY_WITH_TIMEOUT(stopB->isEnabled(), 10000);
0252         QEXPECT_FAIL("", "Capture button remains active while looping.", Continue);
0253         QVERIFY(!captureB->isEnabled());
0254         QEXPECT_FAIL("", "Loop button remains active while looping.", Continue);
0255         QVERIFY(!loopB->isEnabled());
0256         QTRY_VERIFY_WITH_TIMEOUT((dynamic_cast <KLed*> (preparingStateLed))->state() == KLed::On, 5000);
0257         QVERIFY((dynamic_cast <KLed*> (idlingStateLed))->state() == KLed::Off);
0258         QVERIFY((dynamic_cast <KLed*> (runningStateLed))->state() == KLed::Off);
0259         KTRY_GUIDE_CLICK(stopB);
0260         QTest::qWait(1000);
0261         QTRY_VERIFY_WITH_TIMEOUT(loopB->isEnabled(), 10000);
0262         QEXPECT_FAIL("", "Stop button remains active after stopping.", Continue);
0263         QVERIFY(!stopB->isEnabled());
0264         QVERIFY(captureB->isEnabled());
0265         QTRY_VERIFY_WITH_TIMEOUT((dynamic_cast <KLed*> (preparingStateLed))->state() == KLed::Off, 5000);
0266         QVERIFY((dynamic_cast <KLed*> (preparingStateLed))->state() == KLed::Off);
0267         QVERIFY((dynamic_cast <KLed*> (runningStateLed))->state() == KLed::Off);
0268     }
0269 
0270     // Disconnect
0271     KTRY_GUIDE_CLICK(externalDisconnectB);
0272     QTRY_VERIFY_WITH_TIMEOUT(externalConnectB->isEnabled(), 10000);
0273 }
0274 
0275 void TestEkosGuide::testPHD2Calibration()
0276 {
0277     KTRY_GUIDE_GADGET(QPushButton, guideB);
0278     KTRY_GUIDE_GADGET(QPushButton, stopB);
0279     KTRY_GUIDE_GADGET(QPushButton, clearCalibrationB);
0280     KTRY_GUIDE_GADGET(QWidget, idlingStateLed);
0281     KTRY_GUIDE_GADGET(QWidget, preparingStateLed);
0282     KTRY_GUIDE_GADGET(QWidget, runningStateLed);
0283     KTRY_GUIDE_GADGET(QPushButton, externalConnectB);
0284     KTRY_GUIDE_GADGET(QPushButton, externalDisconnectB);
0285 
0286     // We will need to wait a bit more than 30+20 times the default exposure, which is 1 second, plus processing, 2s
0287 
0288     uint const calibration_timeout = Options::guideCalibrationTimeout();
0289     Options::setGuideCalibrationTimeout(50 * 3);
0290 
0291     uint const loststar_timeout = Options::guideLostStarTimeout();
0292     Options::setGuideLostStarTimeout(10);
0293 
0294     QWARN("As of 202103 it is not possible to use the CCD Simulator at NCP because of jitter - skipping");
0295     if (false)
0296     {
0297         // Run a calibration with the telescope pointing at NCP (default position) - Calibration is bound to fail bcause of RA
0298         // Two options: PHD2 either fails to see movement, or just switches to looping without doing anything
0299         // KTRY_MOUNT_SYNC(90, false, -1);
0300         QTRY_VERIFY_WITH_TIMEOUT(guideB->isEnabled(), 500);
0301         KTRY_GUIDE_CLICK(guideB);
0302 
0303         // Wait for calibration to start");
0304         QTRY_VERIFY_WITH_TIMEOUT((dynamic_cast <KLed*> (preparingStateLed))->state() == KLed::On, 5000);
0305 
0306         // Wait for the calibration to fail
0307         QTRY_VERIFY_WITH_TIMEOUT((dynamic_cast <KLed*> (runningStateLed))->color() == Qt::red,
0308                                  Options::guideCalibrationTimeout() * 1200);
0309     }
0310 
0311     KTELL("Run a calibration with the telescope pointing after Meridian");
0312     // And don't forget to track!
0313     //KTRY_MOUNT_SYNC(20, true, -1);
0314     KTRY_MOUNT_SYNC_NAMED("Mizar", true);
0315     QTRY_VERIFY_WITH_TIMEOUT(guideB->isEnabled(), 500);
0316     KTRY_GUIDE_CLICK(guideB);
0317 
0318     double const ra = dms(Ekos::Manager::Instance()->mountModule()->raOUT->text(), false).Hours();
0319     double const dec = dms(Ekos::Manager::Instance()->mountModule()->decOUT->text(), true).Degrees();
0320 
0321     KTELL("We need to wait a bit more than 50 times the default exposure, with a processing time of 3 seconds");
0322     QTRY_VERIFY_WITH_TIMEOUT((dynamic_cast <KLed*> (runningStateLed))->color() == Qt::green,
0323                              Options::guideCalibrationTimeout() * 1200);
0324 
0325     KTELL("Wait a bit while guiding");
0326     QTest::qWait(5000);
0327 
0328     KTELL("We can stop guiding now that calibration is done");
0329     KTRY_GUIDE_CLICK(stopB);
0330     QWARN("Guide aborts without waiting for PHD2 to report abort, so wait after stopping before restarting.");
0331     QTest::qWait(500);
0332     QTRY_VERIFY_WITH_TIMEOUT((dynamic_cast <KLed*> (runningStateLed))->color() == Qt::red, 10000);
0333 
0334     KTELL("We can restart, and there will be no calibration to wait for");
0335     QTRY_VERIFY_WITH_TIMEOUT(guideB->isEnabled(), 5000);
0336     QTest::qWait(500);
0337     KTRY_GUIDE_CLICK(guideB);
0338     QTRY_VERIFY_WITH_TIMEOUT((dynamic_cast <KLed*> (runningStateLed))->color() == Qt::green, 10000);
0339 
0340     KTELL("Sync the telescope just enough for the star mass to drop, PHD2 will notify star lost");
0341     Ekos::Manager::Instance()->mountModule()->sync(ra - 0.01, dec);
0342     QTRY_VERIFY_WITH_TIMEOUT((dynamic_cast <KLed*> (runningStateLed))->color() == Qt::yellow, 10000);
0343 
0344     KTELL("Sync the telescope back for the star mass to return, PHD2 will notify star selected");
0345     Ekos::Manager::Instance()->mountModule()->sync(ra - 0.00, dec);
0346     QTRY_VERIFY_WITH_TIMEOUT((dynamic_cast <KLed*> (runningStateLed))->color() == Qt::green, 10000);
0347 
0348     KTELL("Sync the telescope just enough for the star mass to drop, PHD2 will notify star lost again");
0349     Ekos::Manager::Instance()->mountModule()->sync(ra - 0.01, dec);
0350     QTRY_VERIFY_WITH_TIMEOUT((dynamic_cast <KLed*> (runningStateLed))->color() == Qt::yellow, 10000);
0351 
0352     KTELL("Wait for guiding to abort");
0353     QTRY_VERIFY_WITH_TIMEOUT((dynamic_cast <KLed*> (runningStateLed))->color() == Qt::red,
0354                              Options::guideLostStarTimeout() * 1200);
0355     QVERIFY((dynamic_cast <KLed*> (preparingStateLed))->color() == Qt::red);
0356     QVERIFY((dynamic_cast <KLed*> (idlingStateLed))->color() == Qt::green);
0357 
0358     // We can restart, and wait for calibration to end");
0359     // However that test is not stable - sometimes PHD2 will refuse to continue, sometimes will catch something
0360     QWARN("Restarting to guide when there is no star locked may or may not look for a lock position, bypassing test.");
0361     //KTRY_GUIDE_CLICK(guideB);
0362     //QTRY_VERIFY_WITH_TIMEOUT((dynamic_cast <KLed*> (runningStateLed))->color() == Qt::green, Options::guideCalibrationTimeout() * 1200);
0363 
0364     KTELL("Clear calibration and restart");
0365     QTRY_VERIFY_WITH_TIMEOUT(clearCalibrationB->isEnabled(), 10000);
0366     KTRY_GUIDE_CLICK(clearCalibrationB);
0367     QWARN("No feedback available on PHD2 calibration removal, so wait a bit.");
0368     QTest::qWait(1000);
0369     KTRY_GUIDE_CLICK(guideB);
0370     QTRY_VERIFY_WITH_TIMEOUT((dynamic_cast <KLed*> (runningStateLed))->color() == Qt::green,
0371                              Options::guideCalibrationTimeout() * 1200);
0372 
0373     // It is apparently not possible to check the option "Stop guiding when mount moves" through the server connection
0374     // TODO: update options to enable this
0375 #if 0
0376     // Slew the telescope somewhere else, PHD2 will notify guiding abort
0377     QVERIFY(Ekos::Manager::Instance()->mountModule()->slew(3.1, 0));
0378     QTRY_VERIFY_WITH_TIMEOUT(Ekos::Manager::Instance()->mountModule()->status() == ISD::Mount::MOUNT_TRACKING, 10000);
0379     QTRY_VERIFY_WITH_TIMEOUT((dynamic_cast <KLed*> (runningStateLed))->color() == Qt::red, 10000);
0380 
0381     // We can restart, and there will be no calibration to wait for
0382     KTRY_GUIDE_CLICK(guideB);
0383     QTRY_VERIFY_WITH_TIMEOUT((dynamic_cast <KLed*> (runningStateLed))->color() == Qt::green, 5000);
0384 #endif
0385 
0386     // Run device disconnection tests
0387     // TODO: Ekos::Manager::Instance()->mountModule()->currentTelescope->Disconnect();
0388     // TODO: Ekos::Manager::Instance()->captureModule()->currentCCD->Disconnect();
0389 
0390     // TODO: Manipulate PHD2 directly to dis/connect and loop
0391 
0392     // Stop now - if the previous test successfully started to guide
0393     if (stopB->isEnabled())
0394     {
0395         KTRY_GUIDE_CLICK(stopB);
0396         QWARN("Guide aborts without waiting for PHD2 to report abort, so wait after stopping before restarting.");
0397         QTest::qWait(500);
0398         QTRY_VERIFY_WITH_TIMEOUT((dynamic_cast <KLed*> (runningStateLed))->state() == KLed::Off, 5000);
0399         QTRY_VERIFY_WITH_TIMEOUT(guideB->isEnabled(), 2000);
0400     }
0401 
0402     // Revert timeout options
0403     Options::setGuideCalibrationTimeout(calibration_timeout);
0404     Options::setGuideLostStarTimeout(loststar_timeout);
0405 
0406     // Disconnect
0407     KTRY_GUIDE_CLICK(externalDisconnectB);
0408     QTRY_VERIFY_WITH_TIMEOUT(externalConnectB->isEnabled(), 10000);
0409 }
0410 
0411 QTEST_KSTARS_MAIN(TestEkosGuide)
0412 
0413 #endif