Kstars

camerastate.cpp
1/* Ekos state machine for the Capture module
2 SPDX-FileCopyrightText: 2022 Wolfgang Reissenberger <sterne-jaeger@openfuture.de>
3
4 SPDX-License-Identifier: GPL-2.0-or-later
5*/
6
7#include "camerastate.h"
8#include "ekos/manager/meridianflipstate.h"
9#include "ekos/capture/sequencejob.h"
10#include "ekos/capture/sequencequeue.h"
11#include "fitsviewer/fitsdata.h"
12
13#include "ksnotification.h"
14#include <ekos_capture_debug.h>
15
16#define GD_TIMER_TIMEOUT 60000
17
18namespace Ekos
19{
20void CameraState::init()
21{
22 m_sequenceQueue.reset(new SequenceQueue());
23 m_refocusState.reset(new RefocusState());
24 m_TargetADUTolerance = Options::calibrationADUValueTolerance();
25 connect(m_sequenceQueue.get(), &SequenceQueue::newLog, this, &CameraState::newLog);
26 connect(m_refocusState.get(), &RefocusState::newLog, this, &CameraState::newLog);
27
28 getGuideDeviationTimer().setInterval(GD_TIMER_TIMEOUT);
29 connect(&m_guideDeviationTimer, &QTimer::timeout, this, &CameraState::checkGuideDeviationTimeout);
30
31 setCalibrationPreAction(Options::calibrationPreActionIndex());
32 setFlatFieldDuration(static_cast<FlatFieldDuration>(Options::calibrationFlatDurationIndex()));
33 wallCoord().setAz(Options::calibrationWallAz());
34 wallCoord().setAlt(Options::calibrationWallAlt());
35 setTargetADU(Options::calibrationADUValue());
36 setSkyFlat(Options::calibrationSkyFlat());
37}
38
39CameraState::CameraState(QObject *parent): QObject{parent}
40{
41 init();
42}
43
44QList<SequenceJob *> &CameraState::allJobs()
45{
46 return m_sequenceQueue->allJobs();
47}
48
49const QUrl &CameraState::sequenceURL() const
50{
51 return m_sequenceQueue->sequenceURL();
52}
53
54void CameraState::setSequenceURL(const QUrl &newSequenceURL)
55{
56 m_sequenceQueue->setSequenceURL(newSequenceURL);
57 placeholderPath().setSeqFilename(QFileInfo(newSequenceURL.toLocalFile()));
58}
59
60void CameraState::setActiveJob(SequenceJob *value)
61{
62 // do nothing if active job is not changed
63 if (m_activeJob == value)
64 return;
65
66 // clear existing job connections
67 if (m_activeJob != nullptr)
68 {
69 disconnect(this, nullptr, m_activeJob, nullptr);
70 disconnect(m_activeJob, nullptr, this, nullptr);
71 // ensure that the device adaptor does not send any new events
72 m_activeJob->disconnectDeviceAdaptor();
73 }
74
75 // set the new value
76 m_activeJob = value;
77
78 // create job connections
79 if (m_activeJob != nullptr)
80 {
81 // connect job with device adaptor events
82 m_activeJob->connectDeviceAdaptor();
83 // forward signals to the sequence job
84 connect(this, &CameraState::newGuiderDrift, m_activeJob, &SequenceJob::updateGuiderDrift);
85 // react upon sequence job signals
86 connect(m_activeJob, &SequenceJob::prepareState, this, &CameraState::updatePrepareState);
87 connect(m_activeJob, &SequenceJob::prepareComplete, this, &CameraState::setPrepareComplete, Qt::UniqueConnection);
88 connect(m_activeJob, &SequenceJob::abortCapture, this, &CameraState::abortCapture);
89 connect(m_activeJob, &SequenceJob::captureStarted, this, &CameraState::captureStarted);
90 connect(m_activeJob, &SequenceJob::newLog, this, &CameraState::newLog);
91 // forward the devices and attributes
92 m_activeJob->updateDeviceStates();
93 m_activeJob->setAutoFocusReady(getRefocusState()->isAutoFocusReady());
94 }
95
96}
97
98int CameraState::activeJobID()
99{
100 if (m_activeJob == nullptr)
101 return -1;
102
103 for (int i = 0; i < allJobs().count(); i++)
104 {
105 if (m_activeJob == allJobs().at(i))
106 return i;
107 }
108
109 return -1;
110
111}
112
113void CameraState::initCapturePreparation()
114{
115 setStartingCapture(false);
116
117 // Reset progress option if there is no captured frame map set at the time of start - fixes the end-user setting the option just before starting
118 setIgnoreJobProgress(!hasCapturedFramesMap() && Options::alwaysResetSequenceWhenStarting());
119
120 // Refocus timer should not be reset on deviation error
121 if (isGuidingDeviationDetected() == false && getCaptureState() != CAPTURE_SUSPENDED)
122 {
123 // start timer to measure time until next forced refocus
124 getRefocusState()->startRefocusTimer();
125 }
126
127 // Only reset these counters if we are NOT restarting from deviation errors
128 // So when starting a new job or fresh then we reset them.
129 if (isGuidingDeviationDetected() == false)
130 {
131 resetDitherCounter();
132 getRefocusState()->resetInSequenceFocusCounter();
133 getRefocusState()->setAdaptiveFocusDone(false);
134 }
135
136 setGuidingDeviationDetected(false);
137 resetSpikesDetected();
138
139 setCaptureState(CAPTURE_PROGRESS);
140 setBusy(true);
141
142 initPlaceholderPath();
143
144 if (Options::enforceGuideDeviation() && isGuidingOn() == false)
145 emit newLog(i18n("Warning: Guide deviation is selected but autoguide process was not started."));
146}
147
148void CameraState::setCaptureState(CaptureState value)
149{
150 bool pause_planned = false;
151 // handle new capture state
152 switch (value)
153 {
154 case CAPTURE_IDLE:
155 case CAPTURE_ABORTED:
156 emit resetNonGuidedDither();
157 /* Fall through */
159 case CAPTURE_PAUSED:
160 // meridian flip may take place if requested
161 if (mf_state->getMeridianFlipStage() == MeridianFlipState::MF_REQUESTED)
162 mf_state->updateMeridianFlipStage(MeridianFlipState::MF_READY);
163 break;
165 // remember pause planning before receiving an image
166 pause_planned = (m_CaptureState == CAPTURE_PAUSE_PLANNED);
167 break;
169 emit requestAction(CAPTURE_ACTION_DITHER_REQUEST);
170 default:
171 // do nothing
172 break;
173 }
174
175 // Only emit status if it changed
176 if (m_CaptureState != value)
177 {
178 qCDebug(KSTARS_EKOS_CAPTURE()) << "Capture State changes from" << getCaptureStatusString(
179 m_CaptureState) << "to" << getCaptureStatusString(value);
180 m_CaptureState = value;
181 getMeridianFlipState()->setCaptureState(m_CaptureState);
182 emit newStatus(m_CaptureState);
183 // reset to planned state if necessary
184 if (pause_planned)
185 {
186 m_CaptureState = CAPTURE_PAUSE_PLANNED;
187 emit newStatus(m_CaptureState);
188 }
189 }
190}
191
192void CameraState::setGuideState(GuideState state)
193{
194 if (state != m_GuideState)
195 qCDebug(KSTARS_EKOS_CAPTURE) << "Guiding state changed from" <<
196 Ekos::getGuideStatusString(m_GuideState)
197 << "to" << Ekos::getGuideStatusString(state);
198 switch (state)
199 {
200 case GUIDE_IDLE:
201 case GUIDE_GUIDING:
202 case GUIDE_CALIBRATION_SUCCESS:
203 break;
204
205 case GUIDE_ABORTED:
206 case GUIDE_CALIBRATION_ERROR:
207 processGuidingFailed();
208 break;
209
210 case GUIDE_DITHERING_SUCCESS:
211 qCInfo(KSTARS_EKOS_CAPTURE) << "Dithering succeeded, capture state" << getCaptureStatusString(
212 getCaptureState());
213 // do nothing if something happened during dithering
214 appendLogText(i18n("Dithering succeeded."));
215 if (getCaptureState() != CAPTURE_DITHERING)
216 break;
217
218 if (Options::guidingSettle() > 0)
219 {
220 // N.B. Do NOT convert to i18np since guidingRate is DOUBLE value (e.g. 1.36) so we always use plural with that.
221 appendLogText(i18n("Dither complete. Resuming in %1 seconds...", Options::guidingSettle()));
222 QTimer::singleShot(Options::guidingSettle() * 1000, this, [this]()
223 {
224 setDitheringState(IPS_OK);
225 });
226 }
227 else
228 {
229 appendLogText(i18n("Dither complete."));
230 setDitheringState(IPS_OK);
231 }
232 break;
233
234 case GUIDE_DITHERING_ERROR:
235 qCInfo(KSTARS_EKOS_CAPTURE) << "Dithering failed, capture state" << getCaptureStatusString(
236 getCaptureState());
237 if (getCaptureState() != CAPTURE_DITHERING)
238 break;
239
240 if (Options::guidingSettle() > 0)
241 {
242 // N.B. Do NOT convert to i18np since guidingRate is DOUBLE value (e.g. 1.36) so we always use plural with that.
243 appendLogText(i18n("Warning: Dithering failed. Resuming in %1 seconds...", Options::guidingSettle()));
244 // set dithering state to OK after settling time and signal to proceed
245 QTimer::singleShot(Options::guidingSettle() * 1000, this, [this]()
246 {
247 setDitheringState(IPS_OK);
248 });
249 }
250 else
251 {
252 appendLogText(i18n("Warning: Dithering failed."));
253 // signal OK so that capturing may continue although dithering failed
254 setDitheringState(IPS_OK);
255 }
256
257 break;
258
259 default:
260 break;
261 }
262
263 m_GuideState = state;
264 // forward it to the currently active sequence job
265 if (m_activeJob != nullptr)
266 m_activeJob->setCoreProperty(SequenceJob::SJ_GuiderActive, isActivelyGuiding());
267}
268
269
270
271void CameraState::setCurrentFilterPosition(int position, const QString &name, const QString &focusFilterName)
272{
273 m_CurrentFilterPosition = position;
274 if (position > 0)
275 {
276 m_CurrentFilterName = name;
277 m_CurrentFocusFilterName = focusFilterName;
278 }
279 else
280 {
281 m_CurrentFilterName = "--";
282 m_CurrentFocusFilterName = "--";
283 }
284}
285
286void CameraState::dustCapStateChanged(ISD::DustCap::Status status)
287{
288 switch (status)
289 {
290 case ISD::DustCap::CAP_ERROR:
291 setDustCapState(CAP_ERROR);
292 emit newLog(i18n("Dust cap error."));
293 break;
294 case ISD::DustCap::CAP_PARKED:
295 setDustCapState(CAP_PARKED);
296 emit newLog(i18n("Dust cap parked."));
297 break;
298 case ISD::DustCap::CAP_IDLE:
299 setDustCapState(CAP_IDLE);
300 emit newLog(i18n("Dust cap unparked."));
301 break;
302 case ISD::DustCap::CAP_UNPARKING:
303 setDustCapState(CAP_UNPARKING);
304 break;
305 case ISD::DustCap::CAP_PARKING:
306 setDustCapState(CAP_PARKING);
307 break;
308 }
309}
310
311QSharedPointer<MeridianFlipState> CameraState::getMeridianFlipState()
312{
313 // lazy instantiation
314 if (mf_state.isNull())
315 mf_state.reset(new MeridianFlipState());
316
317 return mf_state;
318}
319
320void CameraState::setMeridianFlipState(QSharedPointer<MeridianFlipState> state)
321{
322 // clear old state machine
323 if (! mf_state.isNull())
324 {
325 mf_state->disconnect(this);
326 mf_state->deleteLater();
327 }
328
329 mf_state = state;
330 connect(mf_state.data(), &Ekos::MeridianFlipState::newMountMFStatus, this, &Ekos::CameraState::updateMFMountState,
332}
333
334void CameraState::setObserverName(const QString &value)
335{
336 m_ObserverName = value;
337 Options::setDefaultObserver(value);
338}
339
340void CameraState::setBusy(bool busy)
341{
342 m_Busy = busy;
343 emit captureBusy(busy);
344}
345
346
347
348bool CameraState::generateFilename(const QString &extension, QString *filename)
349{
350 *filename = placeholderPath().generateOutputFilename(true, true, nextSequenceID(), extension, "");
351
352 QDir currentDir = QFileInfo(*filename).dir();
353 if (currentDir.exists() == false)
354 QDir().mkpath(currentDir.path());
355
356 // Check if the file exists. We try not to overwrite capture files.
357 if (QFile::exists(*filename))
358 {
359 QString oldFilename = *filename;
360 *filename = placeholderPath().repairFilename(*filename);
361 if (*filename != oldFilename)
362 qCWarning(KSTARS_EKOS_CAPTURE) << "File over-write detected: changing" << oldFilename << "to" << *filename;
363 else
364 qCWarning(KSTARS_EKOS_CAPTURE) << "File over-write detected for" << oldFilename << "but could not correct filename";
365 }
366
367 QFile test_file(*filename);
368 if (!test_file.open(QIODevice::WriteOnly))
369 return false;
370 test_file.flush();
371 test_file.close();
372 return true;
373}
374
375void CameraState::decreaseDitherCounter()
376{
377 if (m_ditherCounter > 0)
378 --m_ditherCounter;
379}
380
381void CameraState::resetDitherCounter()
382{
383 uint value = 0;
384 if (m_activeJob)
385 value = m_activeJob->getCoreProperty(SequenceJob::SJ_DitherPerJobFrequency).toInt(0);
386
387 if (value > 0)
388 m_ditherCounter = value;
389 else
390 m_ditherCounter = Options::ditherFrames();
391}
392
393bool CameraState::checkDithering()
394{
395 // No need if preview only
396 if (m_activeJob && m_activeJob->jobType() == SequenceJob::JOBTYPE_PREVIEW)
397 return false;
398
399 if ( (Options::ditherEnabled() || Options::ditherNoGuiding())
400 && (m_activeJob != nullptr && m_activeJob->getCoreProperty(SequenceJob::SJ_DitherPerJobEnabled).toBool())
401 // 2017-09-20 Jasem: No need to dither after post meridian flip guiding
402 && getMeridianFlipState()->getMeridianFlipStage() != MeridianFlipState::MF_GUIDING
403 // We must be either in guide mode or if non-guide dither (via pulsing) is enabled
404 && (getGuideState() == GUIDE_GUIDING || Options::ditherNoGuiding())
405 // Must be only done for light frames
406 && (m_activeJob != nullptr && m_activeJob->getFrameType() == FRAME_LIGHT)
407 // Check dither counter
408 && m_ditherCounter == 0)
409 {
410 // reset the dither counter
411 resetDitherCounter();
412
413 appendLogText(i18n("Dithering requested..."));
414
415 setCaptureState(CAPTURE_DITHERING);
416 setDitheringState(IPS_BUSY);
417
418 return true;
419 }
420 // no dithering required
421 return false;
422}
423
424void CameraState::updateMFMountState(MeridianFlipState::MeridianFlipMountState status)
425{
426 qCDebug(KSTARS_EKOS_CAPTURE) << "updateMFMountState: " << MeridianFlipState::meridianFlipStatusString(status);
427
428 switch (status)
429 {
430 case MeridianFlipState::MOUNT_FLIP_NONE:
431 // MF_NONE as external signal ignored so that re-alignment and guiding are processed first
432 if (getMeridianFlipState()->getMeridianFlipStage() < MeridianFlipState::MF_COMPLETED)
433 updateMeridianFlipStage(MeridianFlipState::MF_NONE);
434 break;
435
436 case MeridianFlipState::MOUNT_FLIP_PLANNED:
437 if (getMeridianFlipState()->getMeridianFlipStage() > MeridianFlipState::MF_REQUESTED)
438 {
439 // This should never happen, since a meridian flip seems to be ongoing
440 qCritical(KSTARS_EKOS_CAPTURE) << "Accepting meridian flip request while being in stage " <<
441 getMeridianFlipState()->getMeridianFlipStage();
442 }
443
444 // If we are autoguiding, we should resume autoguiding after flip
445 getMeridianFlipState()->setResumeGuidingAfterFlip(isGuidingOn());
446
447 // mark flip as requested
448 updateMeridianFlipStage(MeridianFlipState::MF_REQUESTED);
449 // if capture is not running, immediately accept it
450 if (m_CaptureState == CAPTURE_IDLE || m_CaptureState == CAPTURE_ABORTED
451 || m_CaptureState == CAPTURE_COMPLETE || m_CaptureState == CAPTURE_PAUSED)
452 getMeridianFlipState()->updateMFMountState(MeridianFlipState::MOUNT_FLIP_ACCEPTED);
453
454 break;
455
456 case MeridianFlipState::MOUNT_FLIP_RUNNING:
457 updateMeridianFlipStage(MeridianFlipState::MF_INITIATED);
458 setCaptureState(CAPTURE_MERIDIAN_FLIP);
459 break;
460
461 case MeridianFlipState::MOUNT_FLIP_COMPLETED:
462 updateMeridianFlipStage(MeridianFlipState::MF_COMPLETED);
463 break;
464
465 default:
466 break;
467
468 }
469}
470
471void CameraState::updateMeridianFlipStage(const MeridianFlipState::MFStage &stage)
472{
473 // forward the stage to the module state
474 getMeridianFlipState()->updateMeridianFlipStage(stage);
475
476 // handle state changes for other modules
477 switch (stage)
478 {
479 case MeridianFlipState::MF_READY:
480 break;
481
482 case MeridianFlipState::MF_INITIATED:
483 emit meridianFlipStarted();
484 break;
485
486 case MeridianFlipState::MF_COMPLETED:
487
488 // Reset HFR Check counter after meridian flip
489 if (getRefocusState()->isInSequenceFocus())
490 {
491 qCDebug(KSTARS_EKOS_CAPTURE) << "Resetting HFR Check counter after meridian flip.";
492 //firstAutoFocus = true;
493 getRefocusState()->setInSequenceFocusCounter(0);
494 }
495
496 // after a meridian flip we do not need to dither
497 if ( Options::ditherEnabled() || Options::ditherNoGuiding())
498 resetDitherCounter();
499
500 // if requested set flag so it perform refocus before next frame
501 if (Options::refocusAfterMeridianFlip() == true)
502 getRefocusState()->setRefocusAfterMeridianFlip(true);
503
504 KSNotification::event(QLatin1String("MeridianFlipCompleted"), i18n("Meridian flip is successfully completed"),
505 KSNotification::Capture);
506
507 getMeridianFlipState()->processFlipCompleted();
508
509 // if the capturing has been paused before the flip, reset the state to paused, otherwise to idle
510 setCaptureState(m_ContinueAction == CAPTURE_CONTINUE_ACTION_NONE ? CAPTURE_IDLE : CAPTURE_PAUSED);
511 break;
512
513 default:
514 break;
515 }
516 // forward the new stage
517 emit newMeridianFlipStage(stage);
518}
519
520bool CameraState::checkMeridianFlipActive()
521{
522 return (getMeridianFlipState()->checkMeridianFlipRunning() ||
523 checkPostMeridianFlipActions() ||
524 checkMeridianFlipReady());
525}
526
527bool CameraState::checkMeridianFlipReady()
528{
529 if (hasTelescope == false)
530 return false;
531
532 // If active job is taking flat field image at a wall source
533 // then do not flip.
534 if (m_activeJob && m_activeJob->getFrameType() == FRAME_FLAT
535 && m_activeJob->getCalibrationPreAction() & CAPTURE_PREACTION_WALL)
536 return false;
537
538 if (getMeridianFlipState()->getMeridianFlipStage() != MeridianFlipState::MF_REQUESTED)
539 // if no flip has been requested or is already ongoing
540 return false;
541
542 // meridian flip requested or already in action
543
544 // Reset frame if we need to do focusing later on
545 if (m_refocusState->isInSequenceFocus() ||
546 (Options::enforceRefocusEveryN() && m_refocusState->getRefocusEveryNTimerElapsedSec() > 0))
547 emit resetFocusFrame();
548
549 // signal that meridian flip may take place
550 if (getMeridianFlipState()->getMeridianFlipStage() == MeridianFlipState::MF_REQUESTED)
551 getMeridianFlipState()->updateMeridianFlipStage(MeridianFlipState::MF_READY);
552
553
554 return true;
555}
556
557bool CameraState::checkPostMeridianFlipActions()
558{
559 // step 1: If dome is syncing, wait until it stops
560 if (hasDome && (m_domeState == ISD::Dome::DOME_MOVING_CW || m_domeState == ISD::Dome::DOME_MOVING_CCW))
561 return true;
562
563 // step 2: check if post flip alignment is running
564 if (m_CaptureState == CAPTURE_ALIGNING || checkAlignmentAfterFlip())
565 return true;
566
567 // step 2: check if post flip guiding is running
568 // MF_NONE is set as soon as guiding is running and the guide deviation is below the limit
569 if (getMeridianFlipState()->getMeridianFlipStage() >= MeridianFlipState::MF_COMPLETED && m_GuideState != GUIDE_GUIDING
570 && checkGuidingAfterFlip())
571 return true;
572
573 // step 4: in case that a meridian flip has been completed and a guide deviation limit is set, we wait
574 // until the guide deviation is reported to be below the limit (@see setGuideDeviation(double, double)).
575 // Otherwise the meridian flip is complete
576 if (m_CaptureState == CAPTURE_CALIBRATING
577 && getMeridianFlipState()->getMeridianFlipStage() == MeridianFlipState::MF_GUIDING)
578 {
579 if (Options::enforceGuideDeviation() || Options::enforceStartGuiderDrift())
580 return true;
581 else
582 updateMeridianFlipStage(MeridianFlipState::MF_NONE);
583 }
584
585 // all actions completed or no flip running
586 return false;
587}
588
589bool CameraState::checkGuidingAfterFlip()
590{
591 // if no meridian flip has completed, we do not touch guiding
592 if (getMeridianFlipState()->getMeridianFlipStage() < MeridianFlipState::MF_COMPLETED)
593 return false;
594 // If we're not autoguiding then we're done
595 if (getMeridianFlipState()->resumeGuidingAfterFlip() == false)
596 {
597 getMeridianFlipState()->updateMeridianFlipStage(MeridianFlipState::MF_NONE);
598 return false;
599 }
600
601 // if we are waiting for a calibration, start it
602 if (getMeridianFlipState()->getMeridianFlipStage() >= MeridianFlipState::MF_COMPLETED
603 && getMeridianFlipState()->getMeridianFlipStage() < MeridianFlipState::MF_GUIDING)
604 {
605 appendLogText(i18n("Performing post flip re-calibration and guiding..."));
606
607 setCaptureState(CAPTURE_CALIBRATING);
608
609 getMeridianFlipState()->updateMeridianFlipStage(MeridianFlipState::MF_GUIDING);
610 emit guideAfterMeridianFlip();
611 return true;
612 }
613 else if (m_CaptureState == CAPTURE_CALIBRATING)
614 {
615 if (getGuideState() == GUIDE_CALIBRATION_ERROR || getGuideState() == GUIDE_ABORTED)
616 {
617 // restart guiding after failure
618 appendLogText(i18n("Post meridian flip calibration error. Restarting..."));
619 emit guideAfterMeridianFlip();
620 return true;
621 }
622 else if (getGuideState() != GUIDE_GUIDING)
623 // waiting for guiding to start
624 return true;
625 else
626 // guiding is running
627 return false;
628 }
629 else
630 // in all other cases, do not touch
631 return false;
632}
633
634void CameraState::processGuidingFailed()
635{
636 if (m_FocusState > FOCUS_PROGRESS)
637 {
638 appendLogText(i18n("Autoguiding stopped. Waiting for autofocus to finish..."));
639 }
640 // If Autoguiding was started before and now stopped, let's abort (unless we're doing a meridian flip)
641 else if (isGuidingOn()
642 && getMeridianFlipState()->getMeridianFlipStage() == MeridianFlipState::MF_NONE &&
643 // JM 2022.08.03: Only abort if the current job is LIGHT. For calibration frames, we can ignore guide failures.
644 ((m_activeJob && m_activeJob->getStatus() == JOB_BUSY && m_activeJob->getFrameType() == FRAME_LIGHT) ||
645 m_CaptureState == CAPTURE_SUSPENDED || m_CaptureState == CAPTURE_PAUSED))
646 {
647 appendLogText(i18n("Autoguiding stopped. Aborting..."));
648 emit abortCapture();
649 }
650 else if (getMeridianFlipState()->getMeridianFlipStage() == MeridianFlipState::MF_GUIDING)
651 {
652 if (increaseAlignmentRetries() >= 3)
653 {
654 appendLogText(i18n("Post meridian flip calibration error. Aborting..."));
655 emit abortCapture();
656 }
657 }
658}
659
660void CameraState::updateAdaptiveFocusState(bool success)
661{
662 m_refocusState->setAdaptiveFocusDone(true);
663
664 // Always process the adaptive focus state change, incl if a MF has also just started
665 if (success)
666 qCDebug(KSTARS_EKOS_CAPTURE) << "Adaptive focus completed successfully";
667 else
668 qCDebug(KSTARS_EKOS_CAPTURE) << "Adaptive focus failed";
669
670 m_refocusState->setAutoFocusReady(true);
671 // forward to the active job
672 if (m_activeJob != nullptr)
673 m_activeJob->setAutoFocusReady(true);
674
675 setFocusState(FOCUS_COMPLETE);
676 emit newLog(i18n(success ? "Adaptive focus complete." : "Adaptive focus failed. Continuing..."));
677}
678
679void CameraState::updateFocusState(FocusState state)
680{
681 if (state != m_FocusState)
682 qCDebug(KSTARS_EKOS_CAPTURE) << "Focus State changed from" <<
683 Ekos::getFocusStatusString(m_FocusState) <<
684 "to" << Ekos::getFocusStatusString(state);
685 setFocusState(state);
686
687 // Do not process if meridian flip in progress
688 if (getMeridianFlipState()->checkMeridianFlipRunning())
689 return;
690
691 switch (state)
692 {
693 // Do not process when aborted
694 case FOCUS_ABORTED:
695 if (!(getMeridianFlipState()->getMeridianFlipStage() == MeridianFlipState::MF_INITIATED))
696 {
697 // Meridian flip will abort focusing. In this case, after the meridian flip has completed capture
698 // will restart the re-focus attempt. Therefore we only abort capture if meridian flip is not running.
699 emit newFocusStatus(state);
700 appendLogText(i18n("Autofocus failed. Aborting exposure..."));
701 emit abortCapture();
702 }
703 break;
704 case FOCUS_COMPLETE:
705 // enable option to have a refocus event occur if HFR goes over threshold
706 m_refocusState->setAutoFocusReady(true);
707 // forward to the active job
708 if (m_activeJob != nullptr)
709 m_activeJob->setAutoFocusReady(true);
710 // reset the timer if a full autofocus was run (rather than an HFR check)
711 if (m_refocusState->getFocusHFRInAutofocus())
712 m_refocusState->startRefocusTimer(true);
713
714 // update HFR Threshold for those algorithms that use Autofocus as reference
715 if (Options::hFRCheckAlgorithm() == HFR_CHECK_MEDIAN_MEASURE ||
716 (m_refocusState->getFocusHFRInAutofocus() && Options::hFRCheckAlgorithm() == HFR_CHECK_LAST_AUTOFOCUS))
717 {
718 m_refocusState->addHFRValue(getFocusFilterName());
719 updateHFRThreshold();
720 }
721 emit newFocusStatus(state);
722 break;
723 default:
724 break;
725 }
726
727 if (m_activeJob != nullptr)
728 {
729 // Account for in-sequence-focus action that may lead to guide suspension and resumption after it is completed successfully.
730 // In this case, since the guiding was resumed, we should honor the guiding settle duration.
731 // To satisfy this, the guide state must be guiding and focus suspend guiding should be active.
732 if (state == FOCUS_COMPLETE && Options::guidingSettle() > 0 && Options::focusSuspendGuiding()
733 && m_GuideState == GUIDE_GUIDING)
734 {
735 // N.B. Do NOT convert to i18np since guidingRate is DOUBLE value (e.g. 1.36) so we always use plural with that.
736 appendLogText(i18n("Focus complete. Resuming in %1 seconds...", Options::guidingSettle()));
737 QTimer::singleShot(Options::guidingSettle() * 1000, this, [this, state]()
738 {
739 if (m_activeJob != nullptr)
740 m_activeJob->setFocusStatus(state);
741 });
742 }
743 else
744 m_activeJob->setFocusStatus(state);
745 }
746}
747
748AutofocusReason CameraState::getAFReason(RefocusState::RefocusReason state, QString &reasonInfo)
749{
750 AutofocusReason afReason;
751 reasonInfo = "";
752 switch (state)
753 {
754 case RefocusState::REFOCUS_USER_REQUEST:
755 afReason = AutofocusReason::FOCUS_USER_REQUEST;
756 break;
757 case RefocusState::REFOCUS_TEMPERATURE:
758 afReason = AutofocusReason::FOCUS_TEMPERATURE;
759 reasonInfo = i18n("Limit: %1 °C", QString::number(Options::maxFocusTemperatureDelta(), 'f', 2));
760 break;
761 case RefocusState::REFOCUS_TIME_ELAPSED:
762 afReason = AutofocusReason::FOCUS_TIME;
763 reasonInfo = i18n("Limit: %1 mins", Options::refocusEveryN());
764 break;
765 case RefocusState::REFOCUS_POST_MF:
766 afReason = AutofocusReason::FOCUS_MERIDIAN_FLIP;
767 break;
768 default:
769 afReason = AutofocusReason::FOCUS_NONE;
770 }
771 return afReason;
772}
773
774bool CameraState::startFocusIfRequired()
775{
776 // Do not start focus or adaptive focus if:
777 // 1. There is no active job, or
778 // 2. Target frame is not LIGHT
779 // 3. Capture is preview only
780 if (m_activeJob == nullptr || m_activeJob->getFrameType() != FRAME_LIGHT
781 || m_activeJob->jobType() == SequenceJob::JOBTYPE_PREVIEW)
782 return false;
783
784 RefocusState::RefocusReason reason = m_refocusState->checkFocusRequired();
785
786 // no focusing necessary
787 if (reason == RefocusState::REFOCUS_NONE)
788 return false;
789
790 // clear the flag for refocusing after the meridian flip
791 m_refocusState->setRefocusAfterMeridianFlip(false);
792
793 // Post meridian flip we need to reset filter _before_ running in-sequence focusing
794 // as it could have changed for whatever reason (e.g. alignment used a different filter).
795 // Then when focus process begins with the _target_ filter in place, it should take all the necessary actions to make it
796 // work for the next set of captures. This is direct reset to the filter device, not via Filter Manager.
797 if (getMeridianFlipState()->getMeridianFlipStage() != MeridianFlipState::MF_NONE)
798 {
799 int targetFilterPosition = m_activeJob->getTargetFilter();
800 if (targetFilterPosition > 0 && targetFilterPosition != getCurrentFilterPosition())
801 emit newFilterPosition(targetFilterPosition);
802 }
803
804 emit abortFastExposure();
805 updateFocusState(FOCUS_PROGRESS);
806 QString reasonInfo;
807 AutofocusReason afReason;
808
809 switch (reason)
810 {
811 case RefocusState::REFOCUS_HFR:
812 m_refocusState->resetInSequenceFocusCounter();
813 emit checkFocus(Options::hFRDeviation());
814 qCDebug(KSTARS_EKOS_CAPTURE) << "In-sequence focusing started...";
815 break;
816 case RefocusState::REFOCUS_ADAPTIVE:
817 m_refocusState->setAdaptiveFocusDone(true);
818 emit adaptiveFocus();
819 qCDebug(KSTARS_EKOS_CAPTURE) << "Adaptive focus started...";
820 break;
821 case RefocusState::REFOCUS_USER_REQUEST:
822 case RefocusState::REFOCUS_TEMPERATURE:
823 case RefocusState::REFOCUS_TIME_ELAPSED:
824 case RefocusState::REFOCUS_POST_MF:
825 // If we are over 30 mins since last autofocus, we'll reset frame.
826 if (m_refocusState->getRefocusEveryNTimerElapsedSec() >= 1800)
827 emit resetFocusFrame();
828
829 // force refocus
830 afReason = getAFReason(reason, reasonInfo);
831 emit runAutoFocus(afReason, reasonInfo);
832 // restart in sequence counting
833 m_refocusState->resetInSequenceFocusCounter();
834 qCDebug(KSTARS_EKOS_CAPTURE) << "Refocusing started...";
835 break;
836 default:
837 // this should not happen, since this case is handled above
838 return false;
839 }
840
841 setCaptureState(CAPTURE_FOCUSING);
842 return true;
843}
844
845void CameraState::updateHFRThreshold()
846{
847 // For Ficed algo no need to update the HFR threshold
848 if (Options::hFRCheckAlgorithm() == HFR_CHECK_FIXED)
849 return;
850
851 QString finalFilter = getFocusFilterName();
852 QList<double> filterHFRList = m_refocusState->getHFRMap()[finalFilter];
853
854 // Update the limit only if HFR values have been measured for the current filter
855 if (filterHFRList.empty())
856 return;
857
858 double value = 0;
859 if (Options::hFRCheckAlgorithm() == HFR_CHECK_LAST_AUTOFOCUS)
860 value = filterHFRList.last();
861 else // algo = Median Measure
862 {
863 int count = filterHFRList.size();
864 if (count > 1)
865 value = (count % 2) ? filterHFRList[count / 2] : (filterHFRList[count / 2 - 1] + filterHFRList[count / 2]) / 2.0;
866 else if (count == 1)
867 value = filterHFRList[0];
868 }
869 value += value * (Options::hFRThresholdPercentage() / 100.0);
870 Options::setHFRDeviation(value);
871 emit newLimitFocusHFR(value); // Updates the limits UI with the new HFR threshold
872}
873
874QString CameraState::getFocusFilterName()
875{
876 QString finalFilter;
877 if (m_CurrentFilterPosition > 0)
878 // If we are using filters, then we retrieve which filter is currently active.
879 // We check if filter lock is used, and store that instead of the current filter.
880 // e.g. If current filter HA, but lock filter is L, then the HFR value is stored for L filter.
881 // If no lock filter exists, then we store as is (HA)
882 finalFilter = (m_CurrentFocusFilterName == "--" ? m_CurrentFilterName : m_CurrentFocusFilterName);
883 else
884 // No filters
885 finalFilter = "--";
886 return finalFilter;
887}
888
889bool CameraState::checkAlignmentAfterFlip()
890{
891 // if no meridian flip has completed, we do not touch guiding
892 if (getMeridianFlipState()->getMeridianFlipStage() < MeridianFlipState::MF_COMPLETED)
893 {
894 qCDebug(KSTARS_EKOS_CAPTURE) << "checkAlignmentAfterFlip too early, meridian flip stage =" <<
895 getMeridianFlipState()->getMeridianFlipStage();
896 return false;
897 }
898 // If we do not need to align then we're done
899 if (getMeridianFlipState()->resumeAlignmentAfterFlip() == false)
900 {
901 qCDebug(KSTARS_EKOS_CAPTURE) << "No alignment after flip required.";
902 return false;
903 }
904
905 // if we are waiting for a calibration, start it
906 if (m_CaptureState < CAPTURE_ALIGNING)
907 {
908 appendLogText(i18n("Performing post flip re-alignment..."));
909
910 resetAlignmentRetries();
911 setCaptureState(CAPTURE_ALIGNING);
912
913 getMeridianFlipState()->updateMeridianFlipStage(MeridianFlipState::MF_ALIGNING);
914 return true;
915 }
916 else
917 // in all other cases, do not touch
918 return false;
919}
920
921void CameraState::checkGuideDeviationTimeout()
922{
923 if (m_activeJob && m_activeJob->getStatus() == JOB_ABORTED
924 && isGuidingDeviationDetected())
925 {
926 appendLogText(i18n("Guide module timed out."));
927 setGuidingDeviationDetected(false);
928
929 // If capture was suspended, it should be aborted (failed) now.
930 if (m_CaptureState == CAPTURE_SUSPENDED)
931 {
932 setCaptureState(CAPTURE_ABORTED);
933 }
934 }
935}
936
937void CameraState::setGuideDeviation(double deviation_rms)
938{
939 // communicate the new guiding deviation
940 emit newGuiderDrift(deviation_rms);
941
942 const QString deviationText = QString("%1").arg(deviation_rms, 0, 'f', 3);
943
944 // if guiding deviations occur and no job is active, check if a meridian flip is ready to be executed
945 if (m_activeJob == nullptr && checkMeridianFlipReady())
946 return;
947
948 // if the job is in the startup phase and no flip is neither requested nor running, check if the deviation is below the initial guiding limit
949 if (m_CaptureState == CAPTURE_PROGRESS &&
950 getMeridianFlipState()->getMeridianFlipStage() != MeridianFlipState::MF_REQUESTED &&
951 getMeridianFlipState()->checkMeridianFlipRunning() == false)
952 {
953 // initial guiding deviation irrelevant or below limit
954 if (Options::enforceStartGuiderDrift() == false || deviation_rms < Options::startGuideDeviation())
955 {
956 setCaptureState(CAPTURE_CALIBRATING);
957 if (Options::enforceStartGuiderDrift())
958 appendLogText(i18n("Initial guiding deviation %1 below limit value of %2 arcsecs",
959 deviationText, Options::startGuideDeviation()));
960 setGuidingDeviationDetected(false);
961 setStartingCapture(false);
962 }
963 else
964 {
965 // warn only once
966 if (isGuidingDeviationDetected() == false)
967 appendLogText(i18n("Initial guiding deviation %1 exceeded limit value of %2 arcsecs",
968 deviationText, Options::startGuideDeviation()));
969
970 setGuidingDeviationDetected(true);
971
972 // Check if we need to start meridian flip. If yes, we need to start capturing
973 // to ensure that capturing is recovered after the flip
974 if (checkMeridianFlipReady())
975 emit startCapture();
976 }
977
978 // in any case, do not proceed
979 return;
980 }
981
982 // If guiding is started after a meridian flip we will start getting guide deviations again
983 // if the guide deviations are within our limits, we resume the sequence
984 if (getMeridianFlipState()->getMeridianFlipStage() == MeridianFlipState::MF_GUIDING)
985 {
986 // If the user didn't select any guiding deviation, the meridian flip is completed
987 if (Options::enforceGuideDeviation() == false || deviation_rms < Options::guideDeviation())
988 {
989 appendLogText(i18n("Post meridian flip calibration completed successfully."));
990 // N.B. Set meridian flip stage AFTER resumeSequence() always
991 getMeridianFlipState()->updateMeridianFlipStage(MeridianFlipState::MF_NONE);
992 return;
993 }
994 }
995
996 // Check for initial deviation in the middle of a sequence (above just checks at the start of a sequence).
997 if (m_activeJob && m_activeJob->getStatus() == JOB_BUSY && m_activeJob->getFrameType() == FRAME_LIGHT
998 && isStartingCapture() && Options::enforceStartGuiderDrift())
999 {
1000 setStartingCapture(false);
1001 if (deviation_rms > Options::startGuideDeviation())
1002 {
1003 appendLogText(i18n("Guiding deviation at capture startup %1 exceeded limit %2 arcsecs.",
1004 deviationText, Options::startGuideDeviation()));
1005 emit suspendCapture();
1006 setGuidingDeviationDetected(true);
1007
1008 // Check if we need to start meridian flip. If yes, we need to start capturing
1009 // to ensure that capturing is recovered after the flip
1010 if (checkMeridianFlipReady())
1011 emit startCapture();
1012 else
1013 getGuideDeviationTimer().start();
1014 return;
1015 }
1016 else
1017 appendLogText(i18n("Guiding deviation at capture startup %1 below limit value of %2 arcsecs",
1018 deviationText, Options::startGuideDeviation()));
1019 }
1020
1021 if (m_CaptureState != CAPTURE_SUSPENDED)
1022 {
1023
1024 // We don't enforce limit on previews or non-LIGHT frames
1025 if ((Options::enforceGuideDeviation() == false)
1026 ||
1027 (m_activeJob && (m_activeJob->jobType() == SequenceJob::JOBTYPE_PREVIEW ||
1028 m_activeJob->getExposeLeft() == 0.0 ||
1029 m_activeJob->getFrameType() != FRAME_LIGHT)))
1030 return;
1031
1032 // If we have an active busy job, let's abort it if guiding deviation is exceeded.
1033 // And we accounted for the spike
1034 if (m_activeJob && m_activeJob->getStatus() == JOB_BUSY && m_activeJob->getFrameType() == FRAME_LIGHT)
1035 {
1036 if (deviation_rms <= Options::guideDeviation())
1037 resetSpikesDetected();
1038 else
1039 {
1040 // Require several consecutive spikes to fail.
1041 if (increaseSpikesDetected() < Options::guideDeviationReps())
1042 return;
1043
1044 appendLogText(i18n("Guiding deviation %1 exceeded limit value of %2 arcsecs for %4 consecutive samples, "
1045 "suspending exposure and waiting for guider up to %3 seconds.",
1046 deviationText, Options::guideDeviation(),
1047 QString("%L1").arg(getGuideDeviationTimer().interval() / 1000.0, 0, 'f', 3),
1048 Options::guideDeviationReps()));
1049
1050 emit suspendCapture();
1051
1052 resetSpikesDetected();
1053 setGuidingDeviationDetected(true);
1054
1055 // Check if we need to start meridian flip. If yes, we need to start capturing
1056 // to ensure that capturing is recovered after the flip
1057 if (checkMeridianFlipReady())
1058 emit startCapture();
1059 else
1060 getGuideDeviationTimer().start();
1061 }
1062 return;
1063 }
1064 }
1065
1066 // Find the first aborted job
1067 SequenceJob *abortedJob = nullptr;
1068 for(auto &job : allJobs())
1069 {
1070 if (job->getStatus() == JOB_ABORTED)
1071 {
1072 abortedJob = job;
1073 break;
1074 }
1075 }
1076
1077 if (abortedJob != nullptr && isGuidingDeviationDetected())
1078 {
1079 if (deviation_rms <= Options::startGuideDeviation())
1080 {
1081 getGuideDeviationTimer().stop();
1082
1083 // Start with delay if start hasn't been triggered before
1084 if (! getCaptureDelayTimer().isActive())
1085 {
1086 // if capturing has been suspended, restart it
1087 if (m_CaptureState == CAPTURE_SUSPENDED)
1088 {
1089 const int seqDelay = abortedJob->getCoreProperty(SequenceJob::SJ_Delay).toInt();
1090 if (seqDelay == 0)
1091 appendLogText(i18n("Guiding deviation %1 is now lower than limit value of %2 arcsecs, "
1092 "resuming exposure.",
1093 deviationText, Options::startGuideDeviation()));
1094 else
1095 appendLogText(i18n("Guiding deviation %1 is now lower than limit value of %2 arcsecs, "
1096 "resuming exposure in %3 seconds.",
1097 deviationText, Options::startGuideDeviation(), seqDelay / 1000.0));
1098
1099 emit startCapture();
1100 }
1101 }
1102 return;
1103 }
1104 else
1105 {
1106 // stop the delayed capture start if necessary
1107 if (getCaptureDelayTimer().isActive())
1108 getCaptureDelayTimer().stop();
1109
1110 appendLogText(i18n("Guiding deviation %1 is still higher than limit value of %2 arcsecs.",
1111 deviationText, Options::startGuideDeviation()));
1112 }
1113 }
1114}
1115
1116void CameraState::addDownloadTime(double time)
1117{
1118 totalDownloadTime += time;
1119 downloadsCounter++;
1120}
1121
1122int CameraState::pendingJobCount()
1123{
1124 int completedJobs = 0;
1125
1126 foreach (SequenceJob * job, allJobs())
1127 {
1128 if (job->getStatus() == JOB_DONE)
1129 completedJobs++;
1130 }
1131
1132 return (allJobs().count() - completedJobs);
1133
1134}
1135
1136QString CameraState::jobState(int id)
1137{
1138 if (id < allJobs().count())
1139 {
1140 SequenceJob * job = allJobs().at(id);
1141 return job->getStatusString();
1142 }
1143
1144 return QString();
1145
1146}
1147
1148QString CameraState::jobFilterName(int id)
1149{
1150 if (id < allJobs().count())
1151 {
1152 SequenceJob * job = allJobs().at(id);
1153 return job->getCoreProperty(SequenceJob::SJ_Filter).toString();
1154 }
1155
1156 return QString();
1157
1158}
1159
1160CCDFrameType CameraState::jobFrameType(int id)
1161{
1162 if (id < allJobs().count())
1163 {
1164 SequenceJob * job = allJobs().at(id);
1165 return job->getFrameType();
1166 }
1167
1168 return FRAME_NONE;
1169}
1170
1171int CameraState::jobImageProgress(int id)
1172{
1173 if (id < allJobs().count())
1174 {
1175 SequenceJob * job = allJobs().at(id);
1176 return job->getCompleted();
1177 }
1178
1179 return -1;
1180}
1181
1182int CameraState::jobImageCount(int id)
1183{
1184 if (id < allJobs().count())
1185 {
1186 SequenceJob * job = allJobs().at(id);
1187 return job->getCoreProperty(SequenceJob::SJ_Count).toInt();
1188 }
1189
1190 return -1;
1191}
1192
1193double CameraState::jobExposureProgress(int id)
1194{
1195 if (id < allJobs().count())
1196 {
1197 SequenceJob * job = allJobs().at(id);
1198 return job->getExposeLeft();
1199 }
1200
1201 return -1;
1202}
1203
1204double CameraState::jobExposureDuration(int id)
1205{
1206 if (id < allJobs().count())
1207 {
1208 SequenceJob * job = allJobs().at(id);
1209 return job->getCoreProperty(SequenceJob::SJ_Exposure).toDouble();
1210 }
1211
1212 return -1;
1213}
1214
1215double CameraState::progressPercentage()
1216{
1217 int totalImageCount = 0;
1218 int totalImageCompleted = 0;
1219
1220 foreach (SequenceJob * job, allJobs())
1221 {
1222 totalImageCount += job->getCoreProperty(SequenceJob::SJ_Count).toInt();
1223 totalImageCompleted += job->getCompleted();
1224 }
1225
1226 if (totalImageCount != 0)
1227 return ((static_cast<double>(totalImageCompleted) / totalImageCount) * 100.0);
1228 else
1229 return -1;
1230}
1231
1232bool CameraState::isActiveJobPreview()
1233{
1234 return m_activeJob && m_activeJob->jobType() == SequenceJob::JOBTYPE_PREVIEW;
1235}
1236
1237int CameraState::activeJobRemainingTime()
1238{
1239 if (m_activeJob == nullptr)
1240 return -1;
1241
1242 return m_activeJob->getJobRemainingTime(averageDownloadTime());
1243}
1244
1245int CameraState::overallRemainingTime()
1246{
1247 int remaining = 0;
1248 double estimatedDownloadTime = averageDownloadTime();
1249
1250 foreach (SequenceJob * job, allJobs())
1251 remaining += job->getJobRemainingTime(estimatedDownloadTime);
1252
1253 return remaining;
1254}
1255
1256QString CameraState::sequenceQueueStatus()
1257{
1258 if (allJobs().count() == 0)
1259 return "Invalid";
1260
1261 if (isBusy())
1262 return "Running";
1263
1264 int idle = 0, error = 0, complete = 0, aborted = 0, running = 0;
1265
1266 foreach (SequenceJob * job, allJobs())
1267 {
1268 switch (job->getStatus())
1269 {
1270 case JOB_ABORTED:
1271 aborted++;
1272 break;
1273 case JOB_BUSY:
1274 running++;
1275 break;
1276 case JOB_DONE:
1277 complete++;
1278 break;
1279 case JOB_ERROR:
1280 error++;
1281 break;
1282 case JOB_IDLE:
1283 idle++;
1284 break;
1285 }
1286 }
1287
1288 if (error > 0)
1289 return "Error";
1290
1291 if (aborted > 0)
1292 {
1293 if (m_CaptureState == CAPTURE_SUSPENDED)
1294 return "Suspended";
1295 else
1296 return "Aborted";
1297 }
1298
1299 if (running > 0)
1300 return "Running";
1301
1302 if (idle == allJobs().count())
1303 return "Idle";
1304
1305 if (complete == allJobs().count())
1306 return "Complete";
1307
1308 return "Invalid";
1309}
1310
1311QJsonObject CameraState::calibrationSettings()
1312{
1313 QJsonObject settings =
1314 {
1315 {"preAction", static_cast<int>(calibrationPreAction())},
1316 {"duration", flatFieldDuration()},
1317 {"az", wallCoord().az().Degrees()},
1318 {"al", wallCoord().alt().Degrees()},
1319 {"adu", targetADU()},
1320 {"tolerance", targetADUTolerance()},
1321 {"skyflat", skyFlat()},
1322 };
1323
1324 return settings;
1325}
1326
1327void CameraState::setCalibrationSettings(const QJsonObject &settings)
1328{
1329 const int preAction = settings["preAction"].toInt(calibrationPreAction());
1330 const int duration = settings["duration"].toInt(flatFieldDuration());
1331 const double az = settings["az"].toDouble(wallCoord().az().Degrees());
1332 const double al = settings["al"].toDouble(wallCoord().alt().Degrees());
1333 const int adu = settings["adu"].toInt(static_cast<int>(std::round(targetADU())));
1334 const int tolerance = settings["tolerance"].toInt(static_cast<int>(std::round(targetADUTolerance())));
1335 const int skyflat = settings["skyflat"].toBool();
1336
1337 setCalibrationPreAction(static_cast<CalibrationPreActions>(preAction));
1338 setFlatFieldDuration(static_cast<FlatFieldDuration>(duration));
1339 wallCoord().setAz(az);
1340 wallCoord().setAlt(al);
1341 setTargetADU(adu);
1342 setTargetADUTolerance(tolerance);
1343 setSkyFlat(skyflat);
1344}
1345
1346bool CameraState::setDarkFlatExposure(SequenceJob *job)
1347{
1348 const auto darkFlatFilter = job->getCoreProperty(SequenceJob::SJ_Filter).toString();
1349 const auto darkFlatBinning = job->getCoreProperty(SequenceJob::SJ_Binning).toPoint();
1350 const auto darkFlatADU = job->getCoreProperty(SequenceJob::SJ_TargetADU).toInt();
1351
1352 for (auto &oneJob : allJobs())
1353 {
1354 if (oneJob->getFrameType() != FRAME_FLAT)
1355 continue;
1356
1357 const auto filter = oneJob->getCoreProperty(SequenceJob::SJ_Filter).toString();
1358
1359 // Match filter, if one exists.
1360 if (!darkFlatFilter.isEmpty() && darkFlatFilter != filter)
1361 continue;
1362
1363 // Match binning
1364 const auto binning = oneJob->getCoreProperty(SequenceJob::SJ_Binning).toPoint();
1365 if (darkFlatBinning != binning)
1366 continue;
1367
1368 // Match ADU, if used.
1369 const auto adu = oneJob->getCoreProperty(SequenceJob::SJ_TargetADU).toInt();
1370 if (job->getFlatFieldDuration() == DURATION_ADU)
1371 {
1372 if (darkFlatADU != adu)
1373 continue;
1374 }
1375
1376 // Now get the exposure
1377 job->setCoreProperty(SequenceJob::SJ_Exposure, oneJob->getCoreProperty(SequenceJob::SJ_Exposure).toDouble());
1378
1379 return true;
1380 }
1381 return false;
1382}
1383
1384void CameraState::checkSeqBoundary()
1385{
1386 // No updates during meridian flip
1387 if (getMeridianFlipState()->getMeridianFlipStage() >= MeridianFlipState::MF_ALIGNING)
1388 return;
1389
1390 setNextSequenceID(placeholderPath().checkSeqBoundary(*getActiveJob()));
1391}
1392
1393bool CameraState::isModelinDSLRInfo(const QString &model)
1394{
1395 auto pos = std::find_if(m_DSLRInfos.begin(), m_DSLRInfos.end(), [model](QMap<QString, QVariant> &oneDSLRInfo)
1396 {
1397 return (oneDSLRInfo["Model"] == model);
1398 });
1399
1400 return (pos != m_DSLRInfos.end());
1401}
1402
1403void CameraState::setCapturedFramesCount(const QString &signature, uint16_t count)
1404{
1405 m_capturedFramesMap[signature] = count;
1406 qCDebug(KSTARS_EKOS_CAPTURE) <<
1407 QString("Client module indicates that storage for '%1' has already %2 captures processed.").arg(signature).arg(count);
1408 // Scheduler's captured frame map overrides the progress option of the Capture module
1409 setIgnoreJobProgress(false);
1410}
1411
1412void CameraState::changeSequenceValue(int index, QString key, QString value)
1413{
1414 QJsonArray seqArray = getSequence();
1415 QJsonObject oneSequence = seqArray[index].toObject();
1416 oneSequence[key] = value;
1417 seqArray.replace(index, oneSequence);
1418 setSequence(seqArray);
1419 emit sequenceChanged(seqArray);
1420}
1421
1422void CameraState::addCapturedFrame(const QString &signature)
1423{
1424 CapturedFramesMap::iterator frame_item = m_capturedFramesMap.find(signature);
1425 if (m_capturedFramesMap.end() != frame_item)
1426 frame_item.value()++;
1427 else m_capturedFramesMap[signature] = 1;
1428}
1429
1430void CameraState::removeCapturedFrameCount(const QString &signature, uint16_t count)
1431{
1432 CapturedFramesMap::iterator frame_item = m_capturedFramesMap.find(signature);
1433 if (m_capturedFramesMap.end() != frame_item)
1434 {
1435 if (frame_item.value() <= count)
1436 // clear signature entry if none will be left
1437 m_capturedFramesMap.remove(signature);
1438 else
1439 // remove the frame count
1440 frame_item.value() = frame_item.value() - count;
1441 }
1442}
1443
1444void CameraState::appendLogText(const QString &message)
1445{
1446 qCInfo(KSTARS_EKOS_CAPTURE()) << message;
1447 emit newLog(message);
1448}
1449
1450bool CameraState::isGuidingOn()
1451{
1452 // In case we are doing non guiding dither, then we are not performing autoguiding.
1453 if (Options::ditherNoGuiding())
1454 return false;
1455
1456 return (m_GuideState == GUIDE_GUIDING ||
1457 m_GuideState == GUIDE_CALIBRATING ||
1458 m_GuideState == GUIDE_CALIBRATION_SUCCESS ||
1459 m_GuideState == GUIDE_DARK ||
1460 m_GuideState == GUIDE_SUBFRAME ||
1461 m_GuideState == GUIDE_STAR_SELECT ||
1462 m_GuideState == GUIDE_REACQUIRE ||
1463 m_GuideState == GUIDE_DITHERING ||
1464 m_GuideState == GUIDE_DITHERING_SUCCESS ||
1465 m_GuideState == GUIDE_DITHERING_ERROR ||
1466 m_GuideState == GUIDE_DITHERING_SETTLE ||
1467 m_GuideState == GUIDE_SUSPENDED
1468 );
1469}
1470
1471bool CameraState::isActivelyGuiding()
1472{
1473 return isGuidingOn() && (m_GuideState == GUIDE_GUIDING);
1474}
1475
1476void CameraState::setAlignState(AlignState value)
1477{
1478 if (value != m_AlignState)
1479 qCDebug(KSTARS_EKOS_CAPTURE) << "Align State changed from" << Ekos::getAlignStatusString(
1480 m_AlignState) << "to" << Ekos::getAlignStatusString(value);
1481 m_AlignState = value;
1482
1483 getMeridianFlipState()->setResumeAlignmentAfterFlip(true);
1484
1485 switch (value)
1486 {
1487 case ALIGN_COMPLETE:
1488 if (getMeridianFlipState()->getMeridianFlipStage() == MeridianFlipState::MF_ALIGNING)
1489 {
1490 appendLogText(i18n("Post flip re-alignment completed successfully."));
1491 resetAlignmentRetries();
1492 // Trigger guiding if necessary.
1493 if (checkGuidingAfterFlip() == false)
1494 {
1495 // If no guiding is required, the meridian flip is complete
1496 updateMeridianFlipStage(MeridianFlipState::MF_NONE);
1497 setCaptureState(CAPTURE_WAITING);
1498 }
1499 }
1500 break;
1501
1502 case ALIGN_ABORTED:
1503 case ALIGN_FAILED:
1504 // TODO run it 3 times before giving up
1505 if (getMeridianFlipState()->getMeridianFlipStage() == MeridianFlipState::MF_ALIGNING)
1506 {
1507 if (increaseAlignmentRetries() >= 3)
1508 {
1509 appendLogText(i18n("Post-flip alignment failed."));
1510 emit abortCapture();
1511 }
1512 else
1513 {
1514 appendLogText(i18n("Post-flip alignment failed. Retrying..."));
1515 // set back the stage
1516 updateMeridianFlipStage(MeridianFlipState::MF_COMPLETED);
1517 }
1518 }
1519 break;
1520
1521 default:
1522 break;
1523 }
1524}
1525
1526void CameraState::setPrepareComplete(bool success)
1527{
1528
1529 if (success)
1530 {
1531 setCaptureState(CAPTURE_PROGRESS);
1532 emit executeActiveJob();
1533 }
1534 else
1535 {
1536 qWarning(KSTARS_EKOS_CAPTURE) << "Capture preparation failed, aborting.";
1537 setCaptureState(CAPTURE_ABORTED);
1538 emit abortCapture();
1539 }
1540
1541}
1542
1543} // namespace
Sequence Job is a container for the details required to capture a series of images.
QString i18n(const char *text, const TYPE &arg...)
Ekos is an advanced Astrophotography tool for Linux.
Definition align.cpp:83
@ ALIGN_FAILED
Alignment failed.
Definition ekos.h:148
@ ALIGN_ABORTED
Alignment aborted by user or agent.
Definition ekos.h:149
@ ALIGN_COMPLETE
Alignment successfully completed.
Definition ekos.h:147
@ CAPTURE_DITHERING
Definition ekos.h:102
@ CAPTURE_PAUSE_PLANNED
Definition ekos.h:96
@ CAPTURE_PAUSED
Definition ekos.h:97
@ CAPTURE_IMAGE_RECEIVED
Definition ekos.h:101
@ CAPTURE_SUSPENDED
Definition ekos.h:98
@ CAPTURE_ABORTED
Definition ekos.h:99
@ CAPTURE_IDLE
Definition ekos.h:93
void error(QWidget *parent, const QString &text, const QString &title, const KGuiItem &buttonOk, Options options=Notify)
QString name(StandardAction id)
QCA_EXPORT void init()
bool exists() const const
bool mkpath(const QString &dirPath) const const
QString path() const const
bool exists(const QString &fileName)
QDir dir() const const
void replace(qsizetype i, const QJsonValue &value)
bool empty() const const
T & last()
qsizetype size() const const
QMetaObject::Connection connect(const QObject *sender, PointerToMemberFunction signal, Functor functor)
QString arg(Args &&... args) const const
QString number(double n, char format, int precision)
UniqueConnection
QFuture< void > filter(QThreadPool *pool, Sequence &sequence, KeepFunctor &&filterFunction)
QFuture< ArgsType< Signal > > connect(Sender *sender, Signal signal)
void timeout()
QString toLocalFile() const const
This file is part of the KDE documentation.
Documentation copyright © 1996-2025 The KDE developers.
Generated on Fri Jan 31 2025 11:53:46 by doxygen 1.13.2 written by Dimitri van Heesch, © 1997-2006

KDE's Doxygen guidelines are available online.