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

KDE's Doxygen guidelines are available online.