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

KDE's Doxygen guidelines are available online.