Kstars

schedulerprocess.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 "schedulerprocess.h"
7#include "schedulermodulestate.h"
8#include "scheduleradaptor.h"
9#include "greedyscheduler.h"
10#include "schedulerutils.h"
11#include "schedulerjob.h"
12#include "ekos/capture/sequencejob.h"
13#include "Options.h"
14#include "ksmessagebox.h"
15#include "ksnotification.h"
16#include "kstars.h"
17#include "kstarsdata.h"
18#include "indi/indistd.h"
19#include "skymapcomposite.h"
20#include "mosaiccomponent.h"
21#include "mosaictiles.h"
22#include "ekos/auxiliary/opticaltrainmanager.h"
23#include "ekos/auxiliary/stellarsolverprofile.h"
24#include <ekos_scheduler_debug.h>
25
26#include <QtDBus/QDBusReply>
27#include <QtDBus/QDBusInterface>
28
29#define RESTART_GUIDING_DELAY_MS 5000
30
31// This is a temporary debugging printout introduced while gaining experience developing
32// the unit tests in test_ekos_scheduler_ops.cpp.
33// All these printouts should be eventually removed.
34
35namespace Ekos
36{
37
38SchedulerProcess::SchedulerProcess(QSharedPointer<SchedulerModuleState> state, const QString &ekosPathStr,
39 const QString &ekosInterfaceStr) : QObject(KStars::Instance())
40{
41 setObjectName("SchedulerProcess");
42 m_moduleState = state;
43 m_GreedyScheduler = new GreedyScheduler();
44 connect(KConfigDialog::exists("settings"), &KConfigDialog::settingsChanged, this, &SchedulerProcess::applyConfig);
45
46 // Connect simulation clock scale
47 connect(KStarsData::Instance()->clock(), &SimClock::scaleChanged, this, &SchedulerProcess::simClockScaleChanged);
48 connect(KStarsData::Instance()->clock(), &SimClock::timeChanged, this, &SchedulerProcess::simClockTimeChanged);
49
50 // connection to state machine events
51 connect(moduleState().data(), &SchedulerModuleState::schedulerStateChanged, this, &SchedulerProcess::newStatus);
52 connect(moduleState().data(), &SchedulerModuleState::newLog, this, &SchedulerProcess::appendLogText);
53
54 // Set up DBus interfaces
55 new SchedulerAdaptor(this);
56 QDBusConnection::sessionBus().unregisterObject(schedulerProcessPathString);
57 if (!QDBusConnection::sessionBus().registerObject(schedulerProcessPathString, this))
58 qCDebug(KSTARS_EKOS_SCHEDULER) << QString("SchedulerProcess failed to register with dbus");
59
60 setEkosInterface(new QDBusInterface(kstarsInterfaceString, ekosPathStr, ekosInterfaceStr,
62 setIndiInterface(new QDBusInterface(kstarsInterfaceString, INDIPathString, INDIInterfaceString,
64 QDBusConnection::sessionBus().connect(kstarsInterfaceString, ekosPathStr, ekosInterfaceStr, "indiStatusChanged",
65 this, SLOT(setINDICommunicationStatus(Ekos::CommunicationStatus)));
66 QDBusConnection::sessionBus().connect(kstarsInterfaceString, ekosPathStr, ekosInterfaceStr, "ekosStatusChanged",
67 this, SLOT(setEkosCommunicationStatus(Ekos::CommunicationStatus)));
68 QDBusConnection::sessionBus().connect(kstarsInterfaceString, ekosPathStr, ekosInterfaceStr, "newModule", this,
69 SLOT(registerNewModule(QString)));
70 QDBusConnection::sessionBus().connect(kstarsInterfaceString, ekosPathStr, ekosInterfaceStr, "newDevice", this,
71 SLOT(registerNewDevice(QString, int)));
72}
73
74SchedulerState SchedulerProcess::status()
75{
76 return moduleState()->schedulerState();
77}
78
80{
81 switch (moduleState()->schedulerState())
82 {
83 case SCHEDULER_IDLE:
84 /* FIXME: Manage the non-validity of the startup script earlier, and make it a warning only when the scheduler starts */
85 if (!moduleState()->startupScriptURL().isEmpty() && ! moduleState()->startupScriptURL().isValid())
86 {
87 appendLogText(i18n("Warning: startup script URL %1 is not valid.",
88 moduleState()->startupScriptURL().toString(QUrl::PreferLocalFile)));
89 return;
90 }
91
92 /* FIXME: Manage the non-validity of the shutdown script earlier, and make it a warning only when the scheduler starts */
93 if (!moduleState()->shutdownScriptURL().isEmpty() && !moduleState()->shutdownScriptURL().isValid())
94 {
95 appendLogText(i18n("Warning: shutdown script URL %1 is not valid.",
96 moduleState()->shutdownScriptURL().toString(QUrl::PreferLocalFile)));
97 return;
98 }
99
100
101 qCInfo(KSTARS_EKOS_SCHEDULER) << "Scheduler is starting...";
102
103 moduleState()->setSchedulerState(SCHEDULER_RUNNING);
104 moduleState()->setupNextIteration(RUN_SCHEDULER);
105
106 appendLogText(i18n("Scheduler started."));
107 qCDebug(KSTARS_EKOS_SCHEDULER) << "Scheduler started.";
108 break;
109
110 case SCHEDULER_PAUSED:
111 moduleState()->setSchedulerState(SCHEDULER_RUNNING);
112 moduleState()->setupNextIteration(RUN_SCHEDULER);
113
114 appendLogText(i18n("Scheduler resuming."));
115 qCDebug(KSTARS_EKOS_SCHEDULER) << "Scheduler resuming.";
116 break;
117
118 default:
119 break;
120 }
121
122}
123
124// FindNextJob (probably misnamed) deals with what to do when jobs end.
125// For instance, if they complete their capture sequence, they may
126// (a) be done, (b) be part of a repeat N times, or (c) be part of a loop forever.
127// Similarly, if jobs are aborted they may (a) restart right away, (b) restart after a delay, (c) be ended.
129{
130 if (moduleState()->schedulerState() == SCHEDULER_PAUSED)
131 {
132 // everything finished, we can pause
133 setPaused();
134 return;
135 }
136
137 Q_ASSERT_X(activeJob()->getState() == SCHEDJOB_ERROR ||
138 activeJob()->getState() == SCHEDJOB_ABORTED ||
139 activeJob()->getState() == SCHEDJOB_COMPLETE ||
140 activeJob()->getState() == SCHEDJOB_IDLE,
141 __FUNCTION__, "Finding next job requires current to be in error, aborted, idle or complete");
142
143 // Reset failed count
144 moduleState()->resetAlignFailureCount();
145 moduleState()->resetGuideFailureCount();
146 moduleState()->resetFocusFailureCount();
147 moduleState()->resetCaptureFailureCount();
148
149 if (activeJob()->getState() == SCHEDJOB_ERROR || activeJob()->getState() == SCHEDJOB_ABORTED)
150 {
151 emit jobEnded(activeJob()->getName(), activeJob()->getStopReason());
152 moduleState()->resetCaptureBatch();
153 // Stop Guiding if it was used
154 stopGuiding();
155
156 if (activeJob()->getState() == SCHEDJOB_ERROR)
157 appendLogText(i18n("Job '%1' is terminated due to errors.", activeJob()->getName()));
158 else
159 appendLogText(i18n("Job '%1' is aborted.", activeJob()->getName()));
160
161 // Always reset job stage
162 moduleState()->updateJobStage(SCHEDSTAGE_IDLE);
163
164 // restart aborted jobs immediately, if error handling strategy is set to "restart immediately"
165 if (Options::errorHandlingStrategy() == ERROR_RESTART_IMMEDIATELY &&
166 (activeJob()->getState() == SCHEDJOB_ABORTED ||
167 (activeJob()->getState() == SCHEDJOB_ERROR && Options::rescheduleErrors())))
168 {
169 // reset the state so that it will be restarted
170 activeJob()->setState(SCHEDJOB_SCHEDULED);
171
172 appendLogText(i18n("Waiting %1 seconds to restart job '%2'.", Options::errorHandlingStrategyDelay(),
173 activeJob()->getName()));
174
175 // wait the given delay until the jobs will be evaluated again
176 moduleState()->setupNextIteration(RUN_WAKEUP, std::lround((Options::errorHandlingStrategyDelay() * 1000) /
177 KStarsData::Instance()->clock()->scale()));
178 emit changeSleepLabel(i18n("Scheduler waits for a retry."));
179 return;
180 }
181
182 // otherwise start re-evaluation
183 moduleState()->setActiveJob(nullptr);
184 moduleState()->setupNextIteration(RUN_SCHEDULER);
185 }
186 else if (activeJob()->getState() == SCHEDJOB_IDLE)
187 {
188 emit jobEnded(activeJob()->getName(), activeJob()->getStopReason());
189
190 // job constraints no longer valid, start re-evaluation
191 moduleState()->setActiveJob(nullptr);
192 moduleState()->setupNextIteration(RUN_SCHEDULER);
193 }
194 // Job is complete, so check completion criteria to optimize processing
195 // In any case, we're done whether the job completed successfully or not.
196 else if (activeJob()->getCompletionCondition() == FINISH_SEQUENCE)
197 {
198 emit jobEnded(activeJob()->getName(), activeJob()->getStopReason());
199
200 /* If we remember job progress, mark the job idle as well as all its duplicates for re-evaluation */
201 if (Options::rememberJobProgress())
202 {
203 foreach(SchedulerJob *a_job, moduleState()->jobs())
204 if (a_job == activeJob() || a_job->isDuplicateOf(activeJob()))
205 a_job->setState(SCHEDJOB_IDLE);
206 }
207
208 moduleState()->resetCaptureBatch();
209 // Stop Guiding if it was used
210 stopGuiding();
211
212 appendLogText(i18n("Job '%1' is complete.", activeJob()->getName()));
213
214 // Always reset job stage
215 moduleState()->updateJobStage(SCHEDSTAGE_IDLE);
216
217 // If saving remotely, then can't tell later that the job has been completed.
218 // Set it complete now.
219 if (!canCountCaptures(*activeJob()))
220 activeJob()->setState(SCHEDJOB_COMPLETE);
221
222 moduleState()->setActiveJob(nullptr);
223 moduleState()->setupNextIteration(RUN_SCHEDULER);
224 }
225 else if (activeJob()->getCompletionCondition() == FINISH_REPEAT &&
226 (activeJob()->getRepeatsRemaining() <= 1))
227 {
228 /* If the job is about to repeat, decrease its repeat count and reset its start time */
229 if (activeJob()->getRepeatsRemaining() > 0)
230 {
231 // If we can remember job progress, this is done in estimateJobTime()
232 if (!Options::rememberJobProgress())
233 {
234 activeJob()->setRepeatsRemaining(activeJob()->getRepeatsRemaining() - 1);
235 activeJob()->setCompletedIterations(activeJob()->getCompletedIterations() + 1);
236 }
237 activeJob()->setStartupTime(QDateTime());
238 }
239
240 /* Mark the job idle as well as all its duplicates for re-evaluation */
241 foreach(SchedulerJob *a_job, moduleState()->jobs())
242 if (a_job == activeJob() || a_job->isDuplicateOf(activeJob()))
243 a_job->setState(SCHEDJOB_IDLE);
244
245 /* Re-evaluate all jobs, without selecting a new job */
246 evaluateJobs(true);
247
248 /* If current job is actually complete because of previous duplicates, prepare for next job */
249 if (activeJob() == nullptr || activeJob()->getRepeatsRemaining() == 0)
250 {
252
253 if (activeJob() != nullptr)
254 {
255 emit jobEnded(activeJob()->getName(), activeJob()->getStopReason());
256 appendLogText(i18np("Job '%1' is complete after #%2 batch.",
257 "Job '%1' is complete after #%2 batches.",
258 activeJob()->getName(), activeJob()->getRepeatsRequired()));
259 if (!canCountCaptures(*activeJob()))
260 activeJob()->setState(SCHEDJOB_COMPLETE);
261 moduleState()->setActiveJob(nullptr);
262 }
263 moduleState()->setupNextIteration(RUN_SCHEDULER);
264 }
265 /* If job requires more work, continue current observation */
266 else
267 {
268 /* FIXME: raise priority to allow other jobs to schedule in-between */
269 if (executeJob(activeJob()) == false)
270 return;
271
272 /* JM 2020-08-23: If user opts to force realign instead of for each job then we force this FIRST */
273 if (activeJob()->getStepPipeline() & SchedulerJob::USE_ALIGN && Options::forceAlignmentBeforeJob())
274 {
275 stopGuiding();
276 moduleState()->updateJobStage(SCHEDSTAGE_ALIGNING);
278 }
279 /* If we are guiding, continue capturing */
280 else if ( (activeJob()->getStepPipeline() & SchedulerJob::USE_GUIDE) )
281 {
282 moduleState()->updateJobStage(SCHEDSTAGE_CAPTURING);
283 startCapture();
284 }
285 /* If we are not guiding, but using alignment, realign */
286 else if (activeJob()->getStepPipeline() & SchedulerJob::USE_ALIGN)
287 {
288 moduleState()->updateJobStage(SCHEDSTAGE_ALIGNING);
290 }
291 /* Else if we are neither guiding nor using alignment, slew back to target */
292 else if (activeJob()->getStepPipeline() & SchedulerJob::USE_TRACK)
293 {
294 moduleState()->updateJobStage(SCHEDSTAGE_SLEWING);
295 startSlew();
296 }
297 /* Else just start capturing */
298 else
299 {
300 moduleState()->updateJobStage(SCHEDSTAGE_CAPTURING);
301 startCapture();
302 }
303
304 appendLogText(i18np("Job '%1' is repeating, #%2 batch remaining.",
305 "Job '%1' is repeating, #%2 batches remaining.",
306 activeJob()->getName(), activeJob()->getRepeatsRemaining()));
307 /* getActiveJob() remains the same */
308 moduleState()->setupNextIteration(RUN_JOBCHECK);
309 }
310 }
311 else if ((activeJob()->getCompletionCondition() == FINISH_LOOP) ||
312 (activeJob()->getCompletionCondition() == FINISH_REPEAT &&
313 activeJob()->getRepeatsRemaining() > 0))
314 {
315 /* If the job is about to repeat, decrease its repeat count and reset its start time */
316 if ((activeJob()->getCompletionCondition() == FINISH_REPEAT) &&
317 (activeJob()->getRepeatsRemaining() > 1))
318 {
319 // If we can remember job progress, this is done in estimateJobTime()
320 if (!Options::rememberJobProgress())
321 {
322 activeJob()->setRepeatsRemaining(activeJob()->getRepeatsRemaining() - 1);
323 activeJob()->setCompletedIterations(activeJob()->getCompletedIterations() + 1);
324 }
325 activeJob()->setStartupTime(QDateTime());
326 }
327
328 if (executeJob(activeJob()) == false)
329 return;
330
331 if (activeJob()->getStepPipeline() & SchedulerJob::USE_ALIGN && Options::forceAlignmentBeforeJob())
332 {
333 stopGuiding();
334 moduleState()->updateJobStage(SCHEDSTAGE_ALIGNING);
336 }
337 else
338 {
339 moduleState()->updateJobStage(SCHEDSTAGE_CAPTURING);
340 startCapture();
341 }
342
343 moduleState()->increaseCaptureBatch();
344
345 if (activeJob()->getCompletionCondition() == FINISH_REPEAT )
346 appendLogText(i18np("Job '%1' is repeating, #%2 batch remaining.",
347 "Job '%1' is repeating, #%2 batches remaining.",
348 activeJob()->getName(), activeJob()->getRepeatsRemaining()));
349 else
350 appendLogText(i18n("Job '%1' is repeating, looping indefinitely.", activeJob()->getName()));
351
352 /* getActiveJob() remains the same */
353 moduleState()->setupNextIteration(RUN_JOBCHECK);
354 }
355 else if (activeJob()->getCompletionCondition() == FINISH_AT)
356 {
357 if (SchedulerModuleState::getLocalTime().secsTo(activeJob()->getFinishAtTime()) <= 0)
358 {
359 emit jobEnded(activeJob()->getName(), activeJob()->getStopReason());
360
361 /* Mark the job idle as well as all its duplicates for re-evaluation */
362 foreach(SchedulerJob *a_job, moduleState()->jobs())
363 if (a_job == activeJob() || a_job->isDuplicateOf(activeJob()))
364 a_job->setState(SCHEDJOB_IDLE);
366
367 moduleState()->resetCaptureBatch();
368
369 appendLogText(i18np("Job '%1' stopping, reached completion time with #%2 batch done.",
370 "Job '%1' stopping, reached completion time with #%2 batches done.",
371 activeJob()->getName(), moduleState()->captureBatch() + 1));
372
373 // Always reset job stage
374 moduleState()->updateJobStage(SCHEDSTAGE_IDLE);
375
376 moduleState()->setActiveJob(nullptr);
377 moduleState()->setupNextIteration(RUN_SCHEDULER);
378 }
379 else
380 {
381 if (executeJob(activeJob()) == false)
382 return;
383
384 if (activeJob()->getStepPipeline() & SchedulerJob::USE_ALIGN && Options::forceAlignmentBeforeJob())
385 {
386 stopGuiding();
387 moduleState()->updateJobStage(SCHEDSTAGE_ALIGNING);
389 }
390 else
391 {
392 moduleState()->updateJobStage(SCHEDSTAGE_CAPTURING);
393 startCapture();
394 }
395
396 moduleState()->increaseCaptureBatch();
397
398 appendLogText(i18np("Job '%1' completed #%2 batch before completion time, restarted.",
399 "Job '%1' completed #%2 batches before completion time, restarted.",
400 activeJob()->getName(), moduleState()->captureBatch()));
401 /* getActiveJob() remains the same */
402 moduleState()->setupNextIteration(RUN_JOBCHECK);
403 }
404 }
405 else
406 {
407 /* Unexpected situation, mitigate by resetting the job and restarting the scheduler timer */
408 qCDebug(KSTARS_EKOS_SCHEDULER) << "BUGBUG! Job '" << activeJob()->getName() <<
409 "' timer elapsed, but no action to be taken.";
410
411 // Always reset job stage
412 moduleState()->updateJobStage(SCHEDSTAGE_IDLE);
413
414 moduleState()->setActiveJob(nullptr);
415 moduleState()->setupNextIteration(RUN_SCHEDULER);
416 }
417}
418
419void Ekos::SchedulerProcess::stopCapturing(QString train, bool followersOnly)
420{
421 if (train == "" && followersOnly)
422 {
423 for (auto key : m_activeJobs.keys())
424 {
425 // abort capturing of all jobs except for the lead job
426 SchedulerJob *job = m_activeJobs[key];
427 if (! job->isLead())
428 {
429 QList<QVariant> dbusargs;
430 dbusargs.append(job->getOpticalTrain());
431 captureInterface()->callWithArgumentList(QDBus::BlockWithGui, "abort", dbusargs);
432 job->setState(SCHEDJOB_ABORTED);
433 }
434 }
435 }
436 else
437 {
438 QList<QVariant> dbusargs;
439 dbusargs.append(train);
440 captureInterface()->callWithArgumentList(QDBus::BlockWithGui, "abort", dbusargs);
441
442 // set all relevant jobs to aborted
443 for (auto job : m_activeJobs.values())
444 if (train == "" || job->getOpticalTrain() == train)
445 job->setState(SCHEDJOB_ABORTED);
446 }
447}
448
450{
451 if (nullptr != activeJob())
452 {
453 qCDebug(KSTARS_EKOS_SCHEDULER) << "Job '" << activeJob()->getName() << "' is stopping current action..." <<
454 activeJob()->getStage();
455
456 switch (activeJob()->getStage())
457 {
458 case SCHEDSTAGE_IDLE:
459 break;
460
461 case SCHEDSTAGE_SLEWING:
462 mountInterface()->call(QDBus::AutoDetect, "abort");
463 break;
464
465 case SCHEDSTAGE_FOCUSING:
466 focusInterface()->call(QDBus::AutoDetect, "abort");
467 break;
468
469 case SCHEDSTAGE_ALIGNING:
470 alignInterface()->call(QDBus::AutoDetect, "abort");
471 break;
472
473 // N.B. Need to use BlockWithGui as proposed by Wolfgang
474 // to ensure capture is properly aborted before taking any further actions.
475 case SCHEDSTAGE_CAPTURING:
476 stopCapturing();
477 break;
478
479 default:
480 break;
481 }
482
483 /* Reset interrupted job stage */
484 moduleState()->updateJobStage(SCHEDSTAGE_IDLE);
485 }
486
487 /* Guiding being a parallel process, check to stop it */
488 stopGuiding();
489}
490
492{
493 if (moduleState()->preemptiveShutdown())
494 {
495 moduleState()->disablePreemptiveShutdown();
496 appendLogText(i18n("Scheduler is awake."));
497 execute();
498 }
499 else
500 {
501 if (moduleState()->schedulerState() == SCHEDULER_RUNNING)
502 appendLogText(i18n("Scheduler is awake. Jobs shall be started when ready..."));
503 else
504 appendLogText(i18n("Scheduler is awake. Jobs shall be started when scheduler is resumed."));
505
506 moduleState()->setupNextIteration(RUN_SCHEDULER);
507 }
508}
509
511{
512 // New scheduler session shouldn't inherit ABORT or ERROR states from the last one.
513 foreach (auto j, moduleState()->jobs())
514 {
515 j->setState(SCHEDJOB_IDLE);
516 emit updateJobTable(j);
517 }
518 moduleState()->init();
519 iterate();
520}
521
523{
524 // do nothing if the scheduler is not running
525 if (moduleState()->schedulerState() != SCHEDULER_RUNNING)
526 return;
527
528 qCInfo(KSTARS_EKOS_SCHEDULER) << "Scheduler is stopping...";
529
530 // Stop running job and abort all others
531 // in case of soft shutdown we skip this
532 if (!moduleState()->preemptiveShutdown())
533 {
534 for (auto &oneJob : moduleState()->jobs())
535 {
536 if (oneJob == activeJob())
538
539 if (oneJob->getState() <= SCHEDJOB_BUSY)
540 {
541 appendLogText(i18n("Job '%1' has not been processed upon scheduler stop, marking aborted.", oneJob->getName()));
542 oneJob->setState(SCHEDJOB_ABORTED);
543 }
544 }
545 }
546
547 moduleState()->setupNextIteration(RUN_NOTHING);
548 moduleState()->cancelGuidingTimer();
549
550 moduleState()->setSchedulerState(SCHEDULER_IDLE);
551 moduleState()->setParkWaitState(PARKWAIT_IDLE);
552 moduleState()->setEkosState(EKOS_IDLE);
553 moduleState()->setIndiState(INDI_IDLE);
554
555 // Only reset startup state to idle if the startup procedure was interrupted before it had the chance to complete.
556 // Or if we're doing a soft shutdown
557 if (moduleState()->startupState() != STARTUP_COMPLETE || moduleState()->preemptiveShutdown())
558 {
559 if (moduleState()->startupState() == STARTUP_SCRIPT)
560 {
561 scriptProcess().disconnect();
562 scriptProcess().terminate();
563 }
564
565 moduleState()->setStartupState(STARTUP_IDLE);
566 }
567 // Reset startup state to unparking phase (dome -> mount -> cap)
568 // We do not want to run the startup script again but unparking should be checked
569 // whenever the scheduler is running again.
570 else if (moduleState()->startupState() == STARTUP_COMPLETE)
571 {
572 if (Options::schedulerUnparkDome())
573 moduleState()->setStartupState(STARTUP_UNPARK_DOME);
574 else if (Options::schedulerUnparkMount())
575 moduleState()->setStartupState(STARTUP_UNPARK_MOUNT);
576 else if (Options::schedulerOpenDustCover())
577 moduleState()->setStartupState(STARTUP_UNPARK_CAP);
578 }
579
580 moduleState()->setShutdownState(SHUTDOWN_IDLE);
581
582 moduleState()->setActiveJob(nullptr);
583 moduleState()->resetFailureCounters();
584 moduleState()->setAutofocusCompleted(false);
585
586 // If soft shutdown, we return for now
587 if (moduleState()->preemptiveShutdown())
588 {
589 QDateTime const now = SchedulerModuleState::getLocalTime();
590 int const nextObservationTime = now.secsTo(moduleState()->preemptiveShutdownWakeupTime());
591 moduleState()->setupNextIteration(RUN_WAKEUP,
592 std::lround(((nextObservationTime + 1) * 1000)
593 / KStarsData::Instance()->clock()->scale()));
594 // report success
595 emit schedulerStopped();
596 return;
597 }
598
599 // Clear target name in capture interface upon stopping
600 if (captureInterface().isNull() == false)
601 captureInterface()->setProperty("targetName", QString());
602
603 if (scriptProcess().state() == QProcess::Running)
604 scriptProcess().terminate();
605
606 // report success
607 emit schedulerStopped();
608}
609
611{
612 emit clearJobTable();
613
614 qDeleteAll(moduleState()->jobs());
615 moduleState()->mutlableJobs().clear();
616 moduleState()->setCurrentPosition(-1);
617
618}
619
621{
623 return appendEkosScheduleList(fileURL);
624}
625
626void SchedulerProcess::setSequence(const QString &sequenceFileURL)
627{
628 emit changeCurrentSequence(sequenceFileURL);
629}
630
632{
633 if (moduleState()->schedulerState() == SCHEDULER_RUNNING)
634 return;
635
636 // Reset capture count of all jobs before re-evaluating
637 foreach (SchedulerJob *job, moduleState()->jobs())
638 job->setCompletedCount(0);
639
640 // Evaluate all jobs, this refreshes storage and resets job states
642}
643
645{
646 Q_ASSERT_X(nullptr != job, __FUNCTION__,
647 "There must be a valid current job for Scheduler to test sleep requirement");
648
649 if (job->getLightFramesRequired() == false)
650 return false;
651
652 QDateTime const now = SchedulerModuleState::getLocalTime();
653 int const nextObservationTime = now.secsTo(job->getStartupTime());
654
655 // It is possible that the nextObservationTime is far away, but the reason is that
656 // the user has edited the jobs, and now the active job is not the next thing scheduled.
657 if (getGreedyScheduler()->getScheduledJob() != job)
658 return false;
659
660 // If start up procedure is complete and the user selected pre-emptive shutdown, let us check if the next observation time exceed
661 // the pre-emptive shutdown time in hours (default 2). If it exceeds that, we perform complete shutdown until next job is ready
662 if (moduleState()->startupState() == STARTUP_COMPLETE &&
663 Options::preemptiveShutdown() &&
664 nextObservationTime > (Options::preemptiveShutdownTime() * 3600))
665 {
667 "Job '%1' scheduled for execution at %2. "
668 "Observatory scheduled for shutdown until next job is ready.",
669 job->getName(), job->getStartupTime().toString()));
670 moduleState()->enablePreemptiveShutdown(job->getStartupTime());
672 emit schedulerSleeping(true, false);
673 return true;
674 }
675 // Otherwise, sleep until job is ready
676 /* FIXME: if not parking, stop tracking maybe? this would prevent crashes or scheduler stops from leaving the mount to track and bump the pier */
677 // If start up procedure is already complete, and we didn't issue any parking commands before and parking is checked and enabled
678 // Then we park the mount until next job is ready. But only if the job uses TRACK as its first step, otherwise we cannot get into position again.
679 // This is also only performed if next job is due more than the default lead time (5 minutes).
680 // If job is due sooner than that is not worth parking and we simply go into sleep or wait modes.
681 else if (nextObservationTime > Options::leadTime() * 60 &&
682 moduleState()->startupState() == STARTUP_COMPLETE &&
683 moduleState()->parkWaitState() == PARKWAIT_IDLE &&
684 (job->getStepPipeline() & SchedulerJob::USE_TRACK) &&
685 // schedulerParkMount->isEnabled() &&
686 Options::schedulerParkMount())
687 {
689 "Job '%1' scheduled for execution at %2. "
690 "Parking the mount until the job is ready.",
691 job->getName(), job->getStartupTime().toString()));
692
693 moduleState()->setParkWaitState(PARKWAIT_PARK);
694
695 return false;
696 }
697 else if (nextObservationTime > Options::leadTime() * 60)
698 {
699 auto log = i18n("Sleeping until observation job %1 is ready at %2", job->getName(),
700 now.addSecs(nextObservationTime + 1).toString());
701 appendLogText(log);
702 KSNotification::event(QLatin1String("SchedulerSleeping"), log, KSNotification::Scheduler,
703 KSNotification::Info);
704
705 // Warn the user if the next job is really far away - 60/5 = 12 times the lead time
706 if (nextObservationTime > Options::leadTime() * 60 * 12 && !Options::preemptiveShutdown())
707 {
708 dms delay(static_cast<double>(nextObservationTime * 15.0 / 3600.0));
710 "Warning: Job '%1' is %2 away from now, you may want to enable Preemptive Shutdown.",
711 job->getName(), delay.toHMSString()));
712 }
713
714 /* FIXME: stop tracking now */
715
716 // Wake up when job is due.
717 // FIXME: Implement waking up periodically before job is due for weather check.
718 // int const nextWakeup = nextObservationTime < 60 ? nextObservationTime : 60;
719 moduleState()->setupNextIteration(RUN_WAKEUP,
720 std::lround(((nextObservationTime + 1) * 1000) / KStarsData::Instance()->clock()->scale()));
721
722 emit schedulerSleeping(false, true);
723 return true;
724 }
725
726 return false;
727}
728
730{
731 Q_ASSERT_X(nullptr != activeJob(), __FUNCTION__, "Job starting slewing must be valid");
732
733 // If the mount was parked by a pause or the end-user, unpark
734 if (isMountParked())
735 {
736 moduleState()->setParkWaitState(PARKWAIT_UNPARK);
737 return;
738 }
739
740 if (Options::resetMountModelBeforeJob())
741 {
742 mountInterface()->call(QDBus::AutoDetect, "resetModel");
743 }
744
745 SkyPoint target = activeJob()->getTargetCoords();
746 QList<QVariant> telescopeSlew;
747 telescopeSlew.append(target.ra().Hours());
748 telescopeSlew.append(target.dec().Degrees());
749
750 QDBusReply<bool> const slewModeReply = mountInterface()->callWithArgumentList(QDBus::AutoDetect, "slew",
751 telescopeSlew);
752
753 if (slewModeReply.error().type() != QDBusError::NoError)
754 {
755 qCCritical(KSTARS_EKOS_SCHEDULER) << QString("Warning: job '%1' slew request received DBUS error: %2").arg(
756 activeJob()->getName(), QDBusError::errorString(slewModeReply.error().type()));
758 activeJob()->setState(SCHEDJOB_ERROR);
759 }
760 else
761 {
762 moduleState()->updateJobStage(SCHEDSTAGE_SLEWING);
763 appendLogText(i18n("Job '%1' is slewing to target.", activeJob()->getName()));
764 }
765}
766
768{
769 Q_ASSERT_X(nullptr != activeJob(), __FUNCTION__, "Job starting focusing must be valid");
770
771 // 2017-09-30 Jasem: We're skipping post align focusing now as it can be performed
772 // when first focus request is made in capture module
773 if (activeJob()->getStage() == SCHEDSTAGE_RESLEWING_COMPLETE ||
774 activeJob()->getStage() == SCHEDSTAGE_POSTALIGN_FOCUSING)
775 {
776 // Clear the HFR limit value set in the capture module
777 captureInterface()->call(QDBus::AutoDetect, "clearAutoFocusHFR");
778 // Reset Focus frame so that next frame take a full-resolution capture first.
779 focusInterface()->call(QDBus::AutoDetect, "resetFrame");
780 moduleState()->updateJobStage(SCHEDSTAGE_POSTALIGN_FOCUSING_COMPLETE);
782 return;
783 }
784
785 // Check if autofocus is supported
786 QDBusReply<bool> focusModeReply;
787 focusModeReply = focusInterface()->call(QDBus::AutoDetect, "canAutoFocus");
788
789 if (focusModeReply.error().type() != QDBusError::NoError)
790 {
791 qCCritical(KSTARS_EKOS_SCHEDULER) << QString("Warning: job '%1' canAutoFocus request received DBUS error: %2").arg(
792 activeJob()->getName(), QDBusError::errorString(focusModeReply.error().type()));
794 {
795 activeJob()->setState(SCHEDJOB_ERROR);
796 findNextJob();
797 }
798 return;
799 }
800
801 if (focusModeReply.value() == false)
802 {
803 appendLogText(i18n("Warning: job '%1' is unable to proceed with autofocus, not supported.", activeJob()->getName()));
804 activeJob()->setStepPipeline(
805 static_cast<SchedulerJob::StepPipeline>(activeJob()->getStepPipeline() & ~SchedulerJob::USE_FOCUS));
806 moduleState()->updateJobStage(SCHEDSTAGE_FOCUS_COMPLETE);
808 return;
809 }
810
811 // Clear the HFR limit value set in the capture module
812 captureInterface()->call(QDBus::AutoDetect, "clearAutoFocusHFR");
813
814 QDBusMessage reply;
815
816 // We always need to reset frame first
817 if ((reply = focusInterface()->call(QDBus::AutoDetect, "resetFrame")).type() == QDBusMessage::ErrorMessage)
818 {
819 qCCritical(KSTARS_EKOS_SCHEDULER) << QString("Warning: job '%1' resetFrame request received DBUS error: %2").arg(
820 activeJob()->getName(), reply.errorMessage());
822 {
823 activeJob()->setState(SCHEDJOB_ERROR);
824 findNextJob();
825 }
826 return;
827 }
828
829
830 // If we have a LIGHT filter set, let's set it.
831 if (!activeJob()->getInitialFilter().isEmpty())
832 {
833 focusInterface()->setProperty("filter", activeJob()->getInitialFilter());
834 }
835
836 // Set autostar if full field option is false
837 if (Options::focusUseFullField() == false)
838 {
839 QList<QVariant> autoStar;
840 autoStar.append(true);
841 if ((reply = focusInterface()->callWithArgumentList(QDBus::AutoDetect, "setAutoStarEnabled", autoStar)).type() ==
843 {
844 qCCritical(KSTARS_EKOS_SCHEDULER) << QString("Warning: job '%1' setAutoFocusStar request received DBUS error: %1").arg(
845 activeJob()->getName(), reply.errorMessage());
847 {
848 activeJob()->setState(SCHEDJOB_ERROR);
849 findNextJob();
850 }
851 return;
852 }
853 }
854
855 // Start auto-focus
856 if ((reply = focusInterface()->call(QDBus::AutoDetect, "start")).type() == QDBusMessage::ErrorMessage)
857 {
858 qCCritical(KSTARS_EKOS_SCHEDULER) << QString("Warning: job '%1' startFocus request received DBUS error: %2").arg(
859 activeJob()->getName(), reply.errorMessage());
861 {
862 activeJob()->setState(SCHEDJOB_ERROR);
863 findNextJob();
864 }
865 return;
866 }
867
868 moduleState()->updateJobStage(SCHEDSTAGE_FOCUSING);
869 appendLogText(i18n("Job '%1' is focusing.", activeJob()->getName()));
870 moduleState()->startCurrentOperationTimer();
871}
872
874{
875 Q_ASSERT_X(nullptr != activeJob(), __FUNCTION__, "Job starting aligning must be valid");
876
877 QDBusMessage reply;
878 setSolverAction(Align::GOTO_SLEW);
879
880 // Always turn update coords on
881 //QVariant arg(true);
882 //alignInterface->call(QDBus::AutoDetect, "setUpdateCoords", arg);
883
884 // Reset the solver speedup (using the last successful index file and healpix for the
885 // pointing check) when re-aligning.
886 moduleState()->setIndexToUse(-1);
887 moduleState()->setHealpixToUse(-1);
888
889 // If FITS file is specified, then we use load and slew
890 if (activeJob()->getFITSFile().isEmpty() == false)
891 {
892 auto path = activeJob()->getFITSFile().toString(QUrl::PreferLocalFile);
893 // check if the file exists
894 if (QFile::exists(path) == false)
895 {
896 appendLogText(i18n("Warning: job '%1' target FITS file does not exist.", activeJob()->getName()));
897 activeJob()->setState(SCHEDJOB_ERROR);
898 findNextJob();
899 return;
900 }
901
902 QList<QVariant> solveArgs;
903 solveArgs.append(path);
904
905 if ((reply = alignInterface()->callWithArgumentList(QDBus::AutoDetect, "loadAndSlew", solveArgs)).type() ==
907 {
908 appendLogText(i18n("Warning: job '%1' loadAndSlew request received DBUS error: %2",
909 activeJob()->getName(), reply.errorMessage()));
911 {
912 activeJob()->setState(SCHEDJOB_ERROR);
913 findNextJob();
914 }
915 return;
916 }
917 else if (reply.arguments().first().toBool() == false)
918 {
919 appendLogText(i18n("Warning: job '%1' loadAndSlew request failed.", activeJob()->getName()));
920 activeJob()->setState(SCHEDJOB_ABORTED);
921 findNextJob();
922 return;
923 }
924
925 appendLogText(i18n("Job '%1' is plate solving %2.", activeJob()->getName(), activeJob()->getFITSFile().fileName()));
926 }
927 else
928 {
929 // JM 2020.08.20: Send J2000 TargetCoords to Align module so that we always resort back to the
930 // target original targets even if we drifted away due to any reason like guiding calibration failures.
931 const SkyPoint targetCoords = activeJob()->getTargetCoords();
932 QList<QVariant> targetArgs, rotationArgs;
933 targetArgs << targetCoords.ra0().Hours() << targetCoords.dec0().Degrees();
934 rotationArgs << activeJob()->getPositionAngle();
935
936 if ((reply = alignInterface()->callWithArgumentList(QDBus::AutoDetect, "setTargetCoords",
937 targetArgs)).type() == QDBusMessage::ErrorMessage)
938 {
939 appendLogText(i18n("Warning: job '%1' setTargetCoords request received DBUS error: %2",
940 activeJob()->getName(), reply.errorMessage()));
942 {
943 activeJob()->setState(SCHEDJOB_ERROR);
944 findNextJob();
945 }
946 return;
947 }
948
949 // Only send if it has valid value.
950 if (activeJob()->getPositionAngle() >= -180)
951 {
952 if ((reply = alignInterface()->callWithArgumentList(QDBus::AutoDetect, "setTargetPositionAngle",
953 rotationArgs)).type() == QDBusMessage::ErrorMessage)
954 {
955 appendLogText(i18n("Warning: job '%1' setTargetPositionAngle request received DBUS error: %2").arg(
956 activeJob()->getName(), reply.errorMessage()));
958 {
959 activeJob()->setState(SCHEDJOB_ERROR);
960 findNextJob();
961 }
962 return;
963 }
964 }
965
966 if ((reply = alignInterface()->call(QDBus::AutoDetect, "captureAndSolve")).type() == QDBusMessage::ErrorMessage)
967 {
968 appendLogText(i18n("Warning: job '%1' captureAndSolve request received DBUS error: %2").arg(
969 activeJob()->getName(), reply.errorMessage()));
971 {
972 activeJob()->setState(SCHEDJOB_ERROR);
973 findNextJob();
974 }
975 return;
976 }
977 else if (reply.arguments().first().toBool() == false)
978 {
979 appendLogText(i18n("Warning: job '%1' captureAndSolve request failed.", activeJob()->getName()));
980 activeJob()->setState(SCHEDJOB_ABORTED);
981 findNextJob();
982 return;
983 }
984
985 appendLogText(i18n("Job '%1' is capturing and plate solving.", activeJob()->getName()));
986 }
987
988 /* FIXME: not supposed to modify the job */
989 moduleState()->updateJobStage(SCHEDSTAGE_ALIGNING);
990 moduleState()->startCurrentOperationTimer();
991}
992
993void SchedulerProcess::startGuiding(bool resetCalibration)
994{
995 Q_ASSERT_X(nullptr != activeJob(), __FUNCTION__, "Job starting guiding must be valid");
996
997 // avoid starting the guider twice
998 if (resetCalibration == false && getGuidingStatus() == GUIDE_GUIDING)
999 {
1000 moduleState()->updateJobStage(SCHEDSTAGE_GUIDING_COMPLETE);
1001 appendLogText(i18n("Guiding already running for %1, starting next scheduler action...", activeJob()->getName()));
1002 getNextAction();
1003 moduleState()->startCurrentOperationTimer();
1004 return;
1005 }
1006
1007 // Connect Guider
1008 guideInterface()->call(QDBus::AutoDetect, "connectGuider");
1009
1010 // Set Auto Star to true
1011 QVariant arg(true);
1012 guideInterface()->call(QDBus::AutoDetect, "setAutoStarEnabled", arg);
1013
1014 // Only reset calibration on trouble
1015 // and if we are allowed to reset calibration (true by default)
1016 if (resetCalibration && Options::resetGuideCalibration())
1017 {
1018 guideInterface()->call(QDBus::AutoDetect, "clearCalibration");
1019 }
1020
1021 guideInterface()->call(QDBus::AutoDetect, "guide");
1022
1023 moduleState()->updateJobStage(SCHEDSTAGE_GUIDING);
1024
1025 appendLogText(i18n("Starting guiding procedure for %1 ...", activeJob()->getName()));
1026
1027 moduleState()->startCurrentOperationTimer();
1028}
1029
1031{
1032 if (!guideInterface())
1033 return;
1034
1035 // Tell guider to abort if the current job requires guiding - end-user may enable guiding manually before observation
1036 if (nullptr != activeJob() && (activeJob()->getStepPipeline() & SchedulerJob::USE_GUIDE))
1037 {
1038 qCInfo(KSTARS_EKOS_SCHEDULER) << QString("Job '%1' is stopping guiding...").arg(activeJob()->getName());
1039 guideInterface()->call(QDBus::AutoDetect, "abort");
1040 moduleState()->resetGuideFailureCount();
1041 // abort all follower jobs
1042 stopCapturing("", true);
1043 }
1044
1045 // In any case, stop the automatic guider restart
1046 if (moduleState()->isGuidingTimerActive())
1047 moduleState()->cancelGuidingTimer();
1048}
1049
1051{
1052 if ((moduleState()->restartGuidingInterval() > 0) &&
1053 (moduleState()->restartGuidingTime().msecsTo(KStarsData::Instance()->ut()) > moduleState()->restartGuidingInterval()))
1054 {
1055 moduleState()->cancelGuidingTimer();
1056 startGuiding(true);
1057 }
1058}
1059
1061{
1062 Q_ASSERT_X(nullptr != activeJob(), __FUNCTION__, "Job starting capturing must be valid");
1063
1064 // ensure that guiding is running before we start capturing
1065 if (activeJob()->getStepPipeline() & SchedulerJob::USE_GUIDE && getGuidingStatus() != GUIDE_GUIDING)
1066 {
1067 // guiding should run, but it doesn't. So start guiding first
1068 moduleState()->updateJobStage(SCHEDSTAGE_GUIDING);
1069 startGuiding();
1070 return;
1071 }
1072
1073 startSingleCapture(activeJob(), restart);
1074 for (auto follower : activeJob()->followerJobs())
1075 {
1076 // start follower jobs that scheduled or that were already capturing, but stopped
1077 if (follower->getState() == SCHEDJOB_SCHEDULED || (follower->getStage() == SCHEDSTAGE_CAPTURING && follower->isStopped()))
1078 {
1079 follower->setState(SCHEDJOB_BUSY);
1080 follower->setStage(SCHEDSTAGE_CAPTURING);
1081 startSingleCapture(follower, restart);
1082 }
1083 }
1084
1085 moduleState()->updateJobStage(SCHEDSTAGE_CAPTURING);
1086
1087 KSNotification::event(QLatin1String("EkosScheduledImagingStart"),
1088 i18n("Ekos job (%1) - Capture started", activeJob()->getName()), KSNotification::Scheduler);
1089
1090 if (moduleState()->captureBatch() > 0)
1091 appendLogText(i18n("Job '%1' capture is in progress (batch #%2)...", activeJob()->getName(),
1092 moduleState()->captureBatch() + 1));
1093 else
1094 appendLogText(i18n("Job '%1' capture is in progress...", activeJob()->getName()));
1095
1096 moduleState()->startCurrentOperationTimer();
1097}
1098
1099void SchedulerProcess::startSingleCapture(SchedulerJob *job, bool restart)
1100{
1101 captureInterface()->setProperty("targetName", job->getName());
1102
1103 QString url = job->getSequenceFile().toLocalFile();
1104 QVariant train(job->getOpticalTrain());
1105
1106 if (restart == false)
1107 {
1108 QList<QVariant> dbusargs;
1109 QVariant isLead(job->isLead());
1110 // override targets from sequence queue file
1111 QVariant targetName(job->getName());
1112 dbusargs.append(url);
1113 dbusargs.append(train);
1114 dbusargs.append(isLead);
1115 dbusargs.append(targetName);
1116 QDBusReply<bool> const captureReply = captureInterface()->callWithArgumentList(QDBus::AutoDetect,
1117 "loadSequenceQueue",
1118 dbusargs);
1119 if (captureReply.error().type() != QDBusError::NoError)
1120 {
1121 qCCritical(KSTARS_EKOS_SCHEDULER) <<
1122 QString("Warning: job '%1' loadSequenceQueue request received DBUS error: %1").arg(job->getName()).arg(
1123 captureReply.error().message());
1124 if (!manageConnectionLoss())
1125 job->setState(SCHEDJOB_ERROR);
1126 return;
1127 }
1128 // Check if loading sequence fails for whatever reason
1129 else if (captureReply.value() == false)
1130 {
1131 qCCritical(KSTARS_EKOS_SCHEDULER) <<
1132 QString("Warning: job '%1' loadSequenceQueue request failed").arg(job->getName());
1133 if (!manageConnectionLoss())
1134 job->setState(SCHEDJOB_ERROR);
1135 return;
1136 }
1137 }
1138
1139 const CapturedFramesMap fMap = job->getCapturedFramesMap();
1140
1141 for (auto &e : fMap.keys())
1142 {
1143 QList<QVariant> dbusargs;
1144 QDBusMessage reply;
1145 dbusargs.append(e);
1146 dbusargs.append(fMap.value(e));
1147 dbusargs.append(train);
1148
1149 if ((reply = captureInterface()->callWithArgumentList(QDBus::Block, "setCapturedFramesMap",
1150 dbusargs)).type() ==
1152 {
1153 qCCritical(KSTARS_EKOS_SCHEDULER) <<
1154 QString("Warning: job '%1' setCapturedFramesCount request received DBUS error: %1").arg(job->getName()).arg(
1155 reply.errorMessage());
1156 if (!manageConnectionLoss())
1157 job->setState(SCHEDJOB_ERROR);
1158 return;
1159 }
1160 }
1161
1162 // Start capture process
1163 QList<QVariant> dbusargs;
1164 dbusargs.append(train);
1165
1166 QDBusReply<QString> const startReply = captureInterface()->callWithArgumentList(QDBus::AutoDetect, "start",
1167 dbusargs);
1168
1169 if (startReply.error().type() != QDBusError::NoError)
1170 {
1171 qCCritical(KSTARS_EKOS_SCHEDULER) <<
1172 QString("Warning: job '%1' start request received DBUS error: %1").arg(job->getName()).arg(
1173 startReply.error().message());
1174 if (!manageConnectionLoss())
1175 job->setState(SCHEDJOB_ERROR);
1176 return;
1177 }
1178
1179 QString trainName = startReply.value();
1180 m_activeJobs[trainName] = job;
1181 // set the
1182}
1183
1184void SchedulerProcess::setSolverAction(Align::GotoMode mode)
1185{
1186 QVariant gotoMode(static_cast<int>(mode));
1187 alignInterface()->call(QDBus::AutoDetect, "setSolverAction", gotoMode);
1188}
1189
1191{
1192 qCDebug(KSTARS_EKOS_SCHEDULER) << "Loading profiles";
1193 QDBusReply<QStringList> profiles = ekosInterface()->call(QDBus::AutoDetect, "getProfiles");
1194
1195 if (profiles.error().type() == QDBusError::NoError)
1196 moduleState()->updateProfiles(profiles);
1197}
1198
1199void SchedulerProcess::executeScript(const QString &filename)
1200{
1201 appendLogText(i18n("Executing script %1...", filename));
1202
1203 connect(&scriptProcess(), &QProcess::readyReadStandardOutput, this, &SchedulerProcess::readProcessOutput);
1204
1205 connect(&scriptProcess(), static_cast<void (QProcess::*)(int, QProcess::ExitStatus)>(&QProcess::finished),
1206 this, [this](int exitCode, QProcess::ExitStatus)
1207 {
1208 checkProcessExit(exitCode);
1209 });
1210
1211 QStringList arguments;
1212 scriptProcess().start(filename, arguments);
1213}
1214
1216{
1217 if (moduleState()->schedulerState() == SCHEDULER_PAUSED)
1218 return false;
1219
1220 switch (moduleState()->ekosState())
1221 {
1222 case EKOS_IDLE:
1223 {
1224 if (moduleState()->ekosCommunicationStatus() == Ekos::Success)
1225 {
1226 moduleState()->setEkosState(EKOS_READY);
1227 return true;
1228 }
1229 else
1230 {
1231 ekosInterface()->call(QDBus::AutoDetect, "start");
1232 moduleState()->setEkosState(EKOS_STARTING);
1233 moduleState()->startCurrentOperationTimer();
1234
1235 qCInfo(KSTARS_EKOS_SCHEDULER) << "Ekos communication status is" << moduleState()->ekosCommunicationStatus() <<
1236 "Starting Ekos...";
1237
1238 return false;
1239 }
1240 }
1241
1242 case EKOS_STARTING:
1243 {
1244 if (moduleState()->ekosCommunicationStatus() == Ekos::Success)
1245 {
1246 appendLogText(i18n("Ekos started."));
1247 moduleState()->resetEkosConnectFailureCount();
1248 moduleState()->setEkosState(EKOS_READY);
1249 return true;
1250 }
1251 else if (moduleState()->ekosCommunicationStatus() == Ekos::Error)
1252 {
1253 if (moduleState()->increaseEkosConnectFailureCount())
1254 {
1255 appendLogText(i18n("Starting Ekos failed. Retrying..."));
1256 ekosInterface()->call(QDBus::AutoDetect, "start");
1257 return false;
1258 }
1259
1260 appendLogText(i18n("Starting Ekos failed."));
1261 stop();
1262 return false;
1263 }
1264 else if (moduleState()->ekosCommunicationStatus() == Ekos::Idle)
1265 return false;
1266 // If a minute passed, give up
1267 else if (moduleState()->getCurrentOperationMsec() > (60 * 1000))
1268 {
1269 if (moduleState()->increaseEkosConnectFailureCount())
1270 {
1271 appendLogText(i18n("Starting Ekos timed out. Retrying..."));
1272 ekosInterface()->call(QDBus::AutoDetect, "stop");
1273 QTimer::singleShot(1000, this, [&]()
1274 {
1275 ekosInterface()->call(QDBus::AutoDetect, "start");
1276 moduleState()->startCurrentOperationTimer();
1277 });
1278 return false;
1279 }
1280
1281 appendLogText(i18n("Starting Ekos timed out."));
1282 stop();
1283 return false;
1284 }
1285 }
1286 break;
1287
1288 case EKOS_STOPPING:
1289 {
1290 if (moduleState()->ekosCommunicationStatus() == Ekos::Idle)
1291 {
1292 appendLogText(i18n("Ekos stopped."));
1293 moduleState()->setEkosState(EKOS_IDLE);
1294 return true;
1295 }
1296 }
1297 break;
1298
1299 case EKOS_READY:
1300 return true;
1301 }
1302 return false;
1303}
1304
1306{
1307 if (moduleState()->schedulerState() == SCHEDULER_PAUSED)
1308 return false;
1309
1310 switch (moduleState()->indiState())
1311 {
1312 case INDI_IDLE:
1313 {
1314 if (moduleState()->indiCommunicationStatus() == Ekos::Success)
1315 {
1316 moduleState()->setIndiState(INDI_PROPERTY_CHECK);
1317 moduleState()->resetIndiConnectFailureCount();
1318 qCDebug(KSTARS_EKOS_SCHEDULER) << "Checking INDI Properties...";
1319 }
1320 else
1321 {
1322 qCDebug(KSTARS_EKOS_SCHEDULER) << "Connecting INDI devices...";
1323 ekosInterface()->call(QDBus::AutoDetect, "connectDevices");
1324 moduleState()->setIndiState(INDI_CONNECTING);
1325
1326 moduleState()->startCurrentOperationTimer();
1327 }
1328 }
1329 break;
1330
1331 case INDI_CONNECTING:
1332 {
1333 if (moduleState()->indiCommunicationStatus() == Ekos::Success)
1334 {
1335 appendLogText(i18n("INDI devices connected."));
1336 moduleState()->setIndiState(INDI_PROPERTY_CHECK);
1337 }
1338 else if (moduleState()->indiCommunicationStatus() == Ekos::Error)
1339 {
1340 if (moduleState()->increaseIndiConnectFailureCount() <= moduleState()->maxFailureAttempts())
1341 {
1342 appendLogText(i18n("One or more INDI devices failed to connect. Retrying..."));
1343 ekosInterface()->call(QDBus::AutoDetect, "connectDevices");
1344 }
1345 else
1346 {
1347 appendLogText(i18n("One or more INDI devices failed to connect. Check INDI control panel for details."));
1348 stop();
1349 }
1350 }
1351 // If 30 seconds passed, we retry
1352 else if (moduleState()->getCurrentOperationMsec() > (30 * 1000))
1353 {
1354 if (moduleState()->increaseIndiConnectFailureCount() <= moduleState()->maxFailureAttempts())
1355 {
1356 appendLogText(i18n("One or more INDI devices timed out. Retrying..."));
1357 ekosInterface()->call(QDBus::AutoDetect, "connectDevices");
1358 moduleState()->startCurrentOperationTimer();
1359 }
1360 else
1361 {
1362 appendLogText(i18n("One or more INDI devices timed out. Check INDI control panel for details."));
1363 stop();
1364 }
1365 }
1366 }
1367 break;
1368
1369 case INDI_DISCONNECTING:
1370 {
1371 if (moduleState()->indiCommunicationStatus() == Ekos::Idle)
1372 {
1373 appendLogText(i18n("INDI devices disconnected."));
1374 moduleState()->setIndiState(INDI_IDLE);
1375 return true;
1376 }
1377 }
1378 break;
1379
1380 case INDI_PROPERTY_CHECK:
1381 {
1382 qCDebug(KSTARS_EKOS_SCHEDULER) << "Checking INDI properties.";
1383 // If dome unparking is required then we wait for dome interface
1384 if (Options::schedulerUnparkDome() && moduleState()->domeReady() == false)
1385 {
1386 if (moduleState()->getCurrentOperationMsec() > (30 * 1000))
1387 {
1388 moduleState()->startCurrentOperationTimer();
1389 appendLogText(i18n("Warning: dome device not ready after timeout, attempting to recover..."));
1391 stopEkos();
1392 }
1393
1394 appendLogText(i18n("Dome unpark required but dome is not yet ready."));
1395 return false;
1396 }
1397
1398 // If mount unparking is required then we wait for mount interface
1399 if (Options::schedulerUnparkMount() && moduleState()->mountReady() == false)
1400 {
1401 if (moduleState()->getCurrentOperationMsec() > (30 * 1000))
1402 {
1403 moduleState()->startCurrentOperationTimer();
1404 appendLogText(i18n("Warning: mount device not ready after timeout, attempting to recover..."));
1406 stopEkos();
1407 }
1408
1409 qCDebug(KSTARS_EKOS_SCHEDULER) << "Mount unpark required but mount is not yet ready.";
1410 return false;
1411 }
1412
1413 // If cap unparking is required then we wait for cap interface
1414 if (Options::schedulerOpenDustCover() && moduleState()->capReady() == false)
1415 {
1416 if (moduleState()->getCurrentOperationMsec() > (30 * 1000))
1417 {
1418 moduleState()->startCurrentOperationTimer();
1419 appendLogText(i18n("Warning: cap device not ready after timeout, attempting to recover..."));
1421 stopEkos();
1422 }
1423
1424 qCDebug(KSTARS_EKOS_SCHEDULER) << "Cap unpark required but cap is not yet ready.";
1425 return false;
1426 }
1427
1428 // capture interface is required at all times to proceed.
1429 if (captureInterface().isNull())
1430 return false;
1431
1432 if (moduleState()->captureReady() == false)
1433 {
1434 QVariant hasCoolerControl = captureInterface()->property("coolerControl");
1435 qCDebug(KSTARS_EKOS_SCHEDULER) << "Cooler control" << (!hasCoolerControl.isValid() ? "invalid" :
1436 (hasCoolerControl.toBool() ? "True" : "Faklse"));
1437 if (hasCoolerControl.isValid())
1438 moduleState()->setCaptureReady(true);
1439 else
1440 qCWarning(KSTARS_EKOS_SCHEDULER) << "Capture module is not ready yet...";
1441 }
1442
1443 moduleState()->setIndiState(INDI_READY);
1444 moduleState()->resetIndiConnectFailureCount();
1445 return true;
1446 }
1447
1448 case INDI_READY:
1449 return true;
1450 }
1451
1452 return false;
1453}
1454
1456{
1457 // If INDI is not done disconnecting, try again later
1458 if (moduleState()->indiState() == INDI_DISCONNECTING
1459 && checkINDIState() == false)
1460 return false;
1461
1462 // Disconnect INDI if required first
1463 if (moduleState()->indiState() != INDI_IDLE && Options::stopEkosAfterShutdown())
1464 {
1466 return false;
1467 }
1468
1469 // If Ekos is not done stopping, try again later
1470 if (moduleState()->ekosState() == EKOS_STOPPING && checkEkosState() == false)
1471 return false;
1472
1473 // Stop Ekos if required.
1474 if (moduleState()->ekosState() != EKOS_IDLE && Options::stopEkosAfterShutdown())
1475 {
1476 stopEkos();
1477 return false;
1478 }
1479
1480 if (moduleState()->shutdownState() == SHUTDOWN_COMPLETE)
1481 appendLogText(i18n("Shutdown complete."));
1482 else
1483 appendLogText(i18n("Shutdown procedure failed, aborting..."));
1484
1485 // Stop Scheduler
1486 stop();
1487
1488 return true;
1489}
1490
1492{
1493 qCInfo(KSTARS_EKOS_SCHEDULER) << "Disconnecting INDI...";
1494 moduleState()->setIndiState(INDI_DISCONNECTING);
1495 ekosInterface()->call(QDBus::AutoDetect, "disconnectDevices");
1496}
1497
1498void SchedulerProcess::stopEkos()
1499{
1500 qCInfo(KSTARS_EKOS_SCHEDULER) << "Stopping Ekos...";
1501 moduleState()->setEkosState(EKOS_STOPPING);
1502 moduleState()->resetEkosConnectFailureCount();
1503 ekosInterface()->call(QDBus::AutoDetect, "stop");
1504 moduleState()->setMountReady(false);
1505 moduleState()->setCaptureReady(false);
1506 moduleState()->setDomeReady(false);
1507 moduleState()->setCapReady(false);
1508}
1509
1511{
1512 if (SCHEDULER_RUNNING != moduleState()->schedulerState())
1513 return false;
1514
1515 // Don't manage loss if Ekos is actually down in the state machine
1516 switch (moduleState()->ekosState())
1517 {
1518 case EKOS_IDLE:
1519 case EKOS_STOPPING:
1520 return false;
1521
1522 default:
1523 break;
1524 }
1525
1526 // Don't manage loss if INDI is actually down in the state machine
1527 switch (moduleState()->indiState())
1528 {
1529 case INDI_IDLE:
1530 case INDI_DISCONNECTING:
1531 return false;
1532
1533 default:
1534 break;
1535 }
1536
1537 // If Ekos is assumed to be up, check its state
1538 //QDBusReply<int> const isEkosStarted = ekosInterface->call(QDBus::AutoDetect, "getEkosStartingStatus");
1539 if (moduleState()->ekosCommunicationStatus() == Ekos::Success)
1540 {
1541 qCDebug(KSTARS_EKOS_SCHEDULER) << QString("Ekos is currently connected, checking INDI before mitigating connection loss.");
1542
1543 // If INDI is assumed to be up, check its state
1544 if (moduleState()->isINDIConnected())
1545 {
1546 // If both Ekos and INDI are assumed up, and are actually up, no mitigation needed, this is a DBus interface error
1547 qCDebug(KSTARS_EKOS_SCHEDULER) << QString("INDI is currently connected, no connection loss mitigation needed.");
1548 return false;
1549 }
1550 }
1551
1552 // Stop actions of the current job
1554
1555 // Acknowledge INDI and Ekos disconnections
1557 stopEkos();
1558
1559 // Let the Scheduler attempt to connect INDI again
1560 return true;
1561
1562}
1563
1565{
1566 if (capInterface().isNull())
1567 return;
1568
1569 QVariant parkingStatus = capInterface()->property("parkStatus");
1570 qCDebug(KSTARS_EKOS_SCHEDULER) << "Parking status" << (!parkingStatus.isValid() ? -1 : parkingStatus.toInt());
1571
1572 if (parkingStatus.isValid() == false)
1573 {
1574 qCCritical(KSTARS_EKOS_SCHEDULER) << QString("Warning: cap parkStatus request received DBUS error: %1").arg(
1575 capInterface()->lastError().type());
1576 if (!manageConnectionLoss())
1577 parkingStatus = ISD::PARK_ERROR;
1578 }
1579
1580 ISD::ParkStatus status = static_cast<ISD::ParkStatus>(parkingStatus.toInt());
1581
1582 switch (status)
1583 {
1584 case ISD::PARK_PARKED:
1585 if (moduleState()->shutdownState() == SHUTDOWN_PARKING_CAP)
1586 {
1587 appendLogText(i18n("Cap parked."));
1588 moduleState()->setShutdownState(SHUTDOWN_PARK_MOUNT);
1589 }
1590 moduleState()->resetParkingCapFailureCount();
1591 break;
1592
1593 case ISD::PARK_UNPARKED:
1594 if (moduleState()->startupState() == STARTUP_UNPARKING_CAP)
1595 {
1596 moduleState()->setStartupState(STARTUP_COMPLETE);
1597 appendLogText(i18n("Cap unparked."));
1598 }
1599 moduleState()->resetParkingCapFailureCount();
1600 break;
1601
1602 case ISD::PARK_PARKING:
1603 case ISD::PARK_UNPARKING:
1604 // TODO make the timeouts configurable by the user
1605 if (moduleState()->getCurrentOperationMsec() > (60 * 1000))
1606 {
1607 if (moduleState()->increaseParkingCapFailureCount())
1608 {
1609 appendLogText(i18n("Operation timeout. Restarting operation..."));
1610 if (status == ISD::PARK_PARKING)
1611 parkCap();
1612 else
1613 unParkCap();
1614 break;
1615 }
1616 }
1617 break;
1618
1619 case ISD::PARK_ERROR:
1620 if (moduleState()->shutdownState() == SHUTDOWN_PARKING_CAP)
1621 {
1622 appendLogText(i18n("Cap parking error."));
1623 moduleState()->setShutdownState(SHUTDOWN_ERROR);
1624 }
1625 else if (moduleState()->startupState() == STARTUP_UNPARKING_CAP)
1626 {
1627 appendLogText(i18n("Cap unparking error."));
1628 moduleState()->setStartupState(STARTUP_ERROR);
1629 }
1630 moduleState()->resetParkingCapFailureCount();
1631 break;
1632
1633 default:
1634 break;
1635 }
1636}
1637
1638void SchedulerProcess::checkMountParkingStatus()
1639{
1640 if (mountInterface().isNull())
1641 return;
1642
1643 QVariant parkingStatus = mountInterface()->property("parkStatus");
1644 qCDebug(KSTARS_EKOS_SCHEDULER) << "Mount parking status" << (!parkingStatus.isValid() ? -1 : parkingStatus.toInt());
1645
1646 if (parkingStatus.isValid() == false)
1647 {
1648 qCCritical(KSTARS_EKOS_SCHEDULER) << QString("Warning: mount parkStatus request received DBUS error: %1").arg(
1649 mountInterface()->lastError().type());
1650 if (!manageConnectionLoss())
1651 moduleState()->setParkWaitState(PARKWAIT_ERROR);
1652 }
1653
1654 ISD::ParkStatus status = static_cast<ISD::ParkStatus>(parkingStatus.toInt());
1655
1656 switch (status)
1657 {
1658 //case Mount::PARKING_OK:
1659 case ISD::PARK_PARKED:
1660 // If we are starting up, we will unpark the mount in checkParkWaitState soon
1661 // If we are shutting down and mount is parked, proceed to next step
1662 if (moduleState()->shutdownState() == SHUTDOWN_PARKING_MOUNT)
1663 moduleState()->setShutdownState(SHUTDOWN_PARK_DOME);
1664
1665 // Update parking engine state
1666 if (moduleState()->parkWaitState() == PARKWAIT_PARKING)
1667 moduleState()->setParkWaitState(PARKWAIT_PARKED);
1668
1669 appendLogText(i18n("Mount parked."));
1670 moduleState()->resetParkingMountFailureCount();
1671 break;
1672
1673 //case Mount::UNPARKING_OK:
1674 case ISD::PARK_UNPARKED:
1675 // If we are starting up and mount is unparked, proceed to next step
1676 // If we are shutting down, we will park the mount in checkParkWaitState soon
1677 if (moduleState()->startupState() == STARTUP_UNPARKING_MOUNT)
1678 moduleState()->setStartupState(STARTUP_UNPARK_CAP);
1679
1680 // Update parking engine state
1681 if (moduleState()->parkWaitState() == PARKWAIT_UNPARKING)
1682 moduleState()->setParkWaitState(PARKWAIT_UNPARKED);
1683
1684 appendLogText(i18n("Mount unparked."));
1685 moduleState()->resetParkingMountFailureCount();
1686 break;
1687
1688 // FIXME: Create an option for the parking/unparking timeout.
1689
1690 //case Mount::UNPARKING_BUSY:
1691 case ISD::PARK_UNPARKING:
1692 if (moduleState()->getCurrentOperationMsec() > (60 * 1000))
1693 {
1694 if (moduleState()->increaseParkingMountFailureCount())
1695 {
1696 appendLogText(i18n("Warning: mount unpark operation timed out on attempt %1/%2. Restarting operation...",
1697 moduleState()->parkingMountFailureCount(), moduleState()->maxFailureAttempts()));
1698 unParkMount();
1699 }
1700 else
1701 {
1702 appendLogText(i18n("Warning: mount unpark operation timed out on last attempt."));
1703 moduleState()->setParkWaitState(PARKWAIT_ERROR);
1704 }
1705 }
1706 else qCInfo(KSTARS_EKOS_SCHEDULER) << "Unparking mount in progress...";
1707
1708 break;
1709
1710 //case Mount::PARKING_BUSY:
1711 case ISD::PARK_PARKING:
1712 if (moduleState()->getCurrentOperationMsec() > (60 * 1000))
1713 {
1714 if (moduleState()->increaseParkingMountFailureCount())
1715 {
1716 appendLogText(i18n("Warning: mount park operation timed out on attempt %1/%2. Restarting operation...",
1717 moduleState()->parkingMountFailureCount(),
1718 moduleState()->maxFailureAttempts()));
1719 parkMount();
1720 }
1721 else
1722 {
1723 appendLogText(i18n("Warning: mount park operation timed out on last attempt."));
1724 moduleState()->setParkWaitState(PARKWAIT_ERROR);
1725 }
1726 }
1727 else qCInfo(KSTARS_EKOS_SCHEDULER) << "Parking mount in progress...";
1728
1729 break;
1730
1731 //case Mount::PARKING_ERROR:
1732 case ISD::PARK_ERROR:
1733 if (moduleState()->startupState() == STARTUP_UNPARKING_MOUNT)
1734 {
1735 appendLogText(i18n("Mount unparking error."));
1736 moduleState()->setStartupState(STARTUP_ERROR);
1737 moduleState()->resetParkingMountFailureCount();
1738 }
1739 else if (moduleState()->shutdownState() == SHUTDOWN_PARKING_MOUNT)
1740 {
1741 if (moduleState()->increaseParkingMountFailureCount())
1742 {
1743 appendLogText(i18n("Warning: mount park operation failed on attempt %1/%2. Restarting operation...",
1744 moduleState()->parkingMountFailureCount(),
1745 moduleState()->maxFailureAttempts()));
1746 parkMount();
1747 }
1748 else
1749 {
1750 appendLogText(i18n("Mount parking error."));
1751 moduleState()->setShutdownState(SHUTDOWN_ERROR);
1752 moduleState()->resetParkingMountFailureCount();
1753 }
1754
1755 }
1756 else if (moduleState()->parkWaitState() == PARKWAIT_PARKING)
1757 {
1758 appendLogText(i18n("Mount parking error."));
1759 moduleState()->setParkWaitState(PARKWAIT_ERROR);
1760 moduleState()->resetParkingMountFailureCount();
1761 }
1762 else if (moduleState()->parkWaitState() == PARKWAIT_UNPARKING)
1763 {
1764 appendLogText(i18n("Mount unparking error."));
1765 moduleState()->setParkWaitState(PARKWAIT_ERROR);
1766 moduleState()->resetParkingMountFailureCount();
1767 }
1768 break;
1769
1770 //case Mount::PARKING_IDLE:
1771 // FIXME Does this work as intended? check!
1772 case ISD::PARK_UNKNOWN:
1773 // Last parking action did not result in an action, so proceed to next step
1774 if (moduleState()->shutdownState() == SHUTDOWN_PARKING_MOUNT)
1775 moduleState()->setShutdownState(SHUTDOWN_PARK_DOME);
1776
1777 // Last unparking action did not result in an action, so proceed to next step
1778 if (moduleState()->startupState() == STARTUP_UNPARKING_MOUNT)
1779 moduleState()->setStartupState(STARTUP_UNPARK_CAP);
1780
1781 // Update parking engine state
1782 if (moduleState()->parkWaitState() == PARKWAIT_PARKING)
1783 moduleState()->setParkWaitState(PARKWAIT_PARKED);
1784 else if (moduleState()->parkWaitState() == PARKWAIT_UNPARKING)
1785 moduleState()->setParkWaitState(PARKWAIT_UNPARKED);
1786
1787 moduleState()->resetParkingMountFailureCount();
1788 break;
1789 }
1790}
1791
1792void SchedulerProcess::checkDomeParkingStatus()
1793{
1794 if (domeInterface().isNull())
1795 return;
1796
1797 QVariant parkingStatus = domeInterface()->property("parkStatus");
1798 qCDebug(KSTARS_EKOS_SCHEDULER) << "Dome parking status" << (!parkingStatus.isValid() ? -1 : parkingStatus.toInt());
1799
1800 if (parkingStatus.isValid() == false)
1801 {
1802 qCCritical(KSTARS_EKOS_SCHEDULER) << QString("Warning: dome parkStatus request received DBUS error: %1").arg(
1803 mountInterface()->lastError().type());
1804 if (!manageConnectionLoss())
1805 moduleState()->setParkWaitState(PARKWAIT_ERROR);
1806 }
1807
1808 ISD::ParkStatus status = static_cast<ISD::ParkStatus>(parkingStatus.toInt());
1809
1810 switch (status)
1811 {
1812 case ISD::PARK_PARKED:
1813 if (moduleState()->shutdownState() == SHUTDOWN_PARKING_DOME)
1814 {
1815 appendLogText(i18n("Dome parked."));
1816
1817 moduleState()->setShutdownState(SHUTDOWN_SCRIPT);
1818 }
1819 moduleState()->resetParkingDomeFailureCount();
1820 break;
1821
1822 case ISD::PARK_UNPARKED:
1823 if (moduleState()->startupState() == STARTUP_UNPARKING_DOME)
1824 {
1825 moduleState()->setStartupState(STARTUP_UNPARK_MOUNT);
1826 appendLogText(i18n("Dome unparked."));
1827 }
1828 moduleState()->resetParkingDomeFailureCount();
1829 break;
1830
1831 case ISD::PARK_PARKING:
1832 case ISD::PARK_UNPARKING:
1833 // TODO make the timeouts configurable by the user
1834 if (moduleState()->getCurrentOperationMsec() > (120 * 1000))
1835 {
1836 if (moduleState()->increaseParkingDomeFailureCount())
1837 {
1838 appendLogText(i18n("Operation timeout. Restarting operation..."));
1839 if (status == ISD::PARK_PARKING)
1840 parkDome();
1841 else
1842 unParkDome();
1843 break;
1844 }
1845 }
1846 break;
1847
1848 case ISD::PARK_ERROR:
1849 if (moduleState()->shutdownState() == SHUTDOWN_PARKING_DOME)
1850 {
1851 if (moduleState()->increaseParkingDomeFailureCount())
1852 {
1853 appendLogText(i18n("Dome parking failed. Restarting operation..."));
1854 parkDome();
1855 }
1856 else
1857 {
1858 appendLogText(i18n("Dome parking error."));
1859 moduleState()->setShutdownState(SHUTDOWN_ERROR);
1860 moduleState()->resetParkingDomeFailureCount();
1861 }
1862 }
1863 else if (moduleState()->startupState() == STARTUP_UNPARKING_DOME)
1864 {
1865 if (moduleState()->increaseParkingDomeFailureCount())
1866 {
1867 appendLogText(i18n("Dome unparking failed. Restarting operation..."));
1868 unParkDome();
1869 }
1870 else
1871 {
1872 appendLogText(i18n("Dome unparking error."));
1873 moduleState()->setStartupState(STARTUP_ERROR);
1874 moduleState()->resetParkingDomeFailureCount();
1875 }
1876 }
1877 break;
1878
1879 default:
1880 break;
1881 }
1882}
1883
1885{
1886 if (moduleState()->schedulerState() == SCHEDULER_PAUSED)
1887 return false;
1888
1889 qCDebug(KSTARS_EKOS_SCHEDULER) << QString("Checking Startup State (%1)...").arg(moduleState()->startupState());
1890
1891 switch (moduleState()->startupState())
1892 {
1893 case STARTUP_IDLE:
1894 {
1895 KSNotification::event(QLatin1String("ObservatoryStartup"), i18n("Observatory is in the startup process"),
1896 KSNotification::Scheduler);
1897
1898 qCDebug(KSTARS_EKOS_SCHEDULER) << "Startup Idle. Starting startup process...";
1899
1900 // If Ekos is already started, we skip the script and move on to dome unpark step
1901 // unless we do not have light frames, then we skip all
1902 //QDBusReply<int> isEkosStarted;
1903 //isEkosStarted = ekosInterface->call(QDBus::AutoDetect, "getEkosStartingStatus");
1904 //if (isEkosStarted.value() == Ekos::Success)
1905 if (Options::alwaysExecuteStartupScript() == false && moduleState()->ekosCommunicationStatus() == Ekos::Success)
1906 {
1907 if (moduleState()->startupScriptURL().isEmpty() == false)
1908 appendLogText(i18n("Ekos is already started, skipping startup script..."));
1909
1910 if (!activeJob() || activeJob()->getLightFramesRequired())
1911 moduleState()->setStartupState(STARTUP_UNPARK_DOME);
1912 else
1913 moduleState()->setStartupState(STARTUP_COMPLETE);
1914 return true;
1915 }
1916
1917 if (moduleState()->currentProfile() != i18n("Default"))
1918 {
1919 QList<QVariant> profile;
1920 profile.append(moduleState()->currentProfile());
1921 ekosInterface()->callWithArgumentList(QDBus::AutoDetect, "setProfile", profile);
1922 }
1923
1924 if (moduleState()->startupScriptURL().isEmpty() == false)
1925 {
1926 moduleState()->setStartupState(STARTUP_SCRIPT);
1927 executeScript(moduleState()->startupScriptURL().toString(QUrl::PreferLocalFile));
1928 return false;
1929 }
1930
1931 moduleState()->setStartupState(STARTUP_UNPARK_DOME);
1932 return false;
1933 }
1934
1935 case STARTUP_SCRIPT:
1936 return false;
1937
1938 case STARTUP_UNPARK_DOME:
1939 // If there is no job in case of manual startup procedure,
1940 // or if the job requires light frames, let's proceed with
1941 // unparking the dome, otherwise startup process is complete.
1942 if (activeJob() == nullptr || activeJob()->getLightFramesRequired())
1943 {
1944 if (Options::schedulerUnparkDome())
1945 unParkDome();
1946 else
1947 moduleState()->setStartupState(STARTUP_UNPARK_MOUNT);
1948 }
1949 else
1950 {
1951 moduleState()->setStartupState(STARTUP_COMPLETE);
1952 return true;
1953 }
1954
1955 break;
1956
1957 case STARTUP_UNPARKING_DOME:
1958 checkDomeParkingStatus();
1959 break;
1960
1961 case STARTUP_UNPARK_MOUNT:
1962 if (Options::schedulerUnparkMount())
1963 unParkMount();
1964 else
1965 moduleState()->setStartupState(STARTUP_UNPARK_CAP);
1966 break;
1967
1968 case STARTUP_UNPARKING_MOUNT:
1969 checkMountParkingStatus();
1970 break;
1971
1972 case STARTUP_UNPARK_CAP:
1973 if (Options::schedulerOpenDustCover())
1974 unParkCap();
1975 else
1976 moduleState()->setStartupState(STARTUP_COMPLETE);
1977 break;
1978
1979 case STARTUP_UNPARKING_CAP:
1981 break;
1982
1983 case STARTUP_COMPLETE:
1984 return true;
1985
1986 case STARTUP_ERROR:
1987 stop();
1988 return true;
1989 }
1990
1991 return false;
1992}
1993
1995{
1996 qCDebug(KSTARS_EKOS_SCHEDULER) << "Checking shutdown state...";
1997
1998 if (moduleState()->schedulerState() == SCHEDULER_PAUSED)
1999 return false;
2000
2001 switch (moduleState()->shutdownState())
2002 {
2003 case SHUTDOWN_IDLE:
2004
2005 qCInfo(KSTARS_EKOS_SCHEDULER) << "Starting shutdown process...";
2006
2007 moduleState()->setActiveJob(nullptr);
2008 moduleState()->setupNextIteration(RUN_SHUTDOWN);
2009 emit shutdownStarted();
2010
2011 if (Options::schedulerWarmCCD())
2012 {
2013 appendLogText(i18n("Warming up CCD..."));
2014
2015 // Turn it off
2016 //QVariant arg(false);
2017 //captureInterface->call(QDBus::AutoDetect, "setCoolerControl", arg);
2018 if (captureInterface())
2019 {
2020 qCDebug(KSTARS_EKOS_SCHEDULER) << "Setting coolerControl=false";
2021 captureInterface()->setProperty("coolerControl", false);
2022 }
2023 }
2024
2025 // The following steps require a connection to the INDI server
2026 if (moduleState()->isINDIConnected())
2027 {
2028 if (Options::schedulerCloseDustCover())
2029 {
2030 moduleState()->setShutdownState(SHUTDOWN_PARK_CAP);
2031 return false;
2032 }
2033
2034 if (Options::schedulerParkMount())
2035 {
2036 moduleState()->setShutdownState(SHUTDOWN_PARK_MOUNT);
2037 return false;
2038 }
2039
2040 if (Options::schedulerParkDome())
2041 {
2042 moduleState()->setShutdownState(SHUTDOWN_PARK_DOME);
2043 return false;
2044 }
2045 }
2046 else appendLogText(i18n("Warning: Bypassing parking procedures, no INDI connection."));
2047
2048 if (moduleState()->shutdownScriptURL().isEmpty() == false)
2049 {
2050 moduleState()->setShutdownState(SHUTDOWN_SCRIPT);
2051 return false;
2052 }
2053
2054 moduleState()->setShutdownState(SHUTDOWN_COMPLETE);
2055 return true;
2056
2057 case SHUTDOWN_PARK_CAP:
2058 if (!moduleState()->isINDIConnected())
2059 {
2060 qCInfo(KSTARS_EKOS_SCHEDULER) << "Bypassing shutdown step 'park cap', no INDI connection.";
2061 moduleState()->setShutdownState(SHUTDOWN_SCRIPT);
2062 }
2063 else if (Options::schedulerCloseDustCover())
2064 parkCap();
2065 else
2066 moduleState()->setShutdownState(SHUTDOWN_PARK_MOUNT);
2067 break;
2068
2069 case SHUTDOWN_PARKING_CAP:
2071 break;
2072
2073 case SHUTDOWN_PARK_MOUNT:
2074 if (!moduleState()->isINDIConnected())
2075 {
2076 qCInfo(KSTARS_EKOS_SCHEDULER) << "Bypassing shutdown step 'park cap', no INDI connection.";
2077 moduleState()->setShutdownState(SHUTDOWN_SCRIPT);
2078 }
2079 else if (Options::schedulerParkMount())
2080 parkMount();
2081 else
2082 moduleState()->setShutdownState(SHUTDOWN_PARK_DOME);
2083 break;
2084
2085 case SHUTDOWN_PARKING_MOUNT:
2086 checkMountParkingStatus();
2087 break;
2088
2089 case SHUTDOWN_PARK_DOME:
2090 if (!moduleState()->isINDIConnected())
2091 {
2092 qCInfo(KSTARS_EKOS_SCHEDULER) << "Bypassing shutdown step 'park cap', no INDI connection.";
2093 moduleState()->setShutdownState(SHUTDOWN_SCRIPT);
2094 }
2095 else if (Options::schedulerParkDome())
2096 parkDome();
2097 else
2098 moduleState()->setShutdownState(SHUTDOWN_SCRIPT);
2099 break;
2100
2101 case SHUTDOWN_PARKING_DOME:
2102 checkDomeParkingStatus();
2103 break;
2104
2105 case SHUTDOWN_SCRIPT:
2106 if (moduleState()->shutdownScriptURL().isEmpty() == false)
2107 {
2108 // Need to stop Ekos now before executing script if it happens to stop INDI
2109 if (moduleState()->ekosState() != EKOS_IDLE && Options::shutdownScriptTerminatesINDI())
2110 {
2111 stopEkos();
2112 return false;
2113 }
2114
2115 moduleState()->setShutdownState(SHUTDOWN_SCRIPT_RUNNING);
2116 executeScript(moduleState()->shutdownScriptURL().toString(QUrl::PreferLocalFile));
2117 }
2118 else
2119 moduleState()->setShutdownState(SHUTDOWN_COMPLETE);
2120 break;
2121
2122 case SHUTDOWN_SCRIPT_RUNNING:
2123 return false;
2124
2125 case SHUTDOWN_COMPLETE:
2126 return completeShutdown();
2127
2128 case SHUTDOWN_ERROR:
2129 stop();
2130 return true;
2131 }
2132
2133 return false;
2134}
2135
2137{
2138 if (moduleState()->schedulerState() == SCHEDULER_PAUSED)
2139 return false;
2140
2141 if (moduleState()->parkWaitState() == PARKWAIT_IDLE)
2142 return true;
2143
2144 // qCDebug(KSTARS_EKOS_SCHEDULER) << "Checking Park Wait State...";
2145
2146 switch (moduleState()->parkWaitState())
2147 {
2148 case PARKWAIT_PARK:
2149 parkMount();
2150 break;
2151
2152 case PARKWAIT_PARKING:
2153 checkMountParkingStatus();
2154 break;
2155
2156 case PARKWAIT_UNPARK:
2157 unParkMount();
2158 break;
2159
2160 case PARKWAIT_UNPARKING:
2161 checkMountParkingStatus();
2162 break;
2163
2164 case PARKWAIT_IDLE:
2165 case PARKWAIT_PARKED:
2166 case PARKWAIT_UNPARKED:
2167 return true;
2168
2169 case PARKWAIT_ERROR:
2170 appendLogText(i18n("park/unpark wait procedure failed, aborting..."));
2171 stop();
2172 return true;
2173
2174 }
2175
2176 return false;
2177}
2178
2180{
2181 if (moduleState()->startupState() == STARTUP_IDLE
2182 || moduleState()->startupState() == STARTUP_ERROR
2183 || moduleState()->startupState() == STARTUP_COMPLETE)
2184 {
2185 connect(KSMessageBox::Instance(), &KSMessageBox::accepted, this, [this]()
2186 {
2187 KSMessageBox::Instance()->disconnect(this);
2188
2189 appendLogText(i18n("Warning: executing startup procedure manually..."));
2190 moduleState()->setStartupState(STARTUP_IDLE);
2192 QTimer::singleShot(1000, this, SLOT(checkStartupProcedure()));
2193
2194 });
2195
2196 KSMessageBox::Instance()->questionYesNo(i18n("Are you sure you want to execute the startup procedure manually?"));
2197 }
2198 else
2199 {
2200 switch (moduleState()->startupState())
2201 {
2202 case STARTUP_IDLE:
2203 break;
2204
2205 case STARTUP_SCRIPT:
2206 scriptProcess().terminate();
2207 break;
2208
2209 case STARTUP_UNPARK_DOME:
2210 break;
2211
2212 case STARTUP_UNPARKING_DOME:
2213 qCDebug(KSTARS_EKOS_SCHEDULER) << "Aborting unparking dome...";
2214 domeInterface()->call(QDBus::AutoDetect, "abort");
2215 break;
2216
2217 case STARTUP_UNPARK_MOUNT:
2218 break;
2219
2220 case STARTUP_UNPARKING_MOUNT:
2221 qCDebug(KSTARS_EKOS_SCHEDULER) << "Aborting unparking mount...";
2222 mountInterface()->call(QDBus::AutoDetect, "abort");
2223 break;
2224
2225 case STARTUP_UNPARK_CAP:
2226 break;
2227
2228 case STARTUP_UNPARKING_CAP:
2229 break;
2230
2231 case STARTUP_COMPLETE:
2232 break;
2233
2234 case STARTUP_ERROR:
2235 break;
2236 }
2237
2238 moduleState()->setStartupState(STARTUP_IDLE);
2239
2240 appendLogText(i18n("Startup procedure terminated."));
2241 }
2242
2243}
2244
2246{
2247 if (moduleState()->shutdownState() == SHUTDOWN_IDLE
2248 || moduleState()->shutdownState() == SHUTDOWN_ERROR
2249 || moduleState()->shutdownState() == SHUTDOWN_COMPLETE)
2250 {
2251 connect(KSMessageBox::Instance(), &KSMessageBox::accepted, this, [this]()
2252 {
2253 KSMessageBox::Instance()->disconnect(this);
2254 appendLogText(i18n("Warning: executing shutdown procedure manually..."));
2255 moduleState()->setShutdownState(SHUTDOWN_IDLE);
2257 QTimer::singleShot(1000, this, SLOT(checkShutdownProcedure()));
2258 });
2259
2260 KSMessageBox::Instance()->questionYesNo(i18n("Are you sure you want to execute the shutdown procedure manually?"));
2261 }
2262 else
2263 {
2264 switch (moduleState()->shutdownState())
2265 {
2266 case SHUTDOWN_IDLE:
2267 break;
2268
2269 case SHUTDOWN_SCRIPT:
2270 break;
2271
2272 case SHUTDOWN_SCRIPT_RUNNING:
2273 scriptProcess().terminate();
2274 break;
2275
2276 case SHUTDOWN_PARK_DOME:
2277 break;
2278
2279 case SHUTDOWN_PARKING_DOME:
2280 qCDebug(KSTARS_EKOS_SCHEDULER) << "Aborting parking dome...";
2281 domeInterface()->call(QDBus::AutoDetect, "abort");
2282 break;
2283
2284 case SHUTDOWN_PARK_MOUNT:
2285 break;
2286
2287 case SHUTDOWN_PARKING_MOUNT:
2288 qCDebug(KSTARS_EKOS_SCHEDULER) << "Aborting parking mount...";
2289 mountInterface()->call(QDBus::AutoDetect, "abort");
2290 break;
2291
2292 case SHUTDOWN_PARK_CAP:
2293 case SHUTDOWN_PARKING_CAP:
2294 break;
2295
2296 case SHUTDOWN_COMPLETE:
2297 break;
2298
2299 case SHUTDOWN_ERROR:
2300 break;
2301 }
2302
2303 moduleState()->setShutdownState(SHUTDOWN_IDLE);
2304
2305 appendLogText(i18n("Shutdown procedure terminated."));
2306 }
2307}
2308
2310{
2311 moduleState()->setupNextIteration(RUN_NOTHING);
2312 appendLogText(i18n("Scheduler paused."));
2313 emit schedulerPaused();
2314}
2315
2317{
2318 // Reset ALL scheduler jobs to IDLE and force-reset their completed count - no effect when progress is kept
2319 for (SchedulerJob * job : moduleState()->jobs())
2320 {
2321 job->reset();
2322 job->setCompletedCount(0);
2323 }
2324
2325 // Unconditionally update the capture storage
2327}
2328
2330{
2331 auto finished_or_aborted = [](SchedulerJob const * const job)
2332 {
2333 SchedulerJobStatus const s = job->getState();
2334 return SCHEDJOB_ERROR <= s || SCHEDJOB_ABORTED == s;
2335 };
2336
2337 /* This predicate matches jobs that are neither scheduled to run nor aborted */
2338 auto neither_scheduled_nor_aborted = [](SchedulerJob const * const job)
2339 {
2340 SchedulerJobStatus const s = job->getState();
2341 return SCHEDJOB_SCHEDULED != s && SCHEDJOB_ABORTED != s;
2342 };
2343
2344 /* If there are no jobs left to run in the filtered list, stop evaluation */
2345 ErrorHandlingStrategy strategy = static_cast<ErrorHandlingStrategy>(Options::errorHandlingStrategy());
2346 if (jobs.isEmpty() || std::all_of(jobs.begin(), jobs.end(), neither_scheduled_nor_aborted))
2347 {
2348 appendLogText(i18n("No jobs left in the scheduler queue after evaluating."));
2349 moduleState()->setActiveJob(nullptr);
2350 return;
2351 }
2352 /* If there are only aborted jobs that can run, reschedule those and let Scheduler restart one loop */
2353 else if (std::all_of(jobs.begin(), jobs.end(), finished_or_aborted) &&
2354 strategy != ERROR_DONT_RESTART)
2355 {
2356 appendLogText(i18n("Only aborted jobs left in the scheduler queue after evaluating, rescheduling those."));
2357 std::for_each(jobs.begin(), jobs.end(), [](SchedulerJob * job)
2358 {
2359 if (SCHEDJOB_ABORTED == job->getState())
2360 job->setState(SCHEDJOB_EVALUATION);
2361 });
2362
2363 return;
2364 }
2365
2366 // GreedyScheduler::scheduleJobs() must be called first.
2367 SchedulerJob *scheduledJob = getGreedyScheduler()->getScheduledJob();
2368 if (!scheduledJob)
2369 {
2370 appendLogText(i18n("No jobs scheduled."));
2371 moduleState()->setActiveJob(nullptr);
2372 return;
2373 }
2374 if (activeJob() != nullptr && scheduledJob != activeJob())
2375 {
2376 // Changing lead, therefore abort all follower jobs that are still running
2377 for (auto job : m_activeJobs.values())
2378 if (!job->isLead() && job->getState() == SCHEDJOB_BUSY)
2379 stopCapturing(job->getOpticalTrain(), false);
2380
2381 // clear the mapping camera name --> scheduler job
2382 m_activeJobs.clear();
2383 }
2384 moduleState()->setActiveJob(scheduledJob);
2385
2386}
2387
2389{
2390 // Reset all jobs
2391 // other states too?
2392 if (SCHEDULER_RUNNING != moduleState()->schedulerState())
2393 resetJobs();
2394
2395 // reset the iterations counter
2396 moduleState()->resetSequenceExecutionCounter();
2397
2398 // And evaluate all pending jobs per the conditions set in each
2399 evaluateJobs(true);
2400}
2401
2402void SchedulerProcess::evaluateJobs(bool evaluateOnly)
2403{
2404 for (auto job : moduleState()->jobs())
2405 job->clearCache();
2406
2407 /* Don't evaluate if list is empty */
2408 if (moduleState()->jobs().isEmpty())
2409 return;
2410 /* Start by refreshing the number of captures already present - unneeded if not remembering job progress */
2411 if (Options::rememberJobProgress())
2413
2414 moduleState()->calculateDawnDusk();
2415
2416 getGreedyScheduler()->scheduleJobs(moduleState()->jobs(), SchedulerModuleState::getLocalTime(),
2417 moduleState()->capturedFramesCount(), this);
2418
2419 // schedule or job states might have been changed, update the table
2420
2421 if (!evaluateOnly && moduleState()->schedulerState() == SCHEDULER_RUNNING)
2422 // At this step, we finished evaluating jobs.
2423 // We select the first job that has to be run, per schedule.
2424 selectActiveJob(moduleState()->jobs());
2425 else
2426 qCInfo(KSTARS_EKOS_SCHEDULER) << "Ekos finished evaluating jobs, no job selection required.";
2427
2428 emit jobsUpdated(moduleState()->getJSONJobs());
2429}
2430
2432{
2433 if (moduleState()->schedulerState() == SCHEDULER_PAUSED)
2434 {
2435 if (activeJob() == nullptr)
2436 {
2437 setPaused();
2438 return false;
2439 }
2440 switch (activeJob()->getState())
2441 {
2442 case SCHEDJOB_BUSY:
2443 // do nothing
2444 break;
2445 case SCHEDJOB_COMPLETE:
2446 // start finding next job before pausing
2447 break;
2448 default:
2449 // in all other cases pause
2450 setPaused();
2451 break;
2452 }
2453 }
2454
2455 // #1 If no current job selected, let's check if we need to shutdown or evaluate jobs
2456 if (activeJob() == nullptr)
2457 {
2458 // #2.1 If shutdown is already complete or in error, we need to stop
2459 if (moduleState()->shutdownState() == SHUTDOWN_COMPLETE
2460 || moduleState()->shutdownState() == SHUTDOWN_ERROR)
2461 {
2462 return completeShutdown();
2463 }
2464
2465 // #2.2 Check if shutdown is in progress
2466 if (moduleState()->shutdownState() > SHUTDOWN_IDLE)
2467 {
2468 // If Ekos is not done stopping, try again later
2469 if (moduleState()->ekosState() == EKOS_STOPPING && checkEkosState() == false)
2470 return false;
2471
2473 return false;
2474 }
2475
2476 // #2.3 Check if park wait procedure is in progress
2477 if (checkParkWaitState() == false)
2478 return false;
2479
2480 // #2.4 If not in shutdown state, evaluate the jobs
2481 evaluateJobs(false);
2482
2483 // #2.5 check if all jobs have completed and repeat is set
2484 if (nullptr == activeJob() && moduleState()->checkRepeatSequence())
2485 {
2486 // Reset all jobs
2487 resetJobs();
2488 // Re-evaluate all jobs to check whether there is at least one that might be executed
2489 evaluateJobs(false);
2490 // if there is an executable job, restart;
2491 if (activeJob())
2492 {
2493 moduleState()->increaseSequenceExecutionCounter();
2494 appendLogText(i18n("Starting job sequence iteration #%1", moduleState()->sequenceExecutionCounter()));
2495 return true;
2496 }
2497 }
2498
2499 // #2.6 If there is no current job after evaluation, shutdown
2500 if (nullptr == activeJob())
2501 {
2503 return false;
2504 }
2505 }
2506 // JM 2018-12-07: Check if we need to sleep
2507 else if (shouldSchedulerSleep(activeJob()) == false)
2508 {
2509 // #3 Check if startup procedure has failed.
2510 if (moduleState()->startupState() == STARTUP_ERROR)
2511 {
2512 // Stop Scheduler
2513 stop();
2514 return true;
2515 }
2516
2517 // #4 Check if startup procedure Phase #1 is complete (Startup script)
2518 if ((moduleState()->startupState() == STARTUP_IDLE
2519 && checkStartupState() == false)
2520 || moduleState()->startupState() == STARTUP_SCRIPT)
2521 return false;
2522
2523 // #5 Check if Ekos is started
2524 if (checkEkosState() == false)
2525 return false;
2526
2527 // #6 Check if INDI devices are connected.
2528 if (checkINDIState() == false)
2529 return false;
2530
2531 // #6.1 Check if park wait procedure is in progress - in the case we're waiting for a distant job
2532 if (checkParkWaitState() == false)
2533 return false;
2534
2535 // #7 Check if startup procedure Phase #2 is complete (Unparking phase)
2536 if (moduleState()->startupState() > STARTUP_SCRIPT
2537 && moduleState()->startupState() < STARTUP_ERROR
2538 && checkStartupState() == false)
2539 return false;
2540
2541 // #8 Check it it already completed (should only happen starting a paused job)
2542 // Find the next job in this case, otherwise execute the current one
2543 if (activeJob() && activeJob()->getState() == SCHEDJOB_COMPLETE)
2544 findNextJob();
2545
2546 // N.B. We explicitly do not check for return result here because regardless of execution result
2547 // we do not have any pending tasks further down.
2548 executeJob(activeJob());
2549 emit updateJobTable();
2550 }
2551
2552 return true;
2553}
2554
2556{
2557 qCDebug(KSTARS_EKOS_SCHEDULER) << "Get next action...";
2558
2559 switch (activeJob()->getStage())
2560 {
2561 case SCHEDSTAGE_IDLE:
2562 if (activeJob()->getLightFramesRequired())
2563 {
2564 if (activeJob()->getStepPipeline() & SchedulerJob::USE_TRACK)
2565 startSlew();
2566 else if (activeJob()->getStepPipeline() & SchedulerJob::USE_FOCUS && moduleState()->autofocusCompleted() == false)
2567 {
2568 qCDebug(KSTARS_EKOS_SCHEDULER) << "startFocusing on 3485";
2569 startFocusing();
2570 }
2571 else if (activeJob()->getStepPipeline() & SchedulerJob::USE_ALIGN)
2573 else if (activeJob()->getStepPipeline() & SchedulerJob::USE_GUIDE)
2574 if (getGuidingStatus() == GUIDE_GUIDING)
2575 {
2576 appendLogText(i18n("Guiding already running, directly start capturing."));
2577 startCapture();
2578 }
2579 else
2580 startGuiding();
2581 else
2582 startCapture();
2583 }
2584 else
2585 {
2586 if (activeJob()->getStepPipeline())
2588 i18n("Job '%1' is proceeding directly to capture stage because only calibration frames are pending.",
2589 activeJob()->getName()));
2590 startCapture();
2591 }
2592
2593 break;
2594
2595 case SCHEDSTAGE_SLEW_COMPLETE:
2596 if (activeJob()->getStepPipeline() & SchedulerJob::USE_FOCUS && moduleState()->autofocusCompleted() == false)
2597 {
2598 qCDebug(KSTARS_EKOS_SCHEDULER) << "startFocusing on 3514";
2599 startFocusing();
2600 }
2601 else if (activeJob()->getStepPipeline() & SchedulerJob::USE_ALIGN)
2603 else if (activeJob()->getStepPipeline() & SchedulerJob::USE_GUIDE)
2604 startGuiding();
2605 else
2606 startCapture();
2607 break;
2608
2609 case SCHEDSTAGE_FOCUS_COMPLETE:
2610 if (activeJob()->getStepPipeline() & SchedulerJob::USE_ALIGN)
2612 else if (activeJob()->getStepPipeline() & SchedulerJob::USE_GUIDE)
2613 startGuiding();
2614 else
2615 startCapture();
2616 break;
2617
2618 case SCHEDSTAGE_ALIGN_COMPLETE:
2619 moduleState()->updateJobStage(SCHEDSTAGE_RESLEWING);
2620 break;
2621
2622 case SCHEDSTAGE_RESLEWING_COMPLETE:
2623 // If we have in-sequence-focus in the sequence file then we perform post alignment focusing so that the focus
2624 // frame is ready for the capture module in-sequence-focus procedure.
2625 if ((activeJob()->getStepPipeline() & SchedulerJob::USE_FOCUS) && activeJob()->getInSequenceFocus())
2626 // Post alignment re-focusing
2627 {
2628 qCDebug(KSTARS_EKOS_SCHEDULER) << "startFocusing on 3544";
2629 startFocusing();
2630 }
2631 else if (activeJob()->getStepPipeline() & SchedulerJob::USE_GUIDE)
2632 startGuiding();
2633 else
2634 startCapture();
2635 break;
2636
2637 case SCHEDSTAGE_POSTALIGN_FOCUSING_COMPLETE:
2638 if (activeJob()->getStepPipeline() & SchedulerJob::USE_GUIDE)
2639 startGuiding();
2640 else
2641 startCapture();
2642 break;
2643
2644 case SCHEDSTAGE_GUIDING_COMPLETE:
2645 startCapture();
2646 break;
2647
2648 default:
2649 break;
2650 }
2651}
2652
2654{
2655 const int msSleep = runSchedulerIteration();
2656 if (msSleep < 0)
2657 return;
2658
2659 connect(&moduleState()->iterationTimer(), &QTimer::timeout, this, &SchedulerProcess::iterate, Qt::UniqueConnection);
2660 moduleState()->iterationTimer().setSingleShot(true);
2661 moduleState()->iterationTimer().start(msSleep);
2662
2663}
2664
2666{
2667 qint64 now = QDateTime::currentMSecsSinceEpoch();
2668 if (moduleState()->startMSecs() == 0)
2669 moduleState()->setStartMSecs(now);
2670
2671 // printStates(QString("\nrunScheduler Iteration %1 @ %2")
2672 // .arg(moduleState()->increaseSchedulerIteration())
2673 // .arg((now - moduleState()->startMSecs()) / 1000.0, 1, 'f', 3));
2674
2675 SchedulerTimerState keepTimerState = moduleState()->timerState();
2676
2677 // TODO: At some point we should require that timerState and timerInterval
2678 // be explicitly set in all iterations. Not there yet, would require too much
2679 // refactoring of the scheduler. When we get there, we'd exectute the following here:
2680 // timerState = RUN_NOTHING; // don't like this comment, it should always set a state and interval!
2681 // timerInterval = -1;
2682 moduleState()->setIterationSetup(false);
2683 switch (keepTimerState)
2684 {
2685 case RUN_WAKEUP:
2686 changeSleepLabel("", false);
2688 break;
2689 case RUN_SCHEDULER:
2690 checkStatus();
2691 break;
2692 case RUN_JOBCHECK:
2693 checkJobStage();
2694 break;
2695 case RUN_SHUTDOWN:
2697 break;
2698 case RUN_NOTHING:
2699 moduleState()->setTimerInterval(-1);
2700 break;
2701 }
2702 if (!moduleState()->iterationSetup())
2703 {
2704 // See the above TODO.
2705 // Since iterations aren't yet always set up, we repeat the current
2706 // iteration type if one wasn't set up in the current iteration.
2707 // qCDebug(KSTARS_EKOS_SCHEDULER) << "Scheduler iteration never set up.";
2708 moduleState()->setTimerInterval(moduleState()->updatePeriodMs());
2709 }
2710 // printStates(QString("End iteration, sleep %1: ").arg(moduleState()->timerInterval()));
2711 return moduleState()->timerInterval();
2712}
2713
2715{
2716 Q_ASSERT_X(activeJob(), __FUNCTION__, "Actual current job is required to check job stage");
2717 if (!activeJob())
2718 return;
2719
2720 if (checkJobStageCounter == 0)
2721 {
2722 qCDebug(KSTARS_EKOS_SCHEDULER) << "Checking job stage for" << activeJob()->getName() << "startup" <<
2723 activeJob()->getStartupCondition() << activeJob()->getStartupTime().toString() << "state" << activeJob()->getState();
2724 if (checkJobStageCounter++ == 30)
2725 checkJobStageCounter = 0;
2726 }
2727
2728 emit syncGreedyParams();
2729 if (!getGreedyScheduler()->checkJob(moduleState()->leadJobs(), SchedulerModuleState::getLocalTime(), activeJob()))
2730 {
2731 activeJob()->setState(SCHEDJOB_IDLE);
2733 findNextJob();
2734 return;
2735 }
2736 checkJobStageEpilogue();
2737}
2738
2739void SchedulerProcess::checkJobStageEpilogue()
2740{
2741 if (!activeJob())
2742 return;
2743
2744 // #5 Check system status to improve robustness
2745 // This handles external events such as disconnections or end-user manipulating INDI panel
2746 if (!checkStatus())
2747 return;
2748
2749 // #5b Check the guiding timer, and possibly restart guiding.
2751
2752 // #6 Check each stage is processing properly
2753 // FIXME: Vanishing property should trigger a call to its event callback
2754 if (!activeJob()) return;
2755 switch (activeJob()->getStage())
2756 {
2757 case SCHEDSTAGE_IDLE:
2758 // Job is just starting.
2759 emit jobStarted(activeJob()->getName());
2760 getNextAction();
2761 break;
2762
2763 case SCHEDSTAGE_ALIGNING:
2764 // Let's make sure align module does not become unresponsive
2765 if (moduleState()->getCurrentOperationMsec() > static_cast<int>(ALIGN_INACTIVITY_TIMEOUT))
2766 {
2767 QVariant const status = alignInterface()->property("status");
2768 Ekos::AlignState alignStatus = static_cast<Ekos::AlignState>(status.toInt());
2769
2770 if (alignStatus == Ekos::ALIGN_IDLE)
2771 {
2772 if (moduleState()->increaseAlignFailureCount())
2773 {
2774 qCDebug(KSTARS_EKOS_SCHEDULER) << "Align module timed out. Restarting request...";
2776 }
2777 else
2778 {
2779 appendLogText(i18n("Warning: job '%1' alignment procedure failed, marking aborted.", activeJob()->getName()));
2780 activeJob()->setState(SCHEDJOB_ABORTED);
2781 findNextJob();
2782 }
2783 }
2784 else
2785 moduleState()->startCurrentOperationTimer();
2786 }
2787 break;
2788
2789 case SCHEDSTAGE_CAPTURING:
2790 // Let's make sure capture module does not become unresponsive
2791 if (moduleState()->getCurrentOperationMsec() > static_cast<int>(CAPTURE_INACTIVITY_TIMEOUT))
2792 {
2793 QVariant const status = captureInterface()->property("status");
2794 Ekos::CaptureState captureStatus = static_cast<Ekos::CaptureState>(status.toInt());
2795
2796 if (captureStatus == Ekos::CAPTURE_IDLE)
2797 {
2798 if (moduleState()->increaseCaptureFailureCount())
2799 {
2800 qCDebug(KSTARS_EKOS_SCHEDULER) << "capture module timed out. Restarting request...";
2801 startCapture();
2802 }
2803 else
2804 {
2805 appendLogText(i18n("Warning: job '%1' capture procedure failed, marking aborted.", activeJob()->getName()));
2806 activeJob()->setState(SCHEDJOB_ABORTED);
2807 findNextJob();
2808 }
2809 }
2810 else moduleState()->startCurrentOperationTimer();
2811 }
2812 break;
2813
2814 case SCHEDSTAGE_FOCUSING:
2815 // Let's make sure focus module does not become unresponsive
2816 if (moduleState()->getCurrentOperationMsec() > static_cast<int>(FOCUS_INACTIVITY_TIMEOUT))
2817 {
2818 QVariant const status = focusInterface()->property("status");
2819 Ekos::FocusState focusStatus = static_cast<Ekos::FocusState>(status.toInt());
2820
2821 if (focusStatus == Ekos::FOCUS_IDLE || focusStatus == Ekos::FOCUS_WAITING)
2822 {
2823 if (moduleState()->increaseFocusFailureCount())
2824 {
2825 qCDebug(KSTARS_EKOS_SCHEDULER) << "Focus module timed out. Restarting request...";
2826 startFocusing();
2827 }
2828 else
2829 {
2830 appendLogText(i18n("Warning: job '%1' focusing procedure failed, marking aborted.", activeJob()->getName()));
2831 activeJob()->setState(SCHEDJOB_ABORTED);
2832 findNextJob();
2833 }
2834 }
2835 else moduleState()->startCurrentOperationTimer();
2836 }
2837 break;
2838
2839 case SCHEDSTAGE_GUIDING:
2840 // Let's make sure guide module does not become unresponsive
2841 if (moduleState()->getCurrentOperationMsec() > GUIDE_INACTIVITY_TIMEOUT)
2842 {
2843 GuideState guideStatus = getGuidingStatus();
2844
2845 if (guideStatus == Ekos::GUIDE_IDLE || guideStatus == Ekos::GUIDE_CONNECTED || guideStatus == Ekos::GUIDE_DISCONNECTED)
2846 {
2847 if (moduleState()->increaseGuideFailureCount())
2848 {
2849 qCDebug(KSTARS_EKOS_SCHEDULER) << "guide module timed out. Restarting request...";
2850 startGuiding();
2851 }
2852 else
2853 {
2854 appendLogText(i18n("Warning: job '%1' guiding procedure failed, marking aborted.", activeJob()->getName()));
2855 activeJob()->setState(SCHEDJOB_ABORTED);
2856 findNextJob();
2857 }
2858 }
2859 else moduleState()->startCurrentOperationTimer();
2860 }
2861 break;
2862
2863 case SCHEDSTAGE_SLEWING:
2864 case SCHEDSTAGE_RESLEWING:
2865 // While slewing or re-slewing, check slew status can still be obtained
2866 {
2867 QVariant const slewStatus = mountInterface()->property("status");
2868
2869 if (slewStatus.isValid())
2870 {
2871 // Send the slew status periodically to avoid the situation where the mount is already at location and does not send any event
2872 // FIXME: in that case, filter TRACKING events only?
2873 ISD::Mount::Status const status = static_cast<ISD::Mount::Status>(slewStatus.toInt());
2874 setMountStatus(status);
2875 }
2876 else
2877 {
2878 appendLogText(i18n("Warning: job '%1' lost connection to the mount, attempting to reconnect.", activeJob()->getName()));
2879 if (!manageConnectionLoss())
2880 activeJob()->setState(SCHEDJOB_ERROR);
2881 return;
2882 }
2883 }
2884 break;
2885
2886 case SCHEDSTAGE_SLEW_COMPLETE:
2887 case SCHEDSTAGE_RESLEWING_COMPLETE:
2888 // When done slewing or re-slewing and we use a dome, only shift to the next action when the dome is done moving
2889 if (moduleState()->domeReady())
2890 {
2891 QVariant const isDomeMoving = domeInterface()->property("isMoving");
2892
2893 if (!isDomeMoving.isValid())
2894 {
2895 appendLogText(i18n("Warning: job '%1' lost connection to the dome, attempting to reconnect.", activeJob()->getName()));
2896 if (!manageConnectionLoss())
2897 activeJob()->setState(SCHEDJOB_ERROR);
2898 return;
2899 }
2900
2901 if (!isDomeMoving.value<bool>())
2902 getNextAction();
2903 }
2904 else getNextAction();
2905 break;
2906
2907 default:
2908 break;
2909 }
2910}
2911
2913{
2914 moduleState()->calculateDawnDusk();
2915
2916 if (SCHEDULER_RUNNING != moduleState()->schedulerState())
2917 {
2918 evaluateJobs(true);
2919 }
2920}
2921
2922bool SchedulerProcess::executeJob(SchedulerJob * job)
2923{
2924 if (job == nullptr)
2925 return false;
2926
2927 // Don't execute the current job if it is already busy
2928 if (activeJob() == job && SCHEDJOB_BUSY == activeJob()->getState())
2929 return false;
2930
2931 moduleState()->setActiveJob(job);
2932
2933 // If we already started, we check when the next object is scheduled at.
2934 // If it is more than 30 minutes in the future, we park the mount if that is supported
2935 // and we unpark when it is due to start.
2936 //int const nextObservationTime = now.secsTo(getActiveJob()->getStartupTime());
2937
2938 // If the time to wait is greater than the lead time (5 minutes by default)
2939 // then we sleep, otherwise we wait. It's the same thing, just different labels.
2940 if (shouldSchedulerSleep(activeJob()))
2941 return false;
2942 // If job schedule isn't now, wait - continuing to execute would cancel a parking attempt
2943 else if (0 < SchedulerModuleState::getLocalTime().secsTo(activeJob()->getStartupTime()))
2944 return false;
2945
2946 // From this point job can be executed now
2947
2948 if (job->getCompletionCondition() == FINISH_SEQUENCE && Options::rememberJobProgress())
2949 captureInterface()->setProperty("targetName", job->getName());
2950
2951 moduleState()->calculateDawnDusk();
2952
2953 // Reset autofocus so that focus step is applied properly when checked
2954 // When the focus step is not checked, the capture module will eventually run focus periodically
2955 moduleState()->setAutofocusCompleted(false);
2956
2957 qCInfo(KSTARS_EKOS_SCHEDULER) << "Executing Job " << activeJob()->getName();
2958
2959 activeJob()->setState(SCHEDJOB_BUSY);
2960 emit jobsUpdated(moduleState()->getJSONJobs());
2961
2962 KSNotification::event(QLatin1String("EkosSchedulerJobStart"),
2963 i18n("Ekos job started (%1)", activeJob()->getName()), KSNotification::Scheduler);
2964
2965 // No need to continue evaluating jobs as we already have one.
2966 moduleState()->setupNextIteration(RUN_JOBCHECK);
2967 return true;
2968}
2969
2971{
2972 QFile file;
2973 file.setFileName(fileURL.toLocalFile());
2974
2975 if (!file.open(QIODevice::WriteOnly))
2976 {
2977 QString message = i18n("Unable to write to file %1", fileURL.toLocalFile());
2978 KSNotification::sorry(message, i18n("Could Not Open File"));
2979 return false;
2980 }
2981
2982 QTextStream outstream(&file);
2983
2984 // We serialize sequence data to XML using the C locale
2985 QLocale cLocale = QLocale::c();
2986
2987 outstream << "<?xml version=\"1.0\" encoding=\"UTF-8\"?>" << Qt::endl;
2988 outstream << "<SchedulerList version='2.0'>" << Qt::endl;
2989 // ensure to escape special XML characters
2990 outstream << "<Profile>" << QString(entityXML(strdup(moduleState()->currentProfile().toStdString().c_str()))) <<
2991 "</Profile>" << Qt::endl;
2992
2993 auto tiles = KStarsData::Instance()->skyComposite()->mosaicComponent()->tiles();
2994 bool useMosaicInfo = !tiles->sequenceFile().isEmpty();
2995
2996 if (useMosaicInfo)
2997 {
2998 outstream << "<Mosaic>" << Qt::endl;
2999 outstream << "<Target>" << tiles->targetName() << "</Target>" << Qt::endl;
3000 outstream << "<Group>" << tiles->group() << "</Group>" << Qt::endl;
3001
3002 QString ccArg, ccValue = tiles->completionCondition(&ccArg);
3003 if (ccValue == "FinishSequence")
3004 outstream << "<FinishSequence/>" << Qt::endl;
3005 else if (ccValue == "FinishLoop")
3006 outstream << "<FinishLoop/>" << Qt::endl;
3007 else if (ccValue == "FinishRepeat")
3008 outstream << "<FinishRepeat>" << ccArg << "</FinishRepeat>" << Qt::endl;
3009
3010 outstream << "<Sequence>" << tiles->sequenceFile() << "</Sequence>" << Qt::endl;
3011 outstream << "<Directory>" << tiles->outputDirectory() << "</Directory>" << Qt::endl;
3012
3013 outstream << "<FocusEveryN>" << tiles->focusEveryN() << "</FocusEveryN>" << Qt::endl;
3014 outstream << "<AlignEveryN>" << tiles->alignEveryN() << "</AlignEveryN>" << Qt::endl;
3015 if (tiles->isTrackChecked())
3016 outstream << "<TrackChecked/>" << Qt::endl;
3017 if (tiles->isFocusChecked())
3018 outstream << "<FocusChecked/>" << Qt::endl;
3019 if (tiles->isAlignChecked())
3020 outstream << "<AlignChecked/>" << Qt::endl;
3021 if (tiles->isGuideChecked())
3022 outstream << "<GuideChecked/>" << Qt::endl;
3023 outstream << "<Overlap>" << cLocale.toString(tiles->overlap()) << "</Overlap>" << Qt::endl;
3024 outstream << "<CenterRA>" << cLocale.toString(tiles->ra0().Hours()) << "</CenterRA>" << Qt::endl;
3025 outstream << "<CenterDE>" << cLocale.toString(tiles->dec0().Degrees()) << "</CenterDE>" << Qt::endl;
3026 outstream << "<GridW>" << tiles->gridSize().width() << "</GridW>" << Qt::endl;
3027 outstream << "<GridH>" << tiles->gridSize().height() << "</GridH>" << Qt::endl;
3028 outstream << "<FOVW>" << cLocale.toString(tiles->mosaicFOV().width()) << "</FOVW>" << Qt::endl;
3029 outstream << "<FOVH>" << cLocale.toString(tiles->mosaicFOV().height()) << "</FOVH>" << Qt::endl;
3030 outstream << "<CameraFOVW>" << cLocale.toString(tiles->cameraFOV().width()) << "</CameraFOVW>" << Qt::endl;
3031 outstream << "<CameraFOVH>" << cLocale.toString(tiles->cameraFOV().height()) << "</CameraFOVH>" << Qt::endl;
3032 outstream << "</Mosaic>" << Qt::endl;
3033 }
3034
3035 int index = 0;
3036 for (auto &job : moduleState()->jobs())
3037 {
3038 outstream << "<Job>" << Qt::endl;
3039
3040 // ensure to escape special XML characters
3041 outstream << "<JobType lead='" << (job->isLead() ? "true" : "false") << "'/>" << Qt::endl;
3042 if (job->isLead())
3043 {
3044 outstream << "<Name>" << QString(entityXML(strdup(job->getName().toStdString().c_str()))) << "</Name>" << Qt::endl;
3045 outstream << "<Group>" << QString(entityXML(strdup(job->getGroup().toStdString().c_str()))) << "</Group>" << Qt::endl;
3046 outstream << "<Coordinates>" << Qt::endl;
3047 outstream << "<J2000RA>" << cLocale.toString(job->getTargetCoords().ra0().Hours()) << "</J2000RA>" << Qt::endl;
3048 outstream << "<J2000DE>" << cLocale.toString(job->getTargetCoords().dec0().Degrees()) << "</J2000DE>" << Qt::endl;
3049 outstream << "</Coordinates>" << Qt::endl;
3050 }
3051
3052 if (! job->getOpticalTrain().isEmpty())
3053 outstream << "<OpticalTrain>" << QString(entityXML(strdup(job->getOpticalTrain().toStdString().c_str()))) <<
3054 "</OpticalTrain>" << Qt::endl;
3055
3056 if (job->isLead() && job->getFITSFile().isValid() && job->getFITSFile().isEmpty() == false)
3057 outstream << "<FITS>" << job->getFITSFile().toLocalFile() << "</FITS>" << Qt::endl;
3058 else
3059 outstream << "<PositionAngle>" << job->getPositionAngle() << "</PositionAngle>" << Qt::endl;
3060
3061 outstream << "<Sequence>" << job->getSequenceFile().toLocalFile() << "</Sequence>" << Qt::endl;
3062
3063 if (useMosaicInfo && index < tiles->tiles().size())
3064 {
3065 auto oneTile = tiles->tiles().at(index++);
3066 outstream << "<TileCenter>" << Qt::endl;
3067 outstream << "<X>" << cLocale.toString(oneTile->center.x()) << "</X>" << Qt::endl;
3068 outstream << "<Y>" << cLocale.toString(oneTile->center.y()) << "</Y>" << Qt::endl;
3069 outstream << "<Rotation>" << cLocale.toString(oneTile->rotation) << "</Rotation>" << Qt::endl;
3070 outstream << "</TileCenter>" << Qt::endl;
3071 }
3072
3073 if (job->isLead())
3074 {
3075 outstream << "<StartupCondition>" << Qt::endl;
3076 if (job->getFileStartupCondition() == START_ASAP)
3077 outstream << "<Condition>ASAP</Condition>" << Qt::endl;
3078 else if (job->getFileStartupCondition() == START_AT)
3079 outstream << "<Condition value='" << job->getStartAtTime().toString(Qt::ISODate) << "'>At</Condition>"
3080 << Qt::endl;
3081 outstream << "</StartupCondition>" << Qt::endl;
3082
3083 outstream << "<Constraints>" << Qt::endl;
3084 if (job->hasMinAltitude())
3085 outstream << "<Constraint value='" << cLocale.toString(job->getMinAltitude()) << "'>MinimumAltitude</Constraint>" <<
3086 Qt::endl;
3087 if (job->getMinMoonSeparation() > 0)
3088 outstream << "<Constraint value='" << cLocale.toString(job->getMinMoonSeparation()) << "'>MoonSeparation</Constraint>"
3089 << Qt::endl;
3090 if (job->getEnforceWeather())
3091 outstream << "<Constraint>EnforceWeather</Constraint>" << Qt::endl;
3092 if (job->getEnforceTwilight())
3093 outstream << "<Constraint>EnforceTwilight</Constraint>" << Qt::endl;
3094 if (job->getEnforceArtificialHorizon())
3095 outstream << "<Constraint>EnforceArtificialHorizon</Constraint>" << Qt::endl;
3096 outstream << "</Constraints>" << Qt::endl;
3097 }
3098
3099 outstream << "<CompletionCondition>" << Qt::endl;
3100 if (job->getCompletionCondition() == FINISH_SEQUENCE)
3101 outstream << "<Condition>Sequence</Condition>" << Qt::endl;
3102 else if (job->getCompletionCondition() == FINISH_REPEAT)
3103 outstream << "<Condition value='" << cLocale.toString(job->getRepeatsRequired()) << "'>Repeat</Condition>" << Qt::endl;
3104 else if (job->getCompletionCondition() == FINISH_LOOP)
3105 outstream << "<Condition>Loop</Condition>" << Qt::endl;
3106 else if (job->getCompletionCondition() == FINISH_AT)
3107 outstream << "<Condition value='" << job->getFinishAtTime().toString(Qt::ISODate) << "'>At</Condition>"
3108 << Qt::endl;
3109 outstream << "</CompletionCondition>" << Qt::endl;
3110
3111 if (job->isLead())
3112 {
3113 outstream << "<Steps>" << Qt::endl;
3114 if (job->getStepPipeline() & SchedulerJob::USE_TRACK)
3115 outstream << "<Step>Track</Step>" << Qt::endl;
3116 if (job->getStepPipeline() & SchedulerJob::USE_FOCUS)
3117 outstream << "<Step>Focus</Step>" << Qt::endl;
3118 if (job->getStepPipeline() & SchedulerJob::USE_ALIGN)
3119 outstream << "<Step>Align</Step>" << Qt::endl;
3120 if (job->getStepPipeline() & SchedulerJob::USE_GUIDE)
3121 outstream << "<Step>Guide</Step>" << Qt::endl;
3122 outstream << "</Steps>" << Qt::endl;
3123 }
3124 outstream << "</Job>" << Qt::endl;
3125 }
3126
3127 outstream << "<SchedulerAlgorithm value='" << ALGORITHM_GREEDY << "'/>" << Qt::endl;
3128 outstream << "<ErrorHandlingStrategy value='" << Options::errorHandlingStrategy() << "'>" << Qt::endl;
3129 if (Options::rescheduleErrors())
3130 outstream << "<RescheduleErrors />" << Qt::endl;
3131 outstream << "<delay>" << Options::errorHandlingStrategyDelay() << "</delay>" << Qt::endl;
3132 outstream << "</ErrorHandlingStrategy>" << Qt::endl;
3133
3134 outstream << "<StartupProcedure>" << Qt::endl;
3135 if (moduleState()->startupScriptURL().isEmpty() == false)
3136 outstream << "<Procedure value='" << moduleState()->startupScriptURL().toString(QUrl::PreferLocalFile) <<
3137 "'>StartupScript</Procedure>" << Qt::endl;
3138 if (Options::schedulerUnparkDome())
3139 outstream << "<Procedure>UnparkDome</Procedure>" << Qt::endl;
3140 if (Options::schedulerUnparkMount())
3141 outstream << "<Procedure>UnparkMount</Procedure>" << Qt::endl;
3142 if (Options::schedulerOpenDustCover())
3143 outstream << "<Procedure>UnparkCap</Procedure>" << Qt::endl;
3144 outstream << "</StartupProcedure>" << Qt::endl;
3145
3146 outstream << "<ShutdownProcedure>" << Qt::endl;
3147 if (Options::schedulerWarmCCD())
3148 outstream << "<Procedure>WarmCCD</Procedure>" << Qt::endl;
3149 if (Options::schedulerCloseDustCover())
3150 outstream << "<Procedure>ParkCap</Procedure>" << Qt::endl;
3151 if (Options::schedulerParkMount())
3152 outstream << "<Procedure>ParkMount</Procedure>" << Qt::endl;
3153 if (Options::schedulerParkDome())
3154 outstream << "<Procedure>ParkDome</Procedure>" << Qt::endl;
3155 if (moduleState()->shutdownScriptURL().isEmpty() == false)
3156 outstream << "<Procedure value='" << moduleState()->shutdownScriptURL().toString(QUrl::PreferLocalFile) <<
3157 "'>schedulerStartupScript</Procedure>" <<
3158 Qt::endl;
3159 outstream << "</ShutdownProcedure>" << Qt::endl;
3160
3161 outstream << "</SchedulerList>" << Qt::endl;
3162
3163 appendLogText(i18n("Scheduler list saved to %1", fileURL.toLocalFile()));
3164 file.close();
3165 moduleState()->setDirty(false);
3166 return true;
3167}
3168
3169void SchedulerProcess::checkAlignment(const QVariantMap &metadata, const QString &trainname)
3170{
3171 // check if the metadata comes from the lead job
3172 if (activeJob() == nullptr || (activeJob()->getOpticalTrain() != "" && activeJob()->getOpticalTrain() != trainname))
3173 {
3174 qCDebug(KSTARS_EKOS_SCHEDULER) << "Ignoring metadata from train =" << trainname << "for alignment check.";
3175 return;
3176 }
3177
3178 if (activeJob()->getStepPipeline() & SchedulerJob::USE_ALIGN &&
3179 metadata["type"].toInt() == FRAME_LIGHT &&
3180 Options::alignCheckFrequency() > 0 &&
3181 moduleState()->increaseSolverIteration() >= Options::alignCheckFrequency())
3182 {
3183 moduleState()->resetSolverIteration();
3184
3185 auto filename = metadata["filename"].toString();
3186 auto exposure = metadata["exposure"].toDouble();
3187
3188 qCDebug(KSTARS_EKOS_SCHEDULER) << "Checking alignment on train =" << trainname << "for" << filename;
3189
3190 constexpr double minSolverSeconds = 5.0;
3191 double solverTimeout = std::max(exposure - 2, minSolverSeconds);
3192 if (solverTimeout >= minSolverSeconds)
3193 {
3194 auto profiles = getDefaultAlignOptionsProfiles();
3195
3196 SSolver::Parameters parameters;
3197 // Get solver parameters
3198 // In case of exception, use first profile
3199 try
3200 {
3201 parameters = profiles.at(Options::solveOptionsProfile());
3202 }
3203 catch (std::out_of_range const &)
3204 {
3205 parameters = profiles[0];
3206 }
3207
3208 // Double search radius
3209 parameters.search_radius = parameters.search_radius * 2;
3210 m_Solver.reset(new SolverUtils(parameters, solverTimeout), &QObject::deleteLater);
3211 connect(m_Solver.get(), &SolverUtils::done, this, &Ekos::SchedulerProcess::solverDone, Qt::UniqueConnection);
3212 //connect(m_Solver.get(), &SolverUtils::newLog, this, &Ekos::Scheduler::appendLogText, Qt::UniqueConnection);
3213
3214 auto width = metadata["width"].toUInt() / (metadata["binx"].isValid() ? metadata["binx"].toUInt() : 1);
3215 auto height = metadata["height"].toUInt() / (metadata["biny"].isValid() ? metadata["biny"].toUInt() : 1);
3216
3217 auto lowScale = Options::astrometryImageScaleLow();
3218 auto highScale = Options::astrometryImageScaleHigh();
3219
3220 // solver utils uses arcsecs per pixel only
3221 if (Options::astrometryImageScaleUnits() == SSolver::DEG_WIDTH)
3222 {
3223 lowScale = (lowScale * 3600) / std::max(width, height);
3224 highScale = (highScale * 3600) / std::min(width, height);
3225 }
3226 else if (Options::astrometryImageScaleUnits() == SSolver::ARCMIN_WIDTH)
3227 {
3228 lowScale = (lowScale * 60) / std::max(width, height);
3229 highScale = (highScale * 60) / std::min(width, height);
3230 }
3231
3232 m_Solver->useScale(Options::astrometryUseImageScale(), lowScale, highScale);
3233 m_Solver->usePosition(Options::astrometryUsePosition(), activeJob()->getTargetCoords().ra().Degrees(),
3234 activeJob()->getTargetCoords().dec().Degrees());
3235 m_Solver->setHealpix(moduleState()->indexToUse(), moduleState()->healpixToUse());
3236 m_Solver->runSolver(filename);
3237 }
3238 }
3239}
3240
3241void SchedulerProcess::solverDone(bool timedOut, bool success, const FITSImage::Solution &solution, double elapsedSeconds)
3242{
3243 disconnect(m_Solver.get(), &SolverUtils::done, this, &Ekos::SchedulerProcess::solverDone);
3244
3245 if (!activeJob())
3246 return;
3247
3248 QString healpixString = "";
3249 if (moduleState()->indexToUse() != -1 || moduleState()->healpixToUse() != -1)
3250 healpixString = QString("Healpix %1 Index %2").arg(moduleState()->healpixToUse()).arg(moduleState()->indexToUse());
3251
3252 if (timedOut || !success)
3253 {
3254 // Don't use the previous index and healpix next time we solve.
3255 moduleState()->setIndexToUse(-1);
3256 moduleState()->setHealpixToUse(-1);
3257 }
3258 else
3259 {
3260 int index, healpix;
3261 // Get the index and healpix from the successful solve.
3262 m_Solver->getSolutionHealpix(&index, &healpix);
3263 moduleState()->setIndexToUse(index);
3264 moduleState()->setHealpixToUse(healpix);
3265 }
3266
3267 if (timedOut)
3268 appendLogText(i18n("Solver timed out: %1s %2", QString("%L1").arg(elapsedSeconds, 0, 'f', 1), healpixString));
3269 else if (!success)
3270 appendLogText(i18n("Solver failed: %1s %2", QString("%L1").arg(elapsedSeconds, 0, 'f', 1), healpixString));
3271 else
3272 {
3273 const double ra = solution.ra;
3274 const double dec = solution.dec;
3275
3276 const auto target = activeJob()->getTargetCoords();
3277
3278 SkyPoint alignCoord;
3279 alignCoord.setRA0(ra / 15.0);
3280 alignCoord.setDec0(dec);
3281 alignCoord.apparentCoord(static_cast<long double>(J2000), KStars::Instance()->data()->ut().djd());
3282 alignCoord.EquatorialToHorizontal(KStarsData::Instance()->lst(), KStarsData::Instance()->geo()->lat());
3283 const double diffRa = (alignCoord.ra().deltaAngle(target.ra())).Degrees() * 3600;
3284 const double diffDec = (alignCoord.dec().deltaAngle(target.dec())).Degrees() * 3600;
3285
3286 // This is an approximation, probably ok for small angles.
3287 const double diffTotal = hypot(diffRa, diffDec);
3288
3289 // Note--the RA output is in DMS. This is because we're looking at differences in arcseconds
3290 // and HMS coordinates are misleading (one HMS second is really 6 arc-seconds).
3291 qCDebug(KSTARS_EKOS_SCHEDULER) <<
3292 QString("Target Distance: %1\" Target (RA: %2 DE: %3) Current (RA: %4 DE: %5) %6 solved in %7s")
3293 .arg(QString("%L1").arg(diffTotal, 0, 'f', 0),
3294 target.ra().toDMSString(),
3295 target.dec().toDMSString(),
3296 alignCoord.ra().toDMSString(),
3297 alignCoord.dec().toDMSString(),
3298 healpixString,
3299 QString("%L1").arg(elapsedSeconds, 0, 'f', 2));
3300 emit targetDistance(diffTotal);
3301
3302 // If we exceed align check threshold, we abort and re-align.
3303 if (diffTotal / 60 > Options::alignCheckThreshold())
3304 {
3305 appendLogText(i18n("Captured frame is %1 arcminutes away from target, re-aligning...", QString::number(diffTotal / 60.0,
3306 'f', 1)));
3309 }
3310 }
3311}
3312
3314{
3315 SchedulerState const old_state = moduleState()->schedulerState();
3316 moduleState()->setSchedulerState(SCHEDULER_LOADING);
3317
3318 QFile sFile;
3319 sFile.setFileName(fileURL);
3320
3321 if (!sFile.open(QIODevice::ReadOnly))
3322 {
3323 QString message = i18n("Unable to open file %1", fileURL);
3324 KSNotification::sorry(message, i18n("Could Not Open File"));
3325 moduleState()->setSchedulerState(old_state);
3326 return false;
3327 }
3328
3329 LilXML *xmlParser = newLilXML();
3330 char errmsg[MAXRBUF];
3331 XMLEle *root = nullptr;
3332 XMLEle *ep = nullptr;
3333 XMLEle *subEP = nullptr;
3334 char c;
3335
3336 // We expect all data read from the XML to be in the C locale - QLocale::c()
3337 QLocale cLocale = QLocale::c();
3338
3339 // remember previous job
3340 SchedulerJob *lastLead = nullptr;
3341
3342 // retrieve optical trains names to ensure that only known trains are used
3343 const QStringList allTrainNames = OpticalTrainManager::Instance()->getTrainNames();
3344 QStringList remainingTrainNames = allTrainNames;
3345
3346 while (sFile.getChar(&c))
3347 {
3348 root = readXMLEle(xmlParser, c, errmsg);
3349
3350 if (root)
3351 {
3352 for (ep = nextXMLEle(root, 1); ep != nullptr; ep = nextXMLEle(root, 0))
3353 {
3354 const char *tag = tagXMLEle(ep);
3355 if (!strcmp(tag, "Job"))
3356 {
3357 SchedulerJob *newJob = SchedulerUtils::createJob(ep, lastLead);
3358 // remember new lead if such one has been created
3359 if (newJob->isLead())
3360 {
3361 lastLead = newJob;
3362 // reset usable train names
3363 remainingTrainNames = allTrainNames;
3364 }
3365 // check train name
3366 const QString trainname = newJob->getOpticalTrain();
3367 bool allowedName = (newJob->isLead() && trainname.isEmpty()) || allTrainNames.contains(trainname);
3368 bool availableName = (newJob->isLead() && trainname.isEmpty()) || !remainingTrainNames.isEmpty();
3369
3370 if (!allowedName && availableName)
3371 {
3372 const QString message = trainname.isEmpty() ?
3373 i18n("Warning: train name is empty, selecting \"%1\".", remainingTrainNames.first()) :
3374 i18n("Warning: train name %2 does not exist, selecting \"%1\".", remainingTrainNames.first(), trainname);
3375 appendLogText(message);
3376 if(KMessageBox::warningContinueCancel(nullptr, message, i18n("Select optical train"), KStandardGuiItem::cont(),
3377 KStandardGuiItem::cancel(), "correct_missing_train_warning") != KMessageBox::Continue)
3378 break;
3379
3380 newJob->setOpticalTrain(remainingTrainNames.first());
3381 remainingTrainNames.removeFirst();
3382 }
3383 else if (!availableName)
3384 {
3385 const QString message = i18n("Warning: no available train name for scheduler job, select the optical train name manually.");
3386 appendLogText(message);
3387
3388 if(KMessageBox::warningContinueCancel(nullptr, message, i18n("Select optical train"), KStandardGuiItem::cont(),
3389 KStandardGuiItem::cancel(), "correct_missing_train_warning") != KMessageBox::Continue)
3390 break;
3391 }
3392
3393 emit addJob(newJob);
3394 }
3395 else if (!strcmp(tag, "Mosaic"))
3396 {
3397 // If we have mosaic info, load it up.
3398 auto tiles = KStarsData::Instance()->skyComposite()->mosaicComponent()->tiles();
3399 tiles->fromXML(fileURL);
3400 }
3401 else if (!strcmp(tag, "Profile"))
3402 {
3403 moduleState()->setCurrentProfile(pcdataXMLEle(ep));
3404 }
3405 // disabled, there is only one algorithm
3406 else if (!strcmp(tag, "SchedulerAlgorithm"))
3407 {
3408 int algIndex = cLocale.toInt(findXMLAttValu(ep, "value"));
3409 if (algIndex != ALGORITHM_GREEDY)
3410 appendLogText(i18n("Warning: The Classic scheduler algorithm has been retired. Switching you to the Greedy algorithm."));
3411 }
3412 else if (!strcmp(tag, "ErrorHandlingStrategy"))
3413 {
3414 Options::setErrorHandlingStrategy(static_cast<ErrorHandlingStrategy>(cLocale.toInt(findXMLAttValu(ep,
3415 "value"))));
3416
3417 subEP = findXMLEle(ep, "delay");
3418 if (subEP)
3419 {
3420 Options::setErrorHandlingStrategyDelay(cLocale.toInt(pcdataXMLEle(subEP)));
3421 }
3422 subEP = findXMLEle(ep, "RescheduleErrors");
3423 Options::setRescheduleErrors(subEP != nullptr);
3424 }
3425 else if (!strcmp(tag, "StartupProcedure"))
3426 {
3427 XMLEle *procedure;
3428 Options::setSchedulerUnparkDome(false);
3429 Options::setSchedulerUnparkMount(false);
3430 Options::setSchedulerOpenDustCover(false);
3431
3432 for (procedure = nextXMLEle(ep, 1); procedure != nullptr; procedure = nextXMLEle(ep, 0))
3433 {
3434 const char *proc = pcdataXMLEle(procedure);
3435
3436 if (!strcmp(proc, "StartupScript"))
3437 {
3438 moduleState()->setStartupScriptURL(QUrl::fromUserInput(findXMLAttValu(procedure, "value")));
3439 }
3440 else if (!strcmp(proc, "UnparkDome"))
3441 Options::setSchedulerUnparkDome(true);
3442 else if (!strcmp(proc, "UnparkMount"))
3443 Options::setSchedulerUnparkMount(true);
3444 else if (!strcmp(proc, "UnparkCap"))
3445 Options::setSchedulerOpenDustCover(true);
3446 }
3447 }
3448 else if (!strcmp(tag, "ShutdownProcedure"))
3449 {
3450 XMLEle *procedure;
3451 Options::setSchedulerWarmCCD(false);
3452 Options::setSchedulerParkDome(false);
3453 Options::setSchedulerParkMount(false);
3454 Options::setSchedulerCloseDustCover(false);
3455
3456 for (procedure = nextXMLEle(ep, 1); procedure != nullptr; procedure = nextXMLEle(ep, 0))
3457 {
3458 const char *proc = pcdataXMLEle(procedure);
3459
3460 if (!strcmp(proc, "ShutdownScript"))
3461 {
3462 moduleState()->setShutdownScriptURL(QUrl::fromUserInput(findXMLAttValu(procedure, "value")));
3463 }
3464 else if (!strcmp(proc, "WarmCCD"))
3465 Options::setSchedulerWarmCCD(true);
3466 else if (!strcmp(proc, "ParkDome"))
3467 Options::setSchedulerParkDome(true);
3468 else if (!strcmp(proc, "ParkMount"))
3469 Options::setSchedulerParkMount(true);
3470 else if (!strcmp(proc, "ParkCap"))
3471 Options::setSchedulerCloseDustCover(true);
3472 }
3473 }
3474 }
3475 delXMLEle(root);
3476 emit syncGUIToGeneralSettings();
3477 }
3478 else if (errmsg[0])
3479 {
3480 appendLogText(QString(errmsg));
3481 delLilXML(xmlParser);
3482 moduleState()->setSchedulerState(old_state);
3483 return false;
3484 }
3485 }
3486
3487 moduleState()->setDirty(false);
3488 delLilXML(xmlParser);
3489 emit updateSchedulerURL(fileURL);
3490
3491 moduleState()->setSchedulerState(old_state);
3492 return true;
3493}
3494
3496{
3497 /* FIXME: user settings for log length */
3498 int const max_log_count = 2000;
3499 if (moduleState()->logText().size() > max_log_count)
3500 moduleState()->logText().removeLast();
3501
3502 moduleState()->logText().prepend(i18nc("log entry; %1 is the date, %2 is the text", "%1 %2",
3503 SchedulerModuleState::getLocalTime().toString("yyyy-MM-ddThh:mm:ss"), logentry));
3504
3505 qCInfo(KSTARS_EKOS_SCHEDULER) << logentry;
3506
3507 emit newLog(logentry);
3508}
3509
3511{
3512 moduleState()->logText().clear();
3513 emit newLog(QString());
3514}
3515
3516void SchedulerProcess::setAlignStatus(AlignState status)
3517{
3518 if (moduleState()->schedulerState() == SCHEDULER_PAUSED || activeJob() == nullptr)
3519 return;
3520
3521 qCDebug(KSTARS_EKOS_SCHEDULER) << "Align State" << Ekos::getAlignStatusString(status);
3522
3523 /* If current job is scheduled and has not started yet, wait */
3524 if (SCHEDJOB_SCHEDULED == activeJob()->getState())
3525 {
3526 QDateTime const now = SchedulerModuleState::getLocalTime();
3527 if (now < activeJob()->getStartupTime())
3528 return;
3529 }
3530
3531 if (activeJob()->getStage() == SCHEDSTAGE_ALIGNING)
3532 {
3533 // Is solver complete?
3534 if (status == Ekos::ALIGN_COMPLETE)
3535 {
3536 appendLogText(i18n("Job '%1' alignment is complete.", activeJob()->getName()));
3537 moduleState()->resetAlignFailureCount();
3538
3539 moduleState()->updateJobStage(SCHEDSTAGE_ALIGN_COMPLETE);
3540
3541 // If we solved a FITS file, let's use its center coords as our target.
3542 if (activeJob()->getFITSFile().isEmpty() == false)
3543 {
3544 QDBusReply<QList<double>> solutionReply = alignInterface()->call("getTargetCoords");
3545 if (solutionReply.isValid())
3546 {
3547 QList<double> const values = solutionReply.value();
3548 activeJob()->setTargetCoords(dms(values[0] * 15.0), dms(values[1]), KStarsData::Instance()->ut().djd());
3549 }
3550 }
3551 getNextAction();
3552 }
3553 else if (status == Ekos::ALIGN_FAILED || status == Ekos::ALIGN_ABORTED)
3554 {
3555 appendLogText(i18n("Warning: job '%1' alignment failed.", activeJob()->getName()));
3556
3557 if (moduleState()->increaseAlignFailureCount())
3558 {
3559 if (Options::resetMountModelOnAlignFail() && moduleState()->maxFailureAttempts() - 1 < moduleState()->alignFailureCount())
3560 {
3561 appendLogText(i18n("Warning: job '%1' forcing mount model reset after failing alignment #%2.", activeJob()->getName(),
3562 moduleState()->alignFailureCount()));
3563 mountInterface()->call(QDBus::AutoDetect, "resetModel");
3564 }
3565 appendLogText(i18n("Restarting %1 alignment procedure...", activeJob()->getName()));
3567 }
3568 else
3569 {
3570 appendLogText(i18n("Warning: job '%1' alignment procedure failed, marking aborted.", activeJob()->getName()));
3571 activeJob()->setState(SCHEDJOB_ABORTED);
3572
3573 findNextJob();
3574 }
3575 }
3576 }
3577}
3578
3579void SchedulerProcess::setGuideStatus(GuideState status)
3580{
3581 if (moduleState()->schedulerState() == SCHEDULER_PAUSED || activeJob() == nullptr)
3582 return;
3583
3584 qCDebug(KSTARS_EKOS_SCHEDULER) << "Guide State" << Ekos::getGuideStatusString(status);
3585
3586 /* If current job is scheduled and has not started yet, wait */
3587 if (SCHEDJOB_SCHEDULED == activeJob()->getState())
3588 {
3589 QDateTime const now = SchedulerModuleState::getLocalTime();
3590 if (now < activeJob()->getStartupTime())
3591 return;
3592 }
3593
3594 if (activeJob()->getStage() == SCHEDSTAGE_GUIDING)
3595 {
3596 qCDebug(KSTARS_EKOS_SCHEDULER) << "Calibration & Guide stage...";
3597
3598 // If calibration stage complete?
3599 if (status == Ekos::GUIDE_GUIDING)
3600 {
3601 appendLogText(i18n("Job '%1' guiding is in progress.", activeJob()->getName()));
3602 moduleState()->resetGuideFailureCount();
3603 // if guiding recovered while we are waiting, abort the restart
3604 moduleState()->cancelGuidingTimer();
3605
3606 moduleState()->updateJobStage(SCHEDSTAGE_GUIDING_COMPLETE);
3607 getNextAction();
3608 }
3609 else if (status == Ekos::GUIDE_CALIBRATION_ERROR ||
3610 status == Ekos::GUIDE_ABORTED)
3611 {
3612 if (status == Ekos::GUIDE_ABORTED)
3613 appendLogText(i18n("Warning: job '%1' guiding failed.", activeJob()->getName()));
3614 else
3615 appendLogText(i18n("Warning: job '%1' calibration failed.", activeJob()->getName()));
3616
3617 // if the timer for restarting the guiding is already running, we do nothing and
3618 // wait for the action triggered by the timer. This way we avoid that a small guiding problem
3619 // abort the scheduler job
3620
3621 if (moduleState()->isGuidingTimerActive())
3622 return;
3623
3624 if (moduleState()->increaseGuideFailureCount())
3625 {
3626 if (status == Ekos::GUIDE_CALIBRATION_ERROR &&
3627 Options::realignAfterCalibrationFailure())
3628 {
3629 appendLogText(i18n("Restarting %1 alignment procedure...", activeJob()->getName()));
3631 }
3632 else
3633 {
3634 appendLogText(i18n("Job '%1' is guiding, guiding procedure will be restarted in %2 seconds.", activeJob()->getName(),
3635 (RESTART_GUIDING_DELAY_MS * moduleState()->guideFailureCount()) / 1000));
3636 moduleState()->startGuidingTimer(RESTART_GUIDING_DELAY_MS * moduleState()->guideFailureCount());
3637 }
3638 }
3639 else
3640 {
3641 appendLogText(i18n("Warning: job '%1' guiding procedure failed, marking aborted.", activeJob()->getName()));
3642 activeJob()->setState(SCHEDJOB_ABORTED);
3643
3644 findNextJob();
3645 }
3646 }
3647 }
3648}
3649
3650void SchedulerProcess::setCaptureStatus(CaptureState status, const QString &trainname)
3651{
3652 if (activeJob() == nullptr || !m_activeJobs.contains(trainname))
3653 return;
3654
3655 qCDebug(KSTARS_EKOS_SCHEDULER) << "Capture State" << Ekos::getCaptureStatusString(status) << "train =" << trainname;
3656
3657 SchedulerJob *job = m_activeJobs[trainname];
3658
3659 /* If current job is scheduled and has not started yet, wait */
3660 if (SCHEDJOB_SCHEDULED == job->getState())
3661 {
3662 QDateTime const now = SchedulerModuleState::getLocalTime();
3663 if (now < job->getStartupTime())
3664 return;
3665 }
3666
3667 if (job->getStage() == SCHEDSTAGE_CAPTURING)
3668 {
3669 if (status == Ekos::CAPTURE_PROGRESS && (job->getStepPipeline() & SchedulerJob::USE_ALIGN))
3670 {
3671 // alignment is only relevant for the lead job
3672 if (job->isLead())
3673 {
3674 // JM 2021.09.20
3675 // Re-set target coords in align module
3676 // When capture starts, alignment module automatically rests target coords to mount coords.
3677 // However, we want to keep align module target synced with the scheduler target and not
3678 // the mount coord
3679 const SkyPoint targetCoords = activeJob()->getTargetCoords();
3680 QList<QVariant> targetArgs;
3681 targetArgs << targetCoords.ra0().Hours() << targetCoords.dec0().Degrees();
3682 alignInterface()->callWithArgumentList(QDBus::AutoDetect, "setTargetCoords", targetArgs);
3683 }
3684 }
3685 else if (status == Ekos::CAPTURE_ABORTED)
3686 {
3687 appendLogText(i18n("[%2] Warning: job '%1' failed to capture target.", job->getName(), trainname));
3688
3689 if (job->isLead())
3690 {
3691 // if capturing on the lead has failed for less than MAX_FAILURE_ATTEMPTS times
3692 if (moduleState()->increaseCaptureFailureCount())
3693 {
3694 job->setState(SCHEDJOB_ABORTED);
3695
3696 // If capture failed due to guiding error, let's try to restart that
3697 if (activeJob()->getStepPipeline() & SchedulerJob::USE_GUIDE)
3698 {
3699 // Check if it is guiding related.
3700 Ekos::GuideState gStatus = getGuidingStatus();
3701 if (gStatus == Ekos::GUIDE_ABORTED ||
3702 gStatus == Ekos::GUIDE_CALIBRATION_ERROR ||
3703 gStatus == GUIDE_DITHERING_ERROR)
3704 {
3705 appendLogText(i18n("[%2] Job '%1' is capturing, is restarting its guiding procedure (attempt #%3 of %4).",
3706 activeJob()->getName(), trainname,
3707 moduleState()->captureFailureCount(), moduleState()->maxFailureAttempts()));
3708 startGuiding(true);
3709 return;
3710 }
3711 }
3712
3713 /* FIXME: it's not clear whether it is actually possible to continue capturing when capture fails this way */
3714 appendLogText(i18n("Warning: job '%1' failed its capture procedure, restarting capture.", activeJob()->getName()));
3715 startCapture(true);
3716 }
3717 else
3718 {
3719 /* FIXME: it's not clear whether this situation can be recovered at all */
3720 appendLogText(i18n("[%2] Warning: job '%1' failed its capture procedure, marking aborted.", job->getName(), trainname));
3721 activeJob()->setState(SCHEDJOB_ABORTED);
3722 // abort follower capture jobs as well
3723 stopCapturing("", true);
3724
3725 findNextJob();
3726 }
3727 }
3728 else
3729 {
3730 if (job->leadJob()->getStage() == SCHEDSTAGE_CAPTURING)
3731 {
3732 // recover only when the lead job is capturing.
3733 appendLogText(i18n("[%2] Follower job '%1' has been aborted, is restarting.", job->getName(), trainname));
3734 job->setState(SCHEDJOB_ABORTED);
3735 startSingleCapture(job, true);
3736 }
3737 else
3738 {
3739 appendLogText(i18n("[%2] Follower job '%1' has been aborted.", job->getName(), trainname));
3740 job->setState(SCHEDJOB_ABORTED);
3741 }
3742 }
3743 }
3744 else if (status == Ekos::CAPTURE_COMPLETE)
3745 {
3746 KSNotification::event(QLatin1String("EkosScheduledImagingFinished"),
3747 i18n("[%2] Job (%1) - Capture finished", job->getName(), trainname), KSNotification::Scheduler);
3748
3749 if (job->isLead())
3750 {
3751 activeJob()->setState(SCHEDJOB_COMPLETE);
3752 findNextJob();
3753 }
3754 else
3755 {
3756 // Re-evaluate all jobs, without selecting a new job
3757 evaluateJobs(true);
3758
3759 if (job->getCompletionCondition() == FINISH_LOOP ||
3760 (job->getCompletionCondition() == FINISH_REPEAT && job->getRepeatsRemaining() > 0))
3761 {
3762 job->setState(SCHEDJOB_BUSY);
3763 startSingleCapture(job, false);
3764 }
3765 else
3766 {
3767 // follower job is complete
3768 job->setState(SCHEDJOB_COMPLETE);
3769 job->setStage(SCHEDSTAGE_COMPLETE);
3770 }
3771 }
3772 }
3773 else if (status == Ekos::CAPTURE_IMAGE_RECEIVED)
3774 {
3775 // We received a new image, but we don't know precisely where so update the storage map and re-estimate job times.
3776 // FIXME: rework this once capture storage is reworked
3777 if (Options::rememberJobProgress())
3778 {
3780
3781 for (const auto &job : moduleState()->jobs())
3782 SchedulerUtils::estimateJobTime(job, moduleState()->capturedFramesCount(), this);
3783 }
3784 // Else if we don't remember the progress on jobs, increase the completed count for the current job only - no cross-checks
3785 else
3786 activeJob()->setCompletedCount(job->getCompletedCount() + 1);
3787
3788 // reset the failure counter only if the image comes from the lead job
3789 if (job->isLead())
3790 moduleState()->resetCaptureFailureCount();
3791 }
3792 }
3793}
3794
3795void SchedulerProcess::setFocusStatus(FocusState status, const QString &trainname)
3796{
3797 Q_UNUSED(trainname)
3798
3799 if (moduleState()->schedulerState() == SCHEDULER_PAUSED || activeJob() == nullptr)
3800 return;
3801
3802 qCDebug(KSTARS_EKOS_SCHEDULER) << "Focus State" << Ekos::getFocusStatusString(status);
3803
3804 /* If current job is scheduled and has not started yet, wait */
3805 if (SCHEDJOB_SCHEDULED == activeJob()->getState())
3806 {
3807 QDateTime const now = SchedulerModuleState::getLocalTime();
3808 if (now < activeJob()->getStartupTime())
3809 return;
3810 }
3811
3812 if (activeJob()->getStage() == SCHEDSTAGE_FOCUSING)
3813 {
3814 // Is focus complete?
3815 if (status == Ekos::FOCUS_COMPLETE)
3816 {
3817 appendLogText(i18n("Job '%1' focusing is complete.", activeJob()->getName()));
3818
3819 moduleState()->setAutofocusCompleted(true);
3820
3821 moduleState()->updateJobStage(SCHEDSTAGE_FOCUS_COMPLETE);
3822
3823 getNextAction();
3824 }
3825 else if (status == Ekos::FOCUS_FAILED || status == Ekos::FOCUS_ABORTED)
3826 {
3827 appendLogText(i18n("Warning: job '%1' focusing failed.", activeJob()->getName()));
3828
3829 if (moduleState()->increaseFocusFailureCount())
3830 {
3831 appendLogText(i18n("Job '%1' is restarting its focusing procedure.", activeJob()->getName()));
3832 // Reset frame to original size.
3833 focusInterface()->call(QDBus::AutoDetect, "resetFrame");
3834 // Restart focusing
3835 qCDebug(KSTARS_EKOS_SCHEDULER) << "startFocusing on 6883";
3836 startFocusing();
3837 }
3838 else
3839 {
3840 appendLogText(i18n("Warning: job '%1' focusing procedure failed, marking aborted.", activeJob()->getName()));
3841 activeJob()->setState(SCHEDJOB_ABORTED);
3842
3843 findNextJob();
3844 }
3845 }
3846 }
3847}
3848
3849void SchedulerProcess::setMountStatus(ISD::Mount::Status status)
3850{
3851 if (moduleState()->schedulerState() == SCHEDULER_PAUSED || activeJob() == nullptr)
3852 return;
3853
3854 qCDebug(KSTARS_EKOS_SCHEDULER) << "Mount State changed to" << status;
3855
3856 /* If current job is scheduled and has not started yet, wait */
3857 if (SCHEDJOB_SCHEDULED == activeJob()->getState())
3858 if (static_cast<QDateTime const>(SchedulerModuleState::getLocalTime()) < activeJob()->getStartupTime())
3859 return;
3860
3861 switch (activeJob()->getStage())
3862 {
3863 case SCHEDSTAGE_SLEWING:
3864 {
3865 qCDebug(KSTARS_EKOS_SCHEDULER) << "Slewing stage...";
3866
3867 if (status == ISD::Mount::MOUNT_TRACKING)
3868 {
3869 appendLogText(i18n("Job '%1' slew is complete.", activeJob()->getName()));
3870 moduleState()->updateJobStage(SCHEDSTAGE_SLEW_COMPLETE);
3871 /* getNextAction is deferred to checkJobStage for dome support */
3872 }
3873 else if (status == ISD::Mount::MOUNT_ERROR)
3874 {
3875 appendLogText(i18n("Warning: job '%1' slew failed, marking terminated due to errors.", activeJob()->getName()));
3876 activeJob()->setState(SCHEDJOB_ERROR);
3877 findNextJob();
3878 }
3879 else if (status == ISD::Mount::MOUNT_IDLE)
3880 {
3881 appendLogText(i18n("Warning: job '%1' found not slewing, restarting.", activeJob()->getName()));
3882 moduleState()->updateJobStage(SCHEDSTAGE_IDLE);
3883 getNextAction();
3884 }
3885 }
3886 break;
3887
3888 case SCHEDSTAGE_RESLEWING:
3889 {
3890 qCDebug(KSTARS_EKOS_SCHEDULER) << "Re-slewing stage...";
3891
3892 if (status == ISD::Mount::MOUNT_TRACKING)
3893 {
3894 appendLogText(i18n("Job '%1' repositioning is complete.", activeJob()->getName()));
3895 moduleState()->updateJobStage(SCHEDSTAGE_RESLEWING_COMPLETE);
3896 /* getNextAction is deferred to checkJobStage for dome support */
3897 }
3898 else if (status == ISD::Mount::MOUNT_ERROR)
3899 {
3900 appendLogText(i18n("Warning: job '%1' repositioning failed, marking terminated due to errors.", activeJob()->getName()));
3901 activeJob()->setState(SCHEDJOB_ERROR);
3902 findNextJob();
3903 }
3904 else if (status == ISD::Mount::MOUNT_IDLE)
3905 {
3906 appendLogText(i18n("Warning: job '%1' found not repositioning, restarting.", activeJob()->getName()));
3907 moduleState()->updateJobStage(SCHEDSTAGE_IDLE);
3908 getNextAction();
3909 }
3910 }
3911 break;
3912
3913 // In case we are either focusing, aligning, or guiding
3914 // and mount is parked, we need to abort.
3915 case SCHEDSTAGE_FOCUSING:
3916 case SCHEDSTAGE_ALIGNING:
3917 case SCHEDSTAGE_GUIDING:
3918 if (status == ISD::Mount::MOUNT_PARKED)
3919 {
3920 appendLogText(i18n("Warning: Mount is parked while scheduler for job '%1' is active. Aborting.", activeJob()->getName()));
3921 stop();
3922 }
3923 break;
3924
3925 // For capturing, it's more complicated because a mount can be parked by a calibration job.
3926 // so we only abort if light frames are required AND no calibration park mount is required
3927 case SCHEDSTAGE_CAPTURING:
3928 if (status == ISD::Mount::MOUNT_PARKED && activeJob() && activeJob()->getLightFramesRequired()
3929 && activeJob()->getCalibrationMountPark() == false)
3930 {
3931 appendLogText(i18n("Warning: Mount is parked while scheduler for job '%1' is active. Aborting.", activeJob()->getName()));
3932 stop();
3933 }
3934 break;
3935
3936 default:
3937 break;
3938 }
3939}
3940
3941void SchedulerProcess::setWeatherStatus(ISD::Weather::Status status)
3942{
3943 ISD::Weather::Status newStatus = status;
3944
3945 if (newStatus == moduleState()->weatherStatus())
3946 return;
3947
3948 moduleState()->setWeatherStatus(newStatus);
3949
3950 // Shutdown scheduler if it was started and not already in shutdown
3951 // and if weather checkbox is checked.
3952 if (activeJob() && activeJob()->getEnforceWeather() && moduleState()->weatherStatus() == ISD::Weather::WEATHER_ALERT
3953 && moduleState()->schedulerState() != Ekos::SCHEDULER_IDLE && moduleState()->schedulerState() != Ekos::SCHEDULER_SHUTDOWN)
3954 {
3955 appendLogText(i18n("Starting shutdown procedure due to severe weather."));
3956 if (activeJob())
3957 {
3958 activeJob()->setState(SCHEDJOB_ABORTED);
3960 }
3962 }
3963 // forward weather state
3964 emit newWeatherStatus(status);
3965}
3966
3967void SchedulerProcess::checkStartupProcedure()
3968{
3969 if (checkStartupState() == false)
3970 QTimer::singleShot(1000, this, SLOT(checkStartupProcedure()));
3971}
3972
3973void SchedulerProcess::checkShutdownProcedure()
3974{
3975 if (checkShutdownState())
3976 {
3977 // shutdown completed
3978 if (moduleState()->shutdownState() == SHUTDOWN_COMPLETE)
3979 {
3980 appendLogText(i18n("Manual shutdown procedure completed successfully."));
3981 // Stop Ekos
3982 if (Options::stopEkosAfterShutdown())
3983 stopEkos();
3984 }
3985 else if (moduleState()->shutdownState() == SHUTDOWN_ERROR)
3986 appendLogText(i18n("Manual shutdown procedure terminated due to errors."));
3987
3988 moduleState()->setShutdownState(SHUTDOWN_IDLE);
3989 }
3990 else
3991 // If shutdown procedure is not finished yet, let's check again in 1 second.
3992 QTimer::singleShot(1000, this, SLOT(checkShutdownProcedure()));
3993
3994}
3995
3996
3997void SchedulerProcess::parkCap()
3998{
3999 if (capInterface().isNull())
4000 {
4001 appendLogText(i18n("Dust cover park requested but no dust covers detected."));
4002 moduleState()->setShutdownState(SHUTDOWN_ERROR);
4003 return;
4004 }
4005
4006 QVariant parkingStatus = capInterface()->property("parkStatus");
4007 qCDebug(KSTARS_EKOS_SCHEDULER) << "Cap parking status" << (!parkingStatus.isValid() ? -1 : parkingStatus.toInt());
4008
4009 if (parkingStatus.isValid() == false)
4010 {
4011 qCCritical(KSTARS_EKOS_SCHEDULER) << QString("Warning: cap parkStatus request received DBUS error: %1").arg(
4012 mountInterface()->lastError().type());
4013 if (!manageConnectionLoss())
4014 parkingStatus = ISD::PARK_ERROR;
4015 }
4016
4017 ISD::ParkStatus status = static_cast<ISD::ParkStatus>(parkingStatus.toInt());
4018
4019 if (status != ISD::PARK_PARKED)
4020 {
4021 moduleState()->setShutdownState(SHUTDOWN_PARKING_CAP);
4022 qCDebug(KSTARS_EKOS_SCHEDULER) << "Parking dust cap...";
4023 capInterface()->call(QDBus::AutoDetect, "park");
4024 appendLogText(i18n("Parking Cap..."));
4025
4026 moduleState()->startCurrentOperationTimer();
4027 }
4028 else
4029 {
4030 appendLogText(i18n("Cap already parked."));
4031 moduleState()->setShutdownState(SHUTDOWN_PARK_MOUNT);
4032 }
4033}
4034
4035void SchedulerProcess::unParkCap()
4036{
4037 if (capInterface().isNull())
4038 {
4039 appendLogText(i18n("Dust cover unpark requested but no dust covers detected."));
4040 moduleState()->setStartupState(STARTUP_ERROR);
4041 return;
4042 }
4043
4044 QVariant parkingStatus = capInterface()->property("parkStatus");
4045 qCDebug(KSTARS_EKOS_SCHEDULER) << "Cap parking status" << (!parkingStatus.isValid() ? -1 : parkingStatus.toInt());
4046
4047 if (parkingStatus.isValid() == false)
4048 {
4049 qCCritical(KSTARS_EKOS_SCHEDULER) << QString("Warning: cap parkStatus request received DBUS error: %1").arg(
4050 mountInterface()->lastError().type());
4051 if (!manageConnectionLoss())
4052 parkingStatus = ISD::PARK_ERROR;
4053 }
4054
4055 ISD::ParkStatus status = static_cast<ISD::ParkStatus>(parkingStatus.toInt());
4056
4057 if (status != ISD::PARK_UNPARKED)
4058 {
4059 moduleState()->setStartupState(STARTUP_UNPARKING_CAP);
4060 capInterface()->call(QDBus::AutoDetect, "unpark");
4061 appendLogText(i18n("Unparking cap..."));
4062
4063 moduleState()->startCurrentOperationTimer();
4064 }
4065 else
4066 {
4067 appendLogText(i18n("Cap already unparked."));
4068 moduleState()->setStartupState(STARTUP_COMPLETE);
4069 }
4070}
4071
4072void SchedulerProcess::parkMount()
4073{
4074 if (mountInterface().isNull())
4075 {
4076 appendLogText(i18n("Mount park requested but no mounts detected."));
4077 moduleState()->setShutdownState(SHUTDOWN_ERROR);
4078 return;
4079 }
4080
4081 QVariant parkingStatus = mountInterface()->property("parkStatus");
4082 qCDebug(KSTARS_EKOS_SCHEDULER) << "Mount parking status" << (!parkingStatus.isValid() ? -1 : parkingStatus.toInt());
4083
4084 if (parkingStatus.isValid() == false)
4085 {
4086 qCCritical(KSTARS_EKOS_SCHEDULER) << QString("Warning: mount parkStatus request received DBUS error: %1").arg(
4087 mountInterface()->lastError().type());
4088 if (!manageConnectionLoss())
4089 moduleState()->setParkWaitState(PARKWAIT_ERROR);
4090 }
4091
4092 ISD::ParkStatus status = static_cast<ISD::ParkStatus>(parkingStatus.toInt());
4093
4094 switch (status)
4095 {
4096 case ISD::PARK_PARKED:
4097 if (moduleState()->shutdownState() == SHUTDOWN_PARK_MOUNT)
4098 moduleState()->setShutdownState(SHUTDOWN_PARK_DOME);
4099
4100 moduleState()->setParkWaitState(PARKWAIT_PARKED);
4101 appendLogText(i18n("Mount already parked."));
4102 break;
4103
4104 case ISD::PARK_UNPARKING:
4105 //case Mount::UNPARKING_BUSY:
4106 /* FIXME: Handle the situation where we request parking but an unparking procedure is running. */
4107
4108 // case Mount::PARKING_IDLE:
4109 // case Mount::UNPARKING_OK:
4110 case ISD::PARK_ERROR:
4111 case ISD::PARK_UNKNOWN:
4112 case ISD::PARK_UNPARKED:
4113 {
4114 qCDebug(KSTARS_EKOS_SCHEDULER) << "Parking mount...";
4115 QDBusReply<bool> const mountReply = mountInterface()->call(QDBus::AutoDetect, "park");
4116
4117 if (mountReply.error().type() != QDBusError::NoError)
4118 {
4119 qCCritical(KSTARS_EKOS_SCHEDULER) << QString("Warning: mount park request received DBUS error: %1").arg(
4120 QDBusError::errorString(mountReply.error().type()));
4121 if (!manageConnectionLoss())
4122 moduleState()->setParkWaitState(PARKWAIT_ERROR);
4123 }
4124 else moduleState()->startCurrentOperationTimer();
4125 }
4126
4127 // Fall through
4128 case ISD::PARK_PARKING:
4129 //case Mount::PARKING_BUSY:
4130 if (moduleState()->shutdownState() == SHUTDOWN_PARK_MOUNT)
4131 moduleState()->setShutdownState(SHUTDOWN_PARKING_MOUNT);
4132
4133 moduleState()->setParkWaitState(PARKWAIT_PARKING);
4134 appendLogText(i18n("Parking mount in progress..."));
4135 break;
4136
4137 // All cases covered above so no need for default
4138 //default:
4139 // qCWarning(KSTARS_EKOS_SCHEDULER) << QString("BUG: Parking state %1 not managed while parking mount.").arg(mountReply.value());
4140 }
4141
4142}
4143
4144void SchedulerProcess::unParkMount()
4145{
4146 if (mountInterface().isNull())
4147 {
4148 appendLogText(i18n("Mount unpark requested but no mounts detected."));
4149 moduleState()->setStartupState(STARTUP_ERROR);
4150 return;
4151 }
4152
4153 QVariant parkingStatus = mountInterface()->property("parkStatus");
4154 qCDebug(KSTARS_EKOS_SCHEDULER) << "Mount parking status" << (!parkingStatus.isValid() ? -1 : parkingStatus.toInt());
4155
4156 if (parkingStatus.isValid() == false)
4157 {
4158 qCCritical(KSTARS_EKOS_SCHEDULER) << QString("Warning: mount parkStatus request received DBUS error: %1").arg(
4159 mountInterface()->lastError().type());
4160 if (!manageConnectionLoss())
4161 moduleState()->setParkWaitState(PARKWAIT_ERROR);
4162 }
4163
4164 ISD::ParkStatus status = static_cast<ISD::ParkStatus>(parkingStatus.toInt());
4165
4166 switch (status)
4167 {
4168 //case Mount::UNPARKING_OK:
4169 case ISD::PARK_UNPARKED:
4170 if (moduleState()->startupState() == STARTUP_UNPARK_MOUNT)
4171 moduleState()->setStartupState(STARTUP_UNPARK_CAP);
4172
4173 moduleState()->setParkWaitState(PARKWAIT_UNPARKED);
4174 appendLogText(i18n("Mount already unparked."));
4175 break;
4176
4177 //case Mount::PARKING_BUSY:
4178 case ISD::PARK_PARKING:
4179 /* FIXME: Handle the situation where we request unparking but a parking procedure is running. */
4180
4181 // case Mount::PARKING_IDLE:
4182 // case Mount::PARKING_OK:
4183 // case Mount::PARKING_ERROR:
4184 case ISD::PARK_ERROR:
4185 case ISD::PARK_UNKNOWN:
4186 case ISD::PARK_PARKED:
4187 {
4188 QDBusReply<bool> const mountReply = mountInterface()->call(QDBus::AutoDetect, "unpark");
4189
4190 if (mountReply.error().type() != QDBusError::NoError)
4191 {
4192 qCCritical(KSTARS_EKOS_SCHEDULER) << QString("Warning: mount unpark request received DBUS error: %1").arg(
4193 QDBusError::errorString(mountReply.error().type()));
4194 if (!manageConnectionLoss())
4195 moduleState()->setParkWaitState(PARKWAIT_ERROR);
4196 }
4197 else moduleState()->startCurrentOperationTimer();
4198 }
4199
4200 // Fall through
4201 //case Mount::UNPARKING_BUSY:
4202 case ISD::PARK_UNPARKING:
4203 if (moduleState()->startupState() == STARTUP_UNPARK_MOUNT)
4204 moduleState()->setStartupState(STARTUP_UNPARKING_MOUNT);
4205
4206 moduleState()->setParkWaitState(PARKWAIT_UNPARKING);
4207 qCInfo(KSTARS_EKOS_SCHEDULER) << "Unparking mount in progress...";
4208 break;
4209
4210 // All cases covered above
4211 //default:
4212 // qCWarning(KSTARS_EKOS_SCHEDULER) << QString("BUG: Parking state %1 not managed while unparking mount.").arg(mountReply.value());
4213 }
4214}
4215
4217{
4218 if (mountInterface().isNull())
4219 return false;
4220 // First check if the mount is able to park - if it isn't, getParkingStatus will reply PARKING_ERROR and status won't be clear
4221 //QDBusReply<bool> const parkCapableReply = mountInterface->call(QDBus::AutoDetect, "canPark");
4222 QVariant canPark = mountInterface()->property("canPark");
4223 qCDebug(KSTARS_EKOS_SCHEDULER) << "Mount can park:" << (!canPark.isValid() ? "invalid" : (canPark.toBool() ? "T" : "F"));
4224
4225 if (canPark.isValid() == false)
4226 {
4227 qCCritical(KSTARS_EKOS_SCHEDULER) << QString("Warning: mount canPark request received DBUS error: %1").arg(
4228 mountInterface()->lastError().type());
4230 return false;
4231 }
4232 else if (canPark.toBool() == true)
4233 {
4234 // If it is able to park, obtain its current status
4235 //QDBusReply<int> const mountReply = mountInterface->call(QDBus::AutoDetect, "getParkingStatus");
4236 QVariant parkingStatus = mountInterface()->property("parkStatus");
4237 qCDebug(KSTARS_EKOS_SCHEDULER) << "Mount parking status" << (!parkingStatus.isValid() ? -1 : parkingStatus.toInt());
4238
4239 if (parkingStatus.isValid() == false)
4240 {
4241 qCCritical(KSTARS_EKOS_SCHEDULER) << QString("Warning: mount parking status property is invalid %1.").arg(
4242 mountInterface()->lastError().type());
4244 return false;
4245 }
4246
4247 // Deduce state of mount - see getParkingStatus in mount.cpp
4248 switch (static_cast<ISD::ParkStatus>(parkingStatus.toInt()))
4249 {
4250 // case Mount::PARKING_OK: // INDI switch ok, and parked
4251 // case Mount::PARKING_IDLE: // INDI switch idle, and parked
4252 case ISD::PARK_PARKED:
4253 return true;
4254
4255 // case Mount::UNPARKING_OK: // INDI switch idle or ok, and unparked
4256 // case Mount::PARKING_ERROR: // INDI switch error
4257 // case Mount::PARKING_BUSY: // INDI switch busy
4258 // case Mount::UNPARKING_BUSY: // INDI switch busy
4259 default:
4260 return false;
4261 }
4262 }
4263 // If the mount is not able to park, consider it not parked
4264 return false;
4265}
4266
4267void SchedulerProcess::parkDome()
4268{
4269 // If there is no dome, mark error
4270 if (domeInterface().isNull())
4271 {
4272 appendLogText(i18n("Dome park requested but no domes detected."));
4273 moduleState()->setShutdownState(SHUTDOWN_ERROR);
4274 return;
4275 }
4276
4277 //QDBusReply<int> const domeReply = domeInterface->call(QDBus::AutoDetect, "getParkingStatus");
4278 //Dome::ParkingStatus status = static_cast<Dome::ParkingStatus>(domeReply.value());
4279 QVariant parkingStatus = domeInterface()->property("parkStatus");
4280 qCDebug(KSTARS_EKOS_SCHEDULER) << "Dome parking status" << (!parkingStatus.isValid() ? -1 : parkingStatus.toInt());
4281
4282 if (parkingStatus.isValid() == false)
4283 {
4284 qCCritical(KSTARS_EKOS_SCHEDULER) << QString("Warning: dome parkStatus request received DBUS error: %1").arg(
4285 mountInterface()->lastError().type());
4286 if (!manageConnectionLoss())
4287 parkingStatus = ISD::PARK_ERROR;
4288 }
4289
4290 ISD::ParkStatus status = static_cast<ISD::ParkStatus>(parkingStatus.toInt());
4291 if (status != ISD::PARK_PARKED)
4292 {
4293 moduleState()->setShutdownState(SHUTDOWN_PARKING_DOME);
4294 domeInterface()->call(QDBus::AutoDetect, "park");
4295 appendLogText(i18n("Parking dome..."));
4296
4297 moduleState()->startCurrentOperationTimer();
4298 }
4299 else
4300 {
4301 appendLogText(i18n("Dome already parked."));
4302 moduleState()->setShutdownState(SHUTDOWN_SCRIPT);
4303 }
4304}
4305
4306void SchedulerProcess::unParkDome()
4307{
4308 // If there is no dome, mark error
4309 if (domeInterface().isNull())
4310 {
4311 appendLogText(i18n("Dome unpark requested but no domes detected."));
4312 moduleState()->setStartupState(STARTUP_ERROR);
4313 return;
4314 }
4315
4316 QVariant parkingStatus = domeInterface()->property("parkStatus");
4317 qCDebug(KSTARS_EKOS_SCHEDULER) << "Dome parking status" << (!parkingStatus.isValid() ? -1 : parkingStatus.toInt());
4318
4319 if (parkingStatus.isValid() == false)
4320 {
4321 qCCritical(KSTARS_EKOS_SCHEDULER) << QString("Warning: dome parkStatus request received DBUS error: %1").arg(
4322 mountInterface()->lastError().type());
4323 if (!manageConnectionLoss())
4324 parkingStatus = ISD::PARK_ERROR;
4325 }
4326
4327 if (static_cast<ISD::ParkStatus>(parkingStatus.toInt()) != ISD::PARK_UNPARKED)
4328 {
4329 moduleState()->setStartupState(STARTUP_UNPARKING_DOME);
4330 domeInterface()->call(QDBus::AutoDetect, "unpark");
4331 appendLogText(i18n("Unparking dome..."));
4332
4333 moduleState()->startCurrentOperationTimer();
4334 }
4335 else
4336 {
4337 appendLogText(i18n("Dome already unparked."));
4338 moduleState()->setStartupState(STARTUP_UNPARK_MOUNT);
4339 }
4340}
4341
4343{
4344 QVariant guideStatus = guideInterface()->property("status");
4345 Ekos::GuideState gStatus = static_cast<Ekos::GuideState>(guideStatus.toInt());
4346
4347 return gStatus;
4348}
4349
4350const QString &SchedulerProcess::profile() const
4351{
4352 return moduleState()->currentProfile();
4353}
4354
4355void SchedulerProcess::setProfile(const QString &newProfile)
4356{
4357 moduleState()->setCurrentProfile(newProfile);
4358}
4359
4360QString SchedulerProcess::currentJobName()
4361{
4362 auto job = moduleState()->activeJob();
4363 return ( job != nullptr ? job->getName() : QString() );
4364}
4365
4366QString SchedulerProcess::currentJobJson()
4367{
4368 auto job = moduleState()->activeJob();
4369 if( job != nullptr )
4370 {
4371 return QString( QJsonDocument( job->toJson() ).toJson() );
4372 }
4373 else
4374 {
4375 return QString();
4376 }
4377}
4378
4379QString SchedulerProcess::jsonJobs()
4380{
4381 return QString( QJsonDocument( moduleState()->getJSONJobs() ).toJson() );
4382}
4383
4384QStringList SchedulerProcess::logText()
4385{
4386 return moduleState()->logText();
4387}
4388
4390{
4391 if (domeInterface().isNull())
4392 return false;
4393
4394 QVariant parkingStatus = domeInterface()->property("parkStatus");
4395 qCDebug(KSTARS_EKOS_SCHEDULER) << "Dome parking status" << (!parkingStatus.isValid() ? -1 : parkingStatus.toInt());
4396
4397 if (parkingStatus.isValid() == false)
4398 {
4399 qCCritical(KSTARS_EKOS_SCHEDULER) << QString("Warning: dome parkStatus request received DBUS error: %1").arg(
4400 mountInterface()->lastError().type());
4401 if (!manageConnectionLoss())
4402 parkingStatus = ISD::PARK_ERROR;
4403 }
4404
4405 ISD::ParkStatus status = static_cast<ISD::ParkStatus>(parkingStatus.toInt());
4406
4407 return status == ISD::PARK_PARKED;
4408}
4409
4410void SchedulerProcess::simClockScaleChanged(float newScale)
4411{
4412 if (moduleState()->currentlySleeping())
4413 {
4414 QTime const remainingTimeMs = QTime::fromMSecsSinceStartOfDay(std::lround(static_cast<double>
4415 (moduleState()->iterationTimer().remainingTime())
4416 * KStarsData::Instance()->clock()->scale()
4417 / newScale));
4418 appendLogText(i18n("Sleeping for %1 on simulation clock update until next observation job is ready...",
4419 remainingTimeMs.toString("hh:mm:ss")));
4420 moduleState()->iterationTimer().stop();
4421 moduleState()->iterationTimer().start(remainingTimeMs.msecsSinceStartOfDay());
4422 }
4423}
4424
4425void SchedulerProcess::simClockTimeChanged()
4426{
4427 moduleState()->calculateDawnDusk();
4428
4429 // If the Scheduler is not running, reset all jobs and re-evaluate from a new current start point
4430 if (SCHEDULER_RUNNING != moduleState()->schedulerState())
4432}
4433
4434void SchedulerProcess::setINDICommunicationStatus(CommunicationStatus status)
4435{
4436 qCDebug(KSTARS_EKOS_SCHEDULER) << "Scheduler INDI status is" << status;
4437
4438 moduleState()->setIndiCommunicationStatus(status);
4439}
4440
4441void SchedulerProcess::setEkosCommunicationStatus(CommunicationStatus status)
4442{
4443 qCDebug(KSTARS_EKOS_SCHEDULER) << "Scheduler Ekos status is" << status;
4444
4445 moduleState()->setEkosCommunicationStatus(status);
4446}
4447
4448
4449
4450void SchedulerProcess::checkInterfaceReady(QDBusInterface * iface)
4451{
4452 if (iface == mountInterface())
4453 {
4454 if (mountInterface()->property("canPark").isValid())
4455 moduleState()->setMountReady(true);
4456 }
4457 else if (iface == capInterface())
4458 {
4459 if (capInterface()->property("canPark").isValid())
4460 moduleState()->setCapReady(true);
4461 }
4462 else if (iface == observatoryInterface())
4463 {
4464 QVariant status = observatoryInterface()->property("status");
4465 if (status.isValid())
4466 setWeatherStatus(static_cast<ISD::Weather::Status>(status.toInt()));
4467 }
4468 else if (iface == weatherInterface())
4469 {
4470 QVariant status = weatherInterface()->property("status");
4471 if (status.isValid())
4472 setWeatherStatus(static_cast<ISD::Weather::Status>(status.toInt()));
4473 }
4474 else if (iface == domeInterface())
4475 {
4476 if (domeInterface()->property("canPark").isValid())
4477 moduleState()->setDomeReady(true);
4478 }
4479 else if (iface == captureInterface())
4480 {
4481 if (captureInterface()->property("coolerControl").isValid())
4482 moduleState()->setCaptureReady(true);
4483 }
4484 // communicate state to UI
4485 emit interfaceReady(iface);
4486}
4487
4488void SchedulerProcess::registerNewModule(const QString &name)
4489{
4490 qCDebug(KSTARS_EKOS_SCHEDULER) << "Registering new Module (" << name << ")";
4491
4492 if (name == "Focus")
4493 {
4494 delete focusInterface();
4495 setFocusInterface(new QDBusInterface(kstarsInterfaceString, focusPathString, focusInterfaceString,
4497 connect(focusInterface(), SIGNAL(newStatus(Ekos::FocusState, const QString)), this,
4498 SLOT(setFocusStatus(Ekos::FocusState, const QString)), Qt::UniqueConnection);
4499 }
4500 else if (name == "Capture")
4501 {
4502 delete captureInterface();
4503 setCaptureInterface(new QDBusInterface(kstarsInterfaceString, capturePathString, captureInterfaceString,
4505
4506 connect(captureInterface(), SIGNAL(ready()), this, SLOT(syncProperties()));
4507 connect(captureInterface(), SIGNAL(newStatus(Ekos::CaptureState, const QString, int)), this,
4508 SLOT(setCaptureStatus(Ekos::CaptureState, const QString)), Qt::UniqueConnection);
4509 connect(captureInterface(), SIGNAL(captureComplete(QVariantMap, const QString)), this, SLOT(checkAlignment(QVariantMap,
4510 const QString)),
4512 checkInterfaceReady(captureInterface());
4513 }
4514 else if (name == "Mount")
4515 {
4516 delete mountInterface();
4517 setMountInterface(new QDBusInterface(kstarsInterfaceString, mountPathString, mountInterfaceString,
4519
4520 connect(mountInterface(), SIGNAL(ready()), this, SLOT(syncProperties()));
4521 connect(mountInterface(), SIGNAL(newStatus(ISD::Mount::Status)), this, SLOT(setMountStatus(ISD::Mount::Status)),
4523
4524 checkInterfaceReady(mountInterface());
4525 }
4526 else if (name == "Align")
4527 {
4528 delete alignInterface();
4529 setAlignInterface(new QDBusInterface(kstarsInterfaceString, alignPathString, alignInterfaceString,
4531 connect(alignInterface(), SIGNAL(newStatus(Ekos::AlignState)), this, SLOT(setAlignStatus(Ekos::AlignState)),
4533 }
4534 else if (name == "Guide")
4535 {
4536 delete guideInterface();
4537 setGuideInterface(new QDBusInterface(kstarsInterfaceString, guidePathString, guideInterfaceString,
4539 connect(guideInterface(), SIGNAL(newStatus(Ekos::GuideState)), this,
4540 SLOT(setGuideStatus(Ekos::GuideState)), Qt::UniqueConnection);
4541 }
4542 else if (name == "Observatory")
4543 {
4544 delete observatoryInterface();
4545 setObservatoryInterface(new QDBusInterface(kstarsInterfaceString, observatoryPathString, observatoryInterfaceString,
4547 connect(observatoryInterface(), SIGNAL(newStatus(ISD::Weather::Status)), this,
4548 SLOT(setWeatherStatus(ISD::Weather::Status)), Qt::UniqueConnection);
4549 checkInterfaceReady(observatoryInterface());
4550 }
4551}
4552
4553void SchedulerProcess::registerNewDevice(const QString &name, int interface)
4554{
4555 Q_UNUSED(name)
4556
4557 if (interface & INDI::BaseDevice::DOME_INTERFACE)
4558 {
4559 QList<QVariant> dbusargs;
4560 dbusargs.append(INDI::BaseDevice::DOME_INTERFACE);
4561 QDBusReply<QStringList> paths = indiInterface()->callWithArgumentList(QDBus::AutoDetect, "getDevicesPaths",
4562 dbusargs);
4563 if (paths.error().type() == QDBusError::NoError && !paths.value().isEmpty())
4564 {
4565 // Select last device in case a restarted caused multiple instances in the tree
4566 setDomePathString(paths.value().last());
4567 delete domeInterface();
4568 setDomeInterface(new QDBusInterface(kstarsInterfaceString, domePathString,
4569 domeInterfaceString,
4571 connect(domeInterface(), SIGNAL(ready()), this, SLOT(syncProperties()));
4572 checkInterfaceReady(domeInterface());
4573 }
4574 }
4575
4576 // if (interface & INDI::BaseDevice::WEATHER_INTERFACE)
4577 // {
4578 // QList<QVariant> dbusargs;
4579 // dbusargs.append(INDI::BaseDevice::WEATHER_INTERFACE);
4580 // QDBusReply<QStringList> paths = indiInterface()->callWithArgumentList(QDBus::AutoDetect, "getDevicesPaths",
4581 // dbusargs);
4582 // if (paths.error().type() == QDBusError::NoError)
4583 // {
4584 // // Select last device in case a restarted caused multiple instances in the tree
4585 // setWeatherPathString(paths.value().last());
4586 // delete weatherInterface();
4587 // setWeatherInterface(new QDBusInterface(kstarsInterfaceString, weatherPathString,
4588 // weatherInterfaceString,
4589 // QDBusConnection::sessionBus(), this));
4590 // connect(weatherInterface(), SIGNAL(ready()), this, SLOT(syncProperties()));
4591 // connect(weatherInterface(), SIGNAL(newStatus(ISD::Weather::Status)), this,
4592 // SLOT(setWeatherStatus(ISD::Weather::Status)));
4593 // checkInterfaceReady(weatherInterface());
4594 // }
4595 // }
4596
4597 if (interface & INDI::BaseDevice::DUSTCAP_INTERFACE)
4598 {
4599 QList<QVariant> dbusargs;
4600 dbusargs.append(INDI::BaseDevice::DUSTCAP_INTERFACE);
4601 QDBusReply<QStringList> paths = indiInterface()->callWithArgumentList(QDBus::AutoDetect, "getDevicesPaths",
4602 dbusargs);
4603 if (paths.error().type() == QDBusError::NoError && !paths.value().isEmpty())
4604 {
4605 // Select last device in case a restarted caused multiple instances in the tree
4606 setDustCapPathString(paths.value().last());
4607 delete capInterface();
4608 setCapInterface(new QDBusInterface(kstarsInterfaceString, dustCapPathString,
4609 dustCapInterfaceString,
4611 connect(capInterface(), SIGNAL(ready()), this, SLOT(syncProperties()));
4612 checkInterfaceReady(capInterface());
4613 }
4614 }
4615}
4616
4617bool SchedulerProcess::createJobSequence(XMLEle * root, const QString &prefix, const QString &outputDir)
4618{
4619 XMLEle *ep = nullptr;
4620 XMLEle *subEP = nullptr;
4621
4622 for (ep = nextXMLEle(root, 1); ep != nullptr; ep = nextXMLEle(root, 0))
4623 {
4624 if (!strcmp(tagXMLEle(ep), "Job"))
4625 {
4626 for (subEP = nextXMLEle(ep, 1); subEP != nullptr; subEP = nextXMLEle(ep, 0))
4627 {
4628 if (!strcmp(tagXMLEle(subEP), "TargetName"))
4629 {
4630 // Set the target name in sequence file, though scheduler will overwrite this later
4631 editXMLEle(subEP, prefix.toLatin1().constData());
4632 }
4633 else if (!strcmp(tagXMLEle(subEP), "FITSDirectory"))
4634 {
4635 editXMLEle(subEP, outputDir.toLatin1().constData());
4636 }
4637 }
4638 }
4639 }
4640
4641 QDir().mkpath(outputDir);
4642
4643 QString filename = QString("%1/%2.esq").arg(outputDir, prefix);
4644 FILE *outputFile = fopen(filename.toLatin1().constData(), "w");
4645
4646 if (outputFile == nullptr)
4647 {
4648 QString message = i18n("Unable to write to file %1", filename);
4649 KSNotification::sorry(message, i18n("Could Not Open File"));
4650 return false;
4651 }
4652
4653 fprintf(outputFile, "<?xml version=\"1.0\" encoding=\"UTF-8\"?>");
4654 prXMLEle(outputFile, root, 0);
4655
4656 fclose(outputFile);
4657
4658 return true;
4659}
4660
4661XMLEle *SchedulerProcess::getSequenceJobRoot(const QString &filename) const
4662{
4663 QFile sFile;
4664 sFile.setFileName(filename);
4665
4666 if (!sFile.open(QIODevice::ReadOnly))
4667 {
4668 KSNotification::sorry(i18n("Unable to open file %1", sFile.fileName()),
4669 i18n("Could Not Open File"));
4670 return nullptr;
4671 }
4672
4673 LilXML *xmlParser = newLilXML();
4674 char errmsg[MAXRBUF];
4675 XMLEle *root = nullptr;
4676 char c;
4677
4678 while (sFile.getChar(&c))
4679 {
4680 root = readXMLEle(xmlParser, c, errmsg);
4681
4682 if (root)
4683 break;
4684 }
4685
4686 delLilXML(xmlParser);
4687 sFile.close();
4688 return root;
4689}
4690
4691void SchedulerProcess::checkProcessExit(int exitCode)
4692{
4693 scriptProcess().disconnect();
4694
4695 if (exitCode == 0)
4696 {
4697 if (moduleState()->startupState() == STARTUP_SCRIPT)
4698 moduleState()->setStartupState(STARTUP_UNPARK_DOME);
4699 else if (moduleState()->shutdownState() == SHUTDOWN_SCRIPT_RUNNING)
4700 moduleState()->setShutdownState(SHUTDOWN_COMPLETE);
4701
4702 return;
4703 }
4704
4705 if (moduleState()->startupState() == STARTUP_SCRIPT)
4706 {
4707 appendLogText(i18n("Startup script failed, aborting..."));
4708 moduleState()->setStartupState(STARTUP_ERROR);
4709 }
4710 else if (moduleState()->shutdownState() == SHUTDOWN_SCRIPT_RUNNING)
4711 {
4712 appendLogText(i18n("Shutdown script failed, aborting..."));
4713 moduleState()->setShutdownState(SHUTDOWN_ERROR);
4714 }
4715
4716}
4717
4718void SchedulerProcess::readProcessOutput()
4719{
4720 appendLogText(scriptProcess().readAllStandardOutput().simplified());
4721}
4722
4723bool SchedulerProcess::canCountCaptures(const SchedulerJob &job)
4724{
4725 QList<SequenceJob*> seqjobs;
4726 bool hasAutoFocus = false;
4727 SchedulerJob tempJob = job;
4728 if (SchedulerUtils::loadSequenceQueue(tempJob.getSequenceFile().toLocalFile(), &tempJob, seqjobs, hasAutoFocus,
4729 nullptr) == false)
4730 return false;
4731
4732 for (const SequenceJob *oneSeqJob : seqjobs)
4733 {
4734 if (oneSeqJob->getUploadMode() == ISD::Camera::UPLOAD_REMOTE)
4735 return false;
4736 }
4737 return true;
4738}
4739
4741{
4742 /* Use a temporary map in order to limit the number of file searches */
4743 CapturedFramesMap newFramesCount;
4744
4745 /* FIXME: Capture storage cache is refreshed too often, feature requires rework. */
4746
4747 /* Check if one job is idle or requires evaluation - if so, force refresh */
4748 forced |= std::any_of(moduleState()->jobs().begin(),
4749 moduleState()->jobs().end(), [](SchedulerJob * oneJob) -> bool
4750 {
4751 SchedulerJobStatus const state = oneJob->getState();
4752 return state == SCHEDJOB_IDLE || state == SCHEDJOB_EVALUATION;});
4753
4754 /* If update is forced, clear the frame map */
4755 if (forced)
4756 moduleState()->capturedFramesCount().clear();
4757
4758 /* Enumerate SchedulerJobs to count captures that are already stored */
4759 for (SchedulerJob *oneJob : moduleState()->jobs())
4760 {
4761 // This is like newFramesCount, but reset on every job.
4762 // It is useful for properly calling addProgress().
4763 CapturedFramesMap newJobFramesCount;
4764
4765 QList<SequenceJob*> seqjobs;
4766 bool hasAutoFocus = false;
4767
4768 //oneJob->setLightFramesRequired(false);
4769 /* Look into the sequence requirements, bypass if invalid */
4770 if (SchedulerUtils::loadSequenceQueue(oneJob->getSequenceFile().toLocalFile(), oneJob, seqjobs, hasAutoFocus,
4771 this) == false)
4772 {
4773 appendLogText(i18n("Warning: job '%1' has inaccessible sequence '%2', marking invalid.", oneJob->getName(),
4774 oneJob->getSequenceFile().toLocalFile()));
4775 oneJob->setState(SCHEDJOB_INVALID);
4776 continue;
4777 }
4778
4779 oneJob->clearProgress();
4780 /* Enumerate the SchedulerJob's SequenceJobs to count captures stored for each */
4781 for (SequenceJob *oneSeqJob : seqjobs)
4782 {
4783 /* Only consider captures stored on client (Ekos) side */
4784 /* FIXME: ask the remote for the file count */
4785 if (oneSeqJob->getUploadMode() == ISD::Camera::UPLOAD_REMOTE)
4786 continue;
4787
4788 /* FIXME: this signature path is incoherent when there is no filter wheel on the setup - bugfix should be elsewhere though */
4789 QString const signature = oneSeqJob->getSignature();
4790
4791 /* If signature was processed during this run, keep it */
4792 if (newFramesCount.constEnd() != newFramesCount.constFind(signature))
4793 {
4794 if (newJobFramesCount.constEnd() == newJobFramesCount.constFind(signature))
4795 {
4796 // Even though we've seen this before, we haven't seen it for this SchedulerJob.
4797 const int count = newFramesCount.constFind(signature).value();
4798 newJobFramesCount[signature] = count;
4799 oneJob->addProgress(count, oneSeqJob);
4800 }
4801 continue;
4802 }
4803 int count = 0;
4804
4805 CapturedFramesMap::const_iterator const earlierRunIterator =
4806 moduleState()->capturedFramesCount().constFind(signature);
4807
4808 if (moduleState()->capturedFramesCount().constEnd() != earlierRunIterator)
4809 // If signature was processed during an earlier run, use the earlier count.
4810 count = earlierRunIterator.value();
4811 else
4812 // else recount captures already stored
4813 count = PlaceholderPath::getCompletedFiles(signature);
4814
4815 newFramesCount[signature] = count;
4816 newJobFramesCount[signature] = count;
4817 oneJob->addProgress(count, oneSeqJob);
4818 }
4819
4820 // determine whether we need to continue capturing, depending on captured frames
4821 SchedulerUtils::updateLightFramesRequired(oneJob, seqjobs, newFramesCount);
4822 }
4823
4824 moduleState()->setCapturedFramesCount(newFramesCount);
4825
4826 {
4827 qCDebug(KSTARS_EKOS_SCHEDULER) << "Frame map summary:";
4828 CapturedFramesMap::const_iterator it = moduleState()->capturedFramesCount().constBegin();
4829 for (; it != moduleState()->capturedFramesCount().constEnd(); it++)
4830 qCDebug(KSTARS_EKOS_SCHEDULER) << " " << it.key() << ':' << it.value();
4831 }
4832}
4833
4834SchedulerJob *SchedulerProcess::activeJob()
4835{
4836 return moduleState()->activeJob();
4837}
4838
4839void SchedulerProcess::printStates(const QString &label)
4840{
4841 qCDebug(KSTARS_EKOS_SCHEDULER) <<
4842 QString("%1 %2 %3%4 %5 %6 %7 %8 %9\n")
4843 .arg(label)
4844 .arg(timerStr(moduleState()->timerState()))
4845 .arg(getSchedulerStatusString(moduleState()->schedulerState()))
4846 .arg((moduleState()->timerState() == RUN_JOBCHECK && activeJob() != nullptr) ?
4847 QString("(%1 %2)").arg(SchedulerJob::jobStatusString(activeJob()->getState()))
4848 .arg(SchedulerJob::jobStageString(activeJob()->getStage())) : "")
4849 .arg(ekosStateString(moduleState()->ekosState()))
4850 .arg(indiStateString(moduleState()->indiState()))
4851 .arg(startupStateString(moduleState()->startupState()))
4852 .arg(shutdownStateString(moduleState()->shutdownState()))
4853 .arg(parkWaitStateString(moduleState()->parkWaitState())).toLatin1().data();
4854 foreach (auto j, moduleState()->jobs())
4855 qCDebug(KSTARS_EKOS_SCHEDULER) << QString("job %1 %2\n").arg(j->getName()).arg(SchedulerJob::jobStatusString(
4856 j->getState())).toLatin1().data();
4857}
4858
4859} // Ekos namespace
Q_SCRIPTABLE Q_NOREPLY void startAstrometry()
startAstrometry initiation of the capture and solve operation.
bool shouldSchedulerSleep(SchedulerJob *job)
shouldSchedulerSleep Check if the scheduler needs to sleep until the job is ready
Q_SCRIPTABLE Q_NOREPLY void startCapture(bool restart=false)
startCapture The current job file name is solved to an url which is fed to ekos.
void loadProfiles()
loadProfiles Load the existing EKOS profiles
Q_SCRIPTABLE Q_NOREPLY void runStartupProcedure()
runStartupProcedure Execute the startup of the scheduler itself to be prepared for running scheduler ...
void checkCapParkingStatus()
checkDomeParkingStatus check dome parking status and updating corresponding states accordingly.
void getNextAction()
getNextAction Checking for the next appropriate action regarding the current state of the scheduler a...
Q_SCRIPTABLE bool isMountParked()
Q_SCRIPTABLE Q_NOREPLY void startJobEvaluation()
startJobEvaluation Start job evaluation only without starting the scheduler process itself.
Q_SCRIPTABLE Q_NOREPLY void resetJobs()
resetJobs Reset all jobs counters
void selectActiveJob(const QList< SchedulerJob * > &jobs)
selectActiveJob Select the job that should be executed
Q_SCRIPTABLE void wakeUpScheduler()
wakeUpScheduler Wake up scheduler from sleep state
Q_SCRIPTABLE Q_NOREPLY void setPaused()
setPaused pausing the scheduler
Q_SCRIPTABLE bool checkParkWaitState()
checkParkWaitState Check park wait state.
bool executeJob(SchedulerJob *job)
executeJob After the best job is selected, we call this in order to start the process that will execu...
bool createJobSequence(XMLEle *root, const QString &prefix, const QString &outputDir)
createJobSequence Creates a job sequence for the mosaic tool given the prefix and output dir.
void iterate()
Repeatedly runs a scheduler iteration and then sleeps timerInterval millisconds and run the next iter...
Q_SCRIPTABLE Q_NOREPLY void startGuiding(bool resetCalibration=false)
startGuiding After ekos is fed the calibration options, we start the guiding process
void findNextJob()
findNextJob Check if the job met the completion criteria, and if it did, then it search for next job ...
Q_SCRIPTABLE bool appendEkosScheduleList(const QString &fileURL)
appendEkosScheduleList Append the contents of an ESL file to the queue.
Q_SCRIPTABLE void execute()
execute Execute the schedule, start if idle or paused.
Q_SCRIPTABLE bool isDomeParked()
Q_SCRIPTABLE bool saveScheduler(const QUrl &fileURL)
saveScheduler Save scheduler jobs to a file
Q_SCRIPTABLE Q_NOREPLY void appendLogText(const QString &logentry) override
appendLogText Append a new line to the logging.
Q_SCRIPTABLE Q_NOREPLY void start()
DBUS interface function.
Q_SCRIPTABLE Q_NOREPLY void removeAllJobs()
DBUS interface function.
Q_SCRIPTABLE void stopCurrentJobAction()
stopCurrentJobAction Stop whatever action taking place in the current job (eg.
Q_SCRIPTABLE Q_NOREPLY void stopGuiding()
stopGuiding After guiding is done we need to stop the process
bool checkShutdownState()
checkShutdownState Check shutdown procedure stages and make sure all stages are complete.
Q_SCRIPTABLE Q_NOREPLY void startSlew()
startSlew DBus call for initiating slew
bool checkEkosState()
checkEkosState Check ekos startup stages and take whatever action necessary to get Ekos up and runnin...
bool checkStatus()
checkJobStatus Check the overall state of the scheduler, Ekos, and INDI.
bool checkINDIState()
checkINDIState Check INDI startup stages and take whatever action necessary to get INDI devices conne...
XMLEle * getSequenceJobRoot(const QString &filename) const
getSequenceJobRoot Read XML data from capture sequence job
Q_SCRIPTABLE Q_NOREPLY void runShutdownProcedure()
runShutdownProcedure Shutdown the scheduler itself and EKOS (if configured to do so).
Q_SCRIPTABLE Q_NOREPLY void setSequence(const QString &sequenceFileURL)
DBUS interface function.
Q_SCRIPTABLE bool loadScheduler(const QString &fileURL)
DBUS interface function.
Q_SCRIPTABLE Q_NOREPLY void startFocusing()
startFocusing DBus call for feeding ekos the specified settings and initiating focus operation
void checkJobStage()
checkJobStage Check the progress of the job states and make DBUS calls to start the next stage until ...
bool checkStartupState()
checkStartupState Check startup procedure stages and make sure all stages are complete.
void processGuidingTimer()
processGuidingTimer Check the guiding timer, and possibly restart guiding.
GuideState getGuidingStatus()
getGuidingStatus Retrieve the guiding status.
Q_SCRIPTABLE Q_NOREPLY void resetAllJobs()
DBUS interface function.
void applyConfig()
applyConfig Apply configuration changes from the global configuration dialog.
Q_SCRIPTABLE void clearLog()
clearLog Clear log entry
Q_SCRIPTABLE Q_NOREPLY void disconnectINDI()
disconnectINDI disconnect all INDI devices from server.
Q_SCRIPTABLE Q_NOREPLY void stop()
DBUS interface function.
bool manageConnectionLoss()
manageConnectionLoss Mitigate loss of connection with the INDI server.
void updateCompletedJobsCount(bool forced=false)
updateCompletedJobsCount For each scheduler job, examine sequence job storage and count captures.
Q_SCRIPTABLE Q_NOREPLY void evaluateJobs(bool evaluateOnly)
evaluateJobs evaluates the current state of each objects and gives each one a score based on the cons...
int runSchedulerIteration()
Run a single scheduler iteration.
void setSolverAction(Align::GotoMode mode)
setSolverAction set the GOTO mode for the solver
Q_SCRIPTABLE bool completeShutdown()
completeShutdown Try to complete the scheduler shutdown
static KConfigDialog * exists(const QString &name)
void settingsChanged(const QString &dialogName)
static KStars * Instance()
Definition kstars.h:121
The SchedulerState class holds all attributes defining the scheduler's state.
void timeChanged()
The time has changed (emitted by setUTC() )
void scaleChanged(float)
The timestep has changed.
The sky coordinates of a point in the sky.
Definition skypoint.h:45
void apparentCoord(long double jd0, long double jdf)
Computes the apparent coordinates for this SkyPoint for any epoch, accounting for the effects of prec...
Definition skypoint.cpp:720
const CachingDms & dec() const
Definition skypoint.h:269
const CachingDms & ra0() const
Definition skypoint.h:251
const CachingDms & ra() const
Definition skypoint.h:263
void EquatorialToHorizontal(const CachingDms *LST, const CachingDms *lat)
Determine the (Altitude, Azimuth) coordinates of the SkyPoint from its (RA, Dec) coordinates,...
Definition skypoint.cpp:77
void setRA0(dms r)
Sets RA0, the catalog Right Ascension.
Definition skypoint.h:94
const CachingDms & dec0() const
Definition skypoint.h:257
void setDec0(dms d)
Sets Dec0, the catalog Declination.
Definition skypoint.h:119
An angle, stored as degrees, but expressible in many ways.
Definition dms.h:38
double Hours() const
Definition dms.h:168
const QString toDMSString(const bool forceSign=false, const bool machineReadable=false, const bool highPrecision=false) const
Definition dms.cpp:287
const QString toHMSString(const bool machineReadable=false, const bool highPrecision=false) const
Definition dms.cpp:378
const double & Degrees() const
Definition dms.h:141
QString i18np(const char *singular, const char *plural, const TYPE &arg...)
QString i18nc(const char *context, const char *text, const TYPE &arg...)
QString i18n(const char *text, const TYPE &arg...)
Ekos is an advanced Astrophotography tool for Linux.
Definition align.cpp:83
SchedulerJobStatus
States of a SchedulerJob.
@ SCHEDJOB_ABORTED
Job encountered a transitory issue while processing, and will be rescheduled.
@ SCHEDJOB_INVALID
Job has an incorrect configuration, and cannot proceed.
@ SCHEDJOB_ERROR
Job encountered a fatal issue while processing, and must be reset manually.
@ SCHEDJOB_COMPLETE
Job finished all required captures.
@ SCHEDJOB_EVALUATION
Job is being evaluated.
@ SCHEDJOB_SCHEDULED
Job was evaluated, and has a schedule.
@ SCHEDJOB_BUSY
Job is being processed.
@ SCHEDJOB_IDLE
Job was just created, and is not evaluated yet.
QMap< QString, uint16_t > CapturedFramesMap
mapping signature --> frames count
ErrorHandlingStrategy
options what should happen if an error or abort occurs
AlignState
Definition ekos.h:145
@ ALIGN_FAILED
Alignment failed.
Definition ekos.h:148
@ ALIGN_ABORTED
Alignment aborted by user or agent.
Definition ekos.h:149
@ ALIGN_IDLE
No ongoing operations.
Definition ekos.h:146
@ ALIGN_COMPLETE
Alignment successfully completed.
Definition ekos.h:147
CaptureState
Capture states.
Definition ekos.h:92
@ CAPTURE_PROGRESS
Definition ekos.h:94
@ CAPTURE_IMAGE_RECEIVED
Definition ekos.h:101
@ CAPTURE_ABORTED
Definition ekos.h:99
@ CAPTURE_COMPLETE
Definition ekos.h:112
@ CAPTURE_IDLE
Definition ekos.h:93
SchedulerTimerState
IterationTypes, the different types of scheduler iterations that are run.
GeoCoordinates geo(const QVariant &location)
ButtonCode warningContinueCancel(QWidget *parent, const QString &text, const QString &title=QString(), const KGuiItem &buttonContinue=KStandardGuiItem::cont(), const KGuiItem &buttonCancel=KStandardGuiItem::cancel(), const QString &dontAskAgainName=QString(), Options options=Notify)
bool isValid(QStringView ifopt)
VehicleSection::Type type(QStringView coachNumber, QStringView coachClassification)
QString name(StandardAction id)
KGuiItem cont()
KGuiItem cancel()
const char * constData() const const
char * data()
QDateTime addSecs(qint64 s) const const
qint64 currentMSecsSinceEpoch()
qint64 secsTo(const QDateTime &other) const const
QString toString(QStringView format, QCalendar cal) const const
bool connect(const QString &service, const QString &path, const QString &interface, const QString &name, QObject *receiver, const char *slot)
QDBusConnection sessionBus()
void unregisterObject(const QString &path, UnregisterMode mode)
QString errorString(ErrorType error)
QString message() const const
ErrorType type() const const
QList< QVariant > arguments() const const
QString errorMessage() const const
const QDBusError & error()
bool isValid() const const
void accepted()
bool mkpath(const QString &dirPath) const const
bool exists(const QString &fileName)
virtual QString fileName() const const override
bool open(FILE *fh, OpenMode mode, FileHandleFlags handleFlags)
void setFileName(const QString &name)
virtual void close() override
bool getChar(char *c)
void append(QList< T > &&value)
iterator begin()
iterator end()
T & first()
bool isEmpty() const const
void removeFirst()
QLocale c()
int toInt(QStringView s, bool *ok) const const
QString toString(QDate date, FormatType format) const const
const_iterator constEnd() const const
const_iterator constFind(const Key &key) const const
QList< Key > keys() const const
QMetaObject::Connection connect(const QObject *sender, PointerToMemberFunction signal, Functor functor)
void deleteLater()
bool disconnect(const QMetaObject::Connection &connection)
QVariant property(const char *name) const const
void finished(int exitCode, QProcess::ExitStatus exitStatus)
void readyReadStandardOutput()
void start(OpenMode mode)
QString arg(Args &&... args) const const
bool isEmpty() const const
QString number(double n, char format, int precision)
QByteArray toLatin1() const const
std::string toStdString() const const
bool contains(QLatin1StringView str, Qt::CaseSensitivity cs) const const
UniqueConnection
QTextStream & dec(QTextStream &stream)
QTextStream & endl(QTextStream &stream)
QFuture< ArgsType< Signal > > connect(Sender *sender, Signal signal)
QTime fromMSecsSinceStartOfDay(int msecs)
int msecsSinceStartOfDay() const const
QString toString(QStringView format) const const
void timeout()
PreferLocalFile
QUrl fromUserInput(const QString &userInput, const QString &workingDirectory, UserInputResolutionOptions options)
bool isEmpty() const const
bool isValid() const const
QString toLocalFile() const const
bool isValid() const const
bool toBool() const const
int toInt(bool *ok) const const
T value() const const
This file is part of the KDE documentation.
Documentation copyright © 1996-2025 The KDE developers.
Generated on Fri Jan 24 2025 11:53:01 by doxygen 1.13.2 written by Dimitri van Heesch, © 1997-2006

KDE's Doxygen guidelines are available online.