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

KDE's Doxygen guidelines are available online.