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()->resetAutofocusCompleted();
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 // 2017-09-30 Jasem: We're skipping post align focusing now as it can be performed
771 // when first focus request is made in capture module
772 if (activeJob()->getStage() == SCHEDSTAGE_RESLEWING_COMPLETE ||
773 activeJob()->getStage() == SCHEDSTAGE_POSTALIGN_FOCUSING)
774 {
775 // Clear the HFR limit value set in the capture module
776 captureInterface()->call(QDBus::AutoDetect, "clearAutoFocusHFR");
777 // Reset Focus frame so that next frame take a full-resolution capture first.
778 focusInterface()->call(QDBus::AutoDetect, "resetFrame");
779 moduleState()->updateJobStage(SCHEDSTAGE_POSTALIGN_FOCUSING_COMPLETE);
781 return;
782 }
783
784 if (activeJob()->getOpticalTrain() != "")
785 m_activeJobs.insert(activeJob()->getOpticalTrain(), activeJob());
786 else
787 {
788 QVariant opticalTrain = captureInterface()->property("opticalTrain");
789
790 if (opticalTrain.isValid() == false)
791 {
792 qCCritical(KSTARS_EKOS_SCHEDULER) << QString("Warning: job '%1' opticalTrain request failed.").arg(activeJob()->getName());
794 {
795 activeJob()->setState(SCHEDJOB_ERROR);
796 findNextJob();
797 }
798 return;
799 }
800 // use the default optical train for the active job
801 m_activeJobs.insert(opticalTrain.toString(), activeJob());
802 activeJob()->setOpticalTrain(opticalTrain.toString());
803 }
804
805 // start focusing of the lead job
806 startFocusing(activeJob());
807 // start focusing of all follower jobds
808 foreach (auto follower, activeJob()->followerJobs())
809 {
810 m_activeJobs.insert(follower->getOpticalTrain(), follower);
811 startFocusing(follower);
812 }
813}
814
815
816void SchedulerProcess::startFocusing(SchedulerJob *job)
817{
818
819 // Check if autofocus is supported
820 QDBusReply<bool> boolReply;
821 QList<QVariant> dBusArgs;
822 dBusArgs.append(job->getOpticalTrain());
823 boolReply = focusInterface()->callWithArgumentList(QDBus::AutoDetect, "canAutoFocus", dBusArgs);
824
825 if (boolReply.error().type() != QDBusError::NoError)
826 {
827 qCCritical(KSTARS_EKOS_SCHEDULER) << QString("Warning: job '%1' canAutoFocus request received DBUS error: %2").arg(
828 job->getName(), QDBusError::errorString(boolReply.error().type()));
830 {
831 job->setState(SCHEDJOB_ERROR);
832 findNextJob();
833 }
834 return;
835 }
836
837 if (boolReply.value() == false)
838 {
839 appendLogText(i18n("Warning: job '%1' is unable to proceed with autofocus, not supported.", job->getName()));
840 job->setStepPipeline(
841 static_cast<SchedulerJob::StepPipeline>(job->getStepPipeline() & ~SchedulerJob::USE_FOCUS));
842 moduleState()->setAutofocusCompleted(job->getOpticalTrain(), true);
843 if (moduleState()->autofocusCompleted())
844 {
845 moduleState()->updateJobStage(SCHEDSTAGE_FOCUS_COMPLETE);
847 return;
848 }
849 }
850
851 QDBusMessage reply;
852
853 // Clear the HFR limit value set in the capture module
854 if ((reply = captureInterface()->callWithArgumentList(QDBus::AutoDetect, "clearAutoFocusHFR",
855 dBusArgs)).type() == QDBusMessage::ErrorMessage)
856 {
857 qCCritical(KSTARS_EKOS_SCHEDULER) << QString("Warning: job '%1' clearAutoFocusHFR request received DBUS error: %2").arg(
858 job->getName(), reply.errorMessage());
860 {
861 job->setState(SCHEDJOB_ERROR);
862 findNextJob();
863 }
864 return;
865 }
866
867 // We always need to reset frame first
868 if ((reply = focusInterface()->callWithArgumentList(QDBus::AutoDetect, "resetFrame",
869 dBusArgs)).type() == QDBusMessage::ErrorMessage)
870 {
871 qCCritical(KSTARS_EKOS_SCHEDULER) << QString("Warning: job '%1' resetFrame request received DBUS error: %2").arg(
872 job->getName(), reply.errorMessage());
874 {
875 job->setState(SCHEDJOB_ERROR);
876 findNextJob();
877 }
878 return;
879 }
880
881
882 // If we have a LIGHT filter set, let's set it.
883 if (!job->getInitialFilter().isEmpty())
884 {
885 dBusArgs.clear();
886 dBusArgs.append(job->getInitialFilter());
887 dBusArgs.append(job->getOpticalTrain());
888 if ((reply = focusInterface()->callWithArgumentList(QDBus::AutoDetect, "setFilter",
889 dBusArgs)).type() == QDBusMessage::ErrorMessage)
890 {
891 qCCritical(KSTARS_EKOS_SCHEDULER) << QString("Warning: job '%1' setFilter request received DBUS error: %1").arg(
892 job->getName(), reply.errorMessage());
894 {
895 job->setState(SCHEDJOB_ERROR);
896 findNextJob();
897 }
898 return;
899 }
900 }
901
902 dBusArgs.clear();
903 dBusArgs.append(job->getOpticalTrain());
904 boolReply = focusInterface()->callWithArgumentList(QDBus::AutoDetect, "useFullField", dBusArgs);
905
906 if (boolReply.error().type() != QDBusError::NoError)
907 {
908 qCCritical(KSTARS_EKOS_SCHEDULER) << QString("Warning: job '%1' useFullField request received DBUS error: %2").arg(
909 job->getName(), QDBusError::errorString(boolReply.error().type()));
911 {
912 job->setState(SCHEDJOB_ERROR);
913 findNextJob();
914 }
915 return;
916 }
917
918 if (boolReply.value() == false)
919 {
920 // Set autostar if full field option is false
921 dBusArgs.clear();
922 dBusArgs.append(true);
923 dBusArgs.append(job->getOpticalTrain());
924 if ((reply = focusInterface()->callWithArgumentList(QDBus::AutoDetect, "setAutoStarEnabled", dBusArgs)).type() ==
926 {
927 qCCritical(KSTARS_EKOS_SCHEDULER) << QString("Warning: job '%1' setAutoFocusStar request received DBUS error: %1").arg(
928 job->getName(), reply.errorMessage());
930 {
931 job->setState(SCHEDJOB_ERROR);
932 findNextJob();
933 }
934 return;
935 }
936 }
937
938 // Start auto-focus
939 dBusArgs.clear();
940 dBusArgs.append(job->getOpticalTrain());
941 if ((reply = focusInterface()->callWithArgumentList(QDBus::AutoDetect, "start",
942 dBusArgs)).type() == QDBusMessage::ErrorMessage)
943 {
944 qCCritical(KSTARS_EKOS_SCHEDULER) << QString("Warning: job '%1' startFocus request received DBUS error: %2").arg(
945 job->getName(), reply.errorMessage());
947 {
948 job->setState(SCHEDJOB_ERROR);
949 findNextJob();
950 }
951 return;
952 }
953
954 moduleState()->updateJobStage(SCHEDSTAGE_FOCUSING);
955 appendLogText(i18n("Job '%1' is focusing.", job->getName()));
956 moduleState()->startCurrentOperationTimer();
957}
958
960{
961 Q_ASSERT_X(nullptr != activeJob(), __FUNCTION__, "Job starting aligning must be valid");
962
963 QDBusMessage reply;
964 setSolverAction(Align::GOTO_SLEW);
965
966 // Always turn update coords on
967 //QVariant arg(true);
968 //alignInterface->call(QDBus::AutoDetect, "setUpdateCoords", arg);
969
970 // Reset the solver speedup (using the last successful index file and healpix for the
971 // pointing check) when re-aligning.
972 moduleState()->setIndexToUse(-1);
973 moduleState()->setHealpixToUse(-1);
974
975 // If FITS file is specified, then we use load and slew
976 if (activeJob()->getFITSFile().isEmpty() == false)
977 {
978 auto path = activeJob()->getFITSFile().toString(QUrl::PreferLocalFile);
979 // check if the file exists
980 if (QFile::exists(path) == false)
981 {
982 appendLogText(i18n("Warning: job '%1' target FITS file does not exist.", activeJob()->getName()));
983 activeJob()->setState(SCHEDJOB_ERROR);
984 findNextJob();
985 return;
986 }
987
988 QList<QVariant> solveArgs;
989 solveArgs.append(path);
990
991 if ((reply = alignInterface()->callWithArgumentList(QDBus::AutoDetect, "loadAndSlew", solveArgs)).type() ==
993 {
994 appendLogText(i18n("Warning: job '%1' loadAndSlew request received DBUS error: %2",
995 activeJob()->getName(), reply.errorMessage()));
997 {
998 activeJob()->setState(SCHEDJOB_ERROR);
999 findNextJob();
1000 }
1001 return;
1002 }
1003 else if (reply.arguments().first().toBool() == false)
1004 {
1005 appendLogText(i18n("Warning: job '%1' loadAndSlew request failed.", activeJob()->getName()));
1006 activeJob()->setState(SCHEDJOB_ABORTED);
1007 findNextJob();
1008 return;
1009 }
1010
1011 appendLogText(i18n("Job '%1' is plate solving %2.", activeJob()->getName(), activeJob()->getFITSFile().fileName()));
1012 }
1013 else
1014 {
1015 // JM 2020.08.20: Send J2000 TargetCoords to Align module so that we always resort back to the
1016 // target original targets even if we drifted away due to any reason like guiding calibration failures.
1017 const SkyPoint targetCoords = activeJob()->getTargetCoords();
1018 QList<QVariant> targetArgs, rotationArgs;
1019 targetArgs << targetCoords.ra0().Hours() << targetCoords.dec0().Degrees();
1020 rotationArgs << activeJob()->getPositionAngle();
1021
1022 if ((reply = alignInterface()->callWithArgumentList(QDBus::AutoDetect, "setTargetCoords",
1023 targetArgs)).type() == QDBusMessage::ErrorMessage)
1024 {
1025 appendLogText(i18n("Warning: job '%1' setTargetCoords request received DBUS error: %2",
1026 activeJob()->getName(), reply.errorMessage()));
1027 if (!manageConnectionLoss())
1028 {
1029 activeJob()->setState(SCHEDJOB_ERROR);
1030 findNextJob();
1031 }
1032 return;
1033 }
1034
1035 // Only send if it has valid value.
1036 if (activeJob()->getPositionAngle() >= -180)
1037 {
1038 if ((reply = alignInterface()->callWithArgumentList(QDBus::AutoDetect, "setTargetPositionAngle",
1039 rotationArgs)).type() == QDBusMessage::ErrorMessage)
1040 {
1041 appendLogText(i18n("Warning: job '%1' setTargetPositionAngle request received DBUS error: %2").arg(
1042 activeJob()->getName(), reply.errorMessage()));
1043 if (!manageConnectionLoss())
1044 {
1045 activeJob()->setState(SCHEDJOB_ERROR);
1046 findNextJob();
1047 }
1048 return;
1049 }
1050 }
1051
1052 if ((reply = alignInterface()->call(QDBus::AutoDetect, "captureAndSolve")).type() == QDBusMessage::ErrorMessage)
1053 {
1054 appendLogText(i18n("Warning: job '%1' captureAndSolve request received DBUS error: %2").arg(
1055 activeJob()->getName(), reply.errorMessage()));
1056 if (!manageConnectionLoss())
1057 {
1058 activeJob()->setState(SCHEDJOB_ERROR);
1059 findNextJob();
1060 }
1061 return;
1062 }
1063 else if (reply.arguments().first().toBool() == false)
1064 {
1065 appendLogText(i18n("Warning: job '%1' captureAndSolve request failed.", activeJob()->getName()));
1066 activeJob()->setState(SCHEDJOB_ABORTED);
1067 findNextJob();
1068 return;
1069 }
1070
1071 appendLogText(i18n("Job '%1' is capturing and plate solving.", activeJob()->getName()));
1072 }
1073
1074 /* FIXME: not supposed to modify the job */
1075 moduleState()->updateJobStage(SCHEDSTAGE_ALIGNING);
1076 moduleState()->startCurrentOperationTimer();
1077}
1078
1079void SchedulerProcess::startGuiding(bool resetCalibration)
1080{
1081 Q_ASSERT_X(nullptr != activeJob(), __FUNCTION__, "Job starting guiding must be valid");
1082
1083 // avoid starting the guider twice
1084 if (resetCalibration == false && getGuidingStatus() == GUIDE_GUIDING)
1085 {
1086 moduleState()->updateJobStage(SCHEDSTAGE_GUIDING_COMPLETE);
1087 appendLogText(i18n("Guiding already running for %1, starting next scheduler action...", activeJob()->getName()));
1088 getNextAction();
1089 moduleState()->startCurrentOperationTimer();
1090 return;
1091 }
1092
1093 // Connect Guider
1094 guideInterface()->call(QDBus::AutoDetect, "connectGuider");
1095
1096 // Set Auto Star to true
1097 QVariant arg(true);
1098 guideInterface()->call(QDBus::AutoDetect, "setAutoStarEnabled", arg);
1099
1100 // Only reset calibration on trouble
1101 // and if we are allowed to reset calibration (true by default)
1102 if (resetCalibration && Options::resetGuideCalibration())
1103 {
1104 guideInterface()->call(QDBus::AutoDetect, "clearCalibration");
1105 }
1106
1107 guideInterface()->call(QDBus::AutoDetect, "guide");
1108
1109 moduleState()->updateJobStage(SCHEDSTAGE_GUIDING);
1110
1111 appendLogText(i18n("Starting guiding procedure for %1 ...", activeJob()->getName()));
1112
1113 moduleState()->startCurrentOperationTimer();
1114}
1115
1117{
1118 if (!guideInterface())
1119 return;
1120
1121 // Tell guider to abort if the current job requires guiding - end-user may enable guiding manually before observation
1122 if (nullptr != activeJob() && (activeJob()->getStepPipeline() & SchedulerJob::USE_GUIDE))
1123 {
1124 qCInfo(KSTARS_EKOS_SCHEDULER) << QString("Job '%1' is stopping guiding...").arg(activeJob()->getName());
1125 guideInterface()->call(QDBus::AutoDetect, "abort");
1126 moduleState()->resetGuideFailureCount();
1127 // abort all follower jobs
1128 stopCapturing("", true);
1129 }
1130
1131 // In any case, stop the automatic guider restart
1132 if (moduleState()->isGuidingTimerActive())
1133 moduleState()->cancelGuidingTimer();
1134}
1135
1137{
1138 if ((moduleState()->restartGuidingInterval() > 0) &&
1139 (moduleState()->restartGuidingTime().msecsTo(KStarsData::Instance()->ut()) > moduleState()->restartGuidingInterval()))
1140 {
1141 moduleState()->cancelGuidingTimer();
1142 startGuiding(true);
1143 }
1144}
1145
1147{
1148 Q_ASSERT_X(nullptr != activeJob(), __FUNCTION__, "Job starting capturing must be valid");
1149
1150 // ensure that guiding is running before we start capturing
1151 if (activeJob()->getStepPipeline() & SchedulerJob::USE_GUIDE && getGuidingStatus() != GUIDE_GUIDING)
1152 {
1153 // guiding should run, but it doesn't. So start guiding first
1154 moduleState()->updateJobStage(SCHEDSTAGE_GUIDING);
1155 startGuiding();
1156 return;
1157 }
1158
1159 startSingleCapture(activeJob(), restart);
1160 for (auto follower : activeJob()->followerJobs())
1161 {
1162 // start follower jobs that scheduled or that were already capturing, but stopped
1163 if (follower->getState() == SCHEDJOB_SCHEDULED || (follower->getStage() == SCHEDSTAGE_CAPTURING && follower->isStopped()))
1164 {
1165 follower->setState(SCHEDJOB_BUSY);
1166 follower->setStage(SCHEDSTAGE_CAPTURING);
1167 startSingleCapture(follower, restart);
1168 }
1169 }
1170
1171 moduleState()->updateJobStage(SCHEDSTAGE_CAPTURING);
1172
1173 KSNotification::event(QLatin1String("EkosScheduledImagingStart"),
1174 i18n("Ekos job (%1) - Capture started", activeJob()->getName()), KSNotification::Scheduler);
1175
1176 if (moduleState()->captureBatch() > 0)
1177 appendLogText(i18n("Job '%1' capture is in progress (batch #%2)...", activeJob()->getName(),
1178 moduleState()->captureBatch() + 1));
1179 else
1180 appendLogText(i18n("Job '%1' capture is in progress...", activeJob()->getName()));
1181
1182 moduleState()->startCurrentOperationTimer();
1183}
1184
1185void SchedulerProcess::startSingleCapture(SchedulerJob *job, bool restart)
1186{
1187 captureInterface()->setProperty("targetName", job->getName());
1188
1189 QString url = job->getSequenceFile().toLocalFile();
1190 QVariant train(job->getOpticalTrain());
1191
1192 if (restart == false)
1193 {
1194 QList<QVariant> dbusargs;
1195 QVariant isLead(job->isLead());
1196 // override targets from sequence queue file
1197 QVariant targetName(job->getName());
1198 dbusargs.append(url);
1199 dbusargs.append(train);
1200 dbusargs.append(isLead);
1201 dbusargs.append(targetName);
1202 QDBusReply<bool> const captureReply = captureInterface()->callWithArgumentList(QDBus::AutoDetect,
1203 "loadSequenceQueue",
1204 dbusargs);
1205 if (captureReply.error().type() != QDBusError::NoError)
1206 {
1207 qCCritical(KSTARS_EKOS_SCHEDULER) <<
1208 QString("Warning: job '%1' loadSequenceQueue request received DBUS error: %1").arg(job->getName()).arg(
1209 captureReply.error().message());
1210 if (!manageConnectionLoss())
1211 job->setState(SCHEDJOB_ERROR);
1212 return;
1213 }
1214 // Check if loading sequence fails for whatever reason
1215 else if (captureReply.value() == false)
1216 {
1217 qCCritical(KSTARS_EKOS_SCHEDULER) <<
1218 QString("Warning: job '%1' loadSequenceQueue request failed").arg(job->getName());
1219 if (!manageConnectionLoss())
1220 job->setState(SCHEDJOB_ERROR);
1221 return;
1222 }
1223 }
1224
1225 const CapturedFramesMap fMap = job->getCapturedFramesMap();
1226
1227 for (auto &e : fMap.keys())
1228 {
1229 QList<QVariant> dbusargs;
1230 QDBusMessage reply;
1231 dbusargs.append(e);
1232 dbusargs.append(fMap.value(e));
1233 dbusargs.append(train);
1234
1235 if ((reply = captureInterface()->callWithArgumentList(QDBus::Block, "setCapturedFramesMap",
1236 dbusargs)).type() ==
1238 {
1239 qCCritical(KSTARS_EKOS_SCHEDULER) <<
1240 QString("Warning: job '%1' setCapturedFramesCount request received DBUS error: %1").arg(job->getName()).arg(
1241 reply.errorMessage());
1242 if (!manageConnectionLoss())
1243 job->setState(SCHEDJOB_ERROR);
1244 return;
1245 }
1246 }
1247
1248 // Start capture process
1249 QList<QVariant> dbusargs;
1250 dbusargs.append(train);
1251
1252 QDBusReply<QString> const startReply = captureInterface()->callWithArgumentList(QDBus::AutoDetect, "start",
1253 dbusargs);
1254
1255 if (startReply.error().type() != QDBusError::NoError)
1256 {
1257 qCCritical(KSTARS_EKOS_SCHEDULER) <<
1258 QString("Warning: job '%1' start request received DBUS error: %1").arg(job->getName()).arg(
1259 startReply.error().message());
1260 if (!manageConnectionLoss())
1261 job->setState(SCHEDJOB_ERROR);
1262 return;
1263 }
1264
1265 QString trainName = startReply.value();
1266 m_activeJobs[trainName] = job;
1267 // set the
1268}
1269
1270void SchedulerProcess::setSolverAction(Align::GotoMode mode)
1271{
1272 QVariant gotoMode(static_cast<int>(mode));
1273 alignInterface()->call(QDBus::AutoDetect, "setSolverAction", gotoMode);
1274}
1275
1277{
1278 qCDebug(KSTARS_EKOS_SCHEDULER) << "Loading profiles";
1279 QDBusReply<QStringList> profiles = ekosInterface()->call(QDBus::AutoDetect, "getProfiles");
1280
1281 if (profiles.error().type() == QDBusError::NoError)
1282 moduleState()->updateProfiles(profiles);
1283}
1284
1285void SchedulerProcess::executeScript(const QString &filename)
1286{
1287 appendLogText(i18n("Executing script %1...", filename));
1288
1289 connect(&scriptProcess(), &QProcess::readyReadStandardOutput, this, &SchedulerProcess::readProcessOutput);
1290
1291 connect(&scriptProcess(), static_cast<void (QProcess::*)(int, QProcess::ExitStatus)>(&QProcess::finished),
1292 this, [this](int exitCode, QProcess::ExitStatus)
1293 {
1294 checkProcessExit(exitCode);
1295 });
1296
1297 QStringList arguments;
1298 scriptProcess().start(filename, arguments);
1299}
1300
1302{
1303 if (moduleState()->schedulerState() == SCHEDULER_PAUSED)
1304 return false;
1305
1306 switch (moduleState()->ekosState())
1307 {
1308 case EKOS_IDLE:
1309 {
1310 if (moduleState()->ekosCommunicationStatus() == Ekos::Success)
1311 {
1312 moduleState()->setEkosState(EKOS_READY);
1313 return true;
1314 }
1315 else
1316 {
1317 ekosInterface()->call(QDBus::AutoDetect, "start");
1318 moduleState()->setEkosState(EKOS_STARTING);
1319 moduleState()->startCurrentOperationTimer();
1320
1321 qCInfo(KSTARS_EKOS_SCHEDULER) << "Ekos communication status is" << moduleState()->ekosCommunicationStatus() <<
1322 "Starting Ekos...";
1323
1324 return false;
1325 }
1326 }
1327
1328 case EKOS_STARTING:
1329 {
1330 if (moduleState()->ekosCommunicationStatus() == Ekos::Success)
1331 {
1332 appendLogText(i18n("Ekos started."));
1333 moduleState()->resetEkosConnectFailureCount();
1334 moduleState()->setEkosState(EKOS_READY);
1335 return true;
1336 }
1337 else if (moduleState()->ekosCommunicationStatus() == Ekos::Error)
1338 {
1339 if (moduleState()->increaseEkosConnectFailureCount())
1340 {
1341 appendLogText(i18n("Starting Ekos failed. Retrying..."));
1342 ekosInterface()->call(QDBus::AutoDetect, "start");
1343 return false;
1344 }
1345
1346 appendLogText(i18n("Starting Ekos failed."));
1347 stop();
1348 return false;
1349 }
1350 else if (moduleState()->ekosCommunicationStatus() == Ekos::Idle)
1351 return false;
1352 // If a minute passed, give up
1353 else if (moduleState()->getCurrentOperationMsec() > (60 * 1000))
1354 {
1355 if (moduleState()->increaseEkosConnectFailureCount())
1356 {
1357 appendLogText(i18n("Starting Ekos timed out. Retrying..."));
1358 ekosInterface()->call(QDBus::AutoDetect, "stop");
1359 QTimer::singleShot(1000, this, [&]()
1360 {
1361 ekosInterface()->call(QDBus::AutoDetect, "start");
1362 moduleState()->startCurrentOperationTimer();
1363 });
1364 return false;
1365 }
1366
1367 appendLogText(i18n("Starting Ekos timed out."));
1368 stop();
1369 return false;
1370 }
1371 }
1372 break;
1373
1374 case EKOS_STOPPING:
1375 {
1376 if (moduleState()->ekosCommunicationStatus() == Ekos::Idle)
1377 {
1378 appendLogText(i18n("Ekos stopped."));
1379 moduleState()->setEkosState(EKOS_IDLE);
1380 return true;
1381 }
1382 }
1383 break;
1384
1385 case EKOS_READY:
1386 return true;
1387 }
1388 return false;
1389}
1390
1392{
1393 if (moduleState()->schedulerState() == SCHEDULER_PAUSED)
1394 return false;
1395
1396 switch (moduleState()->indiState())
1397 {
1398 case INDI_IDLE:
1399 {
1400 if (moduleState()->indiCommunicationStatus() == Ekos::Success)
1401 {
1402 moduleState()->setIndiState(INDI_PROPERTY_CHECK);
1403 moduleState()->resetIndiConnectFailureCount();
1404 qCDebug(KSTARS_EKOS_SCHEDULER) << "Checking INDI Properties...";
1405 }
1406 else
1407 {
1408 qCDebug(KSTARS_EKOS_SCHEDULER) << "Connecting INDI devices...";
1409 ekosInterface()->call(QDBus::AutoDetect, "connectDevices");
1410 moduleState()->setIndiState(INDI_CONNECTING);
1411
1412 moduleState()->startCurrentOperationTimer();
1413 }
1414 }
1415 break;
1416
1417 case INDI_CONNECTING:
1418 {
1419 if (moduleState()->indiCommunicationStatus() == Ekos::Success)
1420 {
1421 appendLogText(i18n("INDI devices connected."));
1422 moduleState()->setIndiState(INDI_PROPERTY_CHECK);
1423 }
1424 else if (moduleState()->indiCommunicationStatus() == Ekos::Error)
1425 {
1426 if (moduleState()->increaseIndiConnectFailureCount() <= moduleState()->maxFailureAttempts())
1427 {
1428 appendLogText(i18n("One or more INDI devices failed to connect. Retrying..."));
1429 ekosInterface()->call(QDBus::AutoDetect, "connectDevices");
1430 }
1431 else
1432 {
1433 appendLogText(i18n("One or more INDI devices failed to connect. Check INDI control panel for details."));
1434 stop();
1435 }
1436 }
1437 // If 30 seconds passed, we retry
1438 else if (moduleState()->getCurrentOperationMsec() > (30 * 1000))
1439 {
1440 if (moduleState()->increaseIndiConnectFailureCount() <= moduleState()->maxFailureAttempts())
1441 {
1442 appendLogText(i18n("One or more INDI devices timed out. Retrying..."));
1443 ekosInterface()->call(QDBus::AutoDetect, "connectDevices");
1444 moduleState()->startCurrentOperationTimer();
1445 }
1446 else
1447 {
1448 appendLogText(i18n("One or more INDI devices timed out. Check INDI control panel for details."));
1449 stop();
1450 }
1451 }
1452 }
1453 break;
1454
1455 case INDI_DISCONNECTING:
1456 {
1457 if (moduleState()->indiCommunicationStatus() == Ekos::Idle)
1458 {
1459 appendLogText(i18n("INDI devices disconnected."));
1460 moduleState()->setIndiState(INDI_IDLE);
1461 return true;
1462 }
1463 }
1464 break;
1465
1466 case INDI_PROPERTY_CHECK:
1467 {
1468 qCDebug(KSTARS_EKOS_SCHEDULER) << "Checking INDI properties.";
1469 // If dome unparking is required then we wait for dome interface
1470 if (Options::schedulerUnparkDome() && moduleState()->domeReady() == false)
1471 {
1472 if (moduleState()->getCurrentOperationMsec() > (30 * 1000))
1473 {
1474 moduleState()->startCurrentOperationTimer();
1475 appendLogText(i18n("Warning: dome device not ready after timeout, attempting to recover..."));
1477 stopEkos();
1478 }
1479
1480 appendLogText(i18n("Dome unpark required but dome is not yet ready."));
1481 return false;
1482 }
1483
1484 // If mount unparking is required then we wait for mount interface
1485 if (Options::schedulerUnparkMount() && moduleState()->mountReady() == false)
1486 {
1487 if (moduleState()->getCurrentOperationMsec() > (30 * 1000))
1488 {
1489 moduleState()->startCurrentOperationTimer();
1490 appendLogText(i18n("Warning: mount device not ready after timeout, attempting to recover..."));
1492 stopEkos();
1493 }
1494
1495 qCDebug(KSTARS_EKOS_SCHEDULER) << "Mount unpark required but mount is not yet ready.";
1496 return false;
1497 }
1498
1499 // If cap unparking is required then we wait for cap interface
1500 if (Options::schedulerOpenDustCover() && moduleState()->capReady() == false)
1501 {
1502 if (moduleState()->getCurrentOperationMsec() > (30 * 1000))
1503 {
1504 moduleState()->startCurrentOperationTimer();
1505 appendLogText(i18n("Warning: cap device not ready after timeout, attempting to recover..."));
1507 stopEkos();
1508 }
1509
1510 qCDebug(KSTARS_EKOS_SCHEDULER) << "Cap unpark required but cap is not yet ready.";
1511 return false;
1512 }
1513
1514 // capture interface is required at all times to proceed.
1515 if (captureInterface().isNull())
1516 return false;
1517
1518 if (moduleState()->captureReady() == false)
1519 {
1520 QVariant hasCoolerControl = captureInterface()->property("coolerControl");
1521 qCDebug(KSTARS_EKOS_SCHEDULER) << "Cooler control" << (!hasCoolerControl.isValid() ? "invalid" :
1522 (hasCoolerControl.toBool() ? "True" : "Faklse"));
1523 if (hasCoolerControl.isValid())
1524 moduleState()->setCaptureReady(true);
1525 else
1526 qCWarning(KSTARS_EKOS_SCHEDULER) << "Capture module is not ready yet...";
1527 }
1528
1529 moduleState()->setIndiState(INDI_READY);
1530 moduleState()->resetIndiConnectFailureCount();
1531 return true;
1532 }
1533
1534 case INDI_READY:
1535 return true;
1536 }
1537
1538 return false;
1539}
1540
1542{
1543 // If INDI is not done disconnecting, try again later
1544 if (moduleState()->indiState() == INDI_DISCONNECTING
1545 && checkINDIState() == false)
1546 return false;
1547
1548 // Disconnect INDI if required first
1549 if (moduleState()->indiState() != INDI_IDLE && Options::stopEkosAfterShutdown())
1550 {
1552 return false;
1553 }
1554
1555 // If Ekos is not done stopping, try again later
1556 if (moduleState()->ekosState() == EKOS_STOPPING && checkEkosState() == false)
1557 return false;
1558
1559 // Stop Ekos if required.
1560 if (moduleState()->ekosState() != EKOS_IDLE && Options::stopEkosAfterShutdown())
1561 {
1562 stopEkos();
1563 return false;
1564 }
1565
1566 if (moduleState()->shutdownState() == SHUTDOWN_COMPLETE)
1567 appendLogText(i18n("Shutdown complete."));
1568 else
1569 appendLogText(i18n("Shutdown procedure failed, aborting..."));
1570
1571 // Stop Scheduler
1572 stop();
1573
1574 return true;
1575}
1576
1578{
1579 qCInfo(KSTARS_EKOS_SCHEDULER) << "Disconnecting INDI...";
1580 moduleState()->setIndiState(INDI_DISCONNECTING);
1581 ekosInterface()->call(QDBus::AutoDetect, "disconnectDevices");
1582}
1583
1584void SchedulerProcess::stopEkos()
1585{
1586 qCInfo(KSTARS_EKOS_SCHEDULER) << "Stopping Ekos...";
1587 moduleState()->setEkosState(EKOS_STOPPING);
1588 moduleState()->resetEkosConnectFailureCount();
1589 ekosInterface()->call(QDBus::AutoDetect, "stop");
1590 moduleState()->setMountReady(false);
1591 moduleState()->setCaptureReady(false);
1592 moduleState()->setDomeReady(false);
1593 moduleState()->setCapReady(false);
1594}
1595
1597{
1598 if (SCHEDULER_RUNNING != moduleState()->schedulerState())
1599 return false;
1600
1601 // Don't manage loss if Ekos is actually down in the state machine
1602 switch (moduleState()->ekosState())
1603 {
1604 case EKOS_IDLE:
1605 case EKOS_STOPPING:
1606 return false;
1607
1608 default:
1609 break;
1610 }
1611
1612 // Don't manage loss if INDI is actually down in the state machine
1613 switch (moduleState()->indiState())
1614 {
1615 case INDI_IDLE:
1616 case INDI_DISCONNECTING:
1617 return false;
1618
1619 default:
1620 break;
1621 }
1622
1623 // If Ekos is assumed to be up, check its state
1624 //QDBusReply<int> const isEkosStarted = ekosInterface->call(QDBus::AutoDetect, "getEkosStartingStatus");
1625 if (moduleState()->ekosCommunicationStatus() == Ekos::Success)
1626 {
1627 qCDebug(KSTARS_EKOS_SCHEDULER) << QString("Ekos is currently connected, checking INDI before mitigating connection loss.");
1628
1629 // If INDI is assumed to be up, check its state
1630 if (moduleState()->isINDIConnected())
1631 {
1632 // If both Ekos and INDI are assumed up, and are actually up, no mitigation needed, this is a DBus interface error
1633 qCDebug(KSTARS_EKOS_SCHEDULER) << QString("INDI is currently connected, no connection loss mitigation needed.");
1634 return false;
1635 }
1636 }
1637
1638 // Stop actions of the current job
1640
1641 // Acknowledge INDI and Ekos disconnections
1643 stopEkos();
1644
1645 // Let the Scheduler attempt to connect INDI again
1646 return true;
1647
1648}
1649
1651{
1652 if (capInterface().isNull())
1653 return;
1654
1655 QVariant parkingStatus = capInterface()->property("parkStatus");
1656 qCDebug(KSTARS_EKOS_SCHEDULER) << "Parking status" << (!parkingStatus.isValid() ? -1 : parkingStatus.toInt());
1657
1658 if (parkingStatus.isValid() == false)
1659 {
1660 qCCritical(KSTARS_EKOS_SCHEDULER) << QString("Warning: cap parkStatus request received DBUS error: %1").arg(
1661 capInterface()->lastError().type());
1662 if (!manageConnectionLoss())
1663 parkingStatus = ISD::PARK_ERROR;
1664 }
1665
1666 ISD::ParkStatus status = static_cast<ISD::ParkStatus>(parkingStatus.toInt());
1667
1668 switch (status)
1669 {
1670 case ISD::PARK_PARKED:
1671 if (moduleState()->shutdownState() == SHUTDOWN_PARKING_CAP)
1672 {
1673 appendLogText(i18n("Cap parked."));
1674 moduleState()->setShutdownState(SHUTDOWN_PARK_MOUNT);
1675 }
1676 moduleState()->resetParkingCapFailureCount();
1677 break;
1678
1679 case ISD::PARK_UNPARKED:
1680 if (moduleState()->startupState() == STARTUP_UNPARKING_CAP)
1681 {
1682 moduleState()->setStartupState(STARTUP_COMPLETE);
1683 appendLogText(i18n("Cap unparked."));
1684 }
1685 moduleState()->resetParkingCapFailureCount();
1686 break;
1687
1688 case ISD::PARK_PARKING:
1689 case ISD::PARK_UNPARKING:
1690 // TODO make the timeouts configurable by the user
1691 if (moduleState()->getCurrentOperationMsec() > (60 * 1000))
1692 {
1693 if (moduleState()->increaseParkingCapFailureCount())
1694 {
1695 appendLogText(i18n("Operation timeout. Restarting operation..."));
1696 if (status == ISD::PARK_PARKING)
1697 parkCap();
1698 else
1699 unParkCap();
1700 break;
1701 }
1702 }
1703 break;
1704
1705 case ISD::PARK_ERROR:
1706 if (moduleState()->shutdownState() == SHUTDOWN_PARKING_CAP)
1707 {
1708 appendLogText(i18n("Cap parking error."));
1709 moduleState()->setShutdownState(SHUTDOWN_ERROR);
1710 }
1711 else if (moduleState()->startupState() == STARTUP_UNPARKING_CAP)
1712 {
1713 appendLogText(i18n("Cap unparking error."));
1714 moduleState()->setStartupState(STARTUP_ERROR);
1715 }
1716 moduleState()->resetParkingCapFailureCount();
1717 break;
1718
1719 default:
1720 break;
1721 }
1722}
1723
1724void SchedulerProcess::checkMountParkingStatus()
1725{
1726 if (mountInterface().isNull())
1727 return;
1728
1729 QVariant parkingStatus = mountInterface()->property("parkStatus");
1730 qCDebug(KSTARS_EKOS_SCHEDULER) << "Mount parking status" << (!parkingStatus.isValid() ? -1 : parkingStatus.toInt());
1731
1732 if (parkingStatus.isValid() == false)
1733 {
1734 qCCritical(KSTARS_EKOS_SCHEDULER) << QString("Warning: mount parkStatus request received DBUS error: %1").arg(
1735 mountInterface()->lastError().type());
1736 if (!manageConnectionLoss())
1737 moduleState()->setParkWaitState(PARKWAIT_ERROR);
1738 }
1739
1740 ISD::ParkStatus status = static_cast<ISD::ParkStatus>(parkingStatus.toInt());
1741
1742 switch (status)
1743 {
1744 //case Mount::PARKING_OK:
1745 case ISD::PARK_PARKED:
1746 // If we are starting up, we will unpark the mount in checkParkWaitState soon
1747 // If we are shutting down and mount is parked, proceed to next step
1748 if (moduleState()->shutdownState() == SHUTDOWN_PARKING_MOUNT)
1749 moduleState()->setShutdownState(SHUTDOWN_PARK_DOME);
1750
1751 // Update parking engine state
1752 if (moduleState()->parkWaitState() == PARKWAIT_PARKING)
1753 moduleState()->setParkWaitState(PARKWAIT_PARKED);
1754
1755 appendLogText(i18n("Mount parked."));
1756 moduleState()->resetParkingMountFailureCount();
1757 break;
1758
1759 //case Mount::UNPARKING_OK:
1760 case ISD::PARK_UNPARKED:
1761 // If we are starting up and mount is unparked, proceed to next step
1762 // If we are shutting down, we will park the mount in checkParkWaitState soon
1763 if (moduleState()->startupState() == STARTUP_UNPARKING_MOUNT)
1764 moduleState()->setStartupState(STARTUP_UNPARK_CAP);
1765
1766 // Update parking engine state
1767 if (moduleState()->parkWaitState() == PARKWAIT_UNPARKING)
1768 moduleState()->setParkWaitState(PARKWAIT_UNPARKED);
1769
1770 appendLogText(i18n("Mount unparked."));
1771 moduleState()->resetParkingMountFailureCount();
1772 break;
1773
1774 // FIXME: Create an option for the parking/unparking timeout.
1775
1776 //case Mount::UNPARKING_BUSY:
1777 case ISD::PARK_UNPARKING:
1778 if (moduleState()->getCurrentOperationMsec() > (60 * 1000))
1779 {
1780 if (moduleState()->increaseParkingMountFailureCount())
1781 {
1782 appendLogText(i18n("Warning: mount unpark operation timed out on attempt %1/%2. Restarting operation...",
1783 moduleState()->parkingMountFailureCount(), moduleState()->maxFailureAttempts()));
1784 unParkMount();
1785 }
1786 else
1787 {
1788 appendLogText(i18n("Warning: mount unpark operation timed out on last attempt."));
1789 moduleState()->setParkWaitState(PARKWAIT_ERROR);
1790 }
1791 }
1792 else qCInfo(KSTARS_EKOS_SCHEDULER) << "Unparking mount in progress...";
1793
1794 break;
1795
1796 //case Mount::PARKING_BUSY:
1797 case ISD::PARK_PARKING:
1798 if (moduleState()->getCurrentOperationMsec() > (60 * 1000))
1799 {
1800 if (moduleState()->increaseParkingMountFailureCount())
1801 {
1802 appendLogText(i18n("Warning: mount park operation timed out on attempt %1/%2. Restarting operation...",
1803 moduleState()->parkingMountFailureCount(),
1804 moduleState()->maxFailureAttempts()));
1805 parkMount();
1806 }
1807 else
1808 {
1809 appendLogText(i18n("Warning: mount park operation timed out on last attempt."));
1810 moduleState()->setParkWaitState(PARKWAIT_ERROR);
1811 }
1812 }
1813 else qCInfo(KSTARS_EKOS_SCHEDULER) << "Parking mount in progress...";
1814
1815 break;
1816
1817 //case Mount::PARKING_ERROR:
1818 case ISD::PARK_ERROR:
1819 if (moduleState()->startupState() == STARTUP_UNPARKING_MOUNT)
1820 {
1821 appendLogText(i18n("Mount unparking error."));
1822 moduleState()->setStartupState(STARTUP_ERROR);
1823 moduleState()->resetParkingMountFailureCount();
1824 }
1825 else if (moduleState()->shutdownState() == SHUTDOWN_PARKING_MOUNT)
1826 {
1827 if (moduleState()->increaseParkingMountFailureCount())
1828 {
1829 appendLogText(i18n("Warning: mount park operation failed on attempt %1/%2. Restarting operation...",
1830 moduleState()->parkingMountFailureCount(),
1831 moduleState()->maxFailureAttempts()));
1832 parkMount();
1833 }
1834 else
1835 {
1836 appendLogText(i18n("Mount parking error."));
1837 moduleState()->setShutdownState(SHUTDOWN_ERROR);
1838 moduleState()->resetParkingMountFailureCount();
1839 }
1840
1841 }
1842 else if (moduleState()->parkWaitState() == PARKWAIT_PARKING)
1843 {
1844 appendLogText(i18n("Mount parking error."));
1845 moduleState()->setParkWaitState(PARKWAIT_ERROR);
1846 moduleState()->resetParkingMountFailureCount();
1847 }
1848 else if (moduleState()->parkWaitState() == PARKWAIT_UNPARKING)
1849 {
1850 appendLogText(i18n("Mount unparking error."));
1851 moduleState()->setParkWaitState(PARKWAIT_ERROR);
1852 moduleState()->resetParkingMountFailureCount();
1853 }
1854 break;
1855
1856 //case Mount::PARKING_IDLE:
1857 // FIXME Does this work as intended? check!
1858 case ISD::PARK_UNKNOWN:
1859 // Last parking action did not result in an action, so proceed to next step
1860 if (moduleState()->shutdownState() == SHUTDOWN_PARKING_MOUNT)
1861 moduleState()->setShutdownState(SHUTDOWN_PARK_DOME);
1862
1863 // Last unparking action did not result in an action, so proceed to next step
1864 if (moduleState()->startupState() == STARTUP_UNPARKING_MOUNT)
1865 moduleState()->setStartupState(STARTUP_UNPARK_CAP);
1866
1867 // Update parking engine state
1868 if (moduleState()->parkWaitState() == PARKWAIT_PARKING)
1869 moduleState()->setParkWaitState(PARKWAIT_PARKED);
1870 else if (moduleState()->parkWaitState() == PARKWAIT_UNPARKING)
1871 moduleState()->setParkWaitState(PARKWAIT_UNPARKED);
1872
1873 moduleState()->resetParkingMountFailureCount();
1874 break;
1875 }
1876}
1877
1878void SchedulerProcess::checkDomeParkingStatus()
1879{
1880 if (domeInterface().isNull())
1881 return;
1882
1883 QVariant parkingStatus = domeInterface()->property("parkStatus");
1884 qCDebug(KSTARS_EKOS_SCHEDULER) << "Dome parking status" << (!parkingStatus.isValid() ? -1 : parkingStatus.toInt());
1885
1886 if (parkingStatus.isValid() == false)
1887 {
1888 qCCritical(KSTARS_EKOS_SCHEDULER) << QString("Warning: dome parkStatus request received DBUS error: %1").arg(
1889 mountInterface()->lastError().type());
1890 if (!manageConnectionLoss())
1891 moduleState()->setParkWaitState(PARKWAIT_ERROR);
1892 }
1893
1894 ISD::ParkStatus status = static_cast<ISD::ParkStatus>(parkingStatus.toInt());
1895
1896 switch (status)
1897 {
1898 case ISD::PARK_PARKED:
1899 if (moduleState()->shutdownState() == SHUTDOWN_PARKING_DOME)
1900 {
1901 appendLogText(i18n("Dome parked."));
1902
1903 moduleState()->setShutdownState(SHUTDOWN_SCRIPT);
1904 }
1905 moduleState()->resetParkingDomeFailureCount();
1906 break;
1907
1908 case ISD::PARK_UNPARKED:
1909 if (moduleState()->startupState() == STARTUP_UNPARKING_DOME)
1910 {
1911 moduleState()->setStartupState(STARTUP_UNPARK_MOUNT);
1912 appendLogText(i18n("Dome unparked."));
1913 }
1914 moduleState()->resetParkingDomeFailureCount();
1915 break;
1916
1917 case ISD::PARK_PARKING:
1918 case ISD::PARK_UNPARKING:
1919 // TODO make the timeouts configurable by the user
1920 if (moduleState()->getCurrentOperationMsec() > (120 * 1000))
1921 {
1922 if (moduleState()->increaseParkingDomeFailureCount())
1923 {
1924 appendLogText(i18n("Operation timeout. Restarting operation..."));
1925 if (status == ISD::PARK_PARKING)
1926 parkDome();
1927 else
1928 unParkDome();
1929 break;
1930 }
1931 }
1932 break;
1933
1934 case ISD::PARK_ERROR:
1935 if (moduleState()->shutdownState() == SHUTDOWN_PARKING_DOME)
1936 {
1937 if (moduleState()->increaseParkingDomeFailureCount())
1938 {
1939 appendLogText(i18n("Dome parking failed. Restarting operation..."));
1940 parkDome();
1941 }
1942 else
1943 {
1944 appendLogText(i18n("Dome parking error."));
1945 moduleState()->setShutdownState(SHUTDOWN_ERROR);
1946 moduleState()->resetParkingDomeFailureCount();
1947 }
1948 }
1949 else if (moduleState()->startupState() == STARTUP_UNPARKING_DOME)
1950 {
1951 if (moduleState()->increaseParkingDomeFailureCount())
1952 {
1953 appendLogText(i18n("Dome unparking failed. Restarting operation..."));
1954 unParkDome();
1955 }
1956 else
1957 {
1958 appendLogText(i18n("Dome unparking error."));
1959 moduleState()->setStartupState(STARTUP_ERROR);
1960 moduleState()->resetParkingDomeFailureCount();
1961 }
1962 }
1963 break;
1964
1965 default:
1966 break;
1967 }
1968}
1969
1971{
1972 if (moduleState()->schedulerState() == SCHEDULER_PAUSED)
1973 return false;
1974
1975 qCDebug(KSTARS_EKOS_SCHEDULER) << QString("Checking Startup State (%1)...").arg(moduleState()->startupState());
1976
1977 switch (moduleState()->startupState())
1978 {
1979 case STARTUP_IDLE:
1980 {
1981 KSNotification::event(QLatin1String("ObservatoryStartup"), i18n("Observatory is in the startup process"),
1982 KSNotification::Scheduler);
1983
1984 qCDebug(KSTARS_EKOS_SCHEDULER) << "Startup Idle. Starting startup process...";
1985
1986 // If Ekos is already started, we skip the script and move on to dome unpark step
1987 // unless we do not have light frames, then we skip all
1988 //QDBusReply<int> isEkosStarted;
1989 //isEkosStarted = ekosInterface->call(QDBus::AutoDetect, "getEkosStartingStatus");
1990 //if (isEkosStarted.value() == Ekos::Success)
1991 if (Options::alwaysExecuteStartupScript() == false && moduleState()->ekosCommunicationStatus() == Ekos::Success)
1992 {
1993 if (moduleState()->startupScriptURL().isEmpty() == false)
1994 appendLogText(i18n("Ekos is already started, skipping startup script..."));
1995
1996 if (!activeJob() || activeJob()->getLightFramesRequired())
1997 moduleState()->setStartupState(STARTUP_UNPARK_DOME);
1998 else
1999 moduleState()->setStartupState(STARTUP_COMPLETE);
2000 return true;
2001 }
2002
2003 if (moduleState()->currentProfile() != i18n("Default"))
2004 {
2005 QList<QVariant> profile;
2006 profile.append(moduleState()->currentProfile());
2007 ekosInterface()->callWithArgumentList(QDBus::AutoDetect, "setProfile", profile);
2008 }
2009
2010 if (moduleState()->startupScriptURL().isEmpty() == false)
2011 {
2012 moduleState()->setStartupState(STARTUP_SCRIPT);
2013 executeScript(moduleState()->startupScriptURL().toString(QUrl::PreferLocalFile));
2014 return false;
2015 }
2016
2017 moduleState()->setStartupState(STARTUP_UNPARK_DOME);
2018 return false;
2019 }
2020
2021 case STARTUP_SCRIPT:
2022 return false;
2023
2024 case STARTUP_UNPARK_DOME:
2025 // If there is no job in case of manual startup procedure,
2026 // or if the job requires light frames, let's proceed with
2027 // unparking the dome, otherwise startup process is complete.
2028 if (activeJob() == nullptr || activeJob()->getLightFramesRequired())
2029 {
2030 if (Options::schedulerUnparkDome())
2031 unParkDome();
2032 else
2033 moduleState()->setStartupState(STARTUP_UNPARK_MOUNT);
2034 }
2035 else
2036 {
2037 moduleState()->setStartupState(STARTUP_COMPLETE);
2038 return true;
2039 }
2040
2041 break;
2042
2043 case STARTUP_UNPARKING_DOME:
2044 checkDomeParkingStatus();
2045 break;
2046
2047 case STARTUP_UNPARK_MOUNT:
2048 if (Options::schedulerUnparkMount())
2049 unParkMount();
2050 else
2051 moduleState()->setStartupState(STARTUP_UNPARK_CAP);
2052 break;
2053
2054 case STARTUP_UNPARKING_MOUNT:
2055 checkMountParkingStatus();
2056 break;
2057
2058 case STARTUP_UNPARK_CAP:
2059 if (Options::schedulerOpenDustCover())
2060 unParkCap();
2061 else
2062 moduleState()->setStartupState(STARTUP_COMPLETE);
2063 break;
2064
2065 case STARTUP_UNPARKING_CAP:
2067 break;
2068
2069 case STARTUP_COMPLETE:
2070 return true;
2071
2072 case STARTUP_ERROR:
2073 stop();
2074 return true;
2075 }
2076
2077 return false;
2078}
2079
2081{
2082 qCDebug(KSTARS_EKOS_SCHEDULER) << "Checking shutdown state...";
2083
2084 if (moduleState()->schedulerState() == SCHEDULER_PAUSED)
2085 return false;
2086
2087 switch (moduleState()->shutdownState())
2088 {
2089 case SHUTDOWN_IDLE:
2090
2091 qCInfo(KSTARS_EKOS_SCHEDULER) << "Starting shutdown process...";
2092
2093 moduleState()->setActiveJob(nullptr);
2094 moduleState()->setupNextIteration(RUN_SHUTDOWN);
2095 emit shutdownStarted();
2096
2097 if (Options::schedulerWarmCCD())
2098 {
2099 appendLogText(i18n("Warming up CCD..."));
2100
2101 // Turn it off
2102 //QVariant arg(false);
2103 //captureInterface->call(QDBus::AutoDetect, "setCoolerControl", arg);
2104 if (captureInterface())
2105 {
2106 qCDebug(KSTARS_EKOS_SCHEDULER) << "Setting coolerControl=false";
2107 captureInterface()->setProperty("coolerControl", false);
2108 }
2109 }
2110
2111 // The following steps require a connection to the INDI server
2112 if (moduleState()->isINDIConnected())
2113 {
2114 if (Options::schedulerCloseDustCover())
2115 {
2116 moduleState()->setShutdownState(SHUTDOWN_PARK_CAP);
2117 return false;
2118 }
2119
2120 if (Options::schedulerParkMount())
2121 {
2122 moduleState()->setShutdownState(SHUTDOWN_PARK_MOUNT);
2123 return false;
2124 }
2125
2126 if (Options::schedulerParkDome())
2127 {
2128 moduleState()->setShutdownState(SHUTDOWN_PARK_DOME);
2129 return false;
2130 }
2131 }
2132 else appendLogText(i18n("Warning: Bypassing parking procedures, no INDI connection."));
2133
2134 if (moduleState()->shutdownScriptURL().isEmpty() == false)
2135 {
2136 moduleState()->setShutdownState(SHUTDOWN_SCRIPT);
2137 return false;
2138 }
2139
2140 moduleState()->setShutdownState(SHUTDOWN_COMPLETE);
2141 return true;
2142
2143 case SHUTDOWN_PARK_CAP:
2144 if (!moduleState()->isINDIConnected())
2145 {
2146 qCInfo(KSTARS_EKOS_SCHEDULER) << "Bypassing shutdown step 'park cap', no INDI connection.";
2147 moduleState()->setShutdownState(SHUTDOWN_SCRIPT);
2148 }
2149 else if (Options::schedulerCloseDustCover())
2150 parkCap();
2151 else
2152 moduleState()->setShutdownState(SHUTDOWN_PARK_MOUNT);
2153 break;
2154
2155 case SHUTDOWN_PARKING_CAP:
2157 break;
2158
2159 case SHUTDOWN_PARK_MOUNT:
2160 if (!moduleState()->isINDIConnected())
2161 {
2162 qCInfo(KSTARS_EKOS_SCHEDULER) << "Bypassing shutdown step 'park cap', no INDI connection.";
2163 moduleState()->setShutdownState(SHUTDOWN_SCRIPT);
2164 }
2165 else if (Options::schedulerParkMount())
2166 parkMount();
2167 else
2168 moduleState()->setShutdownState(SHUTDOWN_PARK_DOME);
2169 break;
2170
2171 case SHUTDOWN_PARKING_MOUNT:
2172 checkMountParkingStatus();
2173 break;
2174
2175 case SHUTDOWN_PARK_DOME:
2176 if (!moduleState()->isINDIConnected())
2177 {
2178 qCInfo(KSTARS_EKOS_SCHEDULER) << "Bypassing shutdown step 'park cap', no INDI connection.";
2179 moduleState()->setShutdownState(SHUTDOWN_SCRIPT);
2180 }
2181 else if (Options::schedulerParkDome())
2182 parkDome();
2183 else
2184 moduleState()->setShutdownState(SHUTDOWN_SCRIPT);
2185 break;
2186
2187 case SHUTDOWN_PARKING_DOME:
2188 checkDomeParkingStatus();
2189 break;
2190
2191 case SHUTDOWN_SCRIPT:
2192 if (moduleState()->shutdownScriptURL().isEmpty() == false)
2193 {
2194 // Need to stop Ekos now before executing script if it happens to stop INDI
2195 if (moduleState()->ekosState() != EKOS_IDLE && Options::shutdownScriptTerminatesINDI())
2196 {
2197 stopEkos();
2198 return false;
2199 }
2200
2201 moduleState()->setShutdownState(SHUTDOWN_SCRIPT_RUNNING);
2202 executeScript(moduleState()->shutdownScriptURL().toString(QUrl::PreferLocalFile));
2203 }
2204 else
2205 moduleState()->setShutdownState(SHUTDOWN_COMPLETE);
2206 break;
2207
2208 case SHUTDOWN_SCRIPT_RUNNING:
2209 return false;
2210
2211 case SHUTDOWN_COMPLETE:
2212 return completeShutdown();
2213
2214 case SHUTDOWN_ERROR:
2215 stop();
2216 return true;
2217 }
2218
2219 return false;
2220}
2221
2223{
2224 if (moduleState()->schedulerState() == SCHEDULER_PAUSED)
2225 return false;
2226
2227 if (moduleState()->parkWaitState() == PARKWAIT_IDLE)
2228 return true;
2229
2230 // qCDebug(KSTARS_EKOS_SCHEDULER) << "Checking Park Wait State...";
2231
2232 switch (moduleState()->parkWaitState())
2233 {
2234 case PARKWAIT_PARK:
2235 parkMount();
2236 break;
2237
2238 case PARKWAIT_PARKING:
2239 checkMountParkingStatus();
2240 break;
2241
2242 case PARKWAIT_UNPARK:
2243 unParkMount();
2244 break;
2245
2246 case PARKWAIT_UNPARKING:
2247 checkMountParkingStatus();
2248 break;
2249
2250 case PARKWAIT_IDLE:
2251 case PARKWAIT_PARKED:
2252 case PARKWAIT_UNPARKED:
2253 return true;
2254
2255 case PARKWAIT_ERROR:
2256 appendLogText(i18n("park/unpark wait procedure failed, aborting..."));
2257 stop();
2258 return true;
2259
2260 }
2261
2262 return false;
2263}
2264
2266{
2267 if (moduleState()->startupState() == STARTUP_IDLE
2268 || moduleState()->startupState() == STARTUP_ERROR
2269 || moduleState()->startupState() == STARTUP_COMPLETE)
2270 {
2271 connect(KSMessageBox::Instance(), &KSMessageBox::accepted, this, [this]()
2272 {
2273 KSMessageBox::Instance()->disconnect(this);
2274
2275 appendLogText(i18n("Warning: executing startup procedure manually..."));
2276 moduleState()->setStartupState(STARTUP_IDLE);
2278 QTimer::singleShot(1000, this, SLOT(checkStartupProcedure()));
2279
2280 });
2281
2282 KSMessageBox::Instance()->questionYesNo(i18n("Are you sure you want to execute the startup procedure manually?"));
2283 }
2284 else
2285 {
2286 switch (moduleState()->startupState())
2287 {
2288 case STARTUP_IDLE:
2289 break;
2290
2291 case STARTUP_SCRIPT:
2292 scriptProcess().terminate();
2293 break;
2294
2295 case STARTUP_UNPARK_DOME:
2296 break;
2297
2298 case STARTUP_UNPARKING_DOME:
2299 qCDebug(KSTARS_EKOS_SCHEDULER) << "Aborting unparking dome...";
2300 domeInterface()->call(QDBus::AutoDetect, "abort");
2301 break;
2302
2303 case STARTUP_UNPARK_MOUNT:
2304 break;
2305
2306 case STARTUP_UNPARKING_MOUNT:
2307 qCDebug(KSTARS_EKOS_SCHEDULER) << "Aborting unparking mount...";
2308 mountInterface()->call(QDBus::AutoDetect, "abort");
2309 break;
2310
2311 case STARTUP_UNPARK_CAP:
2312 break;
2313
2314 case STARTUP_UNPARKING_CAP:
2315 break;
2316
2317 case STARTUP_COMPLETE:
2318 break;
2319
2320 case STARTUP_ERROR:
2321 break;
2322 }
2323
2324 moduleState()->setStartupState(STARTUP_IDLE);
2325
2326 appendLogText(i18n("Startup procedure terminated."));
2327 }
2328
2329}
2330
2332{
2333 if (moduleState()->shutdownState() == SHUTDOWN_IDLE
2334 || moduleState()->shutdownState() == SHUTDOWN_ERROR
2335 || moduleState()->shutdownState() == SHUTDOWN_COMPLETE)
2336 {
2337 connect(KSMessageBox::Instance(), &KSMessageBox::accepted, this, [this]()
2338 {
2339 KSMessageBox::Instance()->disconnect(this);
2340 appendLogText(i18n("Warning: executing shutdown procedure manually..."));
2341 moduleState()->setShutdownState(SHUTDOWN_IDLE);
2343 QTimer::singleShot(1000, this, SLOT(checkShutdownProcedure()));
2344 });
2345
2346 KSMessageBox::Instance()->questionYesNo(i18n("Are you sure you want to execute the shutdown procedure manually?"));
2347 }
2348 else
2349 {
2350 switch (moduleState()->shutdownState())
2351 {
2352 case SHUTDOWN_IDLE:
2353 break;
2354
2355 case SHUTDOWN_SCRIPT:
2356 break;
2357
2358 case SHUTDOWN_SCRIPT_RUNNING:
2359 scriptProcess().terminate();
2360 break;
2361
2362 case SHUTDOWN_PARK_DOME:
2363 break;
2364
2365 case SHUTDOWN_PARKING_DOME:
2366 qCDebug(KSTARS_EKOS_SCHEDULER) << "Aborting parking dome...";
2367 domeInterface()->call(QDBus::AutoDetect, "abort");
2368 break;
2369
2370 case SHUTDOWN_PARK_MOUNT:
2371 break;
2372
2373 case SHUTDOWN_PARKING_MOUNT:
2374 qCDebug(KSTARS_EKOS_SCHEDULER) << "Aborting parking mount...";
2375 mountInterface()->call(QDBus::AutoDetect, "abort");
2376 break;
2377
2378 case SHUTDOWN_PARK_CAP:
2379 case SHUTDOWN_PARKING_CAP:
2380 break;
2381
2382 case SHUTDOWN_COMPLETE:
2383 break;
2384
2385 case SHUTDOWN_ERROR:
2386 break;
2387 }
2388
2389 moduleState()->setShutdownState(SHUTDOWN_IDLE);
2390
2391 appendLogText(i18n("Shutdown procedure terminated."));
2392 }
2393}
2394
2396{
2397 moduleState()->setupNextIteration(RUN_NOTHING);
2398 appendLogText(i18n("Scheduler paused."));
2399 emit schedulerPaused();
2400}
2401
2403{
2404 // Reset ALL scheduler jobs to IDLE and force-reset their completed count - no effect when progress is kept
2405 for (SchedulerJob * job : moduleState()->jobs())
2406 {
2407 job->reset();
2408 job->setCompletedCount(0);
2409 }
2410
2411 // Unconditionally update the capture storage
2413}
2414
2416{
2417 auto finished_or_aborted = [](SchedulerJob const * const job)
2418 {
2419 SchedulerJobStatus const s = job->getState();
2420 return SCHEDJOB_ERROR <= s || SCHEDJOB_ABORTED == s;
2421 };
2422
2423 /* This predicate matches jobs that are neither scheduled to run nor aborted */
2424 auto neither_scheduled_nor_aborted = [](SchedulerJob const * const job)
2425 {
2426 SchedulerJobStatus const s = job->getState();
2427 return SCHEDJOB_SCHEDULED != s && SCHEDJOB_ABORTED != s;
2428 };
2429
2430 /* If there are no jobs left to run in the filtered list, stop evaluation */
2431 ErrorHandlingStrategy strategy = static_cast<ErrorHandlingStrategy>(Options::errorHandlingStrategy());
2432 if (jobs.isEmpty() || std::all_of(jobs.begin(), jobs.end(), neither_scheduled_nor_aborted))
2433 {
2434 appendLogText(i18n("No jobs left in the scheduler queue after evaluating."));
2435 moduleState()->setActiveJob(nullptr);
2436 return;
2437 }
2438 /* If there are only aborted jobs that can run, reschedule those and let Scheduler restart one loop */
2439 else if (std::all_of(jobs.begin(), jobs.end(), finished_or_aborted) &&
2440 strategy != ERROR_DONT_RESTART)
2441 {
2442 appendLogText(i18n("Only aborted jobs left in the scheduler queue after evaluating, rescheduling those."));
2443 std::for_each(jobs.begin(), jobs.end(), [](SchedulerJob * job)
2444 {
2445 if (SCHEDJOB_ABORTED == job->getState())
2446 job->setState(SCHEDJOB_EVALUATION);
2447 });
2448
2449 return;
2450 }
2451
2452 // GreedyScheduler::scheduleJobs() must be called first.
2453 SchedulerJob *scheduledJob = getGreedyScheduler()->getScheduledJob();
2454 if (!scheduledJob)
2455 {
2456 appendLogText(i18n("No jobs scheduled."));
2457 moduleState()->setActiveJob(nullptr);
2458 return;
2459 }
2460 if (activeJob() != nullptr && scheduledJob != activeJob())
2461 {
2462 // Changing lead, therefore abort all follower jobs that are still running
2463 for (auto job : m_activeJobs.values())
2464 if (!job->isLead() && job->getState() == SCHEDJOB_BUSY)
2465 stopCapturing(job->getOpticalTrain(), false);
2466
2467 // clear the mapping camera name --> scheduler job
2468 m_activeJobs.clear();
2469 }
2470 moduleState()->setActiveJob(scheduledJob);
2471
2472}
2473
2475{
2476 // Reset all jobs
2477 // other states too?
2478 if (SCHEDULER_RUNNING != moduleState()->schedulerState())
2479 resetJobs();
2480
2481 // reset the iterations counter
2482 moduleState()->resetSequenceExecutionCounter();
2483
2484 // And evaluate all pending jobs per the conditions set in each
2485 evaluateJobs(true);
2486}
2487
2488void SchedulerProcess::evaluateJobs(bool evaluateOnly)
2489{
2490 for (auto job : moduleState()->jobs())
2491 job->clearCache();
2492
2493 /* Don't evaluate if list is empty */
2494 if (moduleState()->jobs().isEmpty())
2495 return;
2496 /* Start by refreshing the number of captures already present - unneeded if not remembering job progress */
2497 if (Options::rememberJobProgress())
2499
2500 moduleState()->calculateDawnDusk();
2501
2502 getGreedyScheduler()->scheduleJobs(moduleState()->jobs(), SchedulerModuleState::getLocalTime(),
2503 moduleState()->capturedFramesCount(), this);
2504
2505 // schedule or job states might have been changed, update the table
2506
2507 if (!evaluateOnly && moduleState()->schedulerState() == SCHEDULER_RUNNING)
2508 // At this step, we finished evaluating jobs.
2509 // We select the first job that has to be run, per schedule.
2510 selectActiveJob(moduleState()->jobs());
2511 else
2512 qCInfo(KSTARS_EKOS_SCHEDULER) << "Ekos finished evaluating jobs, no job selection required.";
2513
2514 emit jobsUpdated(moduleState()->getJSONJobs());
2515}
2516
2518{
2519 if (moduleState()->schedulerState() == SCHEDULER_PAUSED)
2520 {
2521 if (activeJob() == nullptr)
2522 {
2523 setPaused();
2524 return false;
2525 }
2526 switch (activeJob()->getState())
2527 {
2528 case SCHEDJOB_BUSY:
2529 // do nothing
2530 break;
2531 case SCHEDJOB_COMPLETE:
2532 // start finding next job before pausing
2533 break;
2534 default:
2535 // in all other cases pause
2536 setPaused();
2537 break;
2538 }
2539 }
2540
2541 // #1 If no current job selected, let's check if we need to shutdown or evaluate jobs
2542 if (activeJob() == nullptr)
2543 {
2544 // #2.1 If shutdown is already complete or in error, we need to stop
2545 if (moduleState()->shutdownState() == SHUTDOWN_COMPLETE
2546 || moduleState()->shutdownState() == SHUTDOWN_ERROR)
2547 {
2548 return completeShutdown();
2549 }
2550
2551 // #2.2 Check if shutdown is in progress
2552 if (moduleState()->shutdownState() > SHUTDOWN_IDLE)
2553 {
2554 // If Ekos is not done stopping, try again later
2555 if (moduleState()->ekosState() == EKOS_STOPPING && checkEkosState() == false)
2556 return false;
2557
2559 return false;
2560 }
2561
2562 // #2.3 Check if park wait procedure is in progress
2563 if (checkParkWaitState() == false)
2564 return false;
2565
2566 // #2.4 If not in shutdown state, evaluate the jobs
2567 evaluateJobs(false);
2568
2569 // #2.5 check if all jobs have completed and repeat is set
2570 if (nullptr == activeJob() && moduleState()->checkRepeatSequence())
2571 {
2572 // Reset all jobs
2573 resetJobs();
2574 // Re-evaluate all jobs to check whether there is at least one that might be executed
2575 evaluateJobs(false);
2576 // if there is an executable job, restart;
2577 if (activeJob())
2578 {
2579 moduleState()->increaseSequenceExecutionCounter();
2580 appendLogText(i18n("Starting job sequence iteration #%1", moduleState()->sequenceExecutionCounter()));
2581 return true;
2582 }
2583 }
2584
2585 // #2.6 If there is no current job after evaluation, shutdown
2586 if (nullptr == activeJob())
2587 {
2589 return false;
2590 }
2591 }
2592 // JM 2018-12-07: Check if we need to sleep
2593 else if (shouldSchedulerSleep(activeJob()) == false)
2594 {
2595 // #3 Check if startup procedure has failed.
2596 if (moduleState()->startupState() == STARTUP_ERROR)
2597 {
2598 // Stop Scheduler
2599 stop();
2600 return true;
2601 }
2602
2603 // #4 Check if startup procedure Phase #1 is complete (Startup script)
2604 if ((moduleState()->startupState() == STARTUP_IDLE
2605 && checkStartupState() == false)
2606 || moduleState()->startupState() == STARTUP_SCRIPT)
2607 return false;
2608
2609 // #5 Check if Ekos is started
2610 if (checkEkosState() == false)
2611 return false;
2612
2613 // #6 Check if INDI devices are connected.
2614 if (checkINDIState() == false)
2615 return false;
2616
2617 // #6.1 Check if park wait procedure is in progress - in the case we're waiting for a distant job
2618 if (checkParkWaitState() == false)
2619 return false;
2620
2621 // #7 Check if startup procedure Phase #2 is complete (Unparking phase)
2622 if (moduleState()->startupState() > STARTUP_SCRIPT
2623 && moduleState()->startupState() < STARTUP_ERROR
2624 && checkStartupState() == false)
2625 return false;
2626
2627 // #8 Check it it already completed (should only happen starting a paused job)
2628 // Find the next job in this case, otherwise execute the current one
2629 if (activeJob() && activeJob()->getState() == SCHEDJOB_COMPLETE)
2630 findNextJob();
2631
2632 // N.B. We explicitly do not check for return result here because regardless of execution result
2633 // we do not have any pending tasks further down.
2634 executeJob(activeJob());
2635 emit updateJobTable();
2636 }
2637
2638 return true;
2639}
2640
2642{
2643 qCDebug(KSTARS_EKOS_SCHEDULER) << "Get next action...";
2644
2645 switch (activeJob()->getStage())
2646 {
2647 case SCHEDSTAGE_IDLE:
2648 if (activeJob()->getLightFramesRequired())
2649 {
2650 if (activeJob()->getStepPipeline() & SchedulerJob::USE_TRACK)
2651 startSlew();
2652 else if (activeJob()->getStepPipeline() & SchedulerJob::USE_FOCUS && moduleState()->autofocusCompleted() == false)
2653 {
2654 qCDebug(KSTARS_EKOS_SCHEDULER) << "startFocusing on 3485";
2655 startFocusing();
2656 }
2657 else if (activeJob()->getStepPipeline() & SchedulerJob::USE_ALIGN)
2659 else if (activeJob()->getStepPipeline() & SchedulerJob::USE_GUIDE)
2660 if (getGuidingStatus() == GUIDE_GUIDING)
2661 {
2662 appendLogText(i18n("Guiding already running, directly start capturing."));
2663 startCapture();
2664 }
2665 else
2666 startGuiding();
2667 else
2668 startCapture();
2669 }
2670 else
2671 {
2672 if (activeJob()->getStepPipeline())
2674 i18n("Job '%1' is proceeding directly to capture stage because only calibration frames are pending.",
2675 activeJob()->getName()));
2676 startCapture();
2677 }
2678
2679 break;
2680
2681 case SCHEDSTAGE_SLEW_COMPLETE:
2682 if (activeJob()->getStepPipeline() & SchedulerJob::USE_FOCUS && moduleState()->autofocusCompleted() == false)
2683 {
2684 qCDebug(KSTARS_EKOS_SCHEDULER) << "startFocusing on 3514";
2685 startFocusing();
2686 }
2687 else if (activeJob()->getStepPipeline() & SchedulerJob::USE_ALIGN)
2689 else if (activeJob()->getStepPipeline() & SchedulerJob::USE_GUIDE)
2690 startGuiding();
2691 else
2692 startCapture();
2693 break;
2694
2695 case SCHEDSTAGE_FOCUS_COMPLETE:
2696 if (activeJob()->getStepPipeline() & SchedulerJob::USE_ALIGN)
2698 else if (activeJob()->getStepPipeline() & SchedulerJob::USE_GUIDE)
2699 startGuiding();
2700 else
2701 startCapture();
2702 break;
2703
2704 case SCHEDSTAGE_ALIGN_COMPLETE:
2705 moduleState()->updateJobStage(SCHEDSTAGE_RESLEWING);
2706 break;
2707
2708 case SCHEDSTAGE_RESLEWING_COMPLETE:
2709 // If we have in-sequence-focus in the sequence file then we perform post alignment focusing so that the focus
2710 // frame is ready for the capture module in-sequence-focus procedure.
2711 if ((activeJob()->getStepPipeline() & SchedulerJob::USE_FOCUS) && activeJob()->getInSequenceFocus())
2712 // Post alignment re-focusing
2713 {
2714 qCDebug(KSTARS_EKOS_SCHEDULER) << "startFocusing on 3544";
2715 startFocusing();
2716 }
2717 else if (activeJob()->getStepPipeline() & SchedulerJob::USE_GUIDE)
2718 startGuiding();
2719 else
2720 startCapture();
2721 break;
2722
2723 case SCHEDSTAGE_POSTALIGN_FOCUSING_COMPLETE:
2724 if (activeJob()->getStepPipeline() & SchedulerJob::USE_GUIDE)
2725 startGuiding();
2726 else
2727 startCapture();
2728 break;
2729
2730 case SCHEDSTAGE_GUIDING_COMPLETE:
2731 startCapture();
2732 break;
2733
2734 default:
2735 break;
2736 }
2737}
2738
2740{
2741 const int msSleep = runSchedulerIteration();
2742 if (msSleep < 0)
2743 return;
2744
2745 connect(&moduleState()->iterationTimer(), &QTimer::timeout, this, &SchedulerProcess::iterate, Qt::UniqueConnection);
2746 moduleState()->iterationTimer().setSingleShot(true);
2747 moduleState()->iterationTimer().start(msSleep);
2748
2749}
2750
2752{
2753 qint64 now = QDateTime::currentMSecsSinceEpoch();
2754 if (moduleState()->startMSecs() == 0)
2755 moduleState()->setStartMSecs(now);
2756
2757 // printStates(QString("\nrunScheduler Iteration %1 @ %2")
2758 // .arg(moduleState()->increaseSchedulerIteration())
2759 // .arg((now - moduleState()->startMSecs()) / 1000.0, 1, 'f', 3));
2760
2761 SchedulerTimerState keepTimerState = moduleState()->timerState();
2762
2763 // TODO: At some point we should require that timerState and timerInterval
2764 // be explicitly set in all iterations. Not there yet, would require too much
2765 // refactoring of the scheduler. When we get there, we'd exectute the following here:
2766 // timerState = RUN_NOTHING; // don't like this comment, it should always set a state and interval!
2767 // timerInterval = -1;
2768 moduleState()->setIterationSetup(false);
2769 switch (keepTimerState)
2770 {
2771 case RUN_WAKEUP:
2772 changeSleepLabel("", false);
2774 break;
2775 case RUN_SCHEDULER:
2776 checkStatus();
2777 break;
2778 case RUN_JOBCHECK:
2779 checkJobStage();
2780 break;
2781 case RUN_SHUTDOWN:
2783 break;
2784 case RUN_NOTHING:
2785 moduleState()->setTimerInterval(-1);
2786 break;
2787 }
2788 if (!moduleState()->iterationSetup())
2789 {
2790 // See the above TODO.
2791 // Since iterations aren't yet always set up, we repeat the current
2792 // iteration type if one wasn't set up in the current iteration.
2793 // qCDebug(KSTARS_EKOS_SCHEDULER) << "Scheduler iteration never set up.";
2794 moduleState()->setTimerInterval(moduleState()->updatePeriodMs());
2795 }
2796 // printStates(QString("End iteration, sleep %1: ").arg(moduleState()->timerInterval()));
2797 return moduleState()->timerInterval();
2798}
2799
2801{
2802 Q_ASSERT_X(activeJob(), __FUNCTION__, "Actual current job is required to check job stage");
2803 if (!activeJob())
2804 return;
2805
2806 if (checkJobStageCounter == 0)
2807 {
2808 qCDebug(KSTARS_EKOS_SCHEDULER) << "Checking job stage for" << activeJob()->getName() << "startup" <<
2809 activeJob()->getStartupCondition() << activeJob()->getStartupTime().toString() << "state" << activeJob()->getState();
2810 if (checkJobStageCounter++ == 30)
2811 checkJobStageCounter = 0;
2812 }
2813
2814 emit syncGreedyParams();
2815 if (!getGreedyScheduler()->checkJob(moduleState()->leadJobs(), SchedulerModuleState::getLocalTime(), activeJob()))
2816 {
2817 activeJob()->setState(SCHEDJOB_IDLE);
2819 findNextJob();
2820 return;
2821 }
2822 checkJobStageEpilogue();
2823}
2824
2825void SchedulerProcess::checkJobStageEpilogue()
2826{
2827 if (!activeJob())
2828 return;
2829
2830 // #5 Check system status to improve robustness
2831 // This handles external events such as disconnections or end-user manipulating INDI panel
2832 if (!checkStatus())
2833 return;
2834
2835 // #5b Check the guiding timer, and possibly restart guiding.
2837
2838 // #6 Check each stage is processing properly
2839 // FIXME: Vanishing property should trigger a call to its event callback
2840 if (!activeJob()) return;
2841 switch (activeJob()->getStage())
2842 {
2843 case SCHEDSTAGE_IDLE:
2844 // Job is just starting.
2845 emit jobStarted(activeJob()->getName());
2846 getNextAction();
2847 break;
2848
2849 case SCHEDSTAGE_ALIGNING:
2850 // Let's make sure align module does not become unresponsive
2851 if (moduleState()->getCurrentOperationMsec() > static_cast<int>(ALIGN_INACTIVITY_TIMEOUT))
2852 {
2853 QVariant const status = alignInterface()->property("status");
2854 Ekos::AlignState alignStatus = static_cast<Ekos::AlignState>(status.toInt());
2855
2856 if (alignStatus == Ekos::ALIGN_IDLE)
2857 {
2858 if (moduleState()->increaseAlignFailureCount())
2859 {
2860 qCDebug(KSTARS_EKOS_SCHEDULER) << "Align module timed out. Restarting request...";
2862 }
2863 else
2864 {
2865 appendLogText(i18n("Warning: job '%1' alignment procedure failed, marking aborted.", activeJob()->getName()));
2866 activeJob()->setState(SCHEDJOB_ABORTED);
2867 findNextJob();
2868 }
2869 }
2870 else
2871 moduleState()->startCurrentOperationTimer();
2872 }
2873 break;
2874
2875 case SCHEDSTAGE_CAPTURING:
2876 // Let's make sure capture module does not become unresponsive
2877 if (moduleState()->getCurrentOperationMsec() > static_cast<int>(CAPTURE_INACTIVITY_TIMEOUT))
2878 {
2879 QVariant const status = captureInterface()->property("status");
2880 Ekos::CaptureState captureStatus = static_cast<Ekos::CaptureState>(status.toInt());
2881
2882 if (captureStatus == Ekos::CAPTURE_IDLE)
2883 {
2884 if (moduleState()->increaseCaptureFailureCount())
2885 {
2886 qCDebug(KSTARS_EKOS_SCHEDULER) << "capture module timed out. Restarting request...";
2887 startCapture();
2888 }
2889 else
2890 {
2891 appendLogText(i18n("Warning: job '%1' capture procedure failed, marking aborted.", activeJob()->getName()));
2892 activeJob()->setState(SCHEDJOB_ABORTED);
2893 findNextJob();
2894 }
2895 }
2896 else moduleState()->startCurrentOperationTimer();
2897 }
2898 break;
2899
2900 case SCHEDSTAGE_FOCUSING:
2901 // Let's make sure focus module does not become unresponsive
2902 if (moduleState()->getCurrentOperationMsec() > static_cast<int>(FOCUS_INACTIVITY_TIMEOUT))
2903 {
2904 bool success = true;
2905 foreach (const QString trainname, m_activeJobs.keys())
2906 {
2907 QList<QVariant> dbusargs;
2908 dbusargs.append(trainname);
2909 QDBusReply<Ekos::FocusState> statusReply = focusInterface()->callWithArgumentList(QDBus::AutoDetect, "status", dbusargs);
2910 if (statusReply.error().type() != QDBusError::NoError)
2911 {
2912 qCCritical(KSTARS_EKOS_SCHEDULER) << QString("Warning: job '%1' status request received DBUS error: %2").arg(
2913 m_activeJobs[trainname]->getName(), QDBusError::errorString(statusReply.error().type()));
2914 success = false;
2915 }
2916 if (success == false && !manageConnectionLoss())
2917 {
2918 activeJob()->setState(SCHEDJOB_ERROR);
2919 findNextJob();
2920 return;
2921 }
2922 Ekos::FocusState focusStatus = statusReply.value();
2923 if (focusStatus == Ekos::FOCUS_IDLE || focusStatus == Ekos::FOCUS_WAITING)
2924 {
2925 if (moduleState()->increaseFocusFailureCount(trainname))
2926 {
2927 qCDebug(KSTARS_EKOS_SCHEDULER) << "Focus module timed out. Restarting request...";
2928 startFocusing(m_activeJobs[trainname]);
2929 }
2930 else
2931 success = false;
2932 }
2933 }
2934
2935 if (success == false)
2936 {
2937 appendLogText(i18n("Warning: job '%1' focusing procedure failed, marking aborted.", activeJob()->getName()));
2938 activeJob()->setState(SCHEDJOB_ABORTED);
2939 findNextJob();
2940 }
2941 }
2942 else moduleState()->startCurrentOperationTimer();
2943 break;
2944
2945 case SCHEDSTAGE_GUIDING:
2946 // Let's make sure guide module does not become unresponsive
2947 if (moduleState()->getCurrentOperationMsec() > GUIDE_INACTIVITY_TIMEOUT)
2948 {
2949 GuideState guideStatus = getGuidingStatus();
2950
2951 if (guideStatus == Ekos::GUIDE_IDLE || guideStatus == Ekos::GUIDE_CONNECTED || guideStatus == Ekos::GUIDE_DISCONNECTED)
2952 {
2953 if (moduleState()->increaseGuideFailureCount())
2954 {
2955 qCDebug(KSTARS_EKOS_SCHEDULER) << "guide module timed out. Restarting request...";
2956 startGuiding();
2957 }
2958 else
2959 {
2960 appendLogText(i18n("Warning: job '%1' guiding procedure failed, marking aborted.", activeJob()->getName()));
2961 activeJob()->setState(SCHEDJOB_ABORTED);
2962 findNextJob();
2963 }
2964 }
2965 else moduleState()->startCurrentOperationTimer();
2966 }
2967 break;
2968
2969 case SCHEDSTAGE_SLEWING:
2970 case SCHEDSTAGE_RESLEWING:
2971 // While slewing or re-slewing, check slew status can still be obtained
2972 {
2973 QVariant const slewStatus = mountInterface()->property("status");
2974
2975 if (slewStatus.isValid())
2976 {
2977 // Send the slew status periodically to avoid the situation where the mount is already at location and does not send any event
2978 // FIXME: in that case, filter TRACKING events only?
2979 ISD::Mount::Status const status = static_cast<ISD::Mount::Status>(slewStatus.toInt());
2980 setMountStatus(status);
2981 }
2982 else
2983 {
2984 appendLogText(i18n("Warning: job '%1' lost connection to the mount, attempting to reconnect.", activeJob()->getName()));
2985 if (!manageConnectionLoss())
2986 activeJob()->setState(SCHEDJOB_ERROR);
2987 return;
2988 }
2989 }
2990 break;
2991
2992 case SCHEDSTAGE_SLEW_COMPLETE:
2993 case SCHEDSTAGE_RESLEWING_COMPLETE:
2994 // When done slewing or re-slewing and we use a dome, only shift to the next action when the dome is done moving
2995 if (moduleState()->domeReady())
2996 {
2997 QVariant const isDomeMoving = domeInterface()->property("isMoving");
2998
2999 if (!isDomeMoving.isValid())
3000 {
3001 appendLogText(i18n("Warning: job '%1' lost connection to the dome, attempting to reconnect.", activeJob()->getName()));
3002 if (!manageConnectionLoss())
3003 activeJob()->setState(SCHEDJOB_ERROR);
3004 return;
3005 }
3006
3007 if (!isDomeMoving.value<bool>())
3008 getNextAction();
3009 }
3010 else getNextAction();
3011 break;
3012
3013 default:
3014 break;
3015 }
3016}
3017
3019{
3020 moduleState()->calculateDawnDusk();
3021
3022 if (SCHEDULER_RUNNING != moduleState()->schedulerState())
3023 {
3024 evaluateJobs(true);
3025 }
3026}
3027
3028bool SchedulerProcess::executeJob(SchedulerJob * job)
3029{
3030 if (job == nullptr)
3031 return false;
3032
3033 // Don't execute the current job if it is already busy
3034 if (activeJob() == job && SCHEDJOB_BUSY == activeJob()->getState())
3035 return false;
3036
3037 moduleState()->setActiveJob(job);
3038
3039 // If we already started, we check when the next object is scheduled at.
3040 // If it is more than 30 minutes in the future, we park the mount if that is supported
3041 // and we unpark when it is due to start.
3042 //int const nextObservationTime = now.secsTo(getActiveJob()->getStartupTime());
3043
3044 // If the time to wait is greater than the lead time (5 minutes by default)
3045 // then we sleep, otherwise we wait. It's the same thing, just different labels.
3046 if (shouldSchedulerSleep(activeJob()))
3047 return false;
3048 // If job schedule isn't now, wait - continuing to execute would cancel a parking attempt
3049 else if (0 < SchedulerModuleState::getLocalTime().secsTo(activeJob()->getStartupTime()))
3050 return false;
3051
3052 // From this point job can be executed now
3053
3054 if (job->getCompletionCondition() == FINISH_SEQUENCE && Options::rememberJobProgress())
3055 captureInterface()->setProperty("targetName", job->getName());
3056
3057 moduleState()->calculateDawnDusk();
3058
3059 // Reset autofocus so that focus step is applied properly when checked
3060 // When the focus step is not checked, the capture module will eventually run focus periodically
3061 moduleState()->setAutofocusCompleted(job->getOpticalTrain(), false);
3062
3063 qCInfo(KSTARS_EKOS_SCHEDULER) << "Executing Job " << activeJob()->getName();
3064
3065 activeJob()->setState(SCHEDJOB_BUSY);
3066 emit jobsUpdated(moduleState()->getJSONJobs());
3067
3068 KSNotification::event(QLatin1String("EkosSchedulerJobStart"),
3069 i18n("Ekos job started (%1)", activeJob()->getName()), KSNotification::Scheduler);
3070
3071 // No need to continue evaluating jobs as we already have one.
3072 moduleState()->setupNextIteration(RUN_JOBCHECK);
3073 return true;
3074}
3075
3077{
3078 QFile file;
3079 file.setFileName(fileURL.toLocalFile());
3080
3081 if (!file.open(QIODevice::WriteOnly))
3082 {
3083 QString message = i18n("Unable to write to file %1", fileURL.toLocalFile());
3084 KSNotification::sorry(message, i18n("Could Not Open File"));
3085 return false;
3086 }
3087
3088 QTextStream outstream(&file);
3089
3090 // We serialize sequence data to XML using the C locale
3091 QLocale cLocale = QLocale::c();
3092
3093 outstream << "<?xml version=\"1.0\" encoding=\"UTF-8\"?>" << Qt::endl;
3094 outstream << "<SchedulerList version='2.1'>" << Qt::endl;
3095 // ensure to escape special XML characters
3096 outstream << "<Profile>" << QString(entityXML(strdup(moduleState()->currentProfile().toStdString().c_str()))) <<
3097 "</Profile>" << Qt::endl;
3098
3099 auto tiles = KStarsData::Instance()->skyComposite()->mosaicComponent()->tiles();
3100 bool useMosaicInfo = !tiles->sequenceFile().isEmpty();
3101
3102 if (useMosaicInfo)
3103 {
3104 outstream << "<Mosaic>" << Qt::endl;
3105 outstream << "<Target>" << tiles->targetName() << "</Target>" << Qt::endl;
3106 outstream << "<Group>" << tiles->group() << "</Group>" << Qt::endl;
3107
3108 QString ccArg, ccValue = tiles->completionCondition(&ccArg);
3109 if (ccValue == "FinishSequence")
3110 outstream << "<FinishSequence/>" << Qt::endl;
3111 else if (ccValue == "FinishLoop")
3112 outstream << "<FinishLoop/>" << Qt::endl;
3113 else if (ccValue == "FinishRepeat")
3114 outstream << "<FinishRepeat>" << ccArg << "</FinishRepeat>" << Qt::endl;
3115
3116 outstream << "<Sequence>" << tiles->sequenceFile() << "</Sequence>" << Qt::endl;
3117 outstream << "<Directory>" << tiles->outputDirectory() << "</Directory>" << Qt::endl;
3118
3119 outstream << "<FocusEveryN>" << tiles->focusEveryN() << "</FocusEveryN>" << Qt::endl;
3120 outstream << "<AlignEveryN>" << tiles->alignEveryN() << "</AlignEveryN>" << Qt::endl;
3121 if (tiles->isTrackChecked())
3122 outstream << "<TrackChecked/>" << Qt::endl;
3123 if (tiles->isFocusChecked())
3124 outstream << "<FocusChecked/>" << Qt::endl;
3125 if (tiles->isAlignChecked())
3126 outstream << "<AlignChecked/>" << Qt::endl;
3127 if (tiles->isGuideChecked())
3128 outstream << "<GuideChecked/>" << Qt::endl;
3129 outstream << "<Overlap>" << cLocale.toString(tiles->overlap()) << "</Overlap>" << Qt::endl;
3130 outstream << "<CenterRA>" << cLocale.toString(tiles->ra0().Hours()) << "</CenterRA>" << Qt::endl;
3131 outstream << "<CenterDE>" << cLocale.toString(tiles->dec0().Degrees()) << "</CenterDE>" << Qt::endl;
3132 outstream << "<GridW>" << tiles->gridSize().width() << "</GridW>" << Qt::endl;
3133 outstream << "<GridH>" << tiles->gridSize().height() << "</GridH>" << Qt::endl;
3134 outstream << "<FOVW>" << cLocale.toString(tiles->mosaicFOV().width()) << "</FOVW>" << Qt::endl;
3135 outstream << "<FOVH>" << cLocale.toString(tiles->mosaicFOV().height()) << "</FOVH>" << Qt::endl;
3136 outstream << "<CameraFOVW>" << cLocale.toString(tiles->cameraFOV().width()) << "</CameraFOVW>" << Qt::endl;
3137 outstream << "<CameraFOVH>" << cLocale.toString(tiles->cameraFOV().height()) << "</CameraFOVH>" << Qt::endl;
3138 outstream << "</Mosaic>" << Qt::endl;
3139 }
3140
3141 int index = 0;
3142 for (auto &job : moduleState()->jobs())
3143 {
3144 outstream << "<Job>" << Qt::endl;
3145
3146 // ensure to escape special XML characters
3147 outstream << "<JobType lead='" << (job->isLead() ? "true" : "false") << "'/>" << Qt::endl;
3148 if (job->isLead())
3149 {
3150 outstream << "<Name>" << QString(entityXML(strdup(job->getName().toStdString().c_str()))) << "</Name>" << Qt::endl;
3151 outstream << "<Group>" << QString(entityXML(strdup(job->getGroup().toStdString().c_str()))) << "</Group>" << Qt::endl;
3152 outstream << "<Coordinates>" << Qt::endl;
3153 outstream << "<J2000RA>" << cLocale.toString(job->getTargetCoords().ra0().Hours()) << "</J2000RA>" << Qt::endl;
3154 outstream << "<J2000DE>" << cLocale.toString(job->getTargetCoords().dec0().Degrees()) << "</J2000DE>" << Qt::endl;
3155 outstream << "</Coordinates>" << Qt::endl;
3156 }
3157
3158 if (! job->getOpticalTrain().isEmpty())
3159 outstream << "<OpticalTrain>" << QString(entityXML(strdup(job->getOpticalTrain().toStdString().c_str()))) <<
3160 "</OpticalTrain>" << Qt::endl;
3161
3162 if (job->isLead() && job->getFITSFile().isValid() && job->getFITSFile().isEmpty() == false)
3163 outstream << "<FITS>" << job->getFITSFile().toLocalFile() << "</FITS>" << Qt::endl;
3164 else
3165 outstream << "<PositionAngle>" << job->getPositionAngle() << "</PositionAngle>" << Qt::endl;
3166
3167 outstream << "<Sequence>" << job->getSequenceFile().toLocalFile() << "</Sequence>" << Qt::endl;
3168
3169 if (useMosaicInfo && index < tiles->tiles().size())
3170 {
3171 auto oneTile = tiles->tiles().at(index++);
3172 outstream << "<TileCenter>" << Qt::endl;
3173 outstream << "<X>" << cLocale.toString(oneTile->center.x()) << "</X>" << Qt::endl;
3174 outstream << "<Y>" << cLocale.toString(oneTile->center.y()) << "</Y>" << Qt::endl;
3175 outstream << "<Rotation>" << cLocale.toString(oneTile->rotation) << "</Rotation>" << Qt::endl;
3176 outstream << "</TileCenter>" << Qt::endl;
3177 }
3178
3179 if (job->isLead())
3180 {
3181 outstream << "<StartupCondition>" << Qt::endl;
3182 if (job->getFileStartupCondition() == START_ASAP)
3183 outstream << "<Condition>ASAP</Condition>" << Qt::endl;
3184 else if (job->getFileStartupCondition() == START_AT)
3185 outstream << "<Condition value='" << job->getStartAtTime().toString(Qt::ISODate) << "'>At</Condition>"
3186 << Qt::endl;
3187 outstream << "</StartupCondition>" << Qt::endl;
3188
3189 outstream << "<Constraints>" << Qt::endl;
3190 if (job->hasMinAltitude())
3191 outstream << "<Constraint value='" << cLocale.toString(job->getMinAltitude()) << "'>MinimumAltitude</Constraint>" <<
3192 Qt::endl;
3193 if (job->getMinMoonSeparation() > 0)
3194 outstream << "<Constraint value='" << cLocale.toString(job->getMinMoonSeparation()) << "'>MoonSeparation</Constraint>"
3195 << Qt::endl;
3196 if (job->getMaxMoonAltitude() < 90)
3197 outstream << "<Constraint value='" << cLocale.toString(job->getMaxMoonAltitude()) << "'>MoonMaxAltitude</Constraint>"
3198 << Qt::endl;
3199 if (job->getEnforceWeather())
3200 outstream << "<Constraint>EnforceWeather</Constraint>" << Qt::endl;
3201 if (job->getEnforceTwilight())
3202 outstream << "<Constraint>EnforceTwilight</Constraint>" << Qt::endl;
3203 if (job->getEnforceArtificialHorizon())
3204 outstream << "<Constraint>EnforceArtificialHorizon</Constraint>" << Qt::endl;
3205 outstream << "</Constraints>" << Qt::endl;
3206 }
3207
3208 outstream << "<CompletionCondition>" << Qt::endl;
3209 if (job->getCompletionCondition() == FINISH_SEQUENCE)
3210 outstream << "<Condition>Sequence</Condition>" << Qt::endl;
3211 else if (job->getCompletionCondition() == FINISH_REPEAT)
3212 outstream << "<Condition value='" << cLocale.toString(job->getRepeatsRequired()) << "'>Repeat</Condition>" << Qt::endl;
3213 else if (job->getCompletionCondition() == FINISH_LOOP)
3214 outstream << "<Condition>Loop</Condition>" << Qt::endl;
3215 else if (job->getCompletionCondition() == FINISH_AT)
3216 outstream << "<Condition value='" << job->getFinishAtTime().toString(Qt::ISODate) << "'>At</Condition>"
3217 << Qt::endl;
3218 outstream << "</CompletionCondition>" << Qt::endl;
3219
3220 if (job->isLead())
3221 {
3222 outstream << "<Steps>" << Qt::endl;
3223 if (job->getStepPipeline() & SchedulerJob::USE_TRACK)
3224 outstream << "<Step>Track</Step>" << Qt::endl;
3225 if (job->getStepPipeline() & SchedulerJob::USE_FOCUS)
3226 outstream << "<Step>Focus</Step>" << Qt::endl;
3227 if (job->getStepPipeline() & SchedulerJob::USE_ALIGN)
3228 outstream << "<Step>Align</Step>" << Qt::endl;
3229 if (job->getStepPipeline() & SchedulerJob::USE_GUIDE)
3230 outstream << "<Step>Guide</Step>" << Qt::endl;
3231 outstream << "</Steps>" << Qt::endl;
3232 }
3233 outstream << "</Job>" << Qt::endl;
3234 }
3235
3236 outstream << "<SchedulerAlgorithm value='" << ALGORITHM_GREEDY << "'/>" << Qt::endl;
3237 outstream << "<ErrorHandlingStrategy value='" << Options::errorHandlingStrategy() << "'>" << Qt::endl;
3238 if (Options::rescheduleErrors())
3239 outstream << "<RescheduleErrors />" << Qt::endl;
3240 outstream << "<delay>" << Options::errorHandlingStrategyDelay() << "</delay>" << Qt::endl;
3241 outstream << "</ErrorHandlingStrategy>" << Qt::endl;
3242
3243 outstream << "<StartupProcedure>" << Qt::endl;
3244 if (moduleState()->startupScriptURL().isEmpty() == false)
3245 outstream << "<Procedure value='" << moduleState()->startupScriptURL().toString(QUrl::PreferLocalFile) <<
3246 "'>StartupScript</Procedure>" << Qt::endl;
3247 if (Options::schedulerUnparkDome())
3248 outstream << "<Procedure>UnparkDome</Procedure>" << Qt::endl;
3249 if (Options::schedulerUnparkMount())
3250 outstream << "<Procedure>UnparkMount</Procedure>" << Qt::endl;
3251 if (Options::schedulerOpenDustCover())
3252 outstream << "<Procedure>UnparkCap</Procedure>" << Qt::endl;
3253 outstream << "</StartupProcedure>" << Qt::endl;
3254
3255 outstream << "<ShutdownProcedure>" << Qt::endl;
3256 if (Options::schedulerWarmCCD())
3257 outstream << "<Procedure>WarmCCD</Procedure>" << Qt::endl;
3258 if (Options::schedulerCloseDustCover())
3259 outstream << "<Procedure>ParkCap</Procedure>" << Qt::endl;
3260 if (Options::schedulerParkMount())
3261 outstream << "<Procedure>ParkMount</Procedure>" << Qt::endl;
3262 if (Options::schedulerParkDome())
3263 outstream << "<Procedure>ParkDome</Procedure>" << Qt::endl;
3264 if (moduleState()->shutdownScriptURL().isEmpty() == false)
3265 outstream << "<Procedure value='" << moduleState()->shutdownScriptURL().toString(QUrl::PreferLocalFile) <<
3266 "'>schedulerStartupScript</Procedure>" <<
3267 Qt::endl;
3268 outstream << "</ShutdownProcedure>" << Qt::endl;
3269
3270 outstream << "</SchedulerList>" << Qt::endl;
3271
3272 appendLogText(i18n("Scheduler list saved to %1", fileURL.toLocalFile()));
3273 file.close();
3274 moduleState()->setDirty(false);
3275 return true;
3276}
3277
3278void SchedulerProcess::checkAlignment(const QVariantMap &metadata, const QString &trainname)
3279{
3280 // check if the metadata comes from the lead job
3281 if (activeJob() == nullptr || (activeJob()->getOpticalTrain() != "" && activeJob()->getOpticalTrain() != trainname))
3282 {
3283 qCDebug(KSTARS_EKOS_SCHEDULER) << "Ignoring metadata from train =" << trainname << "for alignment check.";
3284 return;
3285 }
3286
3287 if (activeJob()->getStepPipeline() & SchedulerJob::USE_ALIGN &&
3288 metadata["type"].toInt() == FRAME_LIGHT &&
3289 Options::alignCheckFrequency() > 0 &&
3290 moduleState()->increaseSolverIteration() >= Options::alignCheckFrequency())
3291 {
3292 moduleState()->resetSolverIteration();
3293
3294 auto filename = metadata["filename"].toString();
3295 auto exposure = metadata["exposure"].toDouble();
3296
3297 qCDebug(KSTARS_EKOS_SCHEDULER) << "Checking alignment on train =" << trainname << "for" << filename;
3298
3299 constexpr double minSolverSeconds = 5.0;
3300 double solverTimeout = std::max(exposure - 2, minSolverSeconds);
3301 if (solverTimeout >= minSolverSeconds)
3302 {
3303 auto profiles = getDefaultAlignOptionsProfiles();
3304
3305 SSolver::Parameters parameters;
3306 // Get solver parameters
3307 // In case of exception, use first profile
3308 try
3309 {
3310 parameters = profiles.at(Options::solveOptionsProfile());
3311 }
3312 catch (std::out_of_range const &)
3313 {
3314 parameters = profiles[0];
3315 }
3316
3317 // Double search radius
3318 parameters.search_radius = parameters.search_radius * 2;
3319 m_Solver.reset(new SolverUtils(parameters, solverTimeout), &QObject::deleteLater);
3320 connect(m_Solver.get(), &SolverUtils::done, this, &Ekos::SchedulerProcess::solverDone, Qt::UniqueConnection);
3321 //connect(m_Solver.get(), &SolverUtils::newLog, this, &Ekos::Scheduler::appendLogText, Qt::UniqueConnection);
3322
3323 auto width = metadata["width"].toUInt() / (metadata["binx"].isValid() ? metadata["binx"].toUInt() : 1);
3324 auto height = metadata["height"].toUInt() / (metadata["biny"].isValid() ? metadata["biny"].toUInt() : 1);
3325
3326 auto lowScale = Options::astrometryImageScaleLow();
3327 auto highScale = Options::astrometryImageScaleHigh();
3328
3329 // solver utils uses arcsecs per pixel only
3330 if (Options::astrometryImageScaleUnits() == SSolver::DEG_WIDTH)
3331 {
3332 lowScale = (lowScale * 3600) / std::max(width, height);
3333 highScale = (highScale * 3600) / std::min(width, height);
3334 }
3335 else if (Options::astrometryImageScaleUnits() == SSolver::ARCMIN_WIDTH)
3336 {
3337 lowScale = (lowScale * 60) / std::max(width, height);
3338 highScale = (highScale * 60) / std::min(width, height);
3339 }
3340
3341 m_Solver->useScale(Options::astrometryUseImageScale(), lowScale, highScale);
3342 m_Solver->usePosition(Options::astrometryUsePosition(), activeJob()->getTargetCoords().ra().Degrees(),
3343 activeJob()->getTargetCoords().dec().Degrees());
3344 m_Solver->setHealpix(moduleState()->indexToUse(), moduleState()->healpixToUse());
3345 m_Solver->runSolver(filename);
3346 }
3347 }
3348}
3349
3350void SchedulerProcess::solverDone(bool timedOut, bool success, const FITSImage::Solution &solution, double elapsedSeconds)
3351{
3352 disconnect(m_Solver.get(), &SolverUtils::done, this, &Ekos::SchedulerProcess::solverDone);
3353
3354 if (!activeJob())
3355 return;
3356
3357 QString healpixString = "";
3358 if (moduleState()->indexToUse() != -1 || moduleState()->healpixToUse() != -1)
3359 healpixString = QString("Healpix %1 Index %2").arg(moduleState()->healpixToUse()).arg(moduleState()->indexToUse());
3360
3361 if (timedOut || !success)
3362 {
3363 // Don't use the previous index and healpix next time we solve.
3364 moduleState()->setIndexToUse(-1);
3365 moduleState()->setHealpixToUse(-1);
3366 }
3367 else
3368 {
3369 int index, healpix;
3370 // Get the index and healpix from the successful solve.
3371 m_Solver->getSolutionHealpix(&index, &healpix);
3372 moduleState()->setIndexToUse(index);
3373 moduleState()->setHealpixToUse(healpix);
3374 }
3375
3376 if (timedOut)
3377 appendLogText(i18n("Solver timed out: %1s %2", QString("%L1").arg(elapsedSeconds, 0, 'f', 1), healpixString));
3378 else if (!success)
3379 appendLogText(i18n("Solver failed: %1s %2", QString("%L1").arg(elapsedSeconds, 0, 'f', 1), healpixString));
3380 else
3381 {
3382 const double ra = solution.ra;
3383 const double dec = solution.dec;
3384
3385 const auto target = activeJob()->getTargetCoords();
3386
3387 SkyPoint alignCoord;
3388 alignCoord.setRA0(ra / 15.0);
3389 alignCoord.setDec0(dec);
3390 alignCoord.apparentCoord(static_cast<long double>(J2000), KStars::Instance()->data()->ut().djd());
3391 alignCoord.EquatorialToHorizontal(KStarsData::Instance()->lst(), KStarsData::Instance()->geo()->lat());
3392 const double diffRa = (alignCoord.ra().deltaAngle(target.ra())).Degrees() * 3600;
3393 const double diffDec = (alignCoord.dec().deltaAngle(target.dec())).Degrees() * 3600;
3394
3395 // This is an approximation, probably ok for small angles.
3396 const double diffTotal = hypot(diffRa, diffDec);
3397
3398 // Note--the RA output is in DMS. This is because we're looking at differences in arcseconds
3399 // and HMS coordinates are misleading (one HMS second is really 6 arc-seconds).
3400 qCDebug(KSTARS_EKOS_SCHEDULER) <<
3401 QString("Target Distance: %1\" Target (RA: %2 DE: %3) Current (RA: %4 DE: %5) %6 solved in %7s")
3402 .arg(QString("%L1").arg(diffTotal, 0, 'f', 0),
3403 target.ra().toDMSString(),
3404 target.dec().toDMSString(),
3405 alignCoord.ra().toDMSString(),
3406 alignCoord.dec().toDMSString(),
3407 healpixString,
3408 QString("%L1").arg(elapsedSeconds, 0, 'f', 2));
3409 emit targetDistance(diffTotal);
3410
3411 // If we exceed align check threshold, we abort and re-align.
3412 if (diffTotal / 60 > Options::alignCheckThreshold())
3413 {
3414 appendLogText(i18n("Captured frame is %1 arcminutes away from target, re-aligning...", QString::number(diffTotal / 60.0,
3415 'f', 1)));
3418 }
3419 }
3420}
3421
3423{
3424 SchedulerState const old_state = moduleState()->schedulerState();
3425 moduleState()->setSchedulerState(SCHEDULER_LOADING);
3426
3427 QFile sFile;
3428 sFile.setFileName(fileURL);
3429
3430 if (!sFile.open(QIODevice::ReadOnly))
3431 {
3432 QString message = i18n("Unable to open file %1", fileURL);
3433 KSNotification::sorry(message, i18n("Could Not Open File"));
3434 moduleState()->setSchedulerState(old_state);
3435 return false;
3436 }
3437
3438 LilXML *xmlParser = newLilXML();
3439 char errmsg[MAXRBUF];
3440 XMLEle *root = nullptr;
3441 XMLEle *ep = nullptr;
3442 XMLEle *subEP = nullptr;
3443 char c;
3444
3445 // We expect all data read from the XML to be in the C locale - QLocale::c()
3446 QLocale cLocale = QLocale::c();
3447
3448 // remember previous job
3449 SchedulerJob *lastLead = nullptr;
3450
3451 // retrieve optical trains names to ensure that only known trains are used
3452 const QStringList allTrainNames = OpticalTrainManager::Instance()->getTrainNames();
3453 QStringList remainingTrainNames = allTrainNames;
3454
3455 while (sFile.getChar(&c))
3456 {
3457 root = readXMLEle(xmlParser, c, errmsg);
3458
3459 if (root)
3460 {
3461 for (ep = nextXMLEle(root, 1); ep != nullptr; ep = nextXMLEle(root, 0))
3462 {
3463 const char *tag = tagXMLEle(ep);
3464 if (!strcmp(tag, "Job"))
3465 {
3466 SchedulerJob *newJob = SchedulerUtils::createJob(ep, lastLead);
3467 // remember new lead if such one has been created
3468 if (newJob->isLead())
3469 {
3470 lastLead = newJob;
3471 // reset usable train names
3472 remainingTrainNames = allTrainNames;
3473 }
3474 // check train name
3475 const QString trainname = newJob->getOpticalTrain();
3476 bool allowedName = (newJob->isLead() && trainname.isEmpty()) || allTrainNames.contains(trainname);
3477 bool availableName = (newJob->isLead() && trainname.isEmpty()) || !remainingTrainNames.isEmpty();
3478
3479 if (!allowedName && availableName)
3480 {
3481 const QString message = trainname.isEmpty() ?
3482 i18n("Warning: train name is empty, selecting \"%1\".", remainingTrainNames.first()) :
3483 i18n("Warning: train name %2 does not exist, selecting \"%1\".", remainingTrainNames.first(), trainname);
3484 appendLogText(message);
3485 if(KMessageBox::warningContinueCancel(nullptr, message, i18n("Select optical train"), KStandardGuiItem::cont(),
3486 KStandardGuiItem::cancel(), "correct_missing_train_warning") != KMessageBox::Continue)
3487 break;
3488
3489 newJob->setOpticalTrain(remainingTrainNames.first());
3490 remainingTrainNames.removeFirst();
3491 }
3492 else if (!availableName)
3493 {
3494 const QString message = i18n("Warning: no available train name for scheduler job, select the optical train name manually.");
3495 appendLogText(message);
3496
3497 if(KMessageBox::warningContinueCancel(nullptr, message, i18n("Select optical train"), KStandardGuiItem::cont(),
3498 KStandardGuiItem::cancel(), "correct_missing_train_warning") != KMessageBox::Continue)
3499 break;
3500 }
3501
3502 emit addJob(newJob);
3503 }
3504 else if (!strcmp(tag, "Mosaic"))
3505 {
3506 // If we have mosaic info, load it up.
3507 auto tiles = KStarsData::Instance()->skyComposite()->mosaicComponent()->tiles();
3508 tiles->fromXML(fileURL);
3509 }
3510 else if (!strcmp(tag, "Profile"))
3511 {
3512 moduleState()->setCurrentProfile(pcdataXMLEle(ep));
3513 }
3514 // disabled, there is only one algorithm
3515 else if (!strcmp(tag, "SchedulerAlgorithm"))
3516 {
3517 int algIndex = cLocale.toInt(findXMLAttValu(ep, "value"));
3518 if (algIndex != ALGORITHM_GREEDY)
3519 appendLogText(i18n("Warning: The Classic scheduler algorithm has been retired. Switching you to the Greedy algorithm."));
3520 }
3521 else if (!strcmp(tag, "ErrorHandlingStrategy"))
3522 {
3523 Options::setErrorHandlingStrategy(static_cast<ErrorHandlingStrategy>(cLocale.toInt(findXMLAttValu(ep,
3524 "value"))));
3525
3526 subEP = findXMLEle(ep, "delay");
3527 if (subEP)
3528 {
3529 Options::setErrorHandlingStrategyDelay(cLocale.toInt(pcdataXMLEle(subEP)));
3530 }
3531 subEP = findXMLEle(ep, "RescheduleErrors");
3532 Options::setRescheduleErrors(subEP != nullptr);
3533 }
3534 else if (!strcmp(tag, "StartupProcedure"))
3535 {
3536 XMLEle *procedure;
3537 Options::setSchedulerUnparkDome(false);
3538 Options::setSchedulerUnparkMount(false);
3539 Options::setSchedulerOpenDustCover(false);
3540
3541 for (procedure = nextXMLEle(ep, 1); procedure != nullptr; procedure = nextXMLEle(ep, 0))
3542 {
3543 const char *proc = pcdataXMLEle(procedure);
3544
3545 if (!strcmp(proc, "StartupScript"))
3546 {
3547 moduleState()->setStartupScriptURL(QUrl::fromUserInput(findXMLAttValu(procedure, "value")));
3548 }
3549 else if (!strcmp(proc, "UnparkDome"))
3550 Options::setSchedulerUnparkDome(true);
3551 else if (!strcmp(proc, "UnparkMount"))
3552 Options::setSchedulerUnparkMount(true);
3553 else if (!strcmp(proc, "UnparkCap"))
3554 Options::setSchedulerOpenDustCover(true);
3555 }
3556 }
3557 else if (!strcmp(tag, "ShutdownProcedure"))
3558 {
3559 XMLEle *procedure;
3560 Options::setSchedulerWarmCCD(false);
3561 Options::setSchedulerParkDome(false);
3562 Options::setSchedulerParkMount(false);
3563 Options::setSchedulerCloseDustCover(false);
3564
3565 for (procedure = nextXMLEle(ep, 1); procedure != nullptr; procedure = nextXMLEle(ep, 0))
3566 {
3567 const char *proc = pcdataXMLEle(procedure);
3568
3569 if (!strcmp(proc, "ShutdownScript"))
3570 {
3571 moduleState()->setShutdownScriptURL(QUrl::fromUserInput(findXMLAttValu(procedure, "value")));
3572 }
3573 else if (!strcmp(proc, "WarmCCD"))
3574 Options::setSchedulerWarmCCD(true);
3575 else if (!strcmp(proc, "ParkDome"))
3576 Options::setSchedulerParkDome(true);
3577 else if (!strcmp(proc, "ParkMount"))
3578 Options::setSchedulerParkMount(true);
3579 else if (!strcmp(proc, "ParkCap"))
3580 Options::setSchedulerCloseDustCover(true);
3581 }
3582 }
3583 }
3584 delXMLEle(root);
3585 emit syncGUIToGeneralSettings();
3586 }
3587 else if (errmsg[0])
3588 {
3589 appendLogText(QString(errmsg));
3590 delLilXML(xmlParser);
3591 moduleState()->setSchedulerState(old_state);
3592 return false;
3593 }
3594 }
3595
3596 moduleState()->setDirty(false);
3597 delLilXML(xmlParser);
3598 emit updateSchedulerURL(fileURL);
3599
3600 moduleState()->setSchedulerState(old_state);
3601 return true;
3602}
3603
3605{
3606 /* FIXME: user settings for log length */
3607 int const max_log_count = 2000;
3608 if (moduleState()->logText().size() > max_log_count)
3609 moduleState()->logText().removeLast();
3610
3611 moduleState()->logText().prepend(i18nc("log entry; %1 is the date, %2 is the text", "%1 %2",
3612 SchedulerModuleState::getLocalTime().toString("yyyy-MM-ddThh:mm:ss"), logentry));
3613
3614 qCInfo(KSTARS_EKOS_SCHEDULER) << logentry;
3615
3616 emit newLog(logentry);
3617}
3618
3620{
3621 moduleState()->logText().clear();
3622 emit newLog(QString());
3623}
3624
3625void SchedulerProcess::setAlignStatus(AlignState status)
3626{
3627 if (moduleState()->schedulerState() == SCHEDULER_PAUSED || activeJob() == nullptr)
3628 return;
3629
3630 qCDebug(KSTARS_EKOS_SCHEDULER) << "Align State" << Ekos::getAlignStatusString(status);
3631
3632 /* If current job is scheduled and has not started yet, wait */
3633 if (SCHEDJOB_SCHEDULED == activeJob()->getState())
3634 {
3635 QDateTime const now = SchedulerModuleState::getLocalTime();
3636 if (now < activeJob()->getStartupTime())
3637 return;
3638 }
3639
3640 if (activeJob()->getStage() == SCHEDSTAGE_ALIGNING)
3641 {
3642 // Is solver complete?
3643 if (status == Ekos::ALIGN_COMPLETE)
3644 {
3645 appendLogText(i18n("Job '%1' alignment is complete.", activeJob()->getName()));
3646 moduleState()->resetAlignFailureCount();
3647
3648 moduleState()->updateJobStage(SCHEDSTAGE_ALIGN_COMPLETE);
3649
3650 // If we solved a FITS file, let's use its center coords as our target.
3651 if (activeJob()->getFITSFile().isEmpty() == false)
3652 {
3653 QDBusReply<QList<double>> solutionReply = alignInterface()->call("getTargetCoords");
3654 if (solutionReply.isValid())
3655 {
3656 QList<double> const values = solutionReply.value();
3657 activeJob()->setTargetCoords(dms(values[0] * 15.0), dms(values[1]), KStarsData::Instance()->ut().djd());
3658 }
3659 }
3660 getNextAction();
3661 }
3662 else if (status == Ekos::ALIGN_FAILED || status == Ekos::ALIGN_ABORTED)
3663 {
3664 appendLogText(i18n("Warning: job '%1' alignment failed.", activeJob()->getName()));
3665
3666 if (moduleState()->increaseAlignFailureCount())
3667 {
3668 if (Options::resetMountModelOnAlignFail() && moduleState()->maxFailureAttempts() - 1 < moduleState()->alignFailureCount())
3669 {
3670 appendLogText(i18n("Warning: job '%1' forcing mount model reset after failing alignment #%2.", activeJob()->getName(),
3671 moduleState()->alignFailureCount()));
3672 mountInterface()->call(QDBus::AutoDetect, "resetModel");
3673 }
3674 appendLogText(i18n("Restarting %1 alignment procedure...", activeJob()->getName()));
3676 }
3677 else
3678 {
3679 appendLogText(i18n("Warning: job '%1' alignment procedure failed, marking aborted.", activeJob()->getName()));
3680 activeJob()->setState(SCHEDJOB_ABORTED);
3681
3682 findNextJob();
3683 }
3684 }
3685 }
3686}
3687
3688void SchedulerProcess::setGuideStatus(GuideState status)
3689{
3690 if (moduleState()->schedulerState() == SCHEDULER_PAUSED || activeJob() == nullptr)
3691 return;
3692
3693 qCDebug(KSTARS_EKOS_SCHEDULER) << "Guide State" << Ekos::getGuideStatusString(status);
3694
3695 /* If current job is scheduled and has not started yet, wait */
3696 if (SCHEDJOB_SCHEDULED == activeJob()->getState())
3697 {
3698 QDateTime const now = SchedulerModuleState::getLocalTime();
3699 if (now < activeJob()->getStartupTime())
3700 return;
3701 }
3702
3703 if (activeJob()->getStage() == SCHEDSTAGE_GUIDING)
3704 {
3705 qCDebug(KSTARS_EKOS_SCHEDULER) << "Calibration & Guide stage...";
3706
3707 // If calibration stage complete?
3708 if (status == Ekos::GUIDE_GUIDING)
3709 {
3710 appendLogText(i18n("Job '%1' guiding is in progress.", activeJob()->getName()));
3711 moduleState()->resetGuideFailureCount();
3712 // if guiding recovered while we are waiting, abort the restart
3713 moduleState()->cancelGuidingTimer();
3714
3715 moduleState()->updateJobStage(SCHEDSTAGE_GUIDING_COMPLETE);
3716 getNextAction();
3717 }
3718 else if (status == Ekos::GUIDE_CALIBRATION_ERROR ||
3719 status == Ekos::GUIDE_ABORTED)
3720 {
3721 if (status == Ekos::GUIDE_ABORTED)
3722 appendLogText(i18n("Warning: job '%1' guiding failed.", activeJob()->getName()));
3723 else
3724 appendLogText(i18n("Warning: job '%1' calibration failed.", activeJob()->getName()));
3725
3726 // if the timer for restarting the guiding is already running, we do nothing and
3727 // wait for the action triggered by the timer. This way we avoid that a small guiding problem
3728 // abort the scheduler job
3729
3730 if (moduleState()->isGuidingTimerActive())
3731 return;
3732
3733 if (moduleState()->increaseGuideFailureCount())
3734 {
3735 if (status == Ekos::GUIDE_CALIBRATION_ERROR &&
3736 Options::realignAfterCalibrationFailure())
3737 {
3738 appendLogText(i18n("Restarting %1 alignment procedure...", activeJob()->getName()));
3740 }
3741 else
3742 {
3743 appendLogText(i18n("Job '%1' is guiding, guiding procedure will be restarted in %2 seconds.", activeJob()->getName(),
3744 (RESTART_GUIDING_DELAY_MS * moduleState()->guideFailureCount()) / 1000));
3745 moduleState()->startGuidingTimer(RESTART_GUIDING_DELAY_MS * moduleState()->guideFailureCount());
3746 }
3747 }
3748 else
3749 {
3750 appendLogText(i18n("Warning: job '%1' guiding procedure failed, marking aborted.", activeJob()->getName()));
3751 activeJob()->setState(SCHEDJOB_ABORTED);
3752
3753 findNextJob();
3754 }
3755 }
3756 }
3757}
3758
3759void SchedulerProcess::setCaptureStatus(CaptureState status, const QString &trainname)
3760{
3761 if (activeJob() == nullptr || !m_activeJobs.contains(trainname))
3762 return;
3763
3764 qCDebug(KSTARS_EKOS_SCHEDULER) << "Capture State" << Ekos::getCaptureStatusString(status) << "train =" << trainname;
3765
3766 SchedulerJob *job = m_activeJobs[trainname];
3767
3768 /* If current job is scheduled and has not started yet, wait */
3769 if (SCHEDJOB_SCHEDULED == job->getState())
3770 {
3771 QDateTime const now = SchedulerModuleState::getLocalTime();
3772 if (now < job->getStartupTime())
3773 return;
3774 }
3775
3776 if (job->getStage() == SCHEDSTAGE_CAPTURING)
3777 {
3778 if (status == Ekos::CAPTURE_PROGRESS && (job->getStepPipeline() & SchedulerJob::USE_ALIGN))
3779 {
3780 // alignment is only relevant for the lead job
3781 if (job->isLead())
3782 {
3783 // JM 2021.09.20
3784 // Re-set target coords in align module
3785 // When capture starts, alignment module automatically rests target coords to mount coords.
3786 // However, we want to keep align module target synced with the scheduler target and not
3787 // the mount coord
3788 const SkyPoint targetCoords = activeJob()->getTargetCoords();
3789 QList<QVariant> targetArgs;
3790 targetArgs << targetCoords.ra0().Hours() << targetCoords.dec0().Degrees();
3791 alignInterface()->callWithArgumentList(QDBus::AutoDetect, "setTargetCoords", targetArgs);
3792 }
3793 }
3794 else if (status == Ekos::CAPTURE_ABORTED)
3795 {
3796 appendLogText(i18n("[%2] Warning: job '%1' failed to capture target.", job->getName(), trainname));
3797
3798 if (job->isLead())
3799 {
3800 // if capturing on the lead has failed for less than MAX_FAILURE_ATTEMPTS times
3801 if (moduleState()->increaseCaptureFailureCount())
3802 {
3803 job->setState(SCHEDJOB_ABORTED);
3804
3805 // If capture failed due to guiding error, let's try to restart that
3806 if (activeJob()->getStepPipeline() & SchedulerJob::USE_GUIDE)
3807 {
3808 // Check if it is guiding related.
3809 Ekos::GuideState gStatus = getGuidingStatus();
3810 if (gStatus == Ekos::GUIDE_ABORTED ||
3811 gStatus == Ekos::GUIDE_CALIBRATION_ERROR ||
3812 gStatus == GUIDE_DITHERING_ERROR)
3813 {
3814 appendLogText(i18n("[%2] Job '%1' is capturing, is restarting its guiding procedure (attempt #%3 of %4).",
3815 activeJob()->getName(), trainname,
3816 moduleState()->captureFailureCount(), moduleState()->maxFailureAttempts()));
3817 startGuiding(true);
3818 return;
3819 }
3820 }
3821
3822 /* FIXME: it's not clear whether it is actually possible to continue capturing when capture fails this way */
3823 appendLogText(i18n("Warning: job '%1' failed its capture procedure, restarting capture.", activeJob()->getName()));
3824 startCapture(true);
3825 }
3826 else
3827 {
3828 /* FIXME: it's not clear whether this situation can be recovered at all */
3829 appendLogText(i18n("[%2] Warning: job '%1' failed its capture procedure, marking aborted.", job->getName(), trainname));
3830 activeJob()->setState(SCHEDJOB_ABORTED);
3831 // abort follower capture jobs as well
3832 stopCapturing("", true);
3833
3834 findNextJob();
3835 }
3836 }
3837 else
3838 {
3839 if (job->leadJob()->getStage() == SCHEDSTAGE_CAPTURING)
3840 {
3841 // recover only when the lead job is capturing.
3842 appendLogText(i18n("[%2] Follower job '%1' has been aborted, is restarting.", job->getName(), trainname));
3843 job->setState(SCHEDJOB_ABORTED);
3844 startSingleCapture(job, true);
3845 }
3846 else
3847 {
3848 appendLogText(i18n("[%2] Follower job '%1' has been aborted.", job->getName(), trainname));
3849 job->setState(SCHEDJOB_ABORTED);
3850 }
3851 }
3852 }
3853 else if (status == Ekos::CAPTURE_COMPLETE)
3854 {
3855 KSNotification::event(QLatin1String("EkosScheduledImagingFinished"),
3856 i18n("[%2] Job (%1) - Capture finished", job->getName(), trainname), KSNotification::Scheduler);
3857
3858 if (job->isLead())
3859 {
3860 activeJob()->setState(SCHEDJOB_COMPLETE);
3861 findNextJob();
3862 }
3863 else
3864 {
3865 // Re-evaluate all jobs, without selecting a new job
3866 evaluateJobs(true);
3867
3868 if (job->getCompletionCondition() == FINISH_LOOP ||
3869 (job->getCompletionCondition() == FINISH_REPEAT && job->getRepeatsRemaining() > 0))
3870 {
3871 job->setState(SCHEDJOB_BUSY);
3872 startSingleCapture(job, false);
3873 }
3874 else
3875 {
3876 // follower job is complete
3877 job->setState(SCHEDJOB_COMPLETE);
3878 job->setStage(SCHEDSTAGE_COMPLETE);
3879 }
3880 }
3881 }
3882 else if (status == Ekos::CAPTURE_IMAGE_RECEIVED)
3883 {
3884 // We received a new image, but we don't know precisely where so update the storage map and re-estimate job times.
3885 // FIXME: rework this once capture storage is reworked
3886 if (Options::rememberJobProgress())
3887 {
3889
3890 for (const auto &job : moduleState()->jobs())
3891 SchedulerUtils::estimateJobTime(job, moduleState()->capturedFramesCount(), this);
3892 }
3893 // Else if we don't remember the progress on jobs, increase the completed count for the current job only - no cross-checks
3894 else
3895 activeJob()->setCompletedCount(job->getCompletedCount() + 1);
3896
3897 // reset the failure counter only if the image comes from the lead job
3898 if (job->isLead())
3899 moduleState()->resetCaptureFailureCount();
3900 }
3901 }
3902}
3903
3904void SchedulerProcess::setFocusStatus(FocusState status, const QString &trainname)
3905{
3906 if (moduleState()->schedulerState() == SCHEDULER_PAUSED || activeJob() == nullptr)
3907 return;
3908
3909 qCDebug(KSTARS_EKOS_SCHEDULER) << "Train " << trainname << "focus state" << Ekos::getFocusStatusString(status);
3910
3911 // ensure that there is an active job with the given train name
3912 if (m_activeJobs.contains(trainname) == false)
3913 return;
3914
3915 SchedulerJob *currentJob = m_activeJobs[trainname];
3916
3917 /* If current job is scheduled and has not started yet, wait */
3918 if (SCHEDJOB_SCHEDULED == activeJob()->getState())
3919 {
3920 QDateTime const now = SchedulerModuleState::getLocalTime();
3921 if (now < activeJob()->getStartupTime())
3922 return;
3923 }
3924
3925 if (activeJob()->getStage() == SCHEDSTAGE_FOCUSING)
3926 {
3927 // Is focus complete?
3928 if (status == Ekos::FOCUS_COMPLETE)
3929 {
3930 appendLogText(i18n("Job '%1' focusing train '%2' is complete.", currentJob->getName(), trainname));
3931
3932 moduleState()->setAutofocusCompleted(trainname, true);
3933
3934 if (moduleState()->autofocusCompleted())
3935 {
3936 moduleState()->updateJobStage(SCHEDSTAGE_FOCUS_COMPLETE);
3937 getNextAction();
3938 }
3939 }
3940 else if (status == Ekos::FOCUS_FAILED || status == Ekos::FOCUS_ABORTED)
3941 {
3942 appendLogText(i18n("Warning: job '%1' focusing failed.", currentJob->getName()));
3943
3944 if (moduleState()->increaseFocusFailureCount(trainname))
3945 {
3946 appendLogText(i18n("Job '%1' for train '%2' is restarting its focusing procedure.", currentJob->getName(), trainname));
3947 startFocusing(currentJob);
3948 }
3949 else
3950 {
3951 appendLogText(i18n("Warning: job '%1' on train '%2' focusing procedure failed, marking aborted.", activeJob()->getName(),
3952 trainname));
3953 activeJob()->setState(SCHEDJOB_ABORTED);
3954
3955 findNextJob();
3956 }
3957 }
3958 }
3959}
3960
3961void SchedulerProcess::setMountStatus(ISD::Mount::Status status)
3962{
3963 if (moduleState()->schedulerState() == SCHEDULER_PAUSED || activeJob() == nullptr)
3964 return;
3965
3966 qCDebug(KSTARS_EKOS_SCHEDULER) << "Mount State changed to" << status;
3967
3968 /* If current job is scheduled and has not started yet, wait */
3969 if (SCHEDJOB_SCHEDULED == activeJob()->getState())
3970 if (static_cast<QDateTime const>(SchedulerModuleState::getLocalTime()) < activeJob()->getStartupTime())
3971 return;
3972
3973 switch (activeJob()->getStage())
3974 {
3975 case SCHEDSTAGE_SLEWING:
3976 {
3977 qCDebug(KSTARS_EKOS_SCHEDULER) << "Slewing stage...";
3978
3979 if (status == ISD::Mount::MOUNT_TRACKING)
3980 {
3981 appendLogText(i18n("Job '%1' slew is complete.", activeJob()->getName()));
3982 moduleState()->updateJobStage(SCHEDSTAGE_SLEW_COMPLETE);
3983 /* getNextAction is deferred to checkJobStage for dome support */
3984 }
3985 else if (status == ISD::Mount::MOUNT_ERROR)
3986 {
3987 appendLogText(i18n("Warning: job '%1' slew failed, marking terminated due to errors.", activeJob()->getName()));
3988 activeJob()->setState(SCHEDJOB_ERROR);
3989 findNextJob();
3990 }
3991 else if (status == ISD::Mount::MOUNT_IDLE)
3992 {
3993 appendLogText(i18n("Warning: job '%1' found not slewing, restarting.", activeJob()->getName()));
3994 moduleState()->updateJobStage(SCHEDSTAGE_IDLE);
3995 getNextAction();
3996 }
3997 }
3998 break;
3999
4000 case SCHEDSTAGE_RESLEWING:
4001 {
4002 qCDebug(KSTARS_EKOS_SCHEDULER) << "Re-slewing stage...";
4003
4004 if (status == ISD::Mount::MOUNT_TRACKING)
4005 {
4006 appendLogText(i18n("Job '%1' repositioning is complete.", activeJob()->getName()));
4007 moduleState()->updateJobStage(SCHEDSTAGE_RESLEWING_COMPLETE);
4008 /* getNextAction is deferred to checkJobStage for dome support */
4009 }
4010 else if (status == ISD::Mount::MOUNT_ERROR)
4011 {
4012 appendLogText(i18n("Warning: job '%1' repositioning failed, marking terminated due to errors.", activeJob()->getName()));
4013 activeJob()->setState(SCHEDJOB_ERROR);
4014 findNextJob();
4015 }
4016 else if (status == ISD::Mount::MOUNT_IDLE)
4017 {
4018 appendLogText(i18n("Warning: job '%1' found not repositioning, restarting.", activeJob()->getName()));
4019 moduleState()->updateJobStage(SCHEDSTAGE_IDLE);
4020 getNextAction();
4021 }
4022 }
4023 break;
4024
4025 // In case we are either focusing, aligning, or guiding
4026 // and mount is parked, we need to abort.
4027 case SCHEDSTAGE_FOCUSING:
4028 case SCHEDSTAGE_ALIGNING:
4029 case SCHEDSTAGE_GUIDING:
4030 if (status == ISD::Mount::MOUNT_PARKED)
4031 {
4032 appendLogText(i18n("Warning: Mount is parked while scheduler for job '%1' is active. Aborting.", activeJob()->getName()));
4033 stop();
4034 }
4035 break;
4036
4037 // For capturing, it's more complicated because a mount can be parked by a calibration job.
4038 // so we only abort if light frames are required AND no calibration park mount is required
4039 case SCHEDSTAGE_CAPTURING:
4040 if (status == ISD::Mount::MOUNT_PARKED && activeJob() && activeJob()->getLightFramesRequired()
4041 && activeJob()->getCalibrationMountPark() == false)
4042 {
4043 appendLogText(i18n("Warning: Mount is parked while scheduler for job '%1' is active. Aborting.", activeJob()->getName()));
4044 stop();
4045 }
4046 break;
4047
4048 default:
4049 break;
4050 }
4051}
4052
4053void SchedulerProcess::setWeatherStatus(ISD::Weather::Status status)
4054{
4055 ISD::Weather::Status newStatus = status;
4056
4057 if (newStatus == moduleState()->weatherStatus())
4058 return;
4059
4060 moduleState()->setWeatherStatus(newStatus);
4061
4062 // Shutdown scheduler if it was started and not already in shutdown
4063 // and if weather checkbox is checked.
4064 if (activeJob() && activeJob()->getEnforceWeather() && moduleState()->weatherStatus() == ISD::Weather::WEATHER_ALERT
4065 && moduleState()->schedulerState() != Ekos::SCHEDULER_IDLE && moduleState()->schedulerState() != Ekos::SCHEDULER_SHUTDOWN)
4066 {
4067 appendLogText(i18n("Starting shutdown procedure due to severe weather."));
4068 if (activeJob())
4069 {
4070 activeJob()->setState(SCHEDJOB_ABORTED);
4072 }
4074 }
4075 // forward weather state
4076 emit newWeatherStatus(status);
4077}
4078
4079void SchedulerProcess::checkStartupProcedure()
4080{
4081 if (checkStartupState() == false)
4082 QTimer::singleShot(1000, this, SLOT(checkStartupProcedure()));
4083}
4084
4085void SchedulerProcess::checkShutdownProcedure()
4086{
4087 if (checkShutdownState())
4088 {
4089 // shutdown completed
4090 if (moduleState()->shutdownState() == SHUTDOWN_COMPLETE)
4091 {
4092 appendLogText(i18n("Manual shutdown procedure completed successfully."));
4093 // Stop Ekos
4094 if (Options::stopEkosAfterShutdown())
4095 stopEkos();
4096 }
4097 else if (moduleState()->shutdownState() == SHUTDOWN_ERROR)
4098 appendLogText(i18n("Manual shutdown procedure terminated due to errors."));
4099
4100 moduleState()->setShutdownState(SHUTDOWN_IDLE);
4101 }
4102 else
4103 // If shutdown procedure is not finished yet, let's check again in 1 second.
4104 QTimer::singleShot(1000, this, SLOT(checkShutdownProcedure()));
4105
4106}
4107
4108
4109void SchedulerProcess::parkCap()
4110{
4111 if (capInterface().isNull())
4112 {
4113 appendLogText(i18n("Dust cover park requested but no dust covers detected."));
4114 moduleState()->setShutdownState(SHUTDOWN_ERROR);
4115 return;
4116 }
4117
4118 QVariant parkingStatus = capInterface()->property("parkStatus");
4119 qCDebug(KSTARS_EKOS_SCHEDULER) << "Cap parking status" << (!parkingStatus.isValid() ? -1 : parkingStatus.toInt());
4120
4121 if (parkingStatus.isValid() == false)
4122 {
4123 qCCritical(KSTARS_EKOS_SCHEDULER) << QString("Warning: cap parkStatus request received DBUS error: %1").arg(
4124 mountInterface()->lastError().type());
4125 if (!manageConnectionLoss())
4126 parkingStatus = ISD::PARK_ERROR;
4127 }
4128
4129 ISD::ParkStatus status = static_cast<ISD::ParkStatus>(parkingStatus.toInt());
4130
4131 if (status != ISD::PARK_PARKED)
4132 {
4133 moduleState()->setShutdownState(SHUTDOWN_PARKING_CAP);
4134 qCDebug(KSTARS_EKOS_SCHEDULER) << "Parking dust cap...";
4135 capInterface()->call(QDBus::AutoDetect, "park");
4136 appendLogText(i18n("Parking Cap..."));
4137
4138 moduleState()->startCurrentOperationTimer();
4139 }
4140 else
4141 {
4142 appendLogText(i18n("Cap already parked."));
4143 moduleState()->setShutdownState(SHUTDOWN_PARK_MOUNT);
4144 }
4145}
4146
4147void SchedulerProcess::unParkCap()
4148{
4149 if (capInterface().isNull())
4150 {
4151 appendLogText(i18n("Dust cover unpark requested but no dust covers detected."));
4152 moduleState()->setStartupState(STARTUP_ERROR);
4153 return;
4154 }
4155
4156 QVariant parkingStatus = capInterface()->property("parkStatus");
4157 qCDebug(KSTARS_EKOS_SCHEDULER) << "Cap parking status" << (!parkingStatus.isValid() ? -1 : parkingStatus.toInt());
4158
4159 if (parkingStatus.isValid() == false)
4160 {
4161 qCCritical(KSTARS_EKOS_SCHEDULER) << QString("Warning: cap parkStatus request received DBUS error: %1").arg(
4162 mountInterface()->lastError().type());
4163 if (!manageConnectionLoss())
4164 parkingStatus = ISD::PARK_ERROR;
4165 }
4166
4167 ISD::ParkStatus status = static_cast<ISD::ParkStatus>(parkingStatus.toInt());
4168
4169 if (status != ISD::PARK_UNPARKED)
4170 {
4171 moduleState()->setStartupState(STARTUP_UNPARKING_CAP);
4172 capInterface()->call(QDBus::AutoDetect, "unpark");
4173 appendLogText(i18n("Unparking cap..."));
4174
4175 moduleState()->startCurrentOperationTimer();
4176 }
4177 else
4178 {
4179 appendLogText(i18n("Cap already unparked."));
4180 moduleState()->setStartupState(STARTUP_COMPLETE);
4181 }
4182}
4183
4184void SchedulerProcess::parkMount()
4185{
4186 if (mountInterface().isNull())
4187 {
4188 appendLogText(i18n("Mount park requested but no mounts detected."));
4189 moduleState()->setShutdownState(SHUTDOWN_ERROR);
4190 return;
4191 }
4192
4193 QVariant parkingStatus = mountInterface()->property("parkStatus");
4194 qCDebug(KSTARS_EKOS_SCHEDULER) << "Mount parking status" << (!parkingStatus.isValid() ? -1 : parkingStatus.toInt());
4195
4196 if (parkingStatus.isValid() == false)
4197 {
4198 qCCritical(KSTARS_EKOS_SCHEDULER) << QString("Warning: mount parkStatus request received DBUS error: %1").arg(
4199 mountInterface()->lastError().type());
4200 if (!manageConnectionLoss())
4201 moduleState()->setParkWaitState(PARKWAIT_ERROR);
4202 }
4203
4204 ISD::ParkStatus status = static_cast<ISD::ParkStatus>(parkingStatus.toInt());
4205
4206 switch (status)
4207 {
4208 case ISD::PARK_PARKED:
4209 if (moduleState()->shutdownState() == SHUTDOWN_PARK_MOUNT)
4210 moduleState()->setShutdownState(SHUTDOWN_PARK_DOME);
4211
4212 moduleState()->setParkWaitState(PARKWAIT_PARKED);
4213 appendLogText(i18n("Mount already parked."));
4214 break;
4215
4216 case ISD::PARK_UNPARKING:
4217 //case Mount::UNPARKING_BUSY:
4218 /* FIXME: Handle the situation where we request parking but an unparking procedure is running. */
4219
4220 // case Mount::PARKING_IDLE:
4221 // case Mount::UNPARKING_OK:
4222 case ISD::PARK_ERROR:
4223 case ISD::PARK_UNKNOWN:
4224 case ISD::PARK_UNPARKED:
4225 {
4226 qCDebug(KSTARS_EKOS_SCHEDULER) << "Parking mount...";
4227 QDBusReply<bool> const mountReply = mountInterface()->call(QDBus::AutoDetect, "park");
4228
4229 if (mountReply.error().type() != QDBusError::NoError)
4230 {
4231 qCCritical(KSTARS_EKOS_SCHEDULER) << QString("Warning: mount park request received DBUS error: %1").arg(
4232 QDBusError::errorString(mountReply.error().type()));
4233 if (!manageConnectionLoss())
4234 moduleState()->setParkWaitState(PARKWAIT_ERROR);
4235 }
4236 else moduleState()->startCurrentOperationTimer();
4237 }
4238
4239 // Fall through
4240 case ISD::PARK_PARKING:
4241 //case Mount::PARKING_BUSY:
4242 if (moduleState()->shutdownState() == SHUTDOWN_PARK_MOUNT)
4243 moduleState()->setShutdownState(SHUTDOWN_PARKING_MOUNT);
4244
4245 moduleState()->setParkWaitState(PARKWAIT_PARKING);
4246 appendLogText(i18n("Parking mount in progress..."));
4247 break;
4248
4249 // All cases covered above so no need for default
4250 //default:
4251 // qCWarning(KSTARS_EKOS_SCHEDULER) << QString("BUG: Parking state %1 not managed while parking mount.").arg(mountReply.value());
4252 }
4253
4254}
4255
4256void SchedulerProcess::unParkMount()
4257{
4258 if (mountInterface().isNull())
4259 {
4260 appendLogText(i18n("Mount unpark requested but no mounts detected."));
4261 moduleState()->setStartupState(STARTUP_ERROR);
4262 return;
4263 }
4264
4265 QVariant parkingStatus = mountInterface()->property("parkStatus");
4266 qCDebug(KSTARS_EKOS_SCHEDULER) << "Mount parking status" << (!parkingStatus.isValid() ? -1 : parkingStatus.toInt());
4267
4268 if (parkingStatus.isValid() == false)
4269 {
4270 qCCritical(KSTARS_EKOS_SCHEDULER) << QString("Warning: mount parkStatus request received DBUS error: %1").arg(
4271 mountInterface()->lastError().type());
4272 if (!manageConnectionLoss())
4273 moduleState()->setParkWaitState(PARKWAIT_ERROR);
4274 }
4275
4276 ISD::ParkStatus status = static_cast<ISD::ParkStatus>(parkingStatus.toInt());
4277
4278 switch (status)
4279 {
4280 //case Mount::UNPARKING_OK:
4281 case ISD::PARK_UNPARKED:
4282 if (moduleState()->startupState() == STARTUP_UNPARK_MOUNT)
4283 moduleState()->setStartupState(STARTUP_UNPARK_CAP);
4284
4285 moduleState()->setParkWaitState(PARKWAIT_UNPARKED);
4286 appendLogText(i18n("Mount already unparked."));
4287 break;
4288
4289 //case Mount::PARKING_BUSY:
4290 case ISD::PARK_PARKING:
4291 /* FIXME: Handle the situation where we request unparking but a parking procedure is running. */
4292
4293 // case Mount::PARKING_IDLE:
4294 // case Mount::PARKING_OK:
4295 // case Mount::PARKING_ERROR:
4296 case ISD::PARK_ERROR:
4297 case ISD::PARK_UNKNOWN:
4298 case ISD::PARK_PARKED:
4299 {
4300 QDBusReply<bool> const mountReply = mountInterface()->call(QDBus::AutoDetect, "unpark");
4301
4302 if (mountReply.error().type() != QDBusError::NoError)
4303 {
4304 qCCritical(KSTARS_EKOS_SCHEDULER) << QString("Warning: mount unpark request received DBUS error: %1").arg(
4305 QDBusError::errorString(mountReply.error().type()));
4306 if (!manageConnectionLoss())
4307 moduleState()->setParkWaitState(PARKWAIT_ERROR);
4308 }
4309 else moduleState()->startCurrentOperationTimer();
4310 }
4311
4312 // Fall through
4313 //case Mount::UNPARKING_BUSY:
4314 case ISD::PARK_UNPARKING:
4315 if (moduleState()->startupState() == STARTUP_UNPARK_MOUNT)
4316 moduleState()->setStartupState(STARTUP_UNPARKING_MOUNT);
4317
4318 moduleState()->setParkWaitState(PARKWAIT_UNPARKING);
4319 qCInfo(KSTARS_EKOS_SCHEDULER) << "Unparking mount in progress...";
4320 break;
4321
4322 // All cases covered above
4323 //default:
4324 // qCWarning(KSTARS_EKOS_SCHEDULER) << QString("BUG: Parking state %1 not managed while unparking mount.").arg(mountReply.value());
4325 }
4326}
4327
4329{
4330 QVariant var = mountInterface()->property("equatorialCoords");
4331
4332 // result must be two double values
4333 if (var.isValid() == false || var.canConvert<QList<double>>() == false)
4334 {
4335 qCCritical(KSTARS_EKOS_SCHEDULER) << "Warning: reading equatorial coordinates received an unexpected value:" << var;
4336 return SkyPoint();
4337 }
4338 // check if we received exactly two values
4339 const QList<double> coords = var.value<QList<double>>();
4340 if (coords.size() != 2)
4341 {
4342 qCCritical(KSTARS_EKOS_SCHEDULER) << "Warning: reading equatorial coordinates received" << coords.size() <<
4343 "instead of 2 values: " << coords;
4344 return SkyPoint();
4345 }
4346
4347 return SkyPoint(coords[0], coords[1]);
4348}
4349
4351{
4352 if (mountInterface().isNull())
4353 return false;
4354 // First check if the mount is able to park - if it isn't, getParkingStatus will reply PARKING_ERROR and status won't be clear
4355 //QDBusReply<bool> const parkCapableReply = mountInterface->call(QDBus::AutoDetect, "canPark");
4356 QVariant canPark = mountInterface()->property("canPark");
4357 qCDebug(KSTARS_EKOS_SCHEDULER) << "Mount can park:" << (!canPark.isValid() ? "invalid" : (canPark.toBool() ? "T" : "F"));
4358
4359 if (canPark.isValid() == false)
4360 {
4361 qCCritical(KSTARS_EKOS_SCHEDULER) << QString("Warning: mount canPark request received DBUS error: %1").arg(
4362 mountInterface()->lastError().type());
4364 return false;
4365 }
4366 else if (canPark.toBool() == true)
4367 {
4368 // If it is able to park, obtain its current status
4369 //QDBusReply<int> const mountReply = mountInterface->call(QDBus::AutoDetect, "getParkingStatus");
4370 QVariant parkingStatus = mountInterface()->property("parkStatus");
4371 qCDebug(KSTARS_EKOS_SCHEDULER) << "Mount parking status" << (!parkingStatus.isValid() ? -1 : parkingStatus.toInt());
4372
4373 if (parkingStatus.isValid() == false)
4374 {
4375 qCCritical(KSTARS_EKOS_SCHEDULER) << QString("Warning: mount parking status property is invalid %1.").arg(
4376 mountInterface()->lastError().type());
4378 return false;
4379 }
4380
4381 // Deduce state of mount - see getParkingStatus in mount.cpp
4382 switch (static_cast<ISD::ParkStatus>(parkingStatus.toInt()))
4383 {
4384 // case Mount::PARKING_OK: // INDI switch ok, and parked
4385 // case Mount::PARKING_IDLE: // INDI switch idle, and parked
4386 case ISD::PARK_PARKED:
4387 return true;
4388
4389 // case Mount::UNPARKING_OK: // INDI switch idle or ok, and unparked
4390 // case Mount::PARKING_ERROR: // INDI switch error
4391 // case Mount::PARKING_BUSY: // INDI switch busy
4392 // case Mount::UNPARKING_BUSY: // INDI switch busy
4393 default:
4394 return false;
4395 }
4396 }
4397 // If the mount is not able to park, consider it not parked
4398 return false;
4399}
4400
4401void SchedulerProcess::parkDome()
4402{
4403 // If there is no dome, mark error
4404 if (domeInterface().isNull())
4405 {
4406 appendLogText(i18n("Dome park requested but no domes detected."));
4407 moduleState()->setShutdownState(SHUTDOWN_ERROR);
4408 return;
4409 }
4410
4411 //QDBusReply<int> const domeReply = domeInterface->call(QDBus::AutoDetect, "getParkingStatus");
4412 //Dome::ParkingStatus status = static_cast<Dome::ParkingStatus>(domeReply.value());
4413 QVariant parkingStatus = domeInterface()->property("parkStatus");
4414 qCDebug(KSTARS_EKOS_SCHEDULER) << "Dome parking status" << (!parkingStatus.isValid() ? -1 : parkingStatus.toInt());
4415
4416 if (parkingStatus.isValid() == false)
4417 {
4418 qCCritical(KSTARS_EKOS_SCHEDULER) << QString("Warning: dome parkStatus request received DBUS error: %1").arg(
4419 mountInterface()->lastError().type());
4420 if (!manageConnectionLoss())
4421 parkingStatus = ISD::PARK_ERROR;
4422 }
4423
4424 ISD::ParkStatus status = static_cast<ISD::ParkStatus>(parkingStatus.toInt());
4425 if (status != ISD::PARK_PARKED)
4426 {
4427 moduleState()->setShutdownState(SHUTDOWN_PARKING_DOME);
4428 domeInterface()->call(QDBus::AutoDetect, "park");
4429 appendLogText(i18n("Parking dome..."));
4430
4431 moduleState()->startCurrentOperationTimer();
4432 }
4433 else
4434 {
4435 appendLogText(i18n("Dome already parked."));
4436 moduleState()->setShutdownState(SHUTDOWN_SCRIPT);
4437 }
4438}
4439
4440void SchedulerProcess::unParkDome()
4441{
4442 // If there is no dome, mark error
4443 if (domeInterface().isNull())
4444 {
4445 appendLogText(i18n("Dome unpark requested but no domes detected."));
4446 moduleState()->setStartupState(STARTUP_ERROR);
4447 return;
4448 }
4449
4450 QVariant parkingStatus = domeInterface()->property("parkStatus");
4451 qCDebug(KSTARS_EKOS_SCHEDULER) << "Dome parking status" << (!parkingStatus.isValid() ? -1 : parkingStatus.toInt());
4452
4453 if (parkingStatus.isValid() == false)
4454 {
4455 qCCritical(KSTARS_EKOS_SCHEDULER) << QString("Warning: dome parkStatus request received DBUS error: %1").arg(
4456 mountInterface()->lastError().type());
4457 if (!manageConnectionLoss())
4458 parkingStatus = ISD::PARK_ERROR;
4459 }
4460
4461 if (static_cast<ISD::ParkStatus>(parkingStatus.toInt()) != ISD::PARK_UNPARKED)
4462 {
4463 moduleState()->setStartupState(STARTUP_UNPARKING_DOME);
4464 domeInterface()->call(QDBus::AutoDetect, "unpark");
4465 appendLogText(i18n("Unparking dome..."));
4466
4467 moduleState()->startCurrentOperationTimer();
4468 }
4469 else
4470 {
4471 appendLogText(i18n("Dome already unparked."));
4472 moduleState()->setStartupState(STARTUP_UNPARK_MOUNT);
4473 }
4474}
4475
4477{
4478 QVariant guideStatus = guideInterface()->property("status");
4479 Ekos::GuideState gStatus = static_cast<Ekos::GuideState>(guideStatus.toInt());
4480
4481 return gStatus;
4482}
4483
4484const QString &SchedulerProcess::profile() const
4485{
4486 return moduleState()->currentProfile();
4487}
4488
4489void SchedulerProcess::setProfile(const QString &newProfile)
4490{
4491 moduleState()->setCurrentProfile(newProfile);
4492}
4493
4494QString SchedulerProcess::currentJobName()
4495{
4496 auto job = moduleState()->activeJob();
4497 return ( job != nullptr ? job->getName() : QString() );
4498}
4499
4500QString SchedulerProcess::currentJobJson()
4501{
4502 auto job = moduleState()->activeJob();
4503 if( job != nullptr )
4504 {
4505 return QString( QJsonDocument( job->toJson() ).toJson() );
4506 }
4507 else
4508 {
4509 return QString();
4510 }
4511}
4512
4513QString SchedulerProcess::jsonJobs()
4514{
4515 return QString( QJsonDocument( moduleState()->getJSONJobs() ).toJson() );
4516}
4517
4518QStringList SchedulerProcess::logText()
4519{
4520 return moduleState()->logText();
4521}
4522
4524{
4525 if (domeInterface().isNull())
4526 return false;
4527
4528 QVariant parkingStatus = domeInterface()->property("parkStatus");
4529 qCDebug(KSTARS_EKOS_SCHEDULER) << "Dome parking status" << (!parkingStatus.isValid() ? -1 : parkingStatus.toInt());
4530
4531 if (parkingStatus.isValid() == false)
4532 {
4533 qCCritical(KSTARS_EKOS_SCHEDULER) << QString("Warning: dome parkStatus request received DBUS error: %1").arg(
4534 mountInterface()->lastError().type());
4535 if (!manageConnectionLoss())
4536 parkingStatus = ISD::PARK_ERROR;
4537 }
4538
4539 ISD::ParkStatus status = static_cast<ISD::ParkStatus>(parkingStatus.toInt());
4540
4541 return status == ISD::PARK_PARKED;
4542}
4543
4544void SchedulerProcess::simClockScaleChanged(float newScale)
4545{
4546 if (moduleState()->currentlySleeping())
4547 {
4548 QTime const remainingTimeMs = QTime::fromMSecsSinceStartOfDay(std::lround(static_cast<double>
4549 (moduleState()->iterationTimer().remainingTime())
4550 * KStarsData::Instance()->clock()->scale()
4551 / newScale));
4552 appendLogText(i18n("Sleeping for %1 on simulation clock update until next observation job is ready...",
4553 remainingTimeMs.toString("hh:mm:ss")));
4554 moduleState()->iterationTimer().stop();
4555 moduleState()->iterationTimer().start(remainingTimeMs.msecsSinceStartOfDay());
4556 }
4557}
4558
4559void SchedulerProcess::simClockTimeChanged()
4560{
4561 moduleState()->calculateDawnDusk();
4562
4563 // If the Scheduler is not running, reset all jobs and re-evaluate from a new current start point
4564 if (SCHEDULER_RUNNING != moduleState()->schedulerState())
4566}
4567
4568void SchedulerProcess::setINDICommunicationStatus(CommunicationStatus status)
4569{
4570 qCDebug(KSTARS_EKOS_SCHEDULER) << "Scheduler INDI status is" << status;
4571
4572 moduleState()->setIndiCommunicationStatus(status);
4573}
4574
4575void SchedulerProcess::setEkosCommunicationStatus(CommunicationStatus status)
4576{
4577 qCDebug(KSTARS_EKOS_SCHEDULER) << "Scheduler Ekos status is" << status;
4578
4579 moduleState()->setEkosCommunicationStatus(status);
4580}
4581
4582
4583
4584void SchedulerProcess::checkInterfaceReady(QDBusInterface * iface)
4585{
4586 if (iface == mountInterface())
4587 {
4588 if (mountInterface()->property("canPark").isValid())
4589 moduleState()->setMountReady(true);
4590 }
4591 else if (iface == capInterface())
4592 {
4593 if (capInterface()->property("canPark").isValid())
4594 moduleState()->setCapReady(true);
4595 }
4596 else if (iface == observatoryInterface())
4597 {
4598 QVariant status = observatoryInterface()->property("status");
4599 if (status.isValid())
4600 setWeatherStatus(static_cast<ISD::Weather::Status>(status.toInt()));
4601 }
4602 else if (iface == weatherInterface())
4603 {
4604 QVariant status = weatherInterface()->property("status");
4605 if (status.isValid())
4606 setWeatherStatus(static_cast<ISD::Weather::Status>(status.toInt()));
4607 }
4608 else if (iface == domeInterface())
4609 {
4610 if (domeInterface()->property("canPark").isValid())
4611 moduleState()->setDomeReady(true);
4612 }
4613 else if (iface == captureInterface())
4614 {
4615 if (captureInterface()->property("coolerControl").isValid())
4616 moduleState()->setCaptureReady(true);
4617 }
4618 // communicate state to UI
4619 emit interfaceReady(iface);
4620}
4621
4622void SchedulerProcess::registerNewModule(const QString &name)
4623{
4624 qCDebug(KSTARS_EKOS_SCHEDULER) << "Registering new Module (" << name << ")";
4625
4626 if (name == "Focus")
4627 {
4628 delete focusInterface();
4629 setFocusInterface(new QDBusInterface(kstarsInterfaceString, focusPathString, focusInterfaceString,
4631 connect(focusInterface(), SIGNAL(newStatus(Ekos::FocusState, const QString)), this,
4632 SLOT(setFocusStatus(Ekos::FocusState, const QString)), Qt::UniqueConnection);
4633 }
4634 else if (name == "Capture")
4635 {
4636 delete captureInterface();
4637 setCaptureInterface(new QDBusInterface(kstarsInterfaceString, capturePathString, captureInterfaceString,
4639
4640 connect(captureInterface(), SIGNAL(ready()), this, SLOT(syncProperties()));
4641 connect(captureInterface(), SIGNAL(newStatus(Ekos::CaptureState, const QString, int)), this,
4642 SLOT(setCaptureStatus(Ekos::CaptureState, const QString)), Qt::UniqueConnection);
4643 connect(captureInterface(), SIGNAL(captureComplete(QVariantMap, const QString)), this, SLOT(checkAlignment(QVariantMap,
4644 const QString)),
4646 checkInterfaceReady(captureInterface());
4647 }
4648 else if (name == "Mount")
4649 {
4650 delete mountInterface();
4651 setMountInterface(new QDBusInterface(kstarsInterfaceString, mountPathString, mountInterfaceString,
4653
4654 connect(mountInterface(), SIGNAL(ready()), this, SLOT(syncProperties()));
4655 connect(mountInterface(), SIGNAL(newStatus(ISD::Mount::Status)), this, SLOT(setMountStatus(ISD::Mount::Status)),
4657
4658 checkInterfaceReady(mountInterface());
4659 }
4660 else if (name == "Align")
4661 {
4662 delete alignInterface();
4663 setAlignInterface(new QDBusInterface(kstarsInterfaceString, alignPathString, alignInterfaceString,
4665 connect(alignInterface(), SIGNAL(newStatus(Ekos::AlignState)), this, SLOT(setAlignStatus(Ekos::AlignState)),
4667 }
4668 else if (name == "Guide")
4669 {
4670 delete guideInterface();
4671 setGuideInterface(new QDBusInterface(kstarsInterfaceString, guidePathString, guideInterfaceString,
4673 connect(guideInterface(), SIGNAL(newStatus(Ekos::GuideState)), this,
4674 SLOT(setGuideStatus(Ekos::GuideState)), Qt::UniqueConnection);
4675 }
4676 else if (name == "Observatory")
4677 {
4678 delete observatoryInterface();
4679 setObservatoryInterface(new QDBusInterface(kstarsInterfaceString, observatoryPathString, observatoryInterfaceString,
4681 connect(observatoryInterface(), SIGNAL(newStatus(ISD::Weather::Status)), this,
4682 SLOT(setWeatherStatus(ISD::Weather::Status)), Qt::UniqueConnection);
4683 checkInterfaceReady(observatoryInterface());
4684 }
4685}
4686
4687void SchedulerProcess::registerNewDevice(const QString &name, int interface)
4688{
4689 Q_UNUSED(name)
4690
4691 if (interface & INDI::BaseDevice::DOME_INTERFACE)
4692 {
4693 QList<QVariant> dbusargs;
4694 dbusargs.append(INDI::BaseDevice::DOME_INTERFACE);
4695 QDBusReply<QStringList> paths = indiInterface()->callWithArgumentList(QDBus::AutoDetect, "getDevicesPaths",
4696 dbusargs);
4697 if (paths.error().type() == QDBusError::NoError && !paths.value().isEmpty())
4698 {
4699 // Select last device in case a restarted caused multiple instances in the tree
4700 setDomePathString(paths.value().last());
4701 delete domeInterface();
4702 setDomeInterface(new QDBusInterface(kstarsInterfaceString, domePathString,
4703 domeInterfaceString,
4705 connect(domeInterface(), SIGNAL(ready()), this, SLOT(syncProperties()));
4706 checkInterfaceReady(domeInterface());
4707 }
4708 }
4709
4710 // if (interface & INDI::BaseDevice::WEATHER_INTERFACE)
4711 // {
4712 // QList<QVariant> dbusargs;
4713 // dbusargs.append(INDI::BaseDevice::WEATHER_INTERFACE);
4714 // QDBusReply<QStringList> paths = indiInterface()->callWithArgumentList(QDBus::AutoDetect, "getDevicesPaths",
4715 // dbusargs);
4716 // if (paths.error().type() == QDBusError::NoError)
4717 // {
4718 // // Select last device in case a restarted caused multiple instances in the tree
4719 // setWeatherPathString(paths.value().last());
4720 // delete weatherInterface();
4721 // setWeatherInterface(new QDBusInterface(kstarsInterfaceString, weatherPathString,
4722 // weatherInterfaceString,
4723 // QDBusConnection::sessionBus(), this));
4724 // connect(weatherInterface(), SIGNAL(ready()), this, SLOT(syncProperties()));
4725 // connect(weatherInterface(), SIGNAL(newStatus(ISD::Weather::Status)), this,
4726 // SLOT(setWeatherStatus(ISD::Weather::Status)));
4727 // checkInterfaceReady(weatherInterface());
4728 // }
4729 // }
4730
4731 if (interface & INDI::BaseDevice::DUSTCAP_INTERFACE)
4732 {
4733 QList<QVariant> dbusargs;
4734 dbusargs.append(INDI::BaseDevice::DUSTCAP_INTERFACE);
4735 QDBusReply<QStringList> paths = indiInterface()->callWithArgumentList(QDBus::AutoDetect, "getDevicesPaths",
4736 dbusargs);
4737 if (paths.error().type() == QDBusError::NoError && !paths.value().isEmpty())
4738 {
4739 // Select last device in case a restarted caused multiple instances in the tree
4740 setDustCapPathString(paths.value().last());
4741 delete capInterface();
4742 setCapInterface(new QDBusInterface(kstarsInterfaceString, dustCapPathString,
4743 dustCapInterfaceString,
4745 connect(capInterface(), SIGNAL(ready()), this, SLOT(syncProperties()));
4746 checkInterfaceReady(capInterface());
4747 }
4748 }
4749}
4750
4751bool SchedulerProcess::createJobSequence(XMLEle * root, const QString &prefix, const QString &outputDir)
4752{
4753 XMLEle *ep = nullptr;
4754 XMLEle *subEP = nullptr;
4755
4756 for (ep = nextXMLEle(root, 1); ep != nullptr; ep = nextXMLEle(root, 0))
4757 {
4758 if (!strcmp(tagXMLEle(ep), "Job"))
4759 {
4760 for (subEP = nextXMLEle(ep, 1); subEP != nullptr; subEP = nextXMLEle(ep, 0))
4761 {
4762 if (!strcmp(tagXMLEle(subEP), "TargetName"))
4763 {
4764 // Set the target name in sequence file, though scheduler will overwrite this later
4765 editXMLEle(subEP, prefix.toLatin1().constData());
4766 }
4767 else if (!strcmp(tagXMLEle(subEP), "FITSDirectory"))
4768 {
4769 editXMLEle(subEP, outputDir.toLatin1().constData());
4770 }
4771 }
4772 }
4773 }
4774
4775 QDir().mkpath(outputDir);
4776
4777 QString filename = QString("%1/%2.esq").arg(outputDir, prefix);
4778 FILE *outputFile = fopen(filename.toLatin1().constData(), "w");
4779
4780 if (outputFile == nullptr)
4781 {
4782 QString message = i18n("Unable to write to file %1", filename);
4783 KSNotification::sorry(message, i18n("Could Not Open File"));
4784 return false;
4785 }
4786
4787 fprintf(outputFile, "<?xml version=\"1.0\" encoding=\"UTF-8\"?>");
4788 prXMLEle(outputFile, root, 0);
4789
4790 fclose(outputFile);
4791
4792 return true;
4793}
4794
4795XMLEle *SchedulerProcess::getSequenceJobRoot(const QString &filename) const
4796{
4797 QFile sFile;
4798 sFile.setFileName(filename);
4799
4800 if (!sFile.open(QIODevice::ReadOnly))
4801 {
4802 KSNotification::sorry(i18n("Unable to open file %1", sFile.fileName()),
4803 i18n("Could Not Open File"));
4804 return nullptr;
4805 }
4806
4807 LilXML *xmlParser = newLilXML();
4808 char errmsg[MAXRBUF];
4809 XMLEle *root = nullptr;
4810 char c;
4811
4812 while (sFile.getChar(&c))
4813 {
4814 root = readXMLEle(xmlParser, c, errmsg);
4815
4816 if (root)
4817 break;
4818 }
4819
4820 delLilXML(xmlParser);
4821 sFile.close();
4822 return root;
4823}
4824
4825void SchedulerProcess::checkProcessExit(int exitCode)
4826{
4827 scriptProcess().disconnect();
4828
4829 if (exitCode == 0)
4830 {
4831 if (moduleState()->startupState() == STARTUP_SCRIPT)
4832 moduleState()->setStartupState(STARTUP_UNPARK_DOME);
4833 else if (moduleState()->shutdownState() == SHUTDOWN_SCRIPT_RUNNING)
4834 moduleState()->setShutdownState(SHUTDOWN_COMPLETE);
4835
4836 return;
4837 }
4838
4839 if (moduleState()->startupState() == STARTUP_SCRIPT)
4840 {
4841 appendLogText(i18n("Startup script failed, aborting..."));
4842 moduleState()->setStartupState(STARTUP_ERROR);
4843 }
4844 else if (moduleState()->shutdownState() == SHUTDOWN_SCRIPT_RUNNING)
4845 {
4846 appendLogText(i18n("Shutdown script failed, aborting..."));
4847 moduleState()->setShutdownState(SHUTDOWN_ERROR);
4848 }
4849
4850}
4851
4852void SchedulerProcess::readProcessOutput()
4853{
4854 appendLogText(scriptProcess().readAllStandardOutput().simplified());
4855}
4856
4857bool SchedulerProcess::canCountCaptures(const SchedulerJob &job)
4858{
4859 QList<QSharedPointer<SequenceJob>> seqjobs;
4860 bool hasAutoFocus = false;
4861 SchedulerJob tempJob = job;
4862 if (SchedulerUtils::loadSequenceQueue(tempJob.getSequenceFile().toLocalFile(), &tempJob, seqjobs, hasAutoFocus,
4863 nullptr) == false)
4864 return false;
4865
4866 for (auto oneSeqJob : seqjobs)
4867 {
4868 if (oneSeqJob->getUploadMode() == ISD::Camera::UPLOAD_REMOTE)
4869 return false;
4870 }
4871 return true;
4872}
4873
4875{
4876 /* Use a temporary map in order to limit the number of file searches */
4877 CapturedFramesMap newFramesCount;
4878
4879 /* FIXME: Capture storage cache is refreshed too often, feature requires rework. */
4880
4881 /* Check if one job is idle or requires evaluation - if so, force refresh */
4882 forced |= std::any_of(moduleState()->jobs().begin(),
4883 moduleState()->jobs().end(), [](SchedulerJob * oneJob) -> bool
4884 {
4885 SchedulerJobStatus const state = oneJob->getState();
4886 return state == SCHEDJOB_IDLE || state == SCHEDJOB_EVALUATION;});
4887
4888 /* If update is forced, clear the frame map */
4889 if (forced)
4890 moduleState()->capturedFramesCount().clear();
4891
4892 /* Enumerate SchedulerJobs to count captures that are already stored */
4893 for (SchedulerJob *oneJob : moduleState()->jobs())
4894 {
4895 // This is like newFramesCount, but reset on every job.
4896 // It is useful for properly calling addProgress().
4897 CapturedFramesMap newJobFramesCount;
4898
4900 bool hasAutoFocus = false;
4901
4902 //oneJob->setLightFramesRequired(false);
4903 /* Look into the sequence requirements, bypass if invalid */
4904 if (SchedulerUtils::loadSequenceQueue(oneJob->getSequenceFile().toLocalFile(), oneJob, seqjobs, hasAutoFocus,
4905 this) == false)
4906 {
4907 appendLogText(i18n("Warning: job '%1' has inaccessible sequence '%2', marking invalid.", oneJob->getName(),
4908 oneJob->getSequenceFile().toLocalFile()));
4909 oneJob->setState(SCHEDJOB_INVALID);
4910 continue;
4911 }
4912
4913 oneJob->clearProgress();
4914 /* Enumerate the SchedulerJob's SequenceJobs to count captures stored for each */
4915 for (auto oneSeqJob : seqjobs)
4916 {
4917 /* Only consider captures stored on client (Ekos) side */
4918 /* FIXME: ask the remote for the file count */
4919 if (oneSeqJob->getUploadMode() == ISD::Camera::UPLOAD_REMOTE)
4920 continue;
4921
4922 /* FIXME: this signature path is incoherent when there is no filter wheel on the setup - bugfix should be elsewhere though */
4923 QString const signature = oneSeqJob->getSignature();
4924
4925 /* If signature was processed during this run, keep it */
4926 if (newFramesCount.constEnd() != newFramesCount.constFind(signature))
4927 {
4928 if (newJobFramesCount.constEnd() == newJobFramesCount.constFind(signature))
4929 {
4930 // Even though we've seen this before, we haven't seen it for this SchedulerJob.
4931 const int count = newFramesCount.constFind(signature).value();
4932 newJobFramesCount[signature] = count;
4933 oneJob->addProgress(count, oneSeqJob);
4934 }
4935 continue;
4936 }
4937 int count = 0;
4938
4939 CapturedFramesMap::const_iterator const earlierRunIterator =
4940 moduleState()->capturedFramesCount().constFind(signature);
4941
4942 if (moduleState()->capturedFramesCount().constEnd() != earlierRunIterator)
4943 // If signature was processed during an earlier run, use the earlier count.
4944 count = earlierRunIterator.value();
4945 else
4946 // else recount captures already stored
4947 count = PlaceholderPath::getCompletedFiles(signature);
4948
4949 newFramesCount[signature] = count;
4950 newJobFramesCount[signature] = count;
4951 oneJob->addProgress(count, oneSeqJob);
4952 }
4953
4954 // determine whether we need to continue capturing, depending on captured frames
4955 SchedulerUtils::updateLightFramesRequired(oneJob, seqjobs, newFramesCount);
4956 }
4957
4958 moduleState()->setCapturedFramesCount(newFramesCount);
4959
4960 {
4961 qCDebug(KSTARS_EKOS_SCHEDULER) << "Frame map summary:";
4962 CapturedFramesMap::const_iterator it = moduleState()->capturedFramesCount().constBegin();
4963 for (; it != moduleState()->capturedFramesCount().constEnd(); it++)
4964 qCDebug(KSTARS_EKOS_SCHEDULER) << " " << it.key() << ':' << it.value();
4965 }
4966}
4967
4968SchedulerJob *SchedulerProcess::activeJob()
4969{
4970 return moduleState()->activeJob();
4971}
4972
4973void SchedulerProcess::printStates(const QString &label)
4974{
4975 qCDebug(KSTARS_EKOS_SCHEDULER) <<
4976 QString("%1 %2 %3%4 %5 %6 %7 %8 %9\n")
4977 .arg(label)
4978 .arg(timerStr(moduleState()->timerState()))
4979 .arg(getSchedulerStatusString(moduleState()->schedulerState()))
4980 .arg((moduleState()->timerState() == RUN_JOBCHECK && activeJob() != nullptr) ?
4981 QString("(%1 %2)").arg(SchedulerJob::jobStatusString(activeJob()->getState()))
4982 .arg(SchedulerJob::jobStageString(activeJob()->getStage())) : "")
4983 .arg(ekosStateString(moduleState()->ekosState()))
4984 .arg(indiStateString(moduleState()->indiState()))
4985 .arg(startupStateString(moduleState()->startupState()))
4986 .arg(shutdownStateString(moduleState()->shutdownState()))
4987 .arg(parkWaitStateString(moduleState()->parkWaitState())).toLatin1().data();
4988 foreach (auto j, moduleState()->jobs())
4989 qCDebug(KSTARS_EKOS_SCHEDULER) << QString("job %1 %2\n").arg(j->getName()).arg(SchedulerJob::jobStatusString(
4990 j->getState())).toLatin1().data();
4991}
4992
4993} // 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.
SkyPoint mountCoords()
mountCoords read the equatorial coordinates from the mount
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:122
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()
void clear()
iterator end()
T & first()
bool isEmpty() const const
void removeFirst()
qsizetype size() const const
T value(qsizetype i) const const
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
QString toString() const const
T value() const const
This file is part of the KDE documentation.
Documentation copyright © 1996-2025 The KDE developers.
Generated on Fri Feb 28 2025 11:56:00 by doxygen 1.13.2 written by Dimitri van Heesch, © 1997-2006

KDE's Doxygen guidelines are available online.