File indexing completed on 2024-04-21 14:47:26

0001 /*
0002     Tests for meridian flip state machine.
0003 
0004     SPDX-FileCopyrightText: 2022 Wolfgang Reissenberger <sterne-jaeger@openfuture.de>
0005 
0006     SPDX-License-Identifier: GPL-2.0-or-later
0007 */
0008 
0009 #include "test_ekos_meridianflipstate.h"
0010 
0011 #include "kstars.h"
0012 #include "kstars_ui_tests.h"
0013 #include "test_ekos_debug.h"
0014 #include "kstarsdata.h"
0015 #include "Options.h"
0016 #include "skymapcomposite.h"
0017 #include "indicom.h"
0018 
0019 
0020 
0021 
0022 void TestEkosMeridianFlipState::testMeridianFlip()
0023 {
0024     QFETCH(bool, enabled);
0025     QFETCH(bool, captureInterface);
0026     QFETCH(bool, upper);
0027     QFETCH(double, offset);
0028     const double base_offset = 15.0;
0029     // mount should be in tracking mode
0030     QVERIFY(m_MountStatus == ISD::Mount::MOUNT_TRACKING);
0031     // expect SLEW --> TRACK
0032     expectedMountStates.append(ISD::Mount::MOUNT_SLEWING);
0033     expectedMountStates.append(ISD::Mount::MOUNT_TRACKING);
0034     // find a target 10 sec before meridian
0035     SkyPoint target = findMFTestTarget(base_offset, upper);
0036     m_Mount->Slew(target);
0037     // expect finished slew to target close to the meridian
0038     QTRY_VERIFY_WITH_TIMEOUT(expectedMountStates.isEmpty(), 10000);
0039     QVERIFY(m_currentPosition.valid && m_currentPosition.position == target);
0040     QVERIFY2(m_currentPosition.ha.HoursHa() - (upper ? 0 : 12) < 0,
0041              QString("Current HA=%1").arg(m_currentPosition.ha.HoursHa()).toLatin1());
0042     QVERIFY(std::abs(m_currentPosition.ha.HoursHa() - (upper ? 0 : 12) + base_offset / 3600) * 3600 <=
0043             60.0); // less than 60 arcsec error
0044     QVERIFY(m_currentPosition.pierSide == (upper ? ISD::Mount::PIER_WEST : ISD::Mount::PIER_EAST));
0045 
0046     // expect RUNNING --> COMPLETED
0047     expectedMeridianFlipMountStates.append(Ekos::MeridianFlipState::MOUNT_FLIP_RUNNING);
0048     expectedMeridianFlipMountStates.append(Ekos::MeridianFlipState::MOUNT_FLIP_COMPLETED);
0049     // expect SLEW --> TRACK
0050     expectedMountStates.append(ISD::Mount::MOUNT_SLEWING);
0051     expectedMountStates.append(ISD::Mount::MOUNT_TRACKING);
0052 
0053     if (enabled)
0054     {
0055         // wait until the meridian flip is planned
0056         QTRY_VERIFY_WITH_TIMEOUT(m_MeridianFlipMountStatus == Ekos::MeridianFlipState::MOUNT_FLIP_PLANNED, 15000);
0057         // check if the time to the meridian flip is within the range
0058         QTRY_VERIFY_WITH_TIMEOUT(secs_to_mf >= 0, 5000);
0059         QVERIFY(std::abs(secs_to_mf - base_offset - offset * 4 * 60) <= 20.0);
0060 
0061         if (captureInterface)
0062         {
0063             // acknowledge as requested (TODO: this should be shifted to the state machine!)
0064             m_stateMachine->updateMeridianFlipStage(Ekos::MeridianFlipState::MF_REQUESTED);
0065             qCInfo(KSTARS_EKOS_TEST) << "Meridian flip requested.";
0066             // accept the flip
0067             m_stateMachine->updateMFMountState(Ekos::MeridianFlipState::MOUNT_FLIP_ACCEPTED);
0068             qCInfo(KSTARS_EKOS_TEST) << "Meridian flip accepted.";
0069         }
0070 
0071         // wait until the slew completes
0072         QTRY_VERIFY_WITH_TIMEOUT(expectedMountStates.isEmpty(), 10000);
0073         // meridian flip should be completed
0074         QTRY_VERIFY_WITH_TIMEOUT(expectedMeridianFlipMountStates.isEmpty(), 3 * Options::minFlipDuration());
0075         // mount is on the east side for upper, west for lower culmination
0076         QVERIFY(m_currentPosition.pierSide == (upper ? ISD::Mount::PIER_EAST : ISD::Mount::PIER_WEST));
0077     }
0078     else
0079     {
0080         // meridian flip not enabled
0081         QTest::qWait(10.0);
0082         // still no flip planned
0083         QVERIFY(m_MeridianFlipMountStatus == Ekos::MeridianFlipState::MOUNT_FLIP_NONE);
0084         QVERIFY(expectedMeridianFlipMountStates.size() == 2);
0085         QVERIFY(expectedMountStates.size() == 2);
0086     }
0087 }
0088 
0089 void TestEkosMeridianFlipState::testMeridianFlip_data()
0090 {
0091 #if QT_VERSION < QT_VERSION_CHECK(5,9,0)
0092     QSKIP("Bypassing fixture test on old Qt");
0093     Q_UNUSED(enabled)
0094     Q_UNUSED(captureInterface)
0095     Q_UNUSED(culmination)
0096     Q_UNUSED(offset)
0097 #else
0098     QTest::addColumn<QString>("location");      /*!< geographical location? */
0099     QTest::addColumn<bool>("enabled");          /*!< meridian flip enabled? */
0100     QTest::addColumn<bool>("captureInterface"); /*!< capture interface present? */
0101     QTest::addColumn<bool>("upper");            /*!< upper culmination? */
0102     QTest::addColumn<double>("offset");         /*!< meridian flip offset (in degrees) */
0103 
0104     for (auto loc :
0105             {"Cape Town", "Greenwich"
0106             })
0107     {
0108         QTest::newRow(QString("loc=%1 enabled=no").arg(loc).toLatin1()) << loc << false << false << true << 0.0;
0109         QTest::newRow(QString("loc=%1 enabled=yes, capture=no").arg(loc).toLatin1()) << loc << true << false << true << 0.0;
0110         QTest::newRow(QString("loc=%1 enabled=yes, capture=yes, upper=yes").arg(loc).toLatin1()) << loc << true << true << true <<
0111                 0.0;
0112         QTest::newRow(QString("loc=%1 enabled=yes, capture=yes, upper=no").arg(loc).toLatin1()) << loc << true << true << false <<
0113                 0.0;
0114         QTest::newRow(QString("loc=%1 enabled=yes, capture=yes, upper=yes offset=15'").arg(loc).toLatin1()) << loc << true << true
0115                 << true << 0.25;
0116         QTest::newRow(QString("loc=%1 enabled=yes, capture=yes, upper=no offset=15'").arg(loc).toLatin1()) << loc << true << true <<
0117                 false << 0.25;
0118     }
0119 
0120 #endif
0121 }
0122 
0123 void TestEkosMeridianFlipState::connectAdaptor()
0124 {
0125     m_Mount = new MountSimulator();
0126 
0127     // connect to the mount process position changes
0128     connect(m_Mount, &MountSimulator::newCoords, this,
0129             &TestEkosMeridianFlipState::updateTelescopeCoord, Qt::UniqueConnection);
0130     // connect to the mount process status changes
0131     connect(m_Mount, &MountSimulator::newStatus, this,
0132             &TestEkosMeridianFlipState::mountStatusChanged, Qt::UniqueConnection);
0133     // connect to the state machine to receive meridian flip status changes
0134     connect(m_stateMachine, &Ekos::MeridianFlipState::newMountMFStatus, this,
0135             &TestEkosMeridianFlipState::meridianFlipMountStatusChanged, Qt::UniqueConnection);
0136     // publish the meridian flip mount status
0137     connect(m_stateMachine, &Ekos::MeridianFlipState::newMeridianFlipMountStatusText, [ = ](QString statustext)
0138     {
0139         qCInfo(KSTARS_EKOS_TEST) << statustext;
0140         if (secs_to_mf < 0 && statustext.startsWith("Meridian flip in"))
0141             secs_to_mf = m_Helper.secondsToMF(statustext);
0142     });
0143 
0144     // connect the state machine to the mount simulator
0145     connect(m_Mount, &MountSimulator::newStatus, m_stateMachine, &Ekos::MeridianFlipState::setMountStatus,
0146             Qt::UniqueConnection);
0147     connect(m_Mount, &MountSimulator::newCoords, m_stateMachine, &Ekos::MeridianFlipState::updateTelescopeCoord,
0148             Qt::UniqueConnection);
0149     connect(m_stateMachine, &Ekos::MeridianFlipState::slewTelescope, m_Mount, &MountSimulator::Slew, Qt::UniqueConnection);
0150 
0151     // initialize home position
0152     SkyPoint *pos = KStars::Instance()->data()->skyComposite()->findByName("Kocab");
0153     m_Mount->init(*pos);
0154 
0155     // activate the mount in the state machine
0156     m_stateMachine->setMountConnected(true);
0157 }
0158 
0159 void TestEkosMeridianFlipState::disconnectAdaptor()
0160 {
0161     m_Mount->shutdown();
0162     m_stateMachine->setMountConnected(false);
0163     disconnect(m_Mount, nullptr, nullptr, nullptr);
0164     disconnect(m_stateMachine, &Ekos::MeridianFlipState::newMountMFStatus, this,
0165                &TestEkosMeridianFlipState::meridianFlipMountStatusChanged);
0166 
0167     m_Mount->deleteLater();
0168 }
0169 
0170 SkyPoint TestEkosMeridianFlipState::findMFTestTarget(int secsToMF, bool upper)
0171 {
0172     // translate seconds into fractions of degrees
0173     double delta = static_cast<double>(secsToMF) / 240; // 240 = 86400 / 360
0174 
0175     // determine the hemisphere
0176     const bool northern = (KStarsData::Instance()->geo()->lat()->Degrees() > 0);
0177 
0178     // Azimuth position close to meridian
0179     double az = range360(((upper != northern) ? 0.0 : 180.0) + (northern ? -delta : delta));
0180     // we assume that all locations have a longitude > 10 deg
0181     double alt = 10;
0182 
0183     // Define the target
0184     SkyPoint target;
0185     target.setAlt(alt);
0186     target.setAz(az);
0187 
0188     // calculate local sideral time, converted to degrees, and observer's latitude
0189     const dms lst = KStarsData::Instance()->geo()->GSTtoLST(KStarsData::Instance()->clock()->utc().gst());
0190     const dms lat(KStarsData::Instance()->geo()->lat()->Degrees());
0191     // calculate JNow RA/DEC
0192     target.HorizontalToEquatorial(&lst, &lat);
0193 
0194     return target;
0195 }
0196 
0197 void TestEkosMeridianFlipState::updateTelescopeCoord(const SkyPoint &position, ISD::Mount::PierSide pierSide, const dms &ha)
0198 {
0199     m_currentPosition.position = position;
0200     m_currentPosition.pierSide = pierSide;
0201     m_currentPosition.ha       = ha;
0202     m_currentPosition.valid    = true;
0203     qCDebug(KSTARS_EKOS_TEST) << "RA="
0204                               << m_currentPosition.position.ra().toHMSString()
0205                               << ", DEC="
0206                               << m_currentPosition.position.dec().toDMSString()
0207                               << ", ha=" << ha.HoursHa()
0208                               << ", "
0209                               << ISD::Mount::pierSideStateString(m_currentPosition.pierSide);
0210 }
0211 
0212 void TestEkosMeridianFlipState::mountStatusChanged(ISD::Mount::Status status)
0213 {
0214     m_MountStatus = status;
0215     // check if the new state is the next one expected, then remove it from the stack
0216     if (!expectedMountStates.isEmpty() && expectedMountStates.head() == status)
0217         expectedMountStates.dequeue();
0218 }
0219 
0220 void TestEkosMeridianFlipState::meridianFlipMountStatusChanged(Ekos::MeridianFlipState::MeridianFlipMountState status)
0221 {
0222     m_MeridianFlipMountStatus = status;
0223     // check if the new state is the next one expected, then remove it from the stack
0224     if (!expectedMeridianFlipMountStates.isEmpty() && expectedMeridianFlipMountStates.head() == status)
0225         expectedMeridianFlipMountStates.dequeue();
0226 }
0227 
0228 /* *********************************************************************************
0229  * Test infrastructure
0230  * ********************************************************************************* */
0231 void TestEkosMeridianFlipState::init()
0232 {
0233     // set the location
0234     QFETCH(QString, location);
0235     GeoLocation * const geo = KStars::Instance()->data()->locationNamed(location);
0236     QVERIFY(geo != nullptr);
0237     KStars::Instance()->data()->setLocation(*geo);
0238     // initialize the helper
0239     m_Helper.init();
0240     // clear the time to flip
0241     secs_to_mf = -1;
0242     // clear the meridian flip state
0243     m_MeridianFlipMountStatus = Ekos::MeridianFlipState::MOUNT_FLIP_NONE;
0244     // clear queues
0245     expectedMeridianFlipMountStates.clear();
0246     expectedMountStates.clear();
0247 
0248     // run 10x as fast
0249     KStarsData::Instance()->clock()->setClockScale(10.0);
0250     // start the clock
0251     KStarsData::Instance()->clock()->start();
0252     // initialize the state machine
0253     m_stateMachine = new Ekos::MeridianFlipState();
0254     // enable the meridian flip
0255     QFETCH(bool, enabled);
0256     m_stateMachine->setEnabled(enabled);
0257     // simulate the capture interface through the test case
0258     QFETCH(bool, captureInterface);
0259     m_stateMachine->setHasCaptureInterface(captureInterface);
0260     // set the meridian flip offset
0261     QFETCH(double, offset);
0262     m_stateMachine->setOffset(offset);
0263     // clear current position
0264     m_currentPosition.valid = false;
0265     // set delay of 5s to ignore too early tracking
0266     Options::setMinFlipDuration(10);
0267     // connect the test adaptor to the test case
0268     connectAdaptor();
0269 }
0270 
0271 void TestEkosMeridianFlipState::cleanup()
0272 {
0273     disconnectAdaptor();
0274     delete m_stateMachine;
0275 }
0276 
0277 void TestEkosMeridianFlipState::initTestCase()
0278 {
0279 
0280 }
0281 
0282 void TestEkosMeridianFlipState::cleanupTestCase()
0283 {
0284 
0285 }
0286 
0287 /* *********************************************************************************
0288  * Test adapter
0289  * ********************************************************************************* */
0290 
0291 
0292 
0293 void MountSimulator::updatePosition()
0294 {
0295     if (m_status == ISD::Mount::MOUNT_SLEWING && KStarsData::Instance()->clock()->utc() > slewFinishedTime)
0296     {
0297         // slew finished
0298         m_position = m_targetPosition;
0299         m_pierside = m_targetPierside;
0300         emit newCoords(m_position, m_pierside, calcHA(m_position));
0301         setStatus(ISD::Mount::MOUNT_TRACKING);
0302         qCInfo(KSTARS_EKOS_TEST) << "Mount tracking.";
0303     }
0304 
0305     // in any case, report the current position
0306     emit newCoords(m_position, m_pierside, calcHA(m_position));
0307 }
0308 
0309 void MountSimulator::init(const SkyPoint &position)
0310 {
0311     m_position = position;
0312     // regularly report the position and recalculated HA
0313     connect(KStarsData::Instance()->clock(), &SimClock::timeAdvanced, this, &MountSimulator::updatePosition);
0314     // set state to tracking
0315     setStatus(ISD::Mount::MOUNT_TRACKING);
0316 }
0317 
0318 void MountSimulator::shutdown()
0319 {
0320     disconnect(KStarsData::Instance()->clock(), &SimClock::timeAdvanced, this, &MountSimulator::updatePosition);
0321 }
0322 
0323 void MountSimulator::Slew(const SkyPoint &position)
0324 {
0325     // start slewing
0326     setStatus(ISD::Mount::MOUNT_SLEWING);
0327     qCInfo(KSTARS_EKOS_TEST) << "Mount slewing...";
0328     m_targetPosition = position;
0329     m_targetPierside = calcPierSide(position);
0330     // calculate an additional delay if pier side needs to be changed
0331     double delay = (m_pierside == ISD::Mount::PIER_UNKNOWN
0332                     || m_pierside == m_targetPierside) ? 0. : 2. * Options::minFlipDuration();
0333     QTest::qWait(100);
0334     // set time when slew should be finished and tracking should start
0335     slewFinishedTime = KStarsData::Instance()->clock()->utc().addSecs(delay);
0336     // slewing, pier side change and changing back to tracking will be handled by #updatePosition()
0337 }
0338 
0339 void MountSimulator::Sync(const SkyPoint &position)
0340 {
0341     m_position = position;
0342     // set pier side only if unknown
0343     if (m_pierside == ISD::Mount::PIER_UNKNOWN)
0344         m_pierside = calcPierSide(position);
0345 
0346     emit newCoords(m_position, m_pierside, calcHA(position));
0347     setStatus(ISD::Mount::MOUNT_TRACKING);
0348 }
0349 
0350 void MountSimulator::setStatus(ISD::Mount::Status value)
0351 {
0352     if (m_status != value)
0353         emit newStatus(value);
0354 
0355     m_status = value;
0356 }
0357 
0358 dms MountSimulator::calcHA(const SkyPoint &pos)
0359 {
0360     dms lst = KStarsData::Instance()->geo()->GSTtoLST(KStarsData::Instance()->clock()->utc().gst());
0361     return dms(lst.Degrees() - pos.ra().Degrees());
0362 }
0363 
0364 ISD::Mount::PierSide MountSimulator::calcPierSide(const SkyPoint &pos)
0365 {
0366     double ha = calcHA(pos).HoursHa(); // -12 <= ha <= 12
0367     if (ha <= 0)
0368         return ISD::Mount::PIER_WEST;
0369     else
0370         return ISD::Mount::PIER_EAST;
0371 }
0372 
0373 QTEST_KSTARS_MAIN(TestEkosMeridianFlipState)