File indexing completed on 2024-04-21 03:43:36

0001 /*
0002     SPDX-FileCopyrightText: 2023 Wolfgang Reissenberger <sterne-jaeger@openfuture.de>
0003 
0004     SPDX-License-Identifier: GPL-2.0-or-later
0005 */
0006 #include "schedulermodulestate.h"
0007 #include "schedulerjob.h"
0008 #include <ekos_scheduler_debug.h>
0009 #include "schedulerprocess.h"
0010 #include "schedulerjob.h"
0011 #include "kstarsdata.h"
0012 #include "ksalmanac.h"
0013 #include "Options.h"
0014 
0015 #define MAX_FAILURE_ATTEMPTS 5
0016 
0017 namespace Ekos
0018 {
0019 // constants definition
0020 QDateTime SchedulerModuleState::m_Dawn, SchedulerModuleState::m_Dusk, SchedulerModuleState::m_PreDawnDateTime;
0021 GeoLocation *SchedulerModuleState::storedGeo = nullptr;
0022 
0023 SchedulerModuleState::SchedulerModuleState() {}
0024 
0025 void SchedulerModuleState::init()
0026 {
0027     // This is needed to get wakeupScheduler() to call start() and startup,
0028     // instead of assuming it is already initialized (if preemptiveShutdown was not set).
0029     // The time itself is not used.
0030     enablePreemptiveShutdown(SchedulerModuleState::getLocalTime());
0031 
0032     setIterationSetup(false);
0033     setupNextIteration(RUN_WAKEUP, 10);
0034 }
0035 
0036 void SchedulerModuleState::setCurrentProfile(const QString &newName, bool signal)
0037 {
0038     bool changed = (newName != m_currentProfile);
0039 
0040     if (m_profiles.contains(newName))
0041         m_currentProfile = newName;
0042     else
0043     {
0044         changed = (m_currentProfile !=  m_profiles.first());
0045         m_currentProfile = m_profiles.first();
0046     }
0047     // update the UI
0048     if (signal && changed)
0049         emit currentProfileChanged();
0050 }
0051 
0052 void SchedulerModuleState::updateProfiles(const QStringList &newProfiles)
0053 {
0054     QString selected = currentProfile();
0055     // Default profile is always the first one
0056     QStringList allProfiles(i18n("Default"));
0057     allProfiles.append(newProfiles);
0058 
0059     m_profiles = allProfiles;
0060     // ensure that the selected profile still exists
0061     setCurrentProfile(selected, false);
0062     emit profilesChanged();
0063 }
0064 
0065 void SchedulerModuleState::setActiveJob(SchedulerJob *newActiveJob)
0066 {
0067     m_activeJob = newActiveJob;
0068 }
0069 
0070 void SchedulerModuleState::updateJobStage(SchedulerJobStage stage)
0071 {
0072     if (activeJob() == nullptr)
0073     {
0074         emit jobStageChanged(SCHEDSTAGE_IDLE);
0075     }
0076     else
0077     {
0078         activeJob()->setStage(stage);
0079         emit jobStageChanged(stage);
0080     }
0081 }
0082 
0083 QJsonArray SchedulerModuleState::getJSONJobs()
0084 {
0085     QJsonArray jobArray;
0086 
0087     for (const auto &oneJob : jobs())
0088         jobArray.append(oneJob->toJson());
0089 
0090     return jobArray;
0091 }
0092 
0093 void SchedulerModuleState::setSchedulerState(const SchedulerState &newState)
0094 {
0095     m_schedulerState = newState;
0096     emit schedulerStateChanged(newState);
0097 }
0098 
0099 void SchedulerModuleState::setCurrentPosition(int newCurrentPosition)
0100 {
0101     m_currentPosition = newCurrentPosition;
0102     emit currentPositionChanged(newCurrentPosition);
0103 }
0104 
0105 void SchedulerModuleState::setStartupState(StartupState state)
0106 {
0107     if (m_startupState != state)
0108     {
0109         m_startupState = state;
0110         emit startupStateChanged(state);
0111     }
0112 }
0113 
0114 void SchedulerModuleState::setShutdownState(ShutdownState state)
0115 {
0116     if (m_shutdownState != state)
0117     {
0118         m_shutdownState = state;
0119         emit shutdownStateChanged(state);
0120     }
0121 }
0122 
0123 void SchedulerModuleState::setParkWaitState(ParkWaitState state)
0124 {
0125     if (m_parkWaitState != state)
0126     {
0127         m_parkWaitState = state;
0128         emit parkWaitStateChanged(state);
0129     }
0130 }
0131 
0132 bool SchedulerModuleState::removeJob(const int currentRow)
0133 {
0134     /* Don't remove a row that is not selected */
0135     if (currentRow < 0)
0136         return false;
0137 
0138     /* Grab the job currently selected */
0139     SchedulerJob * const job = jobs().at(currentRow);
0140 
0141     // Can't delete the currently running job
0142     if (job == m_activeJob)
0143     {
0144         emit newLog(i18n("Cannot delete currently running job '%1'.", job->getName()));
0145         return false;
0146     }
0147     else if (job == nullptr || (activeJob() == nullptr && schedulerState() != SCHEDULER_IDLE))
0148     {
0149         // Don't allow delete--worried that we're about to schedule job that's being deleted.
0150         emit newLog(i18n("Cannot delete job. Scheduler state: %1",
0151                          getSchedulerStatusString(schedulerState(), true)));
0152         return false;
0153     }
0154 
0155     qCDebug(KSTARS_EKOS_SCHEDULER) << QString("Job '%1' at row #%2 is being deleted.").arg(job->getName()).arg(currentRow + 1);
0156 
0157     /* Remove the job object */
0158     mutlableJobs().removeOne(job);
0159     delete (job);
0160 
0161     // Reduce the current position if the last element has been deleted
0162     if (currentPosition() >= jobs().count())
0163         setCurrentPosition(jobs().count() - 1);
0164 
0165     setDirty(true);
0166     // success
0167     return true;
0168 }
0169 
0170 void SchedulerModuleState::enablePreemptiveShutdown(const QDateTime &wakeupTime)
0171 {
0172     m_preemptiveShutdownWakeupTime = wakeupTime;
0173 }
0174 
0175 void SchedulerModuleState::disablePreemptiveShutdown()
0176 {
0177     m_preemptiveShutdownWakeupTime = QDateTime();
0178 }
0179 
0180 const QDateTime &SchedulerModuleState::preemptiveShutdownWakeupTime() const
0181 {
0182     return m_preemptiveShutdownWakeupTime;
0183 }
0184 
0185 bool SchedulerModuleState::preemptiveShutdown() const
0186 {
0187     return m_preemptiveShutdownWakeupTime.isValid();
0188 }
0189 
0190 void SchedulerModuleState::setEkosState(EkosState state)
0191 {
0192     if (m_ekosState != state)
0193     {
0194         qCDebug(KSTARS_EKOS_SCHEDULER) << "EKOS state changed from" << m_ekosState << "to" << state;
0195         m_ekosState = state;
0196         emit ekosStateChanged(state);
0197     }
0198 }
0199 
0200 bool SchedulerModuleState::increaseEkosConnectFailureCount()
0201 {
0202     return (++m_ekosConnectFailureCount <= MAX_FAILURE_ATTEMPTS);
0203 }
0204 
0205 bool SchedulerModuleState::increaseParkingCapFailureCount()
0206 {
0207     return (++m_parkingCapFailureCount <= MAX_FAILURE_ATTEMPTS);
0208 }
0209 
0210 bool SchedulerModuleState::increaseParkingMountFailureCount()
0211 {
0212     return (++m_parkingMountFailureCount <= MAX_FAILURE_ATTEMPTS);
0213 }
0214 
0215 bool SchedulerModuleState::increaseParkingDomeFailureCount()
0216 {
0217     return (++m_parkingDomeFailureCount <= MAX_FAILURE_ATTEMPTS);
0218 }
0219 
0220 void SchedulerModuleState::resetFailureCounters()
0221 {
0222     resetIndiConnectFailureCount();
0223     resetEkosConnectFailureCount();
0224     resetFocusFailureCount();
0225     resetGuideFailureCount();
0226     resetAlignFailureCount();
0227     resetCaptureFailureCount();
0228 }
0229 
0230 bool SchedulerModuleState::increaseIndiConnectFailureCount()
0231 {
0232     return (++m_indiConnectFailureCount <= MAX_FAILURE_ATTEMPTS);
0233 }
0234 
0235 bool SchedulerModuleState::increaseCaptureFailureCount()
0236 {
0237     return (++m_captureFailureCount <= MAX_FAILURE_ATTEMPTS);
0238 }
0239 
0240 bool SchedulerModuleState::increaseFocusFailureCount()
0241 {
0242     return (++m_focusFailureCount <= MAX_FAILURE_ATTEMPTS);
0243 }
0244 
0245 bool SchedulerModuleState::increaseGuideFailureCount()
0246 {
0247     return (++m_guideFailureCount <= MAX_FAILURE_ATTEMPTS);
0248 }
0249 
0250 bool SchedulerModuleState::increaseAlignFailureCount()
0251 {
0252     return (++m_alignFailureCount <= MAX_FAILURE_ATTEMPTS);
0253 }
0254 
0255 void SchedulerModuleState::setIndiState(INDIState state)
0256 {
0257     if (m_indiState != state)
0258     {
0259         qCDebug(KSTARS_EKOS_SCHEDULER) << "INDI state changed from" << m_indiState << "to" << state;
0260         m_indiState = state;
0261         emit indiStateChanged(state);
0262     }
0263 }
0264 
0265 qint64 SchedulerModuleState::getCurrentOperationMsec() const
0266 {
0267     if (!currentOperationTimeStarted) return 0;
0268     return currentOperationTime.msecsTo(KStarsData::Instance()->ut());
0269 }
0270 
0271 void SchedulerModuleState::startCurrentOperationTimer()
0272 {
0273     currentOperationTimeStarted = true;
0274     currentOperationTime = KStarsData::Instance()->ut();
0275 }
0276 
0277 void SchedulerModuleState::cancelGuidingTimer()
0278 {
0279     m_restartGuidingInterval = -1;
0280     m_restartGuidingTime = KStarsDateTime();
0281 }
0282 
0283 bool SchedulerModuleState::isGuidingTimerActive()
0284 {
0285     return (m_restartGuidingInterval > 0 &&
0286             m_restartGuidingTime.msecsTo(KStarsData::Instance()->ut()) >= 0);
0287 }
0288 
0289 void SchedulerModuleState::startGuidingTimer(int milliseconds)
0290 {
0291     m_restartGuidingInterval = milliseconds;
0292     m_restartGuidingTime = KStarsData::Instance()->ut();
0293 }
0294 
0295 // Allows for unit testing of static Scheduler methods,
0296 // as can't call KStarsData::Instance() during unit testing.
0297 KStarsDateTime *SchedulerModuleState::storedLocalTime = nullptr;
0298 KStarsDateTime SchedulerModuleState::getLocalTime()
0299 {
0300     if (hasLocalTime())
0301         return *storedLocalTime;
0302     return KStarsData::Instance()->geo()->UTtoLT(KStarsData::Instance()->clock()->utc());
0303 }
0304 
0305 void SchedulerModuleState::calculateDawnDusk(const QDateTime &when, QDateTime &nDawn, QDateTime &nDusk)
0306 {
0307     QDateTime startup = when;
0308 
0309     if (!startup.isValid())
0310         startup = getLocalTime();
0311 
0312     // Our local midnight - the KStarsDateTime date+time constructor is safe for local times
0313     // Exact midnight seems unreliable--offset it by a minute.
0314     KStarsDateTime midnight(startup.date(), QTime(0, 1), Qt::LocalTime);
0315 
0316     QDateTime dawn = startup, dusk = startup;
0317 
0318     // Loop dawn and dusk calculation until the events found are the next events
0319     for ( ; dawn <= startup || dusk <= startup ; midnight = midnight.addDays(1))
0320     {
0321         // KSAlmanac computes the closest dawn and dusk events from the local sidereal time corresponding to the midnight argument
0322 
0323 #if 0
0324         KSAlmanac const ksal(midnight, getGeo());
0325         // If dawn is in the past compared to this observation, fetch the next dawn
0326         if (dawn <= startup)
0327             dawn = getGeo()->UTtoLT(ksal.getDate().addSecs((ksal.getDawnAstronomicalTwilight() * 24.0 + Options::dawnOffset()) *
0328                                     3600.0));
0329         // If dusk is in the past compared to this observation, fetch the next dusk
0330         if (dusk <= startup)
0331             dusk = getGeo()->UTtoLT(ksal.getDate().addSecs((ksal.getDuskAstronomicalTwilight() * 24.0 + Options::duskOffset()) *
0332                                     3600.0));
0333 #else
0334         // Creating these almanac instances seems expensive.
0335         static QMap<QString, KSAlmanac const * > almanacMap;
0336         const QString key = QString("%1 %2 %3").arg(midnight.toString()).arg(getGeo()->lat()->Degrees()).arg(
0337                                 getGeo()->lng()->Degrees());
0338         KSAlmanac const * ksal = almanacMap.value(key, nullptr);
0339         if (ksal == nullptr)
0340         {
0341             if (almanacMap.size() > 5)
0342             {
0343                 // don't allow this to grow too large.
0344                 qDeleteAll(almanacMap);
0345                 almanacMap.clear();
0346             }
0347             ksal = new KSAlmanac(midnight, getGeo());
0348             almanacMap[key] = ksal;
0349         }
0350 
0351         // If dawn is in the past compared to this observation, fetch the next dawn
0352         if (dawn <= startup)
0353             dawn = getGeo()->UTtoLT(ksal->getDate().addSecs((ksal->getDawnAstronomicalTwilight() * 24.0 + Options::dawnOffset()) *
0354                                     3600.0));
0355 
0356         // If dusk is in the past compared to this observation, fetch the next dusk
0357         if (dusk <= startup)
0358             dusk = getGeo()->UTtoLT(ksal->getDate().addSecs((ksal->getDuskAstronomicalTwilight() * 24.0 + Options::duskOffset()) *
0359                                     3600.0));
0360 #endif
0361     }
0362 
0363     // Now we have the next events:
0364     // - if dawn comes first, observation runs during the night
0365     // - if dusk comes first, observation runs during the day
0366     nDawn = dawn;
0367     nDusk = dusk;
0368 }
0369 
0370 void SchedulerModuleState::calculateDawnDusk()
0371 {
0372     calculateDawnDusk(QDateTime(), m_Dawn, m_Dusk);
0373 
0374     m_PreDawnDateTime = m_Dawn.addSecs(-60.0 * abs(Options::preDawnTime()));
0375     emit updateNightTime();
0376 }
0377 
0378 const GeoLocation *SchedulerModuleState::getGeo()
0379 {
0380     if (hasGeo())
0381         return storedGeo;
0382     return KStarsData::Instance()->geo();
0383 }
0384 
0385 bool SchedulerModuleState::hasGeo()
0386 {
0387     return storedGeo != nullptr;
0388 }
0389 
0390 void SchedulerModuleState::setupNextIteration(SchedulerTimerState nextState)
0391 {
0392     setupNextIteration(nextState, m_UpdatePeriodMs);
0393 }
0394 
0395 void SchedulerModuleState::setupNextIteration(SchedulerTimerState nextState, int milliseconds)
0396 {
0397     if (iterationSetup())
0398     {
0399         qCDebug(KSTARS_EKOS_SCHEDULER)
0400                 << QString("Multiple setupNextIteration calls: current %1 %2, previous %3 %4")
0401                 .arg(nextState).arg(milliseconds).arg(timerState()).arg(timerInterval());
0402     }
0403     setTimerState(nextState);
0404     // check if setup is called from a thread outside of the iteration timer thread
0405     if (iterationTimer().isActive())
0406     {
0407         // restart the timer to ensure the correct startup delay
0408         int remaining = iterationTimer().remainingTime();
0409         iterationTimer().stop();
0410         setTimerInterval(std::max(0, milliseconds - remaining));
0411         iterationTimer().start(timerInterval());
0412     }
0413     else
0414     {
0415         // setup called from inside the iteration timer thread
0416         setTimerInterval(milliseconds);
0417     }
0418     setIterationSetup(true);
0419 }
0420 
0421 uint SchedulerModuleState::maxFailureAttempts()
0422 {
0423     return MAX_FAILURE_ATTEMPTS;
0424 }
0425 
0426 bool SchedulerModuleState::checkRepeatSequence()
0427 {
0428     return (!Options::rememberJobProgress() && Options::schedulerRepeatSequences() &&
0429             (Options::schedulerExecutionSequencesLimit() == 0
0430              || sequenceExecutionCounter()) < Options::schedulerExecutionSequencesLimit());
0431 }
0432 } // Ekos namespace