Kstars

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

KDE's Doxygen guidelines are available online.