Kstars

schedulermodulestate.cpp
1/*
2 SPDX-FileCopyrightText: 2023 Wolfgang Reissenberger <sterne-jaeger@openfuture.de>
3
4 SPDX-License-Identifier: GPL-2.0-or-later
5*/
6#include "schedulermodulestate.h"
7#include "schedulerjob.h"
8#include <ekos_scheduler_debug.h>
9#include "schedulerprocess.h"
10#include "schedulerjob.h"
11#include "kstarsdata.h"
12#include "ksalmanac.h"
13#include "Options.h"
14
15#define MAX_FAILURE_ATTEMPTS 5
16
17namespace Ekos
18{
19// constants definition
20QDateTime SchedulerModuleState::m_Dawn, SchedulerModuleState::m_Dusk, SchedulerModuleState::m_PreDawnDateTime;
21GeoLocation *SchedulerModuleState::storedGeo = nullptr;
22
23SchedulerModuleState::SchedulerModuleState() {}
24
25void SchedulerModuleState::init()
26{
27 // This is needed to get wakeupScheduler() to call start() and startup,
28 // instead of assuming it is already initialized (if preemptiveShutdown was not set).
29 // The time itself is not used.
30 enablePreemptiveShutdown(SchedulerModuleState::getLocalTime());
31
32 setIterationSetup(false);
33 setupNextIteration(RUN_WAKEUP, 10);
34}
35
36void SchedulerModuleState::setCurrentProfile(const QString &newName, bool signal)
37{
38 bool changed = (newName != m_currentProfile);
39
40 if (m_profiles.contains(newName))
41 m_currentProfile = newName;
42 else
43 {
44 changed = (m_currentProfile != m_profiles.first());
45 m_currentProfile = m_profiles.first();
46 }
47 // update the UI
48 if (signal && changed)
49 emit currentProfileChanged();
50}
51
52void SchedulerModuleState::updateProfiles(const QStringList &newProfiles)
53{
54 QString selected = currentProfile();
55 // Default profile is always the first one
56 QStringList allProfiles(i18n("Default"));
57 allProfiles.append(newProfiles);
58
59 m_profiles = allProfiles;
60 // ensure that the selected profile still exists
61 setCurrentProfile(selected, false);
62 emit profilesChanged();
63}
64
65SchedulerJob *SchedulerModuleState::activeJob(const QString &trainname) const
66{
67 // default take the lead job
68 if (trainname == "" || m_activeJob == nullptr || (m_activeJob != nullptr && m_activeJob->getOpticalTrain() == trainname))
69 return m_activeJob;
70 else
71 {
72 foreach (auto follower, m_activeJob->followerJobs())
73 {
74 if (follower->getOpticalTrain() == trainname)
75 return follower;
76 }
77 // none found
78 return nullptr;
79 }
80
81}
82
83void SchedulerModuleState::setActiveJob(SchedulerJob *newActiveJob)
84{
85 m_activeJob = newActiveJob;
86}
87
88QList<SchedulerJob *> SchedulerModuleState::leadJobs()
89{
90 QList<SchedulerJob *> result;
91 for (auto job : jobs())
92 if (job->isLead())
93 result.append(job);
94
95 return result;
96}
97
98QList<SchedulerJob *> SchedulerModuleState::followerJobs()
99{
100 QList<SchedulerJob *> result;
101 for (auto job : jobs())
102 if (!job->isLead())
103 result.append(job);
104
105 return result;
106}
107
108void SchedulerModuleState::updateJobStage(SchedulerJobStage stage)
109{
110 if (activeJob() == nullptr)
111 {
112 emit jobStageChanged(SCHEDSTAGE_IDLE);
113 }
114 else
115 {
116 activeJob()->setStage(stage);
117 emit jobStageChanged(stage);
118 }
119}
120
121QJsonArray SchedulerModuleState::getJSONJobs()
122{
123 QJsonArray jobArray;
124
125 for (const auto &oneJob : jobs())
126 jobArray.append(oneJob->toJson());
127
128 return jobArray;
129}
130
131void SchedulerModuleState::setSchedulerState(const SchedulerState &newState)
132{
133 m_schedulerState = newState;
134 emit schedulerStateChanged(newState);
135}
136
137void SchedulerModuleState::setCurrentPosition(int newCurrentPosition)
138{
139 m_currentPosition = newCurrentPosition;
140 emit currentPositionChanged(newCurrentPosition);
141}
142
143void SchedulerModuleState::setStartupState(StartupState state)
144{
145 if (m_startupState != state)
146 {
147 m_startupState = state;
148 emit startupStateChanged(state);
149 }
150}
151
152void SchedulerModuleState::setShutdownState(ShutdownState state)
153{
154 if (m_shutdownState != state)
155 {
156 m_shutdownState = state;
157 emit shutdownStateChanged(state);
158 }
159}
160
161void SchedulerModuleState::setParkWaitState(ParkWaitState state)
162{
163 if (m_parkWaitState != state)
164 {
165 m_parkWaitState = state;
166 emit parkWaitStateChanged(state);
167 }
168}
169
170bool SchedulerModuleState::removeJob(const int currentRow)
171{
172 /* Don't remove a row that is not selected */
173 if (currentRow < 0)
174 return false;
175
176 /* Grab the job currently selected */
177 SchedulerJob * const job = jobs().at(currentRow);
178
179 // Can't delete the currently running job
180 if (job == m_activeJob)
181 {
182 emit newLog(i18n("Cannot delete currently running job '%1'.", job->getName()));
183 return false;
184 }
185 else if (job == nullptr || (activeJob() == nullptr && schedulerState() != SCHEDULER_IDLE))
186 {
187 // Don't allow delete--worried that we're about to schedule job that's being deleted.
188 emit newLog(i18n("Cannot delete job. Scheduler state: %1",
189 getSchedulerStatusString(schedulerState(), true)));
190 return false;
191 }
192
193 qCDebug(KSTARS_EKOS_SCHEDULER) << QString("Job '%1' at row #%2 is being deleted.").arg(job->getName()).arg(currentRow + 1);
194
195 // if it was a lead job, re-arrange its follower (if necessary)
196 if (job->isLead() && job->followerJobs().count() > 0)
197 {
198 // search the new lead
199 SchedulerJob *newLead = findLead(currentRow - 1);
200 // if it does not exist, do not allow job deletion
201 if (newLead == nullptr)
202 {
203 return false;
204 }
205 else
206 {
207 // set the new lead and add the follower jobs to the new lead
208 for (auto follower : job->followerJobs())
209 {
210 follower->setLeadJob(newLead);
211 newLead->followerJobs().append(follower);
212 }
213 }
214 }
215 /* Remove the job object */
216 mutlableJobs().removeOne(job);
217
218 delete (job);
219
220 // Reduce the current position if the last element has been deleted
221 if (currentPosition() >= jobs().count())
222 setCurrentPosition(jobs().count() - 1);
223
224 setDirty(true);
225 // success
226 return true;
227}
228
229void SchedulerModuleState::refreshFollowerLists()
230{
231 // clear the follower lists
232 for (auto job : m_jobs)
233 job->followerJobs().clear();
234
235 // iterate over all jobs and append the follower to its lead
236 for (auto job : m_jobs)
237 {
238 SchedulerJob *lead = job->leadJob();
239 if (job->isLead() == false && lead != nullptr)
240 {
241 lead->followerJobs().append(job);
242 lead->updateSharedFollowerAttributes();
243 }
244 }
245}
246
247SchedulerJob *SchedulerModuleState::findLead(int position, bool upward)
248{
249 auto start = std::min(position, static_cast<int>(jobs().count()));
250
251 if (upward)
252 {
253 for (int i = start; i >= 0; i--)
254 if (jobs().at(i)->isLead())
255 return jobs().at(i);
256 }
257 else
258 {
259 for (int i = start; i < jobs().count(); i++)
260 if (jobs().at(i)->isLead())
261 return jobs().at(i);
262 }
263
264 // nothing found
265 return nullptr;
266}
267
268void SchedulerModuleState::enablePreemptiveShutdown(const QDateTime &wakeupTime)
269{
270 m_preemptiveShutdownWakeupTime = wakeupTime;
271}
272
273void SchedulerModuleState::disablePreemptiveShutdown()
274{
275 m_preemptiveShutdownWakeupTime = QDateTime();
276}
277
278const QDateTime &SchedulerModuleState::preemptiveShutdownWakeupTime() const
279{
280 return m_preemptiveShutdownWakeupTime;
281}
282
283bool SchedulerModuleState::preemptiveShutdown() const
284{
285 return m_preemptiveShutdownWakeupTime.isValid();
286}
287
288void SchedulerModuleState::setEkosState(EkosState state)
289{
290 if (m_ekosState != state)
291 {
292 qCDebug(KSTARS_EKOS_SCHEDULER) << "EKOS state changed from" << m_ekosState << "to" << state;
293 m_ekosState = state;
294 emit ekosStateChanged(state);
295 }
296}
297
298bool SchedulerModuleState::increaseEkosConnectFailureCount()
299{
300 return (++m_ekosConnectFailureCount <= MAX_FAILURE_ATTEMPTS);
301}
302
303bool SchedulerModuleState::increaseParkingCapFailureCount()
304{
305 return (++m_parkingCapFailureCount <= MAX_FAILURE_ATTEMPTS);
306}
307
308bool SchedulerModuleState::increaseParkingMountFailureCount()
309{
310 return (++m_parkingMountFailureCount <= MAX_FAILURE_ATTEMPTS);
311}
312
313bool SchedulerModuleState::increaseParkingDomeFailureCount()
314{
315 return (++m_parkingDomeFailureCount <= MAX_FAILURE_ATTEMPTS);
316}
317
318void SchedulerModuleState::resetFailureCounters()
319{
320 resetIndiConnectFailureCount();
321 resetEkosConnectFailureCount();
322 resetFocusFailureCount();
323 resetGuideFailureCount();
324 resetAlignFailureCount();
325 resetCaptureFailureCount();
326}
327
328bool SchedulerModuleState::increaseIndiConnectFailureCount()
329{
330 return (++m_indiConnectFailureCount <= MAX_FAILURE_ATTEMPTS);
331}
332
333bool SchedulerModuleState::increaseCaptureFailureCount()
334{
335 return (++m_captureFailureCount <= MAX_FAILURE_ATTEMPTS);
336}
337
338bool SchedulerModuleState::increaseFocusFailureCount(const QString &trainname)
339{
340 return (++m_focusFailureCount[trainname] <= MAX_FAILURE_ATTEMPTS);
341}
342
343bool SchedulerModuleState::increaseAllFocusFailureCounts()
344{
345 bool result = true;
346
347 // if one of the counters is beyond the threshold, we return false.
348 for (QMap<QString, bool>::const_iterator it = m_autofocusCompleted.cbegin(); it != m_autofocusCompleted.cend(); it++)
349 result &= increaseFocusFailureCount(it.key());
350
351 return result;
352}
353
354bool SchedulerModuleState::autofocusCompleted(const QString &trainname) const
355{
356 if (!trainname.isEmpty())
357 return m_autofocusCompleted[trainname];
358 else
359 return autofocusCompleted();
360}
361
362void SchedulerModuleState::setAutofocusCompleted(const QString &trainname, bool value)
363{
364 if (!trainname.isEmpty())
365 m_autofocusCompleted[trainname] = value;
366 else
367 m_autofocusCompleted.clear();
368}
369
370bool SchedulerModuleState::autofocusCompleted() const
371{
372 if (m_autofocusCompleted.isEmpty())
373 return false;
374
375 for (QMap<QString, bool>::const_iterator it = m_autofocusCompleted.cbegin(); it != m_autofocusCompleted.cend(); it++)
376 {
377 if (it.value() == false)
378 return false;
379 }
380 // all are completed
381 return true;
382}
383
384bool SchedulerModuleState::increaseGuideFailureCount()
385{
386 return (++m_guideFailureCount <= MAX_FAILURE_ATTEMPTS);
387}
388
389bool SchedulerModuleState::increaseAlignFailureCount()
390{
391 return (++m_alignFailureCount <= MAX_FAILURE_ATTEMPTS);
392}
393
394void SchedulerModuleState::setIndiState(INDIState state)
395{
396 if (m_indiState != state)
397 {
398 qCDebug(KSTARS_EKOS_SCHEDULER) << "INDI state changed from" << m_indiState << "to" << state;
399 m_indiState = state;
400 emit indiStateChanged(state);
401 }
402}
403
404qint64 SchedulerModuleState::getCurrentOperationMsec() const
405{
406 if (!currentOperationTimeStarted) return 0;
407 return currentOperationTime.msecsTo(KStarsData::Instance()->ut());
408}
409
410void SchedulerModuleState::startCurrentOperationTimer()
411{
412 currentOperationTimeStarted = true;
413 currentOperationTime = KStarsData::Instance()->ut();
414}
415
416void SchedulerModuleState::cancelGuidingTimer()
417{
418 m_restartGuidingInterval = -1;
419 m_restartGuidingTime = KStarsDateTime();
420}
421
422bool SchedulerModuleState::isGuidingTimerActive()
423{
424 return (m_restartGuidingInterval > 0 &&
425 m_restartGuidingTime.msecsTo(KStarsData::Instance()->ut()) >= 0);
426}
427
428void SchedulerModuleState::startGuidingTimer(int milliseconds)
429{
430 m_restartGuidingInterval = milliseconds;
431 m_restartGuidingTime = KStarsData::Instance()->ut();
432}
433
434// Allows for unit testing of static Scheduler methods,
435// as can't call KStarsData::Instance() during unit testing.
436KStarsDateTime *SchedulerModuleState::storedLocalTime = nullptr;
437KStarsDateTime SchedulerModuleState::getLocalTime()
438{
439 if (hasLocalTime())
440 return *storedLocalTime;
441 return KStarsData::Instance()->geo()->UTtoLT(KStarsData::Instance()->clock()->utc());
442}
443
444void SchedulerModuleState::calculateDawnDusk(const QDateTime &when, QDateTime &nDawn, QDateTime &nDusk)
445{
446 QDateTime startup = when;
447
448 if (!startup.isValid())
449 startup = getLocalTime();
450
451 // Our local midnight - the KStarsDateTime date+time constructor is safe for local times
452 // Exact midnight seems unreliable--offset it by a minute.
453 KStarsDateTime midnight(startup.date(), QTime(0, 1), Qt::LocalTime);
454
455 QDateTime dawn = startup, dusk = startup;
456
457 // Loop dawn and dusk calculation until the events found are the next events
458 for ( ; dawn <= startup || dusk <= startup ; midnight = midnight.addDays(1))
459 {
460 // KSAlmanac computes the closest dawn and dusk events from the local sidereal time corresponding to the midnight argument
461
462#if 0
463 KSAlmanac const ksal(midnight, getGeo());
464 // If dawn is in the past compared to this observation, fetch the next dawn
465 if (dawn <= startup)
466 dawn = getGeo()->UTtoLT(ksal.getDate().addSecs((ksal.getDawnAstronomicalTwilight() * 24.0 + Options::dawnOffset()) *
467 3600.0));
468 // If dusk is in the past compared to this observation, fetch the next dusk
469 if (dusk <= startup)
470 dusk = getGeo()->UTtoLT(ksal.getDate().addSecs((ksal.getDuskAstronomicalTwilight() * 24.0 + Options::duskOffset()) *
471 3600.0));
472#else
473 // Creating these almanac instances seems expensive.
474 static QMap<QString, KSAlmanac const * > almanacMap;
475 const QString key = QString("%1 %2 %3").arg(midnight.toString()).arg(getGeo()->lat()->Degrees()).arg(
476 getGeo()->lng()->Degrees());
477 KSAlmanac const * ksal = almanacMap.value(key, nullptr);
478 if (ksal == nullptr)
479 {
480 if (almanacMap.size() > 5)
481 {
482 // don't allow this to grow too large.
483 qDeleteAll(almanacMap);
484 almanacMap.clear();
485 }
486 ksal = new KSAlmanac(midnight, getGeo());
487 almanacMap[key] = ksal;
488 }
489
490 // If dawn is in the past compared to this observation, fetch the next dawn
491 if (dawn <= startup)
492 dawn = getGeo()->UTtoLT(ksal->getDate().addSecs((ksal->getDawnAstronomicalTwilight() * 24.0 + Options::dawnOffset()) *
493 3600.0));
494
495 // If dusk is in the past compared to this observation, fetch the next dusk
496 if (dusk <= startup)
497 dusk = getGeo()->UTtoLT(ksal->getDate().addSecs((ksal->getDuskAstronomicalTwilight() * 24.0 + Options::duskOffset()) *
498 3600.0));
499#endif
500 }
501
502 // Now we have the next events:
503 // - if dawn comes first, observation runs during the night
504 // - if dusk comes first, observation runs during the day
505 nDawn = dawn;
506 nDusk = dusk;
507}
508
509void SchedulerModuleState::calculateDawnDusk()
510{
511 calculateDawnDusk(QDateTime(), m_Dawn, m_Dusk);
512
513 m_PreDawnDateTime = m_Dawn.addSecs(-60.0 * abs(Options::preDawnTime()));
514 emit updateNightTime();
515}
516
517const GeoLocation *SchedulerModuleState::getGeo()
518{
519 if (hasGeo())
520 return storedGeo;
521 return KStarsData::Instance()->geo();
522}
523
524bool SchedulerModuleState::hasGeo()
525{
526 return storedGeo != nullptr;
527}
528
529void SchedulerModuleState::setupNextIteration(SchedulerTimerState nextState)
530{
531 setupNextIteration(nextState, m_UpdatePeriodMs);
532}
533
534void SchedulerModuleState::setupNextIteration(SchedulerTimerState nextState, int milliseconds)
535{
536 if (iterationSetup())
537 {
538 qCDebug(KSTARS_EKOS_SCHEDULER)
539 << QString("Multiple setupNextIteration calls: current %1 %2, previous %3 %4")
540 .arg(nextState).arg(milliseconds).arg(timerState()).arg(timerInterval());
541 }
542 setTimerState(nextState);
543 // check if setup is called from a thread outside of the iteration timer thread
544 if (iterationTimer().isActive())
545 {
546 // restart the timer to ensure the correct startup delay
547 int remaining = iterationTimer().remainingTime();
548 iterationTimer().stop();
549 setTimerInterval(std::max(0, milliseconds - remaining));
550 iterationTimer().start(timerInterval());
551 }
552 else
553 {
554 // setup called from inside the iteration timer thread
555 setTimerInterval(milliseconds);
556 }
557 tickleTimer().stop();
558 setIterationSetup(true);
559}
560
561uint SchedulerModuleState::maxFailureAttempts()
562{
563 return MAX_FAILURE_ATTEMPTS;
564}
565
566void SchedulerModuleState::clearLog()
567{
568 logText().clear();
569 emit newLog(QString());
570}
571
572bool SchedulerModuleState::checkRepeatSequence()
573{
574 return (!Options::rememberJobProgress() && Options::schedulerRepeatEverything() &&
575 (Options::schedulerExecutionSequencesLimit() == 0
576 || sequenceExecutionCounter()) < Options::schedulerExecutionSequencesLimit());
577}
578} // Ekos namespace
const KStarsDateTime & ut() const
Definition kstarsdata.h:159
GeoLocation * geo()
Definition kstarsdata.h:238
KStarsDateTime addSecs(double s) const
QString i18n(const char *text, const TYPE &arg...)
Ekos is an advanced Astrophotography tool for Linux.
Definition align.cpp:83
SchedulerJobStage
Running stages of a SchedulerJob.
SchedulerTimerState
IterationTypes, the different types of scheduler iterations that are run.
QDateTime addDays(qint64 ndays) const const
QDate date() const const
bool isValid() const const
void append(const QJsonValue &value)
void append(QList< T > &&value)
void clear()
size_type size() const const
T value(const Key &key, const T &defaultValue) const const
bool isEmpty() const const
LocalTime
This file is part of the KDE documentation.
Documentation copyright © 1996-2025 The KDE developers.
Generated on Fri Apr 25 2025 11:58:36 by doxygen 1.13.2 written by Dimitri van Heesch, © 1997-2006

KDE's Doxygen guidelines are available online.