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)