1 /*
2     SPDX-FileCopyrightText: 2015 Jasem Mutlaq <mutlaqja@ikarustech.com>
3 
4     SPDX-License-Identifier: GPL-2.0-or-later
5 */
6 
7 #include "mount.h"
8 
9 #include <QQuickView>
10 #include <QQuickItem>
11 #include <indicom.h>
12 
13 #include <KNotifications/KNotification>
14 #include <KLocalizedContext>
15 #include <KActionCollection>
16 
17 #include "Options.h"
18 
19 #include "ksmessagebox.h"
20 #include "indi/driverinfo.h"
21 #include "indi/indicommon.h"
22 #include "indi/clientmanager.h"
23 #include "indi/indifilter.h"
24 
25 
26 #include "mountadaptor.h"
27 
28 #include "ekos/manager.h"
29 
30 #include "kstars.h"
31 #include "skymapcomposite.h"
32 #include "kspaths.h"
33 #include "dialogs/finddialog.h"
34 #include "kstarsdata.h"
35 #include "ksutils.h"
36 
37 #include <basedevice.h>
38 
39 #include <ekos_mount_debug.h>
40 
41 extern const char *libindi_strings_context;
42 
43 #define ABORT_DISPATCH_LIMIT 3
44 
45 namespace Ekos
46 {
47 
Mount()48 Mount::Mount()
49 {
50     setupUi(this);
51 
52     new MountAdaptor(this);
53     QDBusConnection::sessionBus().registerObject("/KStars/Ekos/Mount", this);
54     // Set up DBus interfaces
55     QPointer<QDBusInterface> ekosInterface = new QDBusInterface("org.kde.kstars", "/KStars/Ekos", "org.kde.kstars.Ekos",
56             QDBusConnection::sessionBus(), this);
57     qDBusRegisterMetaType<SkyPoint>();
58 
59     // Connecting DBus signals
60     QDBusConnection::sessionBus().connect("org.kde.kstars", "/KStars/Ekos", "org.kde.kstars.Ekos", "newModule", this,
61                                           SLOT(registerNewModule(QString)));
62 
63     currentTelescope = nullptr;
64 
65     m_AbortDispatch = -1;
66 
67     minAltLimit->setValue(Options::minimumAltLimit());
68     maxAltLimit->setValue(Options::maximumAltLimit());
69     maxHaLimit->setValue(Options::maximumHaLimit());
70 
71     connect(minAltLimit, &QDoubleSpinBox::editingFinished, this, &Mount::saveLimits);
72     connect(maxAltLimit, &QDoubleSpinBox::editingFinished, this, &Mount::saveLimits);
73     connect(maxHaLimit, &QDoubleSpinBox::editingFinished, this, &Mount::saveLimits);
74 
75     connect(mountToolBoxB, &QPushButton::clicked, this, &Mount::toggleMountToolBox);
76 
77     connect(saveB, &QPushButton::clicked, this, &Mount::save);
78 
79     connect(clearAlignmentModelB, &QPushButton::clicked, this, [this]()
80     {
81         resetModel();
82     });
83 
84     connect(clearParkingB, &QPushButton::clicked, this, [this]()
85     {
86         if (currentTelescope)
87             currentTelescope->clearParking();
88 
89     });
90 
91     connect(purgeConfigB, &QPushButton::clicked, this, [this]()
92     {
93         if (currentTelescope)
94         {
95             if (KMessageBox::questionYesNo(KStars::Instance(),
96                                            i18n("Are you sure you want to clear all mount configurations?"),
97                                            i18n("Mount Configuration"), KStandardGuiItem::yes(), KStandardGuiItem::no(),
98                                            "purge_mount_settings_dialog") == KMessageBox::Yes)
99             {
100                 resetModel();
101                 currentTelescope->clearParking();
102                 currentTelescope->setConfig(PURGE_CONFIG);
103             }
104         }
105     });
106 
107     connect(enableLimitsCheck, &QCheckBox::toggled, this, &Mount::enableAltitudeLimits);
108     enableLimitsCheck->setChecked(Options::enableAltitudeLimits());
109     m_AltitudeLimitEnabled = enableLimitsCheck->isChecked();
110     connect(enableHaLimitCheck, &QCheckBox::toggled, this, &Mount::enableHourAngleLimits);
111     enableHaLimitCheck->setChecked(Options::enableHaLimit());
112     //haLimitEnabled = enableHaLimitCheck->isChecked();
113 
114     // meridian flip
115     meridianFlipCheckBox->setChecked(Options::executeMeridianFlip());
116 
117     // Meridian Flip Unit
118     meridianFlipDegreesR->setChecked(Options::meridianFlipUnitDegrees());
119     meridianFlipHoursR->setChecked(!Options::meridianFlipUnitDegrees());
120 
121     // This is always in hours
122     double offset = Options::meridianFlipOffset();
123     // Hours --> Degrees
124     if (meridianFlipDegreesR->isChecked())
125         offset *= 15.0;
126     meridianFlipTimeBox->setValue(offset);
127     connect(meridianFlipCheckBox, &QCheckBox::toggled, this, &Ekos::Mount::meridianFlipSetupChanged);
128     connect(meridianFlipTimeBox, static_cast<void (QDoubleSpinBox::*)(double)>(&QDoubleSpinBox::valueChanged), this,
129             &Ekos::Mount::meridianFlipSetupChanged);
130     connect(meridianFlipDegreesR, &QRadioButton::toggled, this, [this]()
131     {
132         Options::setMeridianFlipUnitDegrees(meridianFlipDegreesR->isChecked());
133         // Hours ---> Degrees
134         if (meridianFlipDegreesR->isChecked())
135             meridianFlipTimeBox->setValue(meridianFlipTimeBox->value() * 15.0);
136         // Degrees --> Hours
137         else
138             meridianFlipTimeBox->setValue(rangeHA(meridianFlipTimeBox->value() / 15.0));
139     });
140 
141     everyDayCheck->setChecked(Options::parkEveryDay());
142     connect(everyDayCheck, &QCheckBox::toggled, this, [](bool toggled)
143     {
144         Options::setParkEveryDay(toggled);
145     });
146 
147     startupTimeEdit->setTime(QTime::fromString(Options::parkTime()));
148     connect(startupTimeEdit, &QTimeEdit::editingFinished, this, [this]()
149     {
150         Options::setParkTime(startupTimeEdit->time().toString());
151     });
152 
153     connect(&autoParkTimer, &QTimer::timeout, this, &Mount::startAutoPark);
154     connect(startTimerB, &QPushButton::clicked, this, &Mount::startParkTimer);
155     connect(stopTimerB, &QPushButton::clicked, this, &Mount::stopParkTimer);
156 
157     stopTimerB->setEnabled(false);
158 
159     if (everyDayCheck->isChecked())
160         startTimerB->animateClick();
161 
162     // QML Stuff
163     m_BaseView = new QQuickView();
164 
165     m_BaseView->setSource(QUrl("qrc:/qml/mount/mountbox.qml"));
166 
167     m_BaseView->setTitle(i18n("Mount Control"));
168 #ifdef Q_OS_OSX
169     m_BaseView->setFlags(Qt::Tool | Qt::WindowStaysOnTopHint);
170 #else
171     m_BaseView->setFlags(Qt::WindowStaysOnTopHint | Qt::WindowCloseButtonHint);
172 #endif
173 
174     // Theming?
175     m_BaseView->setColor(Qt::black);
176 
177     m_BaseObj = m_BaseView->rootObject();
178 
179     m_Ctxt = m_BaseView->rootContext();
180     ///Use instead of KDeclarative
181     m_Ctxt->setContextObject(new KLocalizedContext(m_BaseView));
182 
183     m_Ctxt->setContextProperty("mount", this);
184 
185     m_BaseView->setResizeMode(QQuickView::SizeViewToRootObject);
186 
187     m_SpeedSlider  = m_BaseObj->findChild<QQuickItem *>("speedSliderObject");
188     m_SpeedLabel   = m_BaseObj->findChild<QQuickItem *>("speedLabelObject");
189     m_raValue      = m_BaseObj->findChild<QQuickItem *>("raValueObject");
190     m_deValue      = m_BaseObj->findChild<QQuickItem *>("deValueObject");
191     m_azValue      = m_BaseObj->findChild<QQuickItem *>("azValueObject");
192     m_altValue     = m_BaseObj->findChild<QQuickItem *>("altValueObject");
193     m_haValue      = m_BaseObj->findChild<QQuickItem *>("haValueObject");
194     m_zaValue      = m_BaseObj->findChild<QQuickItem *>("zaValueObject");
195     m_targetText   = m_BaseObj->findChild<QQuickItem *>("targetTextObject");
196     m_targetRAText = m_BaseObj->findChild<QQuickItem *>("targetRATextObject");
197     m_targetDEText = m_BaseObj->findChild<QQuickItem *>("targetDETextObject");
198     m_J2000Check   = m_BaseObj->findChild<QQuickItem *>("j2000CheckObject");
199     m_JNowCheck    = m_BaseObj->findChild<QQuickItem *>("jnowCheckObject");
200     m_Park         = m_BaseObj->findChild<QQuickItem *>("parkButtonObject");
201     m_Unpark       = m_BaseObj->findChild<QQuickItem *>("unparkButtonObject");
202     m_statusText   = m_BaseObj->findChild<QQuickItem *>("statusTextObject");
203     m_equatorialCheck = m_BaseObj->findChild<QQuickItem *>("equatorialCheckObject");
204     m_horizontalCheck = m_BaseObj->findChild<QQuickItem *>("horizontalCheckObject");
205     m_haEquatorialCheck = m_BaseObj->findChild<QQuickItem *>("haEquatorialCheckObject");
206     m_leftRightCheck = m_BaseObj->findChild<QQuickItem *>("leftRightCheckObject");
207     m_upDownCheck = m_BaseObj->findChild<QQuickItem *>("upDownCheckObject");
208 
209     m_leftRightCheck->setProperty("checked", Options::leftRightReversed());
210     m_upDownCheck->setProperty("checked", Options::upDownReversed());
211 
212     //Note:  This is to prevent a button from being called the default button
213     //and then executing when the user hits the enter key such as when on a Text Box
214     QList<QPushButton *> qButtons = findChildren<QPushButton *>();
215     for (auto &button : qButtons)
216         button->setAutoDefault(false);
217 }
218 
~Mount()219 Mount::~Mount()
220 {
221     delete(m_Ctxt);
222     delete(m_BaseObj);
223     delete(currentTargetPosition);
224 }
225 
setTelescope(ISD::GDInterface * newTelescope)226 void Mount::setTelescope(ISD::GDInterface *newTelescope)
227 {
228     if (newTelescope == currentTelescope)
229     {
230         if (enableLimitsCheck->isChecked())
231             currentTelescope->setAltLimits(minAltLimit->value(), maxAltLimit->value());
232         syncTelescopeInfo();
233         return;
234     }
235 
236     if (currentGPS != nullptr)
237         syncGPS();
238 
239     currentTelescope = static_cast<ISD::Telescope *>(newTelescope);
240 
241     currentTelescope->disconnect(this);
242 
243     connect(currentTelescope, &ISD::GDInterface::numberUpdated, this, &Mount::updateNumber);
244     connect(currentTelescope, &ISD::GDInterface::switchUpdated, this, &Mount::updateSwitch);
245     connect(currentTelescope, &ISD::GDInterface::textUpdated, this, &Mount::updateText);
246     connect(currentTelescope, &ISD::Telescope::newTarget, this, &Mount::newTarget);
247     connect(currentTelescope, &ISD::Telescope::newStatus, this, &Mount::setScopeStatus);
248     connect(currentTelescope, &ISD::Telescope::newCoords, this, &Mount::newCoords);
249     connect(currentTelescope, &ISD::Telescope::newCoords, this, &Mount::updateTelescopeCoords);
250     connect(currentTelescope, &ISD::Telescope::slewRateChanged, this, &Mount::slewRateChanged);
251     connect(currentTelescope, &ISD::Telescope::pierSideChanged, this, &Mount::pierSideChanged);
252     connect(currentTelescope, &ISD::Telescope::Disconnected, [this]()
253     {
254         m_BaseView->hide();
255     });
256     connect(currentTelescope, &ISD::Telescope::newParkStatus, [&](ISD::ParkStatus status)
257     {
258         m_ParkStatus = status;
259         emit newParkStatus(status);
260 
261         // If mount is unparked AND every day auto-paro check is ON
262         // AND auto park timer is not yet started, we try to initiate it.
263         if (status == ISD::PARK_UNPARKED && everyDayCheck->isChecked() && autoParkTimer.isActive() == false)
264             startTimerB->animateClick();
265     });
266     connect(currentTelescope, &ISD::Telescope::ready, this, &Mount::ready);
267 
268     //Disable this for now since ALL INDI drivers now log their messages to verbose output
269     //connect(currentTelescope, SIGNAL(messageUpdated(int)), this, SLOT(updateLog(int)), Qt::UniqueConnection);
270 
271     if (enableLimitsCheck->isChecked())
272         currentTelescope->setAltLimits(minAltLimit->value(), maxAltLimit->value());
273 
274     syncTelescopeInfo();
275 
276     // Send initial status
277     m_Status = currentTelescope->status();
278     emit newStatus(m_Status);
279 
280     m_ParkStatus = currentTelescope->parkStatus();
281     emit newParkStatus(m_ParkStatus);
282 }
283 
removeDevice(ISD::GDInterface * device)284 void Mount::removeDevice(ISD::GDInterface *device)
285 {
286     if (currentTelescope && (currentTelescope->getDeviceName() == device->getDeviceName()))
287     {
288         currentTelescope->disconnect(this);
289         m_BaseView->hide();
290 
291         qCDebug(KSTARS_EKOS_MOUNT) << "Removing mount driver" << device->getDeviceName();
292 
293         currentTelescope = nullptr;
294     }
295     else if (currentGPS && (currentGPS->getDeviceName() == device->getDeviceName()))
296     {
297         currentGPS->disconnect(this);
298         currentGPS = nullptr;
299     }
300 }
301 
syncTelescopeInfo()302 void Mount::syncTelescopeInfo()
303 {
304     if (!currentTelescope || currentTelescope->isConnected() == false)
305         return;
306 
307     auto nvp = currentTelescope->getBaseDevice()->getNumber("TELESCOPE_INFO");
308 
309     if (nvp)
310     {
311         primaryScopeGroup->setTitle(currentTelescope->getDeviceName());
312         guideScopeGroup->setTitle(i18n("%1 guide scope", currentTelescope->getDeviceName()));
313 
314         auto np = nvp->findWidgetByName("TELESCOPE_APERTURE");
315 
316         if (np && np->getValue() > 0)
317             primaryScopeApertureIN->setValue(np->getValue());
318 
319         np = nvp->findWidgetByName("TELESCOPE_FOCAL_LENGTH");
320         if (np && np->getValue() > 0)
321             primaryScopeFocalIN->setValue(np->getValue());
322 
323         np = nvp->findWidgetByName("GUIDER_APERTURE");
324         if (np && np->getValue() > 0)
325             guideScopeApertureIN->setValue(np->getValue());
326 
327         np = nvp->findWidgetByName("GUIDER_FOCAL_LENGTH");
328         if (np && np->getValue() > 0)
329             guideScopeFocalIN->setValue(np->getValue());
330     }
331 
332     auto svp = currentTelescope->getBaseDevice()->getSwitch("TELESCOPE_SLEW_RATE");
333 
334     if (svp)
335     {
336         int index = svp->findOnSwitchIndex();
337 
338         // QtQuick
339         m_SpeedSlider->setEnabled(true);
340         m_SpeedSlider->setProperty("maximumValue", svp->count() - 1);
341         m_SpeedSlider->setProperty("value", index);
342 
343         m_SpeedLabel->setProperty("text", i18nc(libindi_strings_context, svp->at(index)->getLabel()));
344         m_SpeedLabel->setEnabled(true);
345     }
346     else
347     {
348         // QtQuick
349         m_SpeedSlider->setEnabled(false);
350         m_SpeedLabel->setEnabled(false);
351     }
352 
353     if (currentTelescope->canPark())
354     {
355         parkB->setEnabled(!currentTelescope->isParked());
356         unparkB->setEnabled(currentTelescope->isParked());
357         connect(parkB, &QPushButton::clicked, currentTelescope, &ISD::Telescope::Park, Qt::UniqueConnection);
358         connect(unparkB, &QPushButton::clicked, currentTelescope, &ISD::Telescope::UnPark, Qt::UniqueConnection);
359 
360         // QtQuick
361         m_Park->setEnabled(!currentTelescope->isParked());
362         m_Unpark->setEnabled(currentTelescope->isParked());
363     }
364     else
365     {
366         parkB->setEnabled(false);
367         unparkB->setEnabled(false);
368         disconnect(parkB, &QPushButton::clicked, currentTelescope, &ISD::Telescope::Park);
369         disconnect(unparkB, &QPushButton::clicked, currentTelescope, &ISD::Telescope::UnPark);
370 
371         // QtQuick
372         m_Park->setEnabled(false);
373         m_Unpark->setEnabled(false);
374     }
375 
376     // Configs
377     svp = currentTelescope->getBaseDevice()->getSwitch("APPLY_SCOPE_CONFIG");
378     if (svp)
379     {
380         scopeConfigCombo->disconnect();
381         scopeConfigCombo->clear();
382         for (const auto &it : *svp)
383             scopeConfigCombo->addItem(it.getLabel());
384 
385         scopeConfigCombo->setCurrentIndex(IUFindOnSwitchIndex(svp));
386         connect(scopeConfigCombo, static_cast<void(QComboBox::*)(int)>(&QComboBox::activated), this, &Mount::setScopeConfig);
387     }
388 
389     // Tracking State
390     svp = currentTelescope->getBaseDevice()->getSwitch("TELESCOPE_TRACK_STATE");
391     if (svp)
392     {
393         trackingGroup->setEnabled(true);
394         trackOnB->disconnect();
395         trackOffB->disconnect();
396         connect(trackOnB, &QPushButton::clicked, [&]()
397         {
398             currentTelescope->setTrackEnabled(true);
399         });
400         connect(trackOffB, &QPushButton::clicked, [&]()
401         {
402             if (KMessageBox::questionYesNo(KStars::Instance(),
403                                            i18n("Are you sure you want to turn off mount tracking?"),
404                                            i18n("Mount Tracking"), KStandardGuiItem::yes(), KStandardGuiItem::no(),
405                                            "turn_off_mount_tracking_dialog") == KMessageBox::Yes)
406                 currentTelescope->setTrackEnabled(false);
407         });
408     }
409     else
410     {
411         trackOnB->setChecked(false);
412         trackOffB->setChecked(false);
413         trackingGroup->setEnabled(false);
414     }
415 
416     auto tvp = currentTelescope->getBaseDevice()->getText("SCOPE_CONFIG_NAME");
417     if (tvp)
418         scopeConfigNameEdit->setText(tvp->at(0)->getText());
419 }
420 
registerNewModule(const QString & name)421 void Mount::registerNewModule(const QString &name)
422 {
423     if (name == "Capture" && captureInterface == nullptr)
424     {
425         captureInterface = new QDBusInterface("org.kde.kstars", "/KStars/Ekos/Capture", "org.kde.kstars.Ekos.Capture",
426                                               QDBusConnection::sessionBus(), this);
427     }
428 
429 }
430 
431 
updateText(ITextVectorProperty * tvp)432 void Mount::updateText(ITextVectorProperty *tvp)
433 {
434     if (!strcmp(tvp->name, "SCOPE_CONFIG_NAME"))
435     {
436         scopeConfigNameEdit->setText(tvp->tp[0].text);
437     }
438 }
439 
setScopeConfig(int index)440 bool Mount::setScopeConfig(int index)
441 {
442     auto svp = currentTelescope->getBaseDevice()->getSwitch("APPLY_SCOPE_CONFIG");
443     if (!svp)
444         return false;
445 
446     svp->reset();
447     svp->at(index)->setState(ISS_ON);
448 
449     // Clear scope config name so that it gets filled by INDI
450     scopeConfigNameEdit->clear();
451 
452     currentTelescope->getDriverInfo()->getClientManager()->sendNewSwitch(svp);
453     return true;
454 }
455 
updateTelescopeCoords(const SkyPoint & position,ISD::Telescope::PierSide pierSide,const dms & ha)456 void Mount::updateTelescopeCoords(const SkyPoint &position, ISD::Telescope::PierSide pierSide, const dms &ha)
457 {
458     telescopeCoord = position;
459 
460     // No need to update coords if we are still parked.
461     if (m_Status == ISD::Telescope::MOUNT_PARKED && m_Status == currentTelescope->status())
462         return;
463 
464     if (currentTelescope && currentTelescope->isConnected())
465     {
466         // Ekos Mount Tab coords are always in JNow
467         raOUT->setText(telescopeCoord.ra().toHMSString());
468         decOUT->setText(telescopeCoord.dec().toDMSString());
469 
470         // Mount Control Panel coords depend on the switch
471         if (m_JNowCheck->property("checked").toBool())
472         {
473             m_raValue->setProperty("text", telescopeCoord.ra().toHMSString());
474             m_deValue->setProperty("text", telescopeCoord.dec().toDMSString());
475         }
476         else
477         {
478             m_raValue->setProperty("text", telescopeCoord.ra0().toHMSString());
479             m_deValue->setProperty("text", telescopeCoord.dec0().toDMSString());
480         }
481 
482         // Get horizontal coords
483         azOUT->setText(telescopeCoord.az().toDMSString());
484         m_azValue->setProperty("text", telescopeCoord.az().toDMSString());
485         altOUT->setText(telescopeCoord.alt().toDMSString());
486         m_altValue->setProperty("text", telescopeCoord.alt().toDMSString());
487 
488         dms lst = KStarsData::Instance()->geo()->GSTtoLST(KStarsData::Instance()->clock()->utc().gst());
489         dms haSigned(ha);
490         QChar sgn('+');
491 
492         if (haSigned.Hours() > 12.0)
493         {
494             haSigned.setH(24.0 - haSigned.Hours());
495             sgn = '-';
496         }
497 
498         haOUT->setText(QString("%1%2").arg(sgn).arg(haSigned.toHMSString()));
499 
500         m_haValue->setProperty("text", haOUT->text());
501         lstOUT->setText(lst.toHMSString());
502 
503         double currentAlt = telescopeCoord.altRefracted().Degrees();
504 
505         m_zaValue->setProperty("text", dms(90 - currentAlt).toDMSString());
506 
507         if (minAltLimit->isEnabled() && (currentAlt < minAltLimit->value() || currentAlt > maxAltLimit->value()))
508         {
509             if (currentAlt < minAltLimit->value())
510             {
511                 // Only stop if current altitude is less than last altitude indicate worse situation
512                 if (currentAlt < m_LastAltitude &&
513                         (m_AbortDispatch == -1 ||
514                          (currentTelescope->isInMotion() /* && ++abortDispatch > ABORT_DISPATCH_LIMIT*/)))
515                 {
516                     appendLogText(i18n("Telescope altitude is below minimum altitude limit of %1. Aborting motion...",
517                                        QString::number(minAltLimit->value(), 'g', 3)));
518                     currentTelescope->Abort();
519                     currentTelescope->setTrackEnabled(false);
520                     //KNotification::event( QLatin1String( "OperationFailed" ));
521                     KNotification::beep();
522                     m_AbortDispatch++;
523                 }
524             }
525             else
526             {
527                 // Only stop if current altitude is higher than last altitude indicate worse situation
528                 if (currentAlt > m_LastAltitude &&
529                         (m_AbortDispatch == -1 ||
530                          (currentTelescope->isInMotion() /* && ++abortDispatch > ABORT_DISPATCH_LIMIT*/)))
531                 {
532                     appendLogText(i18n("Telescope altitude is above maximum altitude limit of %1. Aborting motion...",
533                                        QString::number(maxAltLimit->value(), 'g', 3)));
534                     currentTelescope->Abort();
535                     currentTelescope->setTrackEnabled(false);
536                     //KNotification::event( QLatin1String( "OperationFailed" ));
537                     KNotification::beep();
538                     m_AbortDispatch++;
539                 }
540             }
541         }
542         else
543             m_AbortDispatch = -1;
544 
545         //qCDebug(KSTARS_EKOS_MOUNT) << "maxHaLimit " << maxHaLimit->isEnabled() << " value " << maxHaLimit->value();
546 
547         double haHours = rangeHA(ha.Hours());
548         // handle Ha limit:
549         // Telescope must report Pier Side
550         // maxHaLimit must be enabled
551         // for PierSide West -> East if Ha > maxHaLimit stop tracking
552         // for PierSide East -> West if Ha > maxHaLimit - 12 stop Tracking
553         if (maxHaLimit->isEnabled())
554         {
555             // get hour angle limit
556             double haLimit = maxHaLimit->value();
557             bool haLimitReached = false;
558             switch(pierSide)
559             {
560                 case ISD::Telescope::PierSide::PIER_WEST:
561                     haLimitReached = haHours > haLimit;
562                     break;
563                 case ISD::Telescope::PierSide::PIER_EAST:
564                     haLimitReached = rangeHA(haHours + 12.0) > haLimit;
565                     break;
566                 default:
567                     // can't tell so always false
568                     haLimitReached = false;
569                     break;
570             }
571 
572             qCDebug(KSTARS_EKOS_MOUNT) << "Ha: " << haHours <<
573                                        " haLimit " << haLimit <<
574                                        " " << pierSideStateString() <<
575                                        " haLimitReached " << (haLimitReached ? "true" : "false") <<
576                                        " lastHa " << m_LastHourAngle;
577 
578             // compare with last ha to avoid multiple calls
579             if (haLimitReached && (rangeHA(haHours - m_LastHourAngle) >= 0 ) &&
580                     (m_AbortDispatch == -1 ||
581                      currentTelescope->isInMotion()))
582             {
583                 // moved past the limit, so stop
584                 appendLogText(i18n("Telescope hour angle is more than the maximum hour angle of %1. Aborting motion...",
585                                    QString::number(maxHaLimit->value(), 'g', 3)));
586                 currentTelescope->Abort();
587                 currentTelescope->setTrackEnabled(false);
588                 //KNotification::event( QLatin1String( "OperationFailed" ));
589                 KNotification::beep();
590                 m_AbortDispatch++;
591                 // ideally we pause and wait until we have passed the pier flip limit,
592                 // then do a pier flip and try to resume
593                 // this will need changing to use a target position because the current HA has stopped.
594             }
595         }
596         else
597             m_AbortDispatch = -1;
598 
599         m_LastAltitude = currentAlt;
600         m_LastHourAngle = haHours;
601 
602         ISD::Telescope::Status currentStatus = currentTelescope->status();
603         if (m_Status != currentStatus)
604         {
605             qCDebug(KSTARS_EKOS_MOUNT) << "Mount status changed from " << currentTelescope->getStatusString(m_Status)
606                                        << " to " << currentTelescope->getStatusString(currentStatus);
607             // If we just finished a slew, let's update initialHA and the current target's position
608             if (currentStatus == ISD::Telescope::MOUNT_TRACKING && m_Status == ISD::Telescope::MOUNT_SLEWING)
609             {
610                 if (m_MFStatus == FLIP_NONE)
611                 {
612                     flipDelayHrs = 0;
613                 }
614                 setInitialHA((sgn == '-' ? -1 : 1) * ha.Hours());
615                 delete currentTargetPosition;
616                 currentTargetPosition = new SkyPoint(telescopeCoord.ra(), telescopeCoord.dec());
617                 qCDebug(KSTARS_EKOS_MOUNT) << "Slew finished, MFStatus " << meridianFlipStatusString(m_MFStatus);
618             }
619 
620             setScopeStatus(currentStatus);
621             parkB->setEnabled(!currentTelescope->isParked());
622             unparkB->setEnabled(currentTelescope->isParked());
623 
624             m_Park->setEnabled(!currentTelescope->isParked());
625             m_Unpark->setEnabled(currentTelescope->isParked());
626 
627             QAction *a = KStars::Instance()->actionCollection()->action("telescope_track");
628             if (a != nullptr)
629                 a->setChecked(currentStatus == ISD::Telescope::MOUNT_TRACKING);
630         }
631 
632         bool isTracking = (currentStatus == ISD::Telescope::MOUNT_TRACKING);
633         if (trackingGroup->isEnabled())
634         {
635             trackOnB->setChecked(isTracking);
636             trackOffB->setChecked(!isTracking);
637         }
638 
639         // handle pier side display
640         pierSideLabel->setText(pierSideStateString());
641 
642         // Auto Park Timer
643         if (autoParkTimer.isActive())
644         {
645             QTime remainingTime(0, 0, 0);
646             remainingTime = remainingTime.addMSecs(autoParkTimer.remainingTime());
647             countdownLabel->setText(remainingTime.toString("hh:mm:ss"));
648         }
649 
650         if (isTracking && checkMeridianFlip(lst))
651             executeMeridianFlip();
652     }
653 }
654 
updateNumber(INumberVectorProperty * nvp)655 void Mount::updateNumber(INumberVectorProperty *nvp)
656 {
657     if (!strcmp(nvp->name, "TELESCOPE_INFO"))
658     {
659         if (nvp->s == IPS_ALERT)
660         {
661             QString newMessage;
662             if (primaryScopeApertureIN->value() <= 1 || primaryScopeFocalIN->value() <= 1)
663                 newMessage = i18n("Error syncing telescope info. Please fill telescope aperture and focal length.");
664             else
665                 newMessage = i18n("Error syncing telescope info. Check INDI control panel for more details.");
666             if (newMessage != lastNotificationMessage)
667             {
668                 appendLogText(newMessage);
669                 lastNotificationMessage = newMessage;
670             }
671         }
672         else
673         {
674             syncTelescopeInfo();
675             QString newMessage = i18n("Telescope info updated successfully.");
676             if (newMessage != lastNotificationMessage)
677             {
678                 appendLogText(newMessage);
679                 lastNotificationMessage = newMessage;
680             }
681         }
682     }
683 
684     if (currentGPS != nullptr && (nvp->device == currentGPS->getDeviceName()) && !strcmp(nvp->name, "GEOGRAPHIC_COORD")
685             && nvp->s == IPS_OK)
686         syncGPS();
687 }
688 
setSlewRate(int index)689 bool Mount::setSlewRate(int index)
690 {
691     if (currentTelescope)
692         return currentTelescope->setSlewRate(index);
693 
694     return false;
695 }
696 
setUpDownReversed(bool enabled)697 void Mount::setUpDownReversed(bool enabled)
698 {
699     Options::setUpDownReversed(enabled);
700 }
701 
setLeftRightReversed(bool enabled)702 void Mount::setLeftRightReversed(bool enabled)
703 {
704     Options::setLeftRightReversed(enabled);
705 }
706 
setMeridianFlipValues(bool activate,double hours)707 void Mount::setMeridianFlipValues(bool activate, double hours)
708 {
709     meridianFlipCheckBox->setChecked(activate);
710     // Hours --> Degrees
711     if (meridianFlipDegreesR->isChecked())
712         meridianFlipTimeBox->setValue(hours * 15.0);
713     else
714         meridianFlipTimeBox->setValue(hours);
715 
716     Options::setExecuteMeridianFlip(meridianFlipCheckBox->isChecked());
717 
718     // It is always saved in hours
719     Options::setMeridianFlipOffset(hours);
720 }
721 
meridianFlipSetupChanged()722 void Mount::meridianFlipSetupChanged()
723 {
724     if (meridianFlipCheckBox->isChecked() == false)
725         // reset meridian flip
726         setMeridianFlipStatus(FLIP_NONE);
727 
728     Options::setExecuteMeridianFlip(meridianFlipCheckBox->isChecked());
729 
730     double offset = meridianFlipTimeBox->value();
731     // Degrees --> Hours
732     if (meridianFlipDegreesR->isChecked())
733         offset /= 15.0;
734     // It is always saved in hours
735     Options::setMeridianFlipOffset(offset);
736 }
737 
setMeridianFlipStatus(MeridianFlipStatus status)738 void Mount::setMeridianFlipStatus(MeridianFlipStatus status)
739 {
740     if (m_MFStatus != status)
741     {
742         m_MFStatus = status;
743         qCDebug (KSTARS_EKOS_MOUNT) << "Setting meridian flip status to " << meridianFlipStatusString(status);
744 
745         meridianFlipStatusChangedInternal(status);
746         emit newMeridianFlipStatus(status);
747     }
748 }
749 
updateSwitch(ISwitchVectorProperty * svp)750 void Mount::updateSwitch(ISwitchVectorProperty *svp)
751 {
752     if (!strcmp(svp->name, "TELESCOPE_SLEW_RATE"))
753     {
754         int index = IUFindOnSwitchIndex(svp);
755 
756         m_SpeedSlider->setProperty("value", index);
757         m_SpeedLabel->setProperty("text", i18nc(libindi_strings_context, svp->sp[index].label));
758     }
759     /*else if (!strcmp(svp->name, "TELESCOPE_PARK"))
760     {
761         ISwitch *sp = IUFindSwitch(svp, "PARK");
762         if (sp)
763         {
764             parkB->setEnabled((sp->s == ISS_OFF));
765             unparkB->setEnabled((sp->s == ISS_ON));
766         }
767     }*/
768 }
769 
appendLogText(const QString & text)770 void Mount::appendLogText(const QString &text)
771 {
772     m_LogText.insert(0, i18nc("log entry; %1 is the date, %2 is the text", "%1 %2",
773                               KStarsData::Instance()->lt().toString("yyyy-MM-ddThh:mm:ss"), text));
774 
775     qCInfo(KSTARS_EKOS_MOUNT) << text;
776 
777     emit newLog(text);
778 }
779 
updateLog(int messageID)780 void Mount::updateLog(int messageID)
781 {
782     INDI::BaseDevice *dv = currentTelescope->getBaseDevice();
783 
784     QString message = QString::fromStdString(dv->messageQueue(messageID));
785 
786     m_LogText.insert(0, i18nc("Message shown in Ekos Mount module", "%1", message));
787 
788     emit newLog(message);
789 }
790 
clearLog()791 void Mount::clearLog()
792 {
793     m_LogText.clear();
794     emit newLog(QString());
795 }
796 
motionCommand(int command,int NS,int WE)797 void Mount::motionCommand(int command, int NS, int WE)
798 {
799     if (NS != -1)
800     {
801         if (Options::upDownReversed())
802             NS = !NS;
803         currentTelescope->MoveNS(static_cast<ISD::Telescope::TelescopeMotionNS>(NS),
804                                  static_cast<ISD::Telescope::TelescopeMotionCommand>(command));
805     }
806 
807     if (WE != -1)
808     {
809         if (Options::leftRightReversed())
810             WE = !WE;
811         currentTelescope->MoveWE(static_cast<ISD::Telescope::TelescopeMotionWE>(WE),
812                                  static_cast<ISD::Telescope::TelescopeMotionCommand>(command));
813     }
814 }
815 
816 
doPulse(GuideDirection ra_dir,int ra_msecs,GuideDirection dec_dir,int dec_msecs)817 void Mount::doPulse(GuideDirection ra_dir, int ra_msecs, GuideDirection dec_dir, int dec_msecs)
818 {
819     currentTelescope->doPulse(ra_dir, ra_msecs, dec_dir, dec_msecs);
820 }
821 
822 
save()823 void Mount::save()
824 {
825     if (currentTelescope == nullptr)
826         return;
827 
828     if (scopeConfigNameEdit->text().isEmpty() == false)
829     {
830         auto tvp = currentTelescope->getBaseDevice()->getText("SCOPE_CONFIG_NAME");
831         if (tvp)
832         {
833             tvp->at(0)->setText(scopeConfigNameEdit->text().toLatin1().constData());
834             currentTelescope->getDriverInfo()->getClientManager()->sendNewText(tvp);
835         }
836     }
837 
838     auto nvp = currentTelescope->getBaseDevice()->getNumber("TELESCOPE_INFO");
839 
840     if (nvp)
841     {
842         bool dirty = false;
843         primaryScopeGroup->setTitle(currentTelescope->getDeviceName());
844         guideScopeGroup->setTitle(i18n("%1 guide scope", currentTelescope->getDeviceName()));
845 
846         auto np = nvp->findWidgetByName("TELESCOPE_APERTURE");
847         if (np && std::fabs(np->getValue() - primaryScopeApertureIN->value()) > 0)
848         {
849             dirty = true;
850             np->setValue(primaryScopeApertureIN->value());
851         }
852 
853         np = nvp->findWidgetByName("TELESCOPE_FOCAL_LENGTH");
854         if (np && std::fabs(np->getValue() - primaryScopeFocalIN->value()) > 0)
855             np->setValue(primaryScopeFocalIN->value());
856 
857         np = nvp->findWidgetByName("GUIDER_APERTURE");
858         if (np && std::fabs(np->getValue() - guideScopeApertureIN->value()) > 0)
859         {
860             dirty = true;
861             np->setValue(guideScopeApertureIN->value() <= 1 ? primaryScopeApertureIN->value() : guideScopeApertureIN->value());
862         }
863 
864         np = nvp->findWidgetByName("GUIDER_FOCAL_LENGTH");
865         if (np && std::fabs(np->getValue() - guideScopeFocalIN->value()) > 0)
866         {
867             dirty = true;
868             np->setValue(guideScopeFocalIN->value() <= 1 ? primaryScopeFocalIN->value() : guideScopeFocalIN->value());
869         }
870 
871         ClientManager *clientManager = currentTelescope->getDriverInfo()->getClientManager();
872 
873         clientManager->sendNewNumber(nvp);
874 
875         if (dirty)
876             currentTelescope->setConfig(SAVE_CONFIG);
877     }
878     else
879         appendLogText(i18n("Failed to save telescope information."));
880 }
881 
saveLimits()882 void Mount::saveLimits()
883 {
884     Options::setMinimumAltLimit(minAltLimit->value());
885     Options::setMaximumAltLimit(maxAltLimit->value());
886     currentTelescope->setAltLimits(minAltLimit->value(), maxAltLimit->value());
887 
888     Options::setMaximumHaLimit(maxHaLimit->value());
889 }
890 
enableAltitudeLimits(bool enable)891 void Mount::enableAltitudeLimits(bool enable)
892 {
893     Options::setEnableAltitudeLimits(enable);
894 
895     if (enable)
896     {
897         minAltLabel->setEnabled(true);
898         maxAltLabel->setEnabled(true);
899 
900         minAltLimit->setEnabled(true);
901         maxAltLimit->setEnabled(true);
902 
903         if (currentTelescope)
904             currentTelescope->setAltLimits(minAltLimit->value(), maxAltLimit->value());
905     }
906     else
907     {
908         minAltLabel->setEnabled(false);
909         maxAltLabel->setEnabled(false);
910 
911         minAltLimit->setEnabled(false);
912         maxAltLimit->setEnabled(false);
913 
914         if (currentTelescope)
915             currentTelescope->setAltLimits(-1, -1);
916     }
917 }
918 
enableAltLimits()919 void Mount::enableAltLimits()
920 {
921     //Only enable if it was already enabled before and the minAltLimit is currently disabled.
922     if (m_AltitudeLimitEnabled && minAltLimit->isEnabled() == false)
923         enableAltitudeLimits(true);
924 }
925 
disableAltLimits()926 void Mount::disableAltLimits()
927 {
928     m_AltitudeLimitEnabled = enableLimitsCheck->isChecked();
929 
930     enableAltitudeLimits(false);
931 }
932 
enableHourAngleLimits(bool enable)933 void Mount::enableHourAngleLimits(bool enable)
934 {
935     Options::setEnableHaLimit(enable);
936 
937     maxHaLabel->setEnabled(enable);
938     maxHaLimit->setEnabled(enable);
939 }
940 
enableHaLimits()941 void Mount::enableHaLimits()
942 {
943     //Only enable if it was already enabled before and the minHaLimit is currently disabled.
944     if (m_HourAngleLimitEnabled && maxHaLimit->isEnabled() == false)
945         enableHourAngleLimits(true);
946 }
947 
disableHaLimits()948 void Mount::disableHaLimits()
949 {
950     m_HourAngleLimitEnabled = enableHaLimitCheck->isChecked();
951 
952     enableHourAngleLimits(false);
953 }
954 
altitudeLimits()955 QList<double> Mount::altitudeLimits()
956 {
957     QList<double> limits;
958 
959     limits.append(minAltLimit->value());
960     limits.append(maxAltLimit->value());
961 
962     return limits;
963 }
964 
setAltitudeLimits(QList<double> limits)965 void Mount::setAltitudeLimits(QList<double> limits)
966 {
967     minAltLimit->setValue(limits[0]);
968     maxAltLimit->setValue(limits[1]);
969 }
970 
setAltitudeLimitsEnabled(bool enable)971 void Mount::setAltitudeLimitsEnabled(bool enable)
972 {
973     enableLimitsCheck->setChecked(enable);
974 }
975 
altitudeLimitsEnabled()976 bool Mount::altitudeLimitsEnabled()
977 {
978     return enableLimitsCheck->isChecked();
979 }
980 
hourAngleLimit()981 double Mount::hourAngleLimit()
982 {
983     return maxHaLimit->value();
984 }
985 
setHourAngleLimit(double limit)986 void Mount::setHourAngleLimit(double limit)
987 {
988     maxHaLimit->setValue(limit);
989 }
990 
setHourAngleLimitEnabled(bool enable)991 void Mount::setHourAngleLimitEnabled(bool enable)
992 {
993     enableHaLimitCheck->setChecked(enable);
994 }
995 
hourAngleLimitEnabled()996 bool Mount::hourAngleLimitEnabled()
997 {
998     return enableHaLimitCheck->isChecked();
999 }
1000 
setJ2000Enabled(bool enabled)1001 void Mount::setJ2000Enabled(bool enabled)
1002 {
1003     m_J2000Check->setProperty("checked", enabled);
1004 }
1005 
gotoTarget(const QString & target)1006 bool Mount::gotoTarget(const QString &target)
1007 {
1008     SkyObject *object = KStarsData::Instance()->skyComposite()->findByName(target);
1009 
1010     if (object != nullptr)
1011     {
1012         object->updateCoordsNow(KStarsData::Instance()->updateNum());
1013         return slew(object->ra().Hours(), object->dec().Degrees());
1014     }
1015 
1016     return false;
1017 }
1018 
syncTarget(const QString & target)1019 bool Mount::syncTarget(const QString &target)
1020 {
1021     SkyObject *object = KStarsData::Instance()->skyComposite()->findByName(target);
1022 
1023     if (object != nullptr)
1024     {
1025         object->updateCoordsNow(KStarsData::Instance()->updateNum());
1026         return sync(object->ra().Hours(), object->dec().Degrees());
1027     }
1028 
1029     return false;
1030 }
1031 
slew(const QString & RA,const QString & DEC)1032 bool Mount::slew(const QString &RA, const QString &DEC)
1033 {
1034     dms ra, de;
1035 
1036     if (m_equatorialCheck->property("checked").toBool())
1037     {
1038         ra = dms::fromString(RA, false);
1039         de = dms::fromString(DEC, true);
1040     }
1041 
1042     if (m_horizontalCheck->property("checked").toBool())
1043     {
1044         dms az = dms::fromString(RA, true);
1045         dms at = dms::fromString(DEC, true);
1046         SkyPoint target;
1047         target.setAz(az);
1048         target.setAlt(at);
1049         target.HorizontalToEquatorial(KStars::Instance()->data()->lst(), KStars::Instance()->data()->geo()->lat());
1050         ra = target.ra();
1051         de = target.dec();
1052     }
1053 
1054     if (m_haEquatorialCheck->property("checked").toBool())
1055     {
1056         dms ha = dms::fromString(RA, false);
1057         de = dms::fromString(DEC, true);
1058         dms lst = KStarsData::Instance()->geo()->GSTtoLST(KStarsData::Instance()->clock()->utc().gst());
1059         ra = (lst - ha + dms(360.0)).reduce();
1060     }
1061 
1062     // If J2000 was checked and the Mount is _not_ already using native J2000 coordinates
1063     // then we need to convert J2000 to JNow. Otherwise, we send J2000 as is.
1064     if (m_J2000Check->property("checked").toBool() && currentTelescope && currentTelescope->isJ2000() == false)
1065     {
1066         // J2000 ---> JNow
1067         SkyPoint J2000Coord(ra, de);
1068         J2000Coord.setRA0(ra);
1069         J2000Coord.setDec0(de);
1070         J2000Coord.apparentCoord(static_cast<long double>(J2000), KStars::Instance()->data()->ut().djd());
1071 
1072         ra = J2000Coord.ra();
1073         de = J2000Coord.dec();
1074     }
1075 
1076     return slew(ra.Hours(), de.Degrees());
1077 }
1078 
slew(double RA,double DEC)1079 bool Mount::slew(double RA, double DEC)
1080 {
1081     if (currentTelescope == nullptr || currentTelescope->isConnected() == false)
1082         return false;
1083 
1084     dms lst = KStarsData::Instance()->geo()->GSTtoLST(KStarsData::Instance()->clock()->utc().gst());
1085     double HA = lst.Hours() - RA;
1086     HA = rangeHA(HA);
1087     //    if (HA > 12.0)
1088     //        HA -= 24.0;
1089     setInitialHA(HA);
1090     // reset the meridian flip status if the slew is not the meridian flip itself
1091     if (m_MFStatus != FLIP_RUNNING)
1092     {
1093         setMeridianFlipStatus(FLIP_NONE);
1094         flipDelayHrs = 0;
1095         qCDebug(KSTARS_EKOS_MOUNT) << "flipDelayHrs set to zero in slew, m_MFStatus=" <<
1096                                    meridianFlipStatusString(m_MFStatus);
1097     }
1098 
1099     delete currentTargetPosition;
1100     currentTargetPosition = new SkyPoint(RA, DEC);
1101     SkyPoint J2000Coord(currentTargetPosition->ra(), currentTargetPosition->dec());
1102     J2000Coord.catalogueCoord(KStars::Instance()->data()->ut().djd());
1103     currentTargetPosition->setRA0(J2000Coord.ra());
1104     currentTargetPosition->setDec0(J2000Coord.dec());
1105 
1106 
1107     qCDebug(KSTARS_EKOS_MOUNT) << "Slewing to RA=" <<
1108                                currentTargetPosition->ra().toHMSString() <<
1109                                "DEC=" << currentTargetPosition->dec().toDMSString();
1110     qCDebug(KSTARS_EKOS_MOUNT) << "Initial HA " << initialHA() << ", flipDelayHrs " << flipDelayHrs <<
1111                                "MFStatus " << meridianFlipStatusString(m_MFStatus);
1112 
1113     // start the slew
1114     return(currentTelescope->Slew(currentTargetPosition));
1115 }
1116 
1117 ///
1118 /// \brief Mount::checkMeridianFlip  This updates the Meridian Flip Status state machine using the LST supplied,
1119 /// the mount target position and the pier side if available.
1120 /// \param lst
1121 /// \return true if a flip slew can be started, false otherwise
1122 ///
checkMeridianFlip(dms lst)1123 bool Mount::checkMeridianFlip(dms lst)
1124 {
1125     // checks if a flip is possible
1126     if (currentTelescope == nullptr || currentTelescope->isConnected() == false)
1127     {
1128         meridianFlipStatusText->setText(i18n("Status: inactive (no scope connected)"));
1129         emit newMeridianFlipText(meridianFlipStatusText->text());
1130         setMeridianFlipStatus(FLIP_NONE);
1131         return false;
1132     }
1133 
1134     if (meridianFlipCheckBox->isChecked() == false)
1135     {
1136         meridianFlipStatusText->setText(i18n("Status: inactive (flip not requested)"));
1137         emit newMeridianFlipText(meridianFlipStatusText->text());
1138         return false;
1139     }
1140 
1141     if (currentTelescope->isParked())
1142     {
1143         meridianFlipStatusText->setText(i18n("Status: inactive (parked)"));
1144         emit newMeridianFlipText(meridianFlipStatusText->text());
1145         return false;
1146     }
1147 
1148     if (currentTargetPosition == nullptr)
1149     {
1150         meridianFlipStatusText->setText(i18n("Status: inactive (no Target set)"));
1151         emit newMeridianFlipText(meridianFlipStatusText->text());
1152         return false;
1153     }
1154 
1155     // get the time after the meridian that the flip is called for
1156     double offset = meridianFlipTimeBox->value();
1157     // Degrees --> Hours
1158     if (meridianFlipDegreesR->isChecked())
1159         offset = rangeHA(offset / 15.0);
1160 
1161     double hrsToFlip = 0;       // time to go to the next flip - hours  -ve means a flip is required
1162 
1163     double ha = rangeHA(lst.Hours() - telescopeCoord.ra().Hours());     // -12 to 0 to +12
1164 
1165     // calculate time to next flip attempt.  This uses the current hour angle, the pier side if available
1166     // and the meridian flip offset to get the time to the flip
1167     //
1168     // *** should it use the target position so it will continue to track the target even if the mount is not tracking?
1169     //
1170     // Note: the PierSide code relies on the mount reporting the pier side correctly
1171     // It is possible that a mount can flip before the meridian and this has caused problems so hrsToFlip is calculated
1172     // assuming the the mount can flip up to three hours early.
1173 
1174     static ISD::Telescope::PierSide
1175     initialPierSide;    // used when the flip has completed to determine if the flip was successful
1176 
1177     // adjust ha according to the pier side.
1178     switch (currentTelescope->pierSide())
1179     {
1180         case ISD::Telescope::PierSide::PIER_WEST:
1181             // this is the normal case, tracking from East to West, flip is near Ha 0.
1182             break;
1183         case ISD::Telescope::PierSide::PIER_EAST:
1184             // this is the below the pole case, tracking West to East, flip is near Ha 12.
1185             // shift ha by 12h
1186             ha = rangeHA(ha + 12);
1187             break;
1188         default:
1189             // This is the case where the PierSide is not available, make one attempt only
1190             flipDelayHrs = 0;
1191             // we can only attempt a flip if the mount started before the meridian, assumed in the unflipped state
1192             if (initialHA() >= 0)
1193             {
1194                 meridianFlipStatusText->setText(i18n("Status: inactive (slew after meridian)"));
1195                 emit newMeridianFlipText(meridianFlipStatusText->text());
1196                 if (m_MFStatus == FLIP_NONE)
1197                     return false;
1198             }
1199             break;
1200     }
1201     // get the time to the next flip, allowing for the pier side and
1202     // the possibility of an early flip
1203     // adjust ha so an early flip is allowed for
1204     if (ha >= 9.0)
1205         ha -= 24.0;
1206     hrsToFlip = offset + flipDelayHrs - ha;
1207 
1208     int hh = static_cast<int> (hrsToFlip);
1209     int mm = static_cast<int> ((hrsToFlip - hh) * 60);
1210     int ss = static_cast<int> ((hrsToFlip - hh - mm / 60.0) * 3600);
1211     QString message = i18n("Meridian flip in %1", QTime(hh, mm, ss).toString(Qt::TextDate));
1212 
1213     // handle the meridian flip state machine
1214     switch (m_MFStatus)
1215     {
1216         case FLIP_NONE:
1217             meridianFlipStatusText->setText(message);
1218             emit newMeridianFlipText(meridianFlipStatusText->text());
1219 
1220             if (hrsToFlip <= 0)
1221             {
1222                 // signal that a flip can be done
1223                 qCDebug(KSTARS_EKOS_MOUNT) << "Meridian flip planned with LST=" <<
1224                                            lst.toHMSString() <<
1225                                            " scope RA=" << telescopeCoord.ra().toHMSString() <<
1226                                            " ha=" << ha <<
1227                                            ", meridian diff=" << offset <<
1228                                            ", hrstoFlip=" << hrsToFlip <<
1229                                            ", flipDelayHrs=" << flipDelayHrs <<
1230                                            ", " << pierSideStateString();
1231 
1232                 initialPierSide = currentTelescope->pierSide();
1233                 setMeridianFlipStatus(FLIP_PLANNED);
1234             }
1235             break;
1236 
1237         case FLIP_PLANNED:
1238             // handle the case where there is no Capture module
1239             if (captureInterface == nullptr)
1240             {
1241                 qCDebug(KSTARS_EKOS_MOUNT) << "no capture interface, starting flip slew.";
1242                 setMeridianFlipStatus(FLIP_ACCEPTED);
1243                 return true;
1244             }
1245             return false;
1246 
1247         case FLIP_ACCEPTED:
1248             // set by the Capture module when it's ready
1249             return true;
1250 
1251         case FLIP_RUNNING:
1252             if (currentTelescope->isTracking())
1253             {
1254                 // meridian flip slew completed, did it work?
1255                 bool flipFailed = false;
1256 
1257                 // pointing state change check only for mounts that report pier side
1258                 if (currentTelescope->pierSide() == ISD::Telescope::PIER_UNKNOWN)
1259                 {
1260                     // check how long it took
1261                     if (minMeridianFlipEndTime > QDateTime::currentDateTimeUtc())
1262                     {
1263                         // don't fail, we have tried but we don't know where the mount was when it started
1264                         appendLogText(i18n("Meridian flip failed - time too short, pier side unknown."));
1265                         // signal that capture can resume
1266                         setMeridianFlipStatus(FLIP_COMPLETED);
1267                         return false;
1268                     }
1269                 }
1270                 else if (currentTelescope->pierSide() == initialPierSide)
1271                 {
1272                     flipFailed = true;
1273                     qCWarning(KSTARS_EKOS_MOUNT) << "Meridian flip failed, pier side not changed";
1274                 }
1275 
1276                 if (flipFailed)
1277                 {
1278                     if (flipDelayHrs <= 1.0)
1279                     {
1280                         // Set next flip attempt to be 4 minutes in the future.
1281                         // These depend on the assignment to flipDelayHrs above.
1282                         constexpr double delayHours = 4.0 / 60.0;
1283                         if (currentTelescope->pierSide() == ISD::Telescope::PierSide::PIER_EAST)
1284                             flipDelayHrs = rangeHA(ha + 12 + delayHours) - offset;
1285                         else
1286                             flipDelayHrs = ha + delayHours - offset;
1287 
1288                         // check to stop an infinite loop, 1.0 hrs for now but should use the Ha limit
1289                         appendLogText(i18n("meridian flip failed, retrying in 4 minutes"));
1290                     }
1291                     else
1292                     {
1293                         appendLogText(i18n("No successful Meridian Flip done, delay too long"));
1294                     }
1295                     setMeridianFlipStatus(FLIP_COMPLETED);   // this will resume imaging and try again after the extra delay
1296                 }
1297                 else
1298                 {
1299                     flipDelayHrs = 0;
1300                     appendLogText(i18n("Meridian flip completed OK."));
1301                     // signal that capture can resume
1302                     setMeridianFlipStatus(FLIP_COMPLETED);
1303                 }
1304             }
1305             break;
1306 
1307         case FLIP_COMPLETED:
1308             setMeridianFlipStatus(FLIP_NONE);
1309             break;
1310 
1311         default:
1312             break;
1313     }
1314     return false;
1315 }
1316 
executeMeridianFlip()1317 bool Mount::executeMeridianFlip()
1318 {
1319     if (/*initialHA() > 0 || */ currentTargetPosition == nullptr)
1320     {
1321         // no meridian flip necessary
1322         qCDebug(KSTARS_EKOS_MOUNT) << "No meridian flip: currentTargetPosition is null";
1323         return false;
1324     }
1325 
1326     if (currentTelescope->status() != ISD::Telescope::MOUNT_TRACKING)
1327     {
1328         // no meridian flip necessary
1329         qCDebug(KSTARS_EKOS_MOUNT) << "No meridian flip: mount not tracking";
1330         return false;
1331     }
1332 
1333     dms lst = KStarsData::Instance()->geo()->GSTtoLST(KStarsData::Instance()->clock()->utc().gst());
1334     double HA = rangeHA(lst.Hours() - currentTargetPosition->ra().Hours());
1335 
1336     // execute meridian flip
1337     qCInfo(KSTARS_EKOS_MOUNT) << "Meridian flip: slewing to RA=" <<
1338                               currentTargetPosition->ra().toHMSString() <<
1339                               "DEC=" << currentTargetPosition->dec().toDMSString() <<
1340                               " Hour Angle " << dms(HA).toHMSString();
1341     setMeridianFlipStatus(FLIP_RUNNING);
1342 
1343     minMeridianFlipEndTime = KStarsData::Instance()->clock()->utc().addSecs(minMeridianFlipDurationSecs);
1344 
1345     if (slew(currentTargetPosition->ra().Hours(), currentTargetPosition->dec().Degrees()))
1346     {
1347         appendLogText(i18n("Meridian flip slew started..."));
1348         return true;
1349     }
1350     else
1351     {
1352         qCWarning(KSTARS_EKOS_MOUNT) << "Meridian flip FAILED: slewing to RA=" <<
1353                                      currentTargetPosition->ra().toHMSString() <<
1354                                      "DEC=" << currentTargetPosition->dec().toDMSString();
1355         return false;
1356     }
1357 }
1358 
1359 // This method should just be called by the signal coming from Capture, indicating the
1360 // internal state of Capture.
meridianFlipStatusChanged(Mount::MeridianFlipStatus status)1361 void Mount::meridianFlipStatusChanged(Mount::MeridianFlipStatus status)
1362 {
1363     qCDebug(KSTARS_EKOS_MOUNT) << "Received capture meridianFlipStatusChange " << meridianFlipStatusString(status);
1364 
1365     // only the states FLIP_WAITING and FLIP_ACCEPTED are relevant as answers
1366     // to FLIP_PLANNED, all other states are set only internally
1367     if (status == FLIP_WAITING || status == FLIP_ACCEPTED)
1368         meridianFlipStatusChangedInternal(status);
1369 }
1370 
meridianFlipStatusChangedInternal(Mount::MeridianFlipStatus status)1371 void Mount::meridianFlipStatusChangedInternal(Mount::MeridianFlipStatus status)
1372 {
1373     m_MFStatus = status;
1374 
1375     qCDebug(KSTARS_EKOS_MOUNT) << "meridianFlipStatusChanged " << meridianFlipStatusString(status);
1376 
1377     switch (status)
1378     {
1379         case FLIP_NONE:
1380             meridianFlipStatusText->setText(i18n("Status: inactive"));
1381             emit newMeridianFlipText(meridianFlipStatusText->text());
1382             break;
1383 
1384         case FLIP_PLANNED:
1385             meridianFlipStatusText->setText(i18n("Meridian flip planned..."));
1386             emit newMeridianFlipText(meridianFlipStatusText->text());
1387             break;
1388 
1389         case FLIP_WAITING:
1390             meridianFlipStatusText->setText(i18n("Meridian flip waiting..."));
1391             emit newMeridianFlipText(meridianFlipStatusText->text());
1392             appendLogText(i18n("Meridian flip waiting."));
1393             break;
1394 
1395         case FLIP_ACCEPTED:
1396             if (currentTelescope == nullptr || currentTelescope->isTracking() == false)
1397                 // if the mount is not tracking, we go back one step
1398                 setMeridianFlipStatus(FLIP_PLANNED);
1399             // otherwise do nothing, execution of meridian flip initianted in updateTelescopeCoords()
1400             break;
1401 
1402         case FLIP_RUNNING:
1403             meridianFlipStatusText->setText(i18n("Meridian flip running..."));
1404             emit newMeridianFlipText(meridianFlipStatusText->text());
1405             appendLogText(i18n("Meridian flip started."));
1406             break;
1407 
1408         case FLIP_COMPLETED:
1409             meridianFlipStatusText->setText(i18n("Meridian flip completed."));
1410             emit newMeridianFlipText(meridianFlipStatusText->text());
1411             appendLogText(i18n("Meridian flip completed."));
1412             break;
1413 
1414         default:
1415             break;
1416     }
1417 }
1418 
currentTarget()1419 SkyPoint Mount::currentTarget()
1420 {
1421     return *currentTargetPosition;
1422 }
1423 
1424 
sync(const QString & RA,const QString & DEC)1425 bool Mount::sync(const QString &RA, const QString &DEC)
1426 {
1427     dms ra, de;
1428 
1429     if (m_equatorialCheck->property("checked").toBool())
1430     {
1431         ra = dms::fromString(RA, false);
1432         de = dms::fromString(DEC, true);
1433     }
1434 
1435     if (m_horizontalCheck->property("checked").toBool())
1436     {
1437         dms az = dms::fromString(RA, true);
1438         dms at = dms::fromString(DEC, true);
1439         SkyPoint target;
1440         target.setAz(az);
1441         target.setAlt(at);
1442         target.HorizontalToEquatorial(KStars::Instance()->data()->lst(), KStars::Instance()->data()->geo()->lat());
1443         ra = target.ra();
1444         de = target.dec();
1445     }
1446 
1447     if (m_haEquatorialCheck->property("checked").toBool())
1448     {
1449         dms ha = dms::fromString(RA, false);
1450         de = dms::fromString(DEC, true);
1451         dms lst = KStarsData::Instance()->geo()->GSTtoLST(KStarsData::Instance()->clock()->utc().gst());
1452         ra = (lst - ha + dms(360.0)).reduce();
1453     }
1454 
1455     if (m_J2000Check->property("checked").toBool())
1456     {
1457         // J2000 ---> JNow
1458         SkyPoint J2000Coord(ra, de);
1459         J2000Coord.setRA0(ra);
1460         J2000Coord.setDec0(de);
1461         J2000Coord.updateCoordsNow(KStarsData::Instance()->updateNum());
1462 
1463         ra = J2000Coord.ra();
1464         de = J2000Coord.dec();
1465     }
1466 
1467     return sync(ra.Hours(), de.Degrees());
1468 }
1469 
sync(double RA,double DEC)1470 bool Mount::sync(double RA, double DEC)
1471 {
1472     if (currentTelescope == nullptr || currentTelescope->isConnected() == false)
1473         return false;
1474 
1475     return currentTelescope->Sync(RA, DEC);
1476 }
1477 
abort()1478 bool Mount::abort()
1479 {
1480     return currentTelescope->Abort();
1481 }
1482 
slewStatus()1483 IPState Mount::slewStatus()
1484 {
1485     if (currentTelescope == nullptr)
1486         return IPS_ALERT;
1487 
1488     return currentTelescope->getState("EQUATORIAL_EOD_COORD");
1489 }
1490 
equatorialCoords()1491 QList<double> Mount::equatorialCoords()
1492 {
1493     double ra, dec;
1494     QList<double> coords;
1495 
1496     currentTelescope->getEqCoords(&ra, &dec);
1497     coords.append(ra);
1498     coords.append(dec);
1499 
1500     return coords;
1501 }
1502 
horizontalCoords()1503 QList<double> Mount::horizontalCoords()
1504 {
1505     QList<double> coords;
1506 
1507     coords.append(telescopeCoord.az().Degrees());
1508     coords.append(telescopeCoord.alt().Degrees());
1509 
1510     return coords;
1511 }
1512 
1513 ///
1514 /// \brief Mount::hourAngle
1515 /// \return returns the current mount hour angle in hours in the range -12 to +12
1516 ///
hourAngle()1517 double Mount::hourAngle()
1518 {
1519     dms lst = KStarsData::Instance()->geo()->GSTtoLST(KStarsData::Instance()->clock()->utc().gst());
1520     dms ha(lst.Degrees() - telescopeCoord.ra().Degrees());
1521     double HA = rangeHA(ha.Hours());
1522 
1523     return HA;
1524     //    if (HA > 12.0)
1525     //        return (HA - 24.0);
1526     //    else
1527     //        return HA;
1528 }
1529 
telescopeInfo()1530 QList<double> Mount::telescopeInfo()
1531 {
1532     QList<double> info;
1533 
1534     info.append(primaryScopeFocalIN->value());
1535     info.append(primaryScopeApertureIN->value());
1536     info.append(guideScopeFocalIN->value());
1537     info.append(guideScopeApertureIN->value());
1538 
1539     return info;
1540 }
1541 
setTelescopeInfo(const QList<double> & info)1542 void Mount::setTelescopeInfo(const QList<double> &info)
1543 {
1544     if (info[0] > 0)
1545         primaryScopeFocalIN->setValue(info[0]);
1546     if (info[1] > 0)
1547         primaryScopeApertureIN->setValue(info[1]);
1548     if (info[2] > 0)
1549         guideScopeFocalIN->setValue(info[2]);
1550     if (info[3] > 0)
1551         guideScopeApertureIN->setValue(info[3]);
1552 
1553     if (scopeConfigNameEdit->text().isEmpty() == false)
1554         appendLogText(i18n("Warning: Overriding %1 configuration.", scopeConfigNameEdit->text()));
1555 
1556     save();
1557 }
1558 
canPark()1559 bool Mount::canPark()
1560 {
1561     if (currentTelescope == nullptr)
1562         return false;
1563 
1564     return currentTelescope->canPark();
1565 }
1566 
park()1567 bool Mount::park()
1568 {
1569     if (currentTelescope == nullptr || currentTelescope->canPark() == false)
1570         return false;
1571 
1572     return currentTelescope->Park();
1573 }
1574 
unpark()1575 bool Mount::unpark()
1576 {
1577     if (currentTelescope == nullptr || currentTelescope->canPark() == false)
1578         return false;
1579 
1580     return currentTelescope->UnPark();
1581 }
1582 
1583 
toggleMountToolBox()1584 void Mount::toggleMountToolBox()
1585 {
1586     if (m_BaseView->isVisible())
1587     {
1588         m_BaseView->hide();
1589         QAction *a = KStars::Instance()->actionCollection()->action("show_mount_box");
1590         if (a)
1591             a->setChecked(false);
1592     }
1593     else
1594     {
1595         m_BaseView->show();
1596         QAction *a = KStars::Instance()->actionCollection()->action("show_mount_box");
1597         if (a)
1598             a->setChecked(true);
1599     }
1600 }
1601 
findTarget()1602 void Mount::findTarget()
1603 {
1604     if (FindDialog::Instance()->execWithParent(Ekos::Manager::Instance()) == QDialog::Accepted)
1605     {
1606         SkyObject *object = FindDialog::Instance()->targetObject();
1607         if (object != nullptr)
1608         {
1609             KStarsData * const data = KStarsData::Instance();
1610 
1611             SkyObject *o = object->clone();
1612             o->updateCoords(data->updateNum(), true, data->geo()->lat(), data->lst(), false);
1613 
1614             m_equatorialCheck->setProperty("checked", true);
1615 
1616             m_targetText->setProperty("text", o->name());
1617 
1618             if (m_JNowCheck->property("checked").toBool())
1619             {
1620                 m_targetRAText->setProperty("text", o->ra().toHMSString());
1621                 m_targetDEText->setProperty("text", o->dec().toDMSString());
1622             }
1623             else
1624             {
1625                 m_targetRAText->setProperty("text", o->ra0().toHMSString());
1626                 m_targetDEText->setProperty("text", o->dec0().toDMSString());
1627             }
1628         }
1629     }
1630 }
1631 
1632 //++++ converters for target coordinate display in Mount Control box
1633 
raDecToAzAlt(QString qsRA,QString qsDec)1634 bool Mount::raDecToAzAlt(QString qsRA, QString qsDec)
1635 {
1636     dms RA, Dec;
1637 
1638     if (!RA.setFromString(qsRA, false) || !Dec.setFromString(qsDec, true))
1639         return false;
1640 
1641     SkyPoint targetCoord(RA, Dec);
1642 
1643     targetCoord.EquatorialToHorizontal(KStarsData::Instance()->lst(),
1644                                        KStarsData::Instance()->geo()->lat());
1645 
1646     m_targetRAText->setProperty("text", targetCoord.az().toDMSString());
1647     m_targetDEText->setProperty("text", targetCoord.alt().toDMSString());
1648 
1649     return true;
1650 }
1651 
raDecToHaDec(QString qsRA)1652 bool  Mount::raDecToHaDec(QString qsRA)
1653 {
1654     dms RA;
1655 
1656     if (!RA.setFromString(qsRA, false))
1657         return false;
1658 
1659     dms lst = KStarsData::Instance()->geo()->GSTtoLST(KStarsData::Instance()->clock()->utc().gst());
1660 
1661     dms HA = (lst - RA + dms(360.0)).reduce();
1662 
1663     QChar sgn('+');
1664     if (HA.Hours() > 12.0)
1665     {
1666         HA.setH(24.0 - HA.Hours());
1667         sgn = '-';
1668     }
1669 
1670     m_targetRAText->setProperty("text", QString("%1%2").arg(sgn).arg(HA.toHMSString()));
1671 
1672     return true;
1673 }
1674 
azAltToRaDec(QString qsAz,QString qsAlt)1675 bool  Mount::azAltToRaDec(QString qsAz, QString qsAlt)
1676 {
1677     dms Az, Alt;
1678 
1679     if (!Az.setFromString(qsAz, true) || !Alt.setFromString(qsAlt, true))
1680         return false;
1681 
1682     SkyPoint targetCoord;
1683     targetCoord.setAz(Az);
1684     targetCoord.setAlt(Alt);
1685 
1686     targetCoord.HorizontalToEquatorial(KStars::Instance()->data()->lst(),
1687                                        KStars::Instance()->data()->geo()->lat());
1688 
1689     m_targetRAText->setProperty("text", targetCoord.ra().toHMSString());
1690     m_targetDEText->setProperty("text", targetCoord.dec().toDMSString());
1691 
1692     return true;
1693 }
1694 
azAltToHaDec(QString qsAz,QString qsAlt)1695 bool  Mount::azAltToHaDec(QString qsAz, QString qsAlt)
1696 {
1697     dms Az, Alt;
1698 
1699     if (!Az.setFromString(qsAz, true) || !Alt.setFromString(qsAlt, true))
1700         return false;
1701 
1702     SkyPoint targetCoord;
1703     targetCoord.setAz(Az);
1704     targetCoord.setAlt(Alt);
1705 
1706     dms lst = KStarsData::Instance()->geo()->GSTtoLST(KStarsData::Instance()->clock()->utc().gst());
1707 
1708     targetCoord.HorizontalToEquatorial(&lst, KStars::Instance()->data()->geo()->lat());
1709 
1710     dms HA = (lst - targetCoord.ra() + dms(360.0)).reduce();
1711 
1712     QChar sgn('+');
1713     if (HA.Hours() > 12.0)
1714     {
1715         HA.setH(24.0 - HA.Hours());
1716         sgn = '-';
1717     }
1718 
1719     m_targetRAText->setProperty("text", QString("%1%2").arg(sgn).arg(HA.toHMSString()));
1720     m_targetDEText->setProperty("text", targetCoord.dec().toDMSString());
1721 
1722 
1723     return true;
1724 }
1725 
haDecToRaDec(QString qsHA)1726 bool  Mount::haDecToRaDec(QString qsHA)
1727 {
1728     dms HA;
1729 
1730     if (!HA.setFromString(qsHA, false))
1731         return false;
1732 
1733     dms lst = KStarsData::Instance()->geo()->GSTtoLST(KStarsData::Instance()->clock()->utc().gst());
1734     dms RA = (lst - HA + dms(360.0)).reduce();
1735 
1736     m_targetRAText->setProperty("text", RA.toHMSString());
1737 
1738     return true;
1739 }
1740 
haDecToAzAlt(QString qsHA,QString qsDec)1741 bool  Mount::haDecToAzAlt(QString qsHA, QString qsDec)
1742 {
1743     dms HA, Dec;
1744 
1745     if (!HA.setFromString(qsHA, false) || !Dec.setFromString(qsDec, true))
1746         return false;
1747 
1748     dms lst = KStarsData::Instance()->geo()->GSTtoLST(KStarsData::Instance()->clock()->utc().gst());
1749     dms RA = (lst - HA + dms(360.0)).reduce();
1750 
1751     SkyPoint targetCoord;
1752     targetCoord.setRA(RA);
1753     targetCoord.setDec(Dec);
1754 
1755     targetCoord.EquatorialToHorizontal(&lst, KStars::Instance()->data()->geo()->lat());
1756 
1757     m_targetRAText->setProperty("text", targetCoord.az().toDMSString());
1758     m_targetDEText->setProperty("text", targetCoord.alt().toDMSString());
1759 
1760     return true;
1761 }
1762 
1763 //---- end: converters for target coordinate display in Mount Control box
1764 
centerMount()1765 void Mount::centerMount()
1766 {
1767     if (currentTelescope)
1768         currentTelescope->runCommand(INDI_FIND_TELESCOPE);
1769 }
1770 
resetModel()1771 bool Mount::resetModel()
1772 {
1773     if (currentTelescope == nullptr)
1774         return false;
1775 
1776     if (currentTelescope->hasAlignmentModel() == false)
1777         return false;
1778 
1779     if (currentTelescope->clearAlignmentModel())
1780     {
1781         appendLogText(i18n("Alignment Model cleared."));
1782         return true;
1783     }
1784 
1785     appendLogText(i18n("Failed to clear Alignment Model."));
1786     return false;
1787 }
1788 
setGPS(ISD::GDInterface * newGPS)1789 void Mount::setGPS(ISD::GDInterface *newGPS)
1790 {
1791     if (newGPS == currentGPS)
1792         return;
1793 
1794     auto executeSetGPS = [this, newGPS]()
1795     {
1796         currentGPS = newGPS;
1797         connect(newGPS, &ISD::GenericDevice::numberUpdated, this, &Ekos::Mount::updateNumber, Qt::UniqueConnection);
1798         appendLogText(i18n("GPS driver detected. KStars and mount time and location settings are now synced to the GPS driver."));
1799         syncGPS();
1800     };
1801 
1802     if (Options::useGPSSource() == false)
1803     {
1804         connect(KSMessageBox::Instance(), &KSMessageBox::accepted, this, [this, executeSetGPS]()
1805         {
1806             //QObject::disconnect(KSMessageBox::Instance(), &KSMessageBox::accepted, this, nullptr);
1807             KSMessageBox::Instance()->disconnect(this);
1808             Options::setUseKStarsSource(false);
1809             Options::setUseMountSource(false);
1810             Options::setUseGPSSource(true);
1811             executeSetGPS();
1812         });
1813 
1814         KSMessageBox::Instance()->questionYesNo(i18n("GPS is detected. Do you want to switch time and location source to GPS?"),
1815                                                 i18n("GPS Settings"), 10);
1816     }
1817     else
1818         executeSetGPS();
1819 }
1820 
syncGPS()1821 void Mount::syncGPS()
1822 {
1823     // We only update when location is OK
1824     auto location = currentGPS->getBaseDevice()->getNumber("GEOGRAPHIC_COORD");
1825     if (!location || location->getState() != IPS_OK)
1826         return;
1827 
1828     // Sync name
1829     if (currentTelescope)
1830     {
1831         auto activeDevices = currentTelescope->getBaseDevice()->getText("ACTIVE_DEVICES");
1832         if (activeDevices)
1833         {
1834             auto activeGPS = activeDevices->findWidgetByName("ACTIVE_GPS");
1835             if (activeGPS)
1836             {
1837                 if (activeGPS->getText() != currentGPS->getDeviceName())
1838                 {
1839                     activeGPS->setText(currentGPS->getDeviceName().toLatin1().constData());
1840                     currentTelescope->getDriverInfo()->getClientManager()->sendNewText(activeDevices);
1841                 }
1842             }
1843         }
1844     }
1845 
1846     // GPS Refresh should only be called once automatically.
1847     if (GPSInitialized == false)
1848     {
1849         auto refreshGPS = currentGPS->getBaseDevice()->getSwitch("GPS_REFRESH");
1850         if (refreshGPS)
1851         {
1852             refreshGPS->at(0)->setState(ISS_ON);
1853             currentGPS->getDriverInfo()->getClientManager()->sendNewSwitch(refreshGPS);
1854             GPSInitialized = true;
1855         }
1856     }
1857 }
1858 
setScopeStatus(ISD::Telescope::Status status)1859 void Mount::setScopeStatus(ISD::Telescope::Status status)
1860 {
1861     if (m_Status != status)
1862     {
1863         m_statusText->setProperty("text", currentTelescope->getStatusString(status));
1864         m_Status = status;
1865         // forward
1866         emit newStatus(status);
1867     }
1868 }
1869 
setTrackEnabled(bool enabled)1870 void Mount::setTrackEnabled(bool enabled)
1871 {
1872     if (enabled)
1873         trackOnB->click();
1874     else
1875         trackOffB->click();
1876 }
1877 
slewRate()1878 int Mount::slewRate()
1879 {
1880     if (currentTelescope == nullptr)
1881         return -1;
1882 
1883     return currentTelescope->getSlewRate();
1884 }
1885 
1886 //QJsonArray Mount::getScopes() const
1887 //{
1888 //    QJsonArray scopes;
1889 //    if (currentTelescope == nullptr)
1890 //        return scopes;
1891 
1892 //    QJsonObject primary =
1893 //    {
1894 //        {"name", "Primary"},
1895 //        {"mount", currentTelescope->getDeviceName()},
1896 //        {"aperture", primaryScopeApertureIN->value()},
1897 //        {"focalLength", primaryScopeFocalIN->value()},
1898 //    };
1899 
1900 //    scopes.append(primary);
1901 
1902 //    QJsonObject guide =
1903 //    {
1904 //        {"name", "Guide"},
1905 //        {"mount", currentTelescope->getDeviceName()},
1906 //        {"aperture", primaryScopeApertureIN->value()},
1907 //        {"focalLength", primaryScopeFocalIN->value()},
1908 //    };
1909 
1910 //    scopes.append(guide);
1911 
1912 //    return scopes;
1913 //}
1914 
autoParkEnabled()1915 bool Mount::autoParkEnabled()
1916 {
1917     return autoParkTimer.isActive();
1918 }
1919 
setAutoParkEnabled(bool enable)1920 void Mount::setAutoParkEnabled(bool enable)
1921 {
1922     if (enable)
1923         startParkTimer();
1924     else
1925         stopParkTimer();
1926 }
1927 
setAutoParkDailyEnabled(bool enabled)1928 void Mount::setAutoParkDailyEnabled(bool enabled)
1929 {
1930     everyDayCheck->setChecked(enabled);
1931 }
1932 
setAutoParkStartup(QTime startup)1933 void Mount::setAutoParkStartup(QTime startup)
1934 {
1935     startupTimeEdit->setTime(startup);
1936 }
1937 
meridianFlipEnabled()1938 bool Mount::meridianFlipEnabled()
1939 {
1940     return meridianFlipCheckBox->isChecked();
1941 }
1942 
meridianFlipValue()1943 double Mount::meridianFlipValue()
1944 {
1945     return meridianFlipTimeBox->value();
1946 }
1947 
startParkTimer()1948 void Mount::startParkTimer()
1949 {
1950     if (currentTelescope == nullptr || m_ParkStatus == ISD::PARK_UNKNOWN)
1951         return;
1952 
1953     if (currentTelescope->isParked())
1954     {
1955         appendLogText(i18n("Mount already parked."));
1956         return;
1957     }
1958 
1959     QTime parkTime = startupTimeEdit->time();
1960 
1961     qCDebug(KSTARS_EKOS_MOUNT) << "Parking time is" << parkTime.toString();
1962     QDateTime currentDateTime = KStarsData::Instance()->lt();
1963     QDateTime parkDateTime(currentDateTime);
1964 
1965     parkDateTime.setTime(parkTime);
1966     qint64 parkMilliSeconds = parkDateTime.msecsTo(currentDateTime);
1967     qCDebug(KSTARS_EKOS_MOUNT) << "Until parking time:" << parkMilliSeconds << "ms or" << parkMilliSeconds / (60 * 60 * 1000)
1968                                << "hours";
1969     if (parkMilliSeconds > 0)
1970     {
1971         qCDebug(KSTARS_EKOS_MOUNT) << "Added a day to parking time...";
1972         parkDateTime = parkDateTime.addDays(1);
1973         parkMilliSeconds = parkDateTime.msecsTo(currentDateTime);
1974 
1975         int hours = static_cast<int>(parkMilliSeconds / (1000 * 60 * 60));
1976         if (hours > 0)
1977         {
1978             // No need to display warning for every day check
1979             if (everyDayCheck->isChecked() == false)
1980                 appendLogText(i18n("Parking time cannot be in the past."));
1981             return;
1982         }
1983         else if (std::abs(hours) > 12)
1984         {
1985             qCDebug(KSTARS_EKOS_MOUNT) << "Parking time is" << hours << "which exceeds 12 hours, auto park is disabled.";
1986             return;
1987         }
1988     }
1989 
1990     parkMilliSeconds = std::abs(parkMilliSeconds);
1991 
1992     if (parkMilliSeconds > 24 * 60 * 60 * 1000)
1993     {
1994         appendLogText(i18n("Parking time must be within 24 hours of current time."));
1995         return;
1996     }
1997 
1998     if (parkMilliSeconds > 12 * 60 * 60 * 1000)
1999         appendLogText(i18n("Warning! Parking time is more than 12 hours away."));
2000 
2001     appendLogText(i18n("Caution: do not use Auto Park while scheduler is active."));
2002 
2003     autoParkTimer.setInterval(static_cast<int>(parkMilliSeconds));
2004     autoParkTimer.start();
2005 
2006     startTimerB->setEnabled(false);
2007     stopTimerB->setEnabled(true);
2008 }
2009 
stopParkTimer()2010 void Mount::stopParkTimer()
2011 {
2012     autoParkTimer.stop();
2013     countdownLabel->setText("00:00:00");
2014     stopTimerB->setEnabled(false);
2015     startTimerB->setEnabled(true);
2016 }
2017 
startAutoPark()2018 void Mount::startAutoPark()
2019 {
2020     appendLogText(i18n("Parking timer is up."));
2021     autoParkTimer.stop();
2022     startTimerB->setEnabled(true);
2023     stopTimerB->setEnabled(false);
2024     countdownLabel->setText("00:00:00");
2025     if (currentTelescope)
2026     {
2027         if (currentTelescope->isParked() == false)
2028         {
2029             appendLogText(i18n("Starting auto park..."));
2030             park();
2031         }
2032     }
2033 }
2034 
meridianFlipStatusString(MeridianFlipStatus status)2035 QString Mount::meridianFlipStatusString(MeridianFlipStatus status)
2036 {
2037     switch (status)
2038     {
2039         case FLIP_NONE:
2040             return "FLIP_NONE";
2041         case FLIP_PLANNED:
2042             return "FLIP_PLANNED";
2043         case FLIP_WAITING:
2044             return "FLIP_WAITING";
2045         case FLIP_ACCEPTED:
2046             return "FLIP_ACCEPTED";
2047         case FLIP_RUNNING:
2048             return "FLIP_RUNNING";
2049         case FLIP_COMPLETED:
2050             return "FLIP_COMPLETED";
2051         case FLIP_ERROR:
2052             return "FLIP_ERROR";
2053     }
2054     return "not possible";
2055 }
2056 
pierSideStateString()2057 QString Mount::pierSideStateString()
2058 {
2059     switch (currentTelescope->pierSide())
2060     {
2061         case ISD::Telescope::PierSide::PIER_EAST:
2062             return "Pier Side: East (pointing West)";
2063         case ISD::Telescope::PierSide::PIER_WEST:
2064             return "Pier Side: West (pointing East)";
2065         default:
2066             return "Pier Side: Unknown";
2067     }
2068 }
2069 }
2070