Kstars

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

KDE's Doxygen guidelines are available online.