File indexing completed on 2024-04-28 15:09:54
0001 /* Ekos state machine for the meridian flip 0002 SPDX-FileCopyrightText: Wolfgang Reissenberger <sterne-jaeger@openfuture.de> 0003 0004 SPDX-License-Identifier: GPL-2.0-or-later 0005 */ 0006 0007 #include "meridianflipstate.h" 0008 #include "ekos/mount/mount.h" 0009 #include "Options.h" 0010 0011 #include "kstarsdata.h" 0012 #include "indicom.h" 0013 0014 #include <ekos_capture_debug.h> 0015 0016 namespace Ekos 0017 { 0018 MeridianFlipState::MeridianFlipState(QObject *parent) : QObject(parent) 0019 { 0020 } 0021 0022 QString MeridianFlipState::MFStageString(MFStage stage) 0023 { 0024 switch(stage) 0025 { 0026 case MF_NONE: 0027 return "MF_NONE"; 0028 case MF_REQUESTED: 0029 return "MF_REQUESTED"; 0030 case MF_READY: 0031 return "MF_READY"; 0032 case MF_INITIATED: 0033 return "MF_INITIATED"; 0034 case MF_FLIPPING: 0035 return "MF_FLIPPING"; 0036 case MF_COMPLETED: 0037 return "MF_COMPLETED"; 0038 case MF_ALIGNING: 0039 return "MF_ALIGNING"; 0040 case MF_GUIDING: 0041 return "MF_GUIDING"; 0042 } 0043 return "MFStage unknown."; 0044 } 0045 0046 void MeridianFlipState::setEnabled(bool value) 0047 { 0048 m_enabled = value; 0049 // reset meridian flip if disabled 0050 if (m_enabled == false) 0051 updateMFMountState(MOUNT_FLIP_NONE); 0052 } 0053 0054 void MeridianFlipState::connectMount(Mount *mount) 0055 { 0056 connect(mount, &Mount::newCoords, this, &MeridianFlipState::updateTelescopeCoord, Qt::UniqueConnection); 0057 connect(mount, &Mount::newStatus, this, &MeridianFlipState::setMountStatus, Qt::UniqueConnection); 0058 } 0059 0060 void MeridianFlipState::updateMeridianFlipStage(const MFStage &stage) 0061 { 0062 qCDebug(KSTARS_EKOS_CAPTURE) << "updateMeridianFlipStage: " << MeridianFlipState::MFStageString(stage); 0063 0064 if (meridianFlipStage != stage) 0065 { 0066 switch (stage) 0067 { 0068 case MeridianFlipState::MF_NONE: 0069 meridianFlipStage = stage; 0070 break; 0071 0072 case MeridianFlipState::MF_READY: 0073 if (getMeridianFlipStage() == MeridianFlipState::MF_REQUESTED) 0074 { 0075 // we keep the stage on requested until the mount starts the meridian flip 0076 updateMFMountState(MeridianFlipState::MOUNT_FLIP_ACCEPTED); 0077 } 0078 else if (m_CaptureState == CAPTURE_PAUSED) 0079 { 0080 // paused after meridian flip requested 0081 meridianFlipStage = stage; 0082 updateMFMountState(MeridianFlipState::MOUNT_FLIP_ACCEPTED); 0083 } 0084 else if (!(checkMeridianFlipRunning() 0085 || getMeridianFlipStage() == MeridianFlipState::MF_COMPLETED)) 0086 { 0087 // if neither a MF has been requested (checked above) or is in a post 0088 // MF calibration phase, no MF needs to take place. 0089 // Hence we set to the stage to NONE 0090 meridianFlipStage = MeridianFlipState::MF_NONE; 0091 break; 0092 } 0093 // in any other case, ignore it 0094 break; 0095 0096 case MeridianFlipState::MF_INITIATED: 0097 meridianFlipStage = MeridianFlipState::MF_INITIATED; 0098 break; 0099 0100 case MeridianFlipState::MF_REQUESTED: 0101 if (m_CaptureState == CAPTURE_PAUSED) 0102 // paused before meridian flip requested 0103 updateMFMountState(MeridianFlipState::MOUNT_FLIP_ACCEPTED); 0104 else 0105 updateMFMountState(MeridianFlipState::MOUNT_FLIP_WAITING); 0106 meridianFlipStage = stage; 0107 break; 0108 0109 case MeridianFlipState::MF_COMPLETED: 0110 meridianFlipStage = MeridianFlipState::MF_COMPLETED; 0111 break; 0112 0113 default: 0114 meridianFlipStage = stage; 0115 break; 0116 } 0117 } 0118 } 0119 0120 0121 0122 bool MeridianFlipState::checkMeridianFlip(dms lst) 0123 { 0124 // checks if a flip is possible 0125 if (m_hasMount == false) 0126 { 0127 publishMFMountStatusText(i18n("Meridian flip inactive (no scope connected)")); 0128 updateMFMountState(MOUNT_FLIP_NONE); 0129 return false; 0130 } 0131 0132 if (isEnabled() == false) 0133 { 0134 publishMFMountStatusText(i18n("Meridian flip inactive (flip not requested)")); 0135 return false; 0136 } 0137 0138 // Will never get called when parked! 0139 if (m_MountParkStatus == ISD::PARK_PARKED) 0140 { 0141 publishMFMountStatusText(i18n("Meridian flip inactive (parked)")); 0142 return false; 0143 } 0144 0145 if (targetPosition.valid == false || isEnabled() == false) 0146 { 0147 publishMFMountStatusText(i18n("Meridian flip inactive (no target set)")); 0148 return false; 0149 } 0150 0151 // get the time after the meridian that the flip is called for (Degrees --> Hours) 0152 double offset = rangeHA(m_offset / 15.0); 0153 0154 double hrsToFlip = 0; // time to go to the next flip - hours -ve means a flip is required 0155 0156 double ha = currentPosition.ha.HoursHa(); // -12 to 0 to +12 0157 0158 // calculate time to next flip attempt. This uses the current hour angle, the pier side if available 0159 // and the meridian flip offset to get the time to the flip 0160 // 0161 // *** should it use the target position so it will continue to track the target even if the mount is not tracking? 0162 // 0163 // Note: the PierSide code relies on the mount reporting the pier side correctly 0164 // It is possible that a mount can flip before the meridian and this has caused problems so hrsToFlip is calculated 0165 // assuming the mount can flip up to three hours early. 0166 0167 static ISD::Mount::PierSide initialPierSide; // used when the flip has completed to determine if the flip was successful 0168 0169 // adjust ha according to the pier side. 0170 switch (currentPosition.pierSide) 0171 { 0172 case ISD::Mount::PierSide::PIER_WEST: 0173 // this is the normal case, tracking from East to West, flip is near Ha 0. 0174 break; 0175 case ISD::Mount::PierSide::PIER_EAST: 0176 // this is the below the pole case, tracking West to East, flip is near Ha 12. 0177 // shift ha by 12h 0178 ha = rangeHA(ha + 12); 0179 break; 0180 default: 0181 // This is the case where the PierSide is not available, make one attempt only 0182 setFlipDelayHrs(0); 0183 // we can only attempt a flip if the mount started before the meridian, assumed in the unflipped state 0184 if (initialPositionHA() >= 0) 0185 { 0186 publishMFMountStatusText(i18n("Meridian flip inactive (slew after meridian)")); 0187 if (getMeridianFlipMountState() == MOUNT_FLIP_NONE) 0188 return false; 0189 } 0190 break; 0191 } 0192 // get the time to the next flip, allowing for the pier side and 0193 // the possibility of an early flip 0194 // adjust ha so an early flip is allowed for 0195 if (ha >= 9.0) 0196 ha -= 24.0; 0197 hrsToFlip = offset + getFlipDelayHrs() - ha; 0198 0199 int hh = static_cast<int> (hrsToFlip); 0200 int mm = static_cast<int> ((hrsToFlip - hh) * 60); 0201 int ss = static_cast<int> ((hrsToFlip - hh - mm / 60.0) * 3600); 0202 QString message = i18n("Meridian flip in %1", QTime(hh, mm, ss).toString(Qt::TextDate)); 0203 0204 // handle the meridian flip state machine 0205 switch (getMeridianFlipMountState()) 0206 { 0207 case MOUNT_FLIP_NONE: 0208 publishMFMountStatusText(message); 0209 0210 if (hrsToFlip <= 0) 0211 { 0212 // signal that a flip can be done 0213 qCInfo(KSTARS_EKOS_MOUNT) << "Meridian flip planned with LST=" << 0214 lst.toHMSString() << 0215 " scope RA=" << currentPosition.position.ra().toHMSString() << 0216 " ha=" << ha << 0217 ", meridian diff=" << offset << 0218 ", hrstoFlip=" << hrsToFlip << 0219 ", flipDelayHrs=" << getFlipDelayHrs() << 0220 ", " << ISD::Mount::pierSideStateString(currentPosition.pierSide); 0221 0222 initialPierSide = currentPosition.pierSide; 0223 updateMFMountState(MOUNT_FLIP_PLANNED); 0224 } 0225 break; 0226 0227 case MOUNT_FLIP_PLANNED: 0228 // handle the case where there is no Capture module 0229 if (m_hasCaptureInterface == false) 0230 { 0231 qCDebug(KSTARS_EKOS_MOUNT) << "no capture interface, starting flip slew."; 0232 updateMFMountState(MOUNT_FLIP_ACCEPTED); 0233 return true; 0234 } 0235 return false; 0236 0237 case MOUNT_FLIP_ACCEPTED: 0238 // set by the Capture module when it's ready 0239 return true; 0240 0241 case MOUNT_FLIP_RUNNING: 0242 if (m_MountStatus == ISD::Mount::MOUNT_TRACKING) 0243 { 0244 if (minMeridianFlipEndTime <= KStarsData::Instance()->clock()->utc()) 0245 { 0246 // meridian flip slew completed, did it work? 0247 // check tracking only when the minimal flip duration has passed 0248 bool flipFailed = false; 0249 0250 // pointing state change check only for mounts that report pier side 0251 if (currentPosition.pierSide == ISD::Mount::PIER_UNKNOWN) 0252 { 0253 appendLogText(i18n("Assuming meridian flip completed, but pier side unknown.")); 0254 // signal that capture can resume 0255 updateMFMountState(MOUNT_FLIP_COMPLETED); 0256 return false; 0257 } 0258 else if (currentPosition.pierSide == initialPierSide) 0259 { 0260 flipFailed = true; 0261 qCWarning(KSTARS_EKOS_MOUNT) << "Meridian flip failed, pier side not changed"; 0262 } 0263 0264 if (flipFailed) 0265 { 0266 if (getFlipDelayHrs() <= 1.0) 0267 { 0268 // Set next flip attempt to be 4 minutes in the future. 0269 // These depend on the assignment to flipDelayHrs above. 0270 constexpr double delayHours = 4.0 / 60.0; 0271 if (currentPosition.pierSide == ISD::Mount::PierSide::PIER_EAST) 0272 setFlipDelayHrs(rangeHA(ha + 12 + delayHours) - offset); 0273 else 0274 setFlipDelayHrs(ha + delayHours - offset); 0275 0276 // check to stop an infinite loop, 1.0 hrs for now but should use the Ha limit 0277 appendLogText(i18n("meridian flip failed, retrying in 4 minutes")); 0278 } 0279 else 0280 { 0281 appendLogText(i18n("No successful Meridian Flip done, delay too long")); 0282 } 0283 updateMFMountState(MOUNT_FLIP_COMPLETED); // this will resume imaging and try again after the extra delay 0284 } 0285 else 0286 { 0287 setFlipDelayHrs(0); 0288 appendLogText(i18n("Meridian flip completed OK.")); 0289 // signal that capture can resume 0290 updateMFMountState(MOUNT_FLIP_COMPLETED); 0291 } 0292 } 0293 else 0294 qCInfo(KSTARS_EKOS_MOUNT) << "Tracking state during meridian flip reached too early, ignored."; 0295 } 0296 break; 0297 0298 case MOUNT_FLIP_COMPLETED: 0299 updateMFMountState(MOUNT_FLIP_NONE); 0300 break; 0301 0302 default: 0303 break; 0304 } 0305 return false; 0306 } 0307 0308 void MeridianFlipState::startMeridianFlip() 0309 { 0310 if (/*initialHA() > 0 || */ targetPosition.valid == false) 0311 { 0312 // no meridian flip necessary 0313 qCDebug(KSTARS_EKOS_MOUNT) << "No meridian flip: no target defined"; 0314 return; 0315 } 0316 0317 if (m_MountStatus != ISD::Mount::MOUNT_TRACKING) 0318 { 0319 // this should never happen 0320 if (m_hasMount == false) 0321 qCWarning(KSTARS_EKOS_MOUNT()) << "No mount connected!"; 0322 0323 // no meridian flip necessary 0324 qCInfo(KSTARS_EKOS_MOUNT) << "No meridian flip: mount not tracking, current state:" << 0325 ISD::Mount::mountStates[m_MountStatus]; 0326 return; 0327 } 0328 0329 dms lst = KStarsData::Instance()->geo()->GSTtoLST(KStarsData::Instance()->clock()->utc().gst()); 0330 double HA = rangeHA(lst.Hours() - targetPosition.position.ra().Hours()); 0331 0332 // execute meridian flip 0333 qCInfo(KSTARS_EKOS_MOUNT) << "Meridian flip: slewing to RA=" << 0334 targetPosition.position.ra().toHMSString() << 0335 "DEC=" << targetPosition.position.dec().toDMSString() << 0336 " Hour Angle " << dms(HA).toHMSString(); 0337 0338 updateMinMeridianFlipEndTime(); 0339 updateMFMountState(MeridianFlipState::MOUNT_FLIP_RUNNING); 0340 0341 // start the re-slew to the current target expecting that the mount firmware changes the pier side 0342 emit slewTelescope(targetPosition.position); 0343 } 0344 0345 bool MeridianFlipState::resetMeridianFlip() 0346 { 0347 0348 // reset the meridian flip status if the slew is not the meridian flip itself 0349 if (meridianFlipMountState != MOUNT_FLIP_RUNNING) 0350 { 0351 updateMFMountState(MOUNT_FLIP_NONE); 0352 setFlipDelayHrs(0); 0353 qCDebug(KSTARS_EKOS_MOUNT) << "flipDelayHrs set to zero in slew, m_MFStatus=" << 0354 meridianFlipStatusString(meridianFlipMountState); 0355 // meridian flip not running, no need for post MF handling 0356 return false; 0357 } 0358 // don't interrupt a meridian flip directly 0359 return true; 0360 } 0361 0362 void MeridianFlipState::processFlipCompleted() 0363 { 0364 appendLogText(i18n("Telescope completed the meridian flip.")); 0365 if (m_CaptureState == CAPTURE_IDLE || m_CaptureState == CAPTURE_ABORTED || m_CaptureState == CAPTURE_COMPLETE) 0366 { 0367 // reset the meridian flip stage and jump directly MF_NONE, since no 0368 // restart of guiding etc. necessary 0369 updateMeridianFlipStage(MeridianFlipState::MF_NONE); 0370 return; 0371 } 0372 0373 } 0374 0375 0376 void MeridianFlipState::setMeridianFlipMountState(MeridianFlipMountState newMeridianFlipMountState) 0377 { 0378 qCDebug (KSTARS_EKOS_MOUNT) << "Setting meridian flip status to " << meridianFlipStatusString(newMeridianFlipMountState); 0379 publishMFMountStatus(newMeridianFlipMountState); 0380 meridianFlipMountState = newMeridianFlipMountState; 0381 } 0382 0383 void MeridianFlipState::appendLogText(QString message) 0384 { 0385 qCInfo(KSTARS_EKOS_MOUNT) << message; 0386 emit newLog(message); 0387 } 0388 0389 void MeridianFlipState::updateMinMeridianFlipEndTime() 0390 { 0391 minMeridianFlipEndTime = KStarsData::Instance()->clock()->utc().addSecs(Options::minFlipDuration()); 0392 } 0393 0394 void MeridianFlipState::updateMFMountState(MeridianFlipMountState status) 0395 { 0396 if (getMeridianFlipMountState() != status) 0397 { 0398 if (status == MOUNT_FLIP_ACCEPTED) 0399 { 0400 // ignore accept signal if none was requested 0401 if (meridianFlipStage != MF_REQUESTED) 0402 return; 0403 } 0404 // in all other cases, handle it straight forward 0405 setMeridianFlipMountState(status); 0406 emit newMountMFStatus(status); 0407 } 0408 } 0409 0410 void MeridianFlipState::publishMFMountStatus(MeridianFlipMountState status) 0411 { 0412 // avoid double entries 0413 if (status == meridianFlipMountState) 0414 return; 0415 0416 switch (status) 0417 { 0418 case MOUNT_FLIP_NONE: 0419 publishMFMountStatusText(i18n("Status: inactive")); 0420 break; 0421 0422 case MOUNT_FLIP_PLANNED: 0423 publishMFMountStatusText(i18n("Meridian flip planned...")); 0424 break; 0425 0426 case MOUNT_FLIP_WAITING: 0427 publishMFMountStatusText(i18n("Meridian flip waiting...")); 0428 break; 0429 0430 case MOUNT_FLIP_ACCEPTED: 0431 publishMFMountStatusText(i18n("Meridian flip ready to start...")); 0432 break; 0433 0434 case MOUNT_FLIP_RUNNING: 0435 publishMFMountStatusText(i18n("Meridian flip running...")); 0436 break; 0437 0438 case MOUNT_FLIP_COMPLETED: 0439 publishMFMountStatusText(i18n("Meridian flip completed.")); 0440 break; 0441 0442 default: 0443 break; 0444 } 0445 0446 } 0447 0448 void MeridianFlipState::publishMFMountStatusText(QString text) 0449 { 0450 // avoid double entries 0451 if (text != m_lastStatusText) 0452 { 0453 emit newMeridianFlipMountStatusText(text); 0454 m_lastStatusText = text; 0455 } 0456 } 0457 0458 QString MeridianFlipState::meridianFlipStatusString(MeridianFlipMountState status) 0459 { 0460 switch (status) 0461 { 0462 case MOUNT_FLIP_NONE: 0463 return "MOUNT_FLIP_NONE"; 0464 case MOUNT_FLIP_PLANNED: 0465 return "MOUNT_FLIP_PLANNED"; 0466 case MOUNT_FLIP_WAITING: 0467 return "MOUNT_FLIP_WAITING"; 0468 case MOUNT_FLIP_ACCEPTED: 0469 return "MOUNT_FLIP_ACCEPTED"; 0470 case MOUNT_FLIP_RUNNING: 0471 return "MOUNT_FLIP_RUNNING"; 0472 case MOUNT_FLIP_COMPLETED: 0473 return "MOUNT_FLIP_COMPLETED"; 0474 case MOUNT_FLIP_ERROR: 0475 return "MOUNT_FLIP_ERROR"; 0476 } 0477 return "not possible"; 0478 } 0479 0480 0481 0482 0483 0484 void MeridianFlipState::setMountStatus(ISD::Mount::Status status) 0485 { 0486 qCDebug(KSTARS_EKOS_MOUNT) << "New mount state for MF:" << ISD::Mount::mountStates[status]; 0487 m_PrevMountStatus = m_MountStatus; 0488 m_MountStatus = status; 0489 } 0490 0491 void MeridianFlipState::setMountParkStatus(ISD::ParkStatus status) 0492 { 0493 // clear the meridian flip when parking 0494 if (status == ISD::PARK_PARKING || status == ISD::PARK_PARKED) 0495 updateMFMountState(MOUNT_FLIP_NONE); 0496 0497 m_MountParkStatus = status; 0498 } 0499 0500 0501 void MeridianFlipState::updatePosition(MountPosition &pos, const SkyPoint &position, ISD::Mount::PierSide pierSide, 0502 const dms &ha, const bool isValid) 0503 { 0504 pos.position = position; 0505 pos.pierSide = pierSide; 0506 pos.ha = ha; 0507 pos.valid = isValid; 0508 } 0509 0510 void MeridianFlipState::updateTelescopeCoord(const SkyPoint &position, ISD::Mount::PierSide pierSide, const dms &ha) 0511 { 0512 updatePosition(currentPosition, position, pierSide, ha, true); 0513 0514 // If we just finished a slew, let's update initialHA and the current target's position, 0515 // but only if the meridian flip is enabled 0516 if (m_MountStatus == ISD::Mount::MOUNT_TRACKING && m_PrevMountStatus == ISD::Mount::MOUNT_SLEWING 0517 && isEnabled()) 0518 { 0519 if (meridianFlipMountState == MOUNT_FLIP_NONE) 0520 { 0521 setFlipDelayHrs(0); 0522 } 0523 // set the target position 0524 updatePosition(targetPosition, currentPosition.position, currentPosition.pierSide, currentPosition.ha, true); 0525 qCDebug(KSTARS_EKOS_MOUNT) << "Slew finished, MFStatus " << 0526 meridianFlipStatusString(meridianFlipMountState); 0527 // ensure that this is executed only once 0528 m_PrevMountStatus = m_MountStatus; 0529 } 0530 0531 QChar sgn(ha.Hours() <= 12.0 ? '+' : '-'); 0532 0533 dms lst = KStarsData::Instance()->geo()->GSTtoLST(KStarsData::Instance()->clock()->utc().gst()); 0534 0535 // don't check the meridian flip while in motion 0536 bool inMotion = (m_MountStatus == ISD::Mount::MOUNT_SLEWING || m_MountStatus == ISD::Mount::MOUNT_MOVING 0537 || m_MountStatus == ISD::Mount::MOUNT_PARKING); 0538 if ((inMotion == false) && checkMeridianFlip(lst)) 0539 startMeridianFlip(); 0540 else 0541 { 0542 const QString message(i18n("Meridian flip inactive (parked)")); 0543 if (m_MountParkStatus == ISD::PARK_PARKED /* && meridianFlipStatusWidget->getStatus() != message */) 0544 { 0545 publishMFMountStatusText(message); 0546 } 0547 } 0548 } 0549 0550 void MeridianFlipState::setTargetPosition(SkyPoint *pos) 0551 { 0552 if (pos != nullptr) 0553 { 0554 dms lst = KStarsData::Instance()->geo()->GSTtoLST(KStarsData::Instance()->clock()->utc().gst()); 0555 dms ha = dms(lst.Degrees() - pos->ra().Degrees()); 0556 0557 qCDebug(KSTARS_EKOS_MOUNT) << "Setting target RA=" << pos->ra().toHMSString() << "DEC=" << pos->dec().toDMSString(); 0558 updatePosition(targetPosition, *pos, ISD::Mount::PIER_UNKNOWN, ha, true); 0559 } 0560 else 0561 { 0562 clearTargetPosition(); 0563 } 0564 } 0565 0566 double MeridianFlipState::initialPositionHA() const 0567 { 0568 double HA = targetPosition.ha.HoursHa(); 0569 return HA; 0570 } 0571 0572 0573 } // namespace 0574