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

KDE's Doxygen guidelines are available online.