File indexing completed on 2024-04-28 03:43:39

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