Kstars

captureprocess.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 "captureprocess.h"
7#include "capturedeviceadaptor.h"
8#include "refocusstate.h"
9#include "sequencejob.h"
10#include "sequencequeue.h"
11#include "ekos/manager.h"
12#include "ekos/auxiliary/darklibrary.h"
13#include "ekos/auxiliary/darkprocessor.h"
14#include "ekos/auxiliary/opticaltrainmanager.h"
15#include "ekos/auxiliary/profilesettings.h"
16#include "ekos/guide/guide.h"
17#include "indi/indilistener.h"
18#include "indi/indirotator.h"
19#include "indi/blobmanager.h"
20#include "indi/indilightbox.h"
21#include "ksmessagebox.h"
22#include "kstars.h"
23
24#ifdef HAVE_CFITSIO
25#include "fitsviewer/fitsdata.h"
26#include "fitsviewer/fitstab.h"
27#endif
28#include "fitsviewer/fitsviewer.h"
29
30#include "ksnotification.h"
31#include <ekos_capture_debug.h>
32
33#ifdef HAVE_STELLARSOLVER
34#include "ekos/auxiliary/stellarsolverprofileeditor.h"
35#endif
36
37namespace Ekos
38{
39CaptureProcess::CaptureProcess(QSharedPointer<CaptureModuleState> newModuleState,
41{
42 setObjectName("CaptureProcess");
43 m_State = newModuleState;
44 m_DeviceAdaptor = newDeviceAdaptor;
45
46 // connect devices to processes
47 connect(devices().data(), &CaptureDeviceAdaptor::newCamera, this, &CaptureProcess::selectCamera);
48
49 //This Timer will update the Exposure time in the capture module to display the estimated download time left
50 //It will also update the Exposure time left in the Summary Screen.
51 //It fires every 100 ms while images are downloading.
52 state()->downloadProgressTimer().setInterval(100);
53 connect(&state()->downloadProgressTimer(), &QTimer::timeout, this, &CaptureProcess::setDownloadProgress);
54
55 // configure dark processor
56 m_DarkProcessor = new DarkProcessor(this);
57 connect(m_DarkProcessor, &DarkProcessor::newLog, this, &CaptureProcess::newLog);
58 connect(m_DarkProcessor, &DarkProcessor::darkFrameCompleted, this, &CaptureProcess::darkFrameCompleted);
59
60 // Pre/post capture/job scripts
61 connect(&m_CaptureScript,
62 static_cast<void (QProcess::*)(int exitCode, QProcess::ExitStatus status)>(&QProcess::finished),
63 this, &CaptureProcess::scriptFinished);
64 connect(&m_CaptureScript, &QProcess::errorOccurred, this, [this](QProcess::ProcessError error)
65 {
66 Q_UNUSED(error)
67 emit newLog(m_CaptureScript.errorString());
68 scriptFinished(-1, QProcess::NormalExit);
69 });
70 connect(&m_CaptureScript, &QProcess::readyReadStandardError, this,
71 [this]()
72 {
73 emit newLog(m_CaptureScript.readAllStandardError());
74 });
75 connect(&m_CaptureScript, &QProcess::readyReadStandardOutput, this,
76 [this]()
77 {
78 emit newLog(m_CaptureScript.readAllStandardOutput());
79 });
80}
81
82bool CaptureProcess::setMount(ISD::Mount *device)
83{
84 if (devices()->mount() && devices()->mount() == device)
85 {
86 updateTelescopeInfo();
87 return false;
88 }
89
90 if (devices()->mount())
91 devices()->mount()->disconnect(state().data());
92
93 devices()->setMount(device);
94
95 if (!devices()->mount())
96 return false;
97
98 devices()->mount()->disconnect(this);
99 connect(devices()->mount(), &ISD::Mount::newTargetName, this, &CaptureProcess::captureTarget);
100
101 updateTelescopeInfo();
102 return true;
103}
104
105bool CaptureProcess::setRotator(ISD::Rotator *device)
106{
107 // do nothing if *real* rotator is already connected
108 if ((devices()->rotator() == device) && (device != nullptr))
109 return false;
110
111 // real & manual rotator initializing depends on present mount process
112 if (devices()->mount())
113 {
114 if (devices()->rotator())
115 devices()->rotator()->disconnect(this);
116
117 // clear initialisation.
118 state()->isInitialized[CaptureModuleState::ACTION_ROTATOR] = false;
119
120 if (device)
121 {
122 Manager::Instance()->createRotatorController(device);
123 connect(devices().data(), &CaptureDeviceAdaptor::rotatorReverseToggled, this, &CaptureProcess::rotatorReverseToggled,
125 }
126 devices()->setRotator(device);
127 return true;
128 }
129 return false;
130}
131
132bool CaptureProcess::setDustCap(ISD::DustCap *device)
133{
134 if (devices()->dustCap() && devices()->dustCap() == device)
135 return false;
136
137 devices()->setDustCap(device);
138 state()->setDustCapState(CaptureModuleState::CAP_UNKNOWN);
139
140 updateFilterInfo();
141 return true;
142
143}
144
145bool CaptureProcess::setLightBox(ISD::LightBox *device)
146{
147 if (devices()->lightBox() == device)
148 return false;
149
150 devices()->setLightBox(device);
151 state()->setLightBoxLightState(CaptureModuleState::CAP_LIGHT_UNKNOWN);
152
153 return true;
154}
155
156bool CaptureProcess::setDome(ISD::Dome *device)
157{
158 if (devices()->dome() == device)
159 return false;
160
161 devices()->setDome(device);
162
163 return true;
164}
165
166bool CaptureProcess::setCamera(ISD::Camera *device)
167{
168 if (devices()->getActiveCamera() == device)
169 return false;
170
171 devices()->setActiveCamera(device);
172
173 // If we capturing, then we need to process capture timeout immediately since this is a crash recovery
174 if (state()->getCaptureTimeout().isActive() && state()->getCaptureState() == CAPTURE_CAPTURING)
175 QTimer::singleShot(100, this, &CaptureProcess::processCaptureTimeout);
176
177 return true;
178
179}
180
181void CaptureProcess::toggleVideo(bool enabled)
182{
183 if (devices()->getActiveCamera() == nullptr)
184 return;
185
186 if (devices()->getActiveCamera()->isBLOBEnabled() == false)
187 {
188 if (Options::guiderType() != Guide::GUIDE_INTERNAL)
189 devices()->getActiveCamera()->setBLOBEnabled(true);
190 else
191 {
192 connect(KSMessageBox::Instance(), &KSMessageBox::accepted, this, [this, enabled]()
193 {
194 KSMessageBox::Instance()->disconnect(this);
195 devices()->getActiveCamera()->setBLOBEnabled(true);
196 devices()->getActiveCamera()->setVideoStreamEnabled(enabled);
197 });
198
199 KSMessageBox::Instance()->questionYesNo(i18n("Image transfer is disabled for this camera. Would you like to enable it?"),
200 i18n("Image Transfer"), 15);
201
202 return;
203 }
204 }
205
206 devices()->getActiveCamera()->setVideoStreamEnabled(enabled);
207
208}
209
210void CaptureProcess::toggleSequence()
211{
212 const CaptureState capturestate = state()->getCaptureState();
214 {
215 // change the state back to capturing only if planned pause is cleared
217 state()->setCaptureState(CAPTURE_CAPTURING);
218
219 emit newLog(i18n("Sequence resumed."));
220
221 // Call from where ever we have left of when we paused
222 switch (state()->getContinueAction())
223 {
224 case CaptureModuleState::CONTINUE_ACTION_CAPTURE_COMPLETE:
225 resumeSequence();
226 break;
227 case CaptureModuleState::CONTINUE_ACTION_NEXT_EXPOSURE:
228 startNextExposure();
229 break;
230 default:
231 break;
232 }
233 }
235 {
236 startNextPendingJob();
237 }
238 else
239 {
240 emit stopCapture(CAPTURE_ABORTED);
241 }
242}
243
244void CaptureProcess::startNextPendingJob()
245{
246 if (state()->allJobs().count() > 0)
247 {
248 SequenceJob *nextJob = findNextPendingJob();
249 if (nextJob != nullptr)
250 {
251 startJob(nextJob);
252 emit jobStarting();
253 }
254 else // do nothing if no job is pending
255 emit newLog(i18n("No pending jobs found. Please add a job to the sequence queue."));
256 }
257 else
258 {
259 // Add a new job from the current capture settings.
260 // If this succeeds, Capture will call this function again.
261 emit createJob();
262 }
263}
264
265void CaptureProcess::jobCreated(SequenceJob *newJob)
266{
267 if (newJob == nullptr)
268 {
269 emit newLog(i18n("No new job created."));
270 return;
271 }
272 // a job has been created successfully
273 switch (newJob->jobType())
274 {
275 case SequenceJob::JOBTYPE_BATCH:
276 startNextPendingJob();
277 break;
278 case SequenceJob::JOBTYPE_PREVIEW:
279 state()->setActiveJob(newJob);
280 capturePreview();
281 break;
282 default:
283 // do nothing
284 break;
285 }
286}
287
288void CaptureProcess::capturePreview(bool loop)
289{
290 if (state()->getFocusState() >= FOCUS_PROGRESS)
291 {
292 emit newLog(i18n("Cannot capture while focus module is busy."));
293 }
294 else if (activeJob() == nullptr)
295 {
296 if (loop && !state()->isLooping())
297 {
298 state()->setLooping(true);
299 emit newLog(i18n("Starting framing..."));
300 }
301 // create a preview job
302 emit createJob(SequenceJob::JOBTYPE_PREVIEW);
303 }
304 else
305 {
306 // job created, start capture preparation
307 prepareJob(activeJob());
308 }
309}
310
311void CaptureProcess::stopCapturing(CaptureState targetState)
312{
313 clearFlatCache();
314
315 state()->resetAlignmentRetries();
316 //seqTotalCount = 0;
317 //seqCurrentCount = 0;
318
319 state()->getCaptureTimeout().stop();
320 state()->getCaptureDelayTimer().stop();
321 if (activeJob() != nullptr)
322 {
323 if (activeJob()->getStatus() == JOB_BUSY)
324 {
326 switch (targetState)
327 {
329 stopText = i18n("CCD capture suspended");
330 resetJobStatus(JOB_BUSY);
331 break;
332
333 case CAPTURE_COMPLETE:
334 stopText = i18n("CCD capture complete");
335 resetJobStatus(JOB_DONE);
336 break;
337
338 case CAPTURE_ABORTED:
339 stopText = state()->isLooping() ? i18n("Framing stopped") : i18n("CCD capture stopped");
340 resetJobStatus(JOB_ABORTED);
341 break;
342
343 default:
344 stopText = i18n("CCD capture stopped");
345 resetJobStatus(JOB_IDLE);
346 break;
347 }
348 emit captureAborted(activeJob()->getCoreProperty(SequenceJob::SJ_Exposure).toDouble());
349 KSNotification::event(QLatin1String("CaptureFailed"), stopText, KSNotification::Capture, KSNotification::Alert);
350 emit newLog(stopText);
351 activeJob()->abort();
352 if (activeJob()->jobType() != SequenceJob::JOBTYPE_PREVIEW)
353 {
354 int index = state()->allJobs().indexOf(activeJob());
355 state()->changeSequenceValue(index, "Status", "Aborted");
356 emit updateJobTable(activeJob());
357 }
358 }
359
360 // In case of batch job
361 if (activeJob()->jobType() != SequenceJob::JOBTYPE_PREVIEW)
362 {
363 }
364 // or preview job in calibration stage
365 else if (activeJob()->getCalibrationStage() == SequenceJobState::CAL_CALIBRATION)
366 {
367 }
368 // or regular preview job
369 else
370 {
371 state()->allJobs().removeOne(activeJob());
372 // Delete preview job
373 activeJob()->deleteLater();
374 // Clear active job
375 state()->setActiveJob(nullptr);
376 }
377 }
378
379 // stop focusing if capture is aborted
380 if (state()->getCaptureState() == CAPTURE_FOCUSING && targetState == CAPTURE_ABORTED)
381 emit abortFocus();
382
383 state()->setCaptureState(targetState);
384
385 state()->setLooping(false);
386 state()->setBusy(false);
387
388 state()->getCaptureDelayTimer().stop();
389
390 state()->setActiveJob(nullptr);
391
392 // Turn off any calibration light, IF they were turned on by Capture module
393 if (devices()->lightBox() && state()->lightBoxLightEnabled())
394 {
395 state()->setLightBoxLightEnabled(false);
396 devices()->lightBox()->setLightEnabled(false);
397 }
398
399 // disconnect camera device
400 setCamera(false);
401
402 // In case of exposure looping, let's abort
403 if (devices()->getActiveCamera() && devices()->getActiveChip()
404 && devices()->getActiveCamera()->isFastExposureEnabled())
405 devices()->getActiveChip()->abortExposure();
406
407 // communicate successful stop
408 emit captureStopped();
409}
410
411void CaptureProcess::pauseCapturing()
412{
413 if (state()->getCaptureState() != CAPTURE_CAPTURING)
414 {
415 // Ensure that the pause function is only called during frame capturing
416 // Handling it this way is by far easier than trying to enable/disable the pause button
417 // Fixme: make pausing possible at all stages. This makes it necessary to separate the pausing states from CaptureState.
418 emit newLog(i18n("Pausing only possible while frame capture is running."));
419 qCInfo(KSTARS_EKOS_CAPTURE) << "Pause button pressed while not capturing.";
420 return;
421 }
422 // we do not decide at this stage how to resume, since pause is only planned here
423 state()->setContinueAction(CaptureModuleState::CONTINUE_ACTION_NONE);
424 state()->setCaptureState(CAPTURE_PAUSE_PLANNED);
425 emit newLog(i18n("Sequence shall be paused after current exposure is complete."));
426}
427
428void CaptureProcess::startJob(SequenceJob *job)
429{
430 state()->initCapturePreparation();
431 prepareJob(job);
432}
433
434void CaptureProcess::prepareJob(SequenceJob * job)
435{
436 state()->setActiveJob(job);
437
438 // If job is Preview and NO view is available, ask to enable it.
439 // if job is batch job, then NO VIEW IS REQUIRED at all. It's optional.
440 if (job->jobType() == SequenceJob::JOBTYPE_PREVIEW && Options::useFITSViewer() == false
441 && Options::useSummaryPreview() == false)
442 {
443 // ask if FITS viewer usage should be enabled
444 connect(KSMessageBox::Instance(), &KSMessageBox::accepted, this, [ = ]()
445 {
446 KSMessageBox::Instance()->disconnect(this);
447 Options::setUseFITSViewer(true);
448 // restart
449 prepareJob(job);
450 });
451 connect(KSMessageBox::Instance(), &KSMessageBox::rejected, this, [&]()
452 {
453 KSMessageBox::Instance()->disconnect(this);
454 activeJob()->abort();
455 });
456 KSMessageBox::Instance()->questionYesNo(i18n("No view available for previews. Enable FITS viewer?"),
457 i18n("Display preview"), 15);
458 // do nothing because currently none of the previews is active.
459 return;
460 }
461
462 if (state()->isLooping() == false)
463 qCDebug(KSTARS_EKOS_CAPTURE) << "Preparing capture job" << job->getSignature() << "for execution.";
464
465 if (activeJob()->jobType() != SequenceJob::JOBTYPE_PREVIEW)
466 {
467 // set the progress info
468
469 if (activeCamera()->getUploadMode() != ISD::Camera::UPLOAD_LOCAL)
470 state()->setNextSequenceID(1);
471
472 // We check if the job is already fully or partially complete by checking how many files of its type exist on the file system
473 // The signature is the unique identification path in the system for a particular job. Format is "<storage path>/<target>/<frame type>/<filter name>".
474 // If the Scheduler is requesting the Capture tab to process a sequence job, a target name will be inserted after the sequence file storage field (e.g. /path/to/storage/target/Light/...)
475 // If the end-user is requesting the Capture tab to process a sequence job, the sequence file storage will be used as is (e.g. /path/to/storage/Light/...)
476 QString signature = activeJob()->getSignature();
477
478 // Now check on the file system ALL the files that exist with the above signature
479 // If 29 files exist for example, then nextSequenceID would be the NEXT file number (30)
480 // Therefore, we know how to number the next file.
481 // However, we do not deduce the number of captures to process from this function.
482 state()->checkSeqBoundary();
483
484 // Captured Frames Map contains a list of signatures:count of _already_ captured files in the file system.
485 // This map is set by the Scheduler in order to complete efficiently the required captures.
486 // When the end-user requests a sequence to be processed, that map is empty.
487 //
488 // Example with a 5xL-5xR-5xG-5xB sequence
489 //
490 // When the end-user loads and runs this sequence, each filter gets to capture 5 frames, then the procedure stops.
491 // When the Scheduler executes a job with this sequence, the procedure depends on what is in the storage.
492 //
493 // Let's consider the Scheduler has 3 instances of this job to run.
494 //
495 // When the first job completes the sequence, there are 20 images in the file system (5 for each filter).
496 // When the second job starts, Scheduler finds those 20 images but requires 20 more images, thus sets the frames map counters to 0 for all LRGB frames.
497 // When the third job starts, Scheduler now has 40 images, but still requires 20 more, thus again sets the frames map counters to 0 for all LRGB frames.
498 //
499 // Now let's consider something went wrong, and the third job was aborted before getting to 60 images, say we have full LRG, but only 1xB.
500 // When Scheduler attempts to run the aborted job again, it will count captures in storage, subtract previous job requirements, and set the frames map counters to 0 for LRG, and 4 for B.
501 // When the sequence runs, the procedure will bypass LRG and proceed to capture 4xB.
502 int count = state()->capturedFramesCount(signature);
503 if (count > 0)
504 {
505
506 // Count how many captures this job has to process, given that previous jobs may have done some work already
507 for (auto &a_job : state()->allJobs())
508 if (a_job == activeJob())
509 break;
510 else if (a_job->getSignature() == activeJob()->getSignature())
511 count -= a_job->getCompleted();
512
513 // This is the current completion count of the current job
514 updatedCaptureCompleted(count);
515 }
516 // JM 2018-09-24: Only set completed jobs to 0 IF the scheduler set captured frames map to begin with
517 // If the map is empty, then no scheduler is used and it should proceed as normal.
518 else if (state()->hasCapturedFramesMap())
519 {
520 // No preliminary information, we reset the job count and run the job unconditionally to clarify the behavior
521 updatedCaptureCompleted(0);
522 }
523 // JM 2018-09-24: In case ignoreJobProgress is enabled
524 // We check if this particular job progress ignore flag is set. If not,
525 // then we set it and reset completed to zero. Next time it is evaluated here again
526 // It will maintain its count regardless
527 else if (state()->ignoreJobProgress()
528 && activeJob()->getJobProgressIgnored() == false)
529 {
530 activeJob()->setJobProgressIgnored(true);
531 updatedCaptureCompleted(0);
532 }
533 // We cannot rely on sequenceID to give us a count - if we don't ignore job progress, we leave the count as it was originally
534
535 // Check whether active job is complete by comparing required captures to what is already available
536 if (activeJob()->getCoreProperty(SequenceJob::SJ_Count).toInt() <=
537 activeJob()->getCompleted())
538 {
539 updatedCaptureCompleted(activeJob()->getCoreProperty(
540 SequenceJob::SJ_Count).toInt());
541 emit newLog(i18n("Job requires %1-second %2 images, has already %3/%4 captures and does not need to run.",
542 QString("%L1").arg(job->getCoreProperty(SequenceJob::SJ_Exposure).toDouble(), 0, 'f', 3),
543 job->getCoreProperty(SequenceJob::SJ_Filter).toString(),
544 activeJob()->getCompleted(),
545 activeJob()->getCoreProperty(SequenceJob::SJ_Count).toInt()));
546 processJobCompletion2();
547
548 /* FIXME: find a clearer way to exit here */
549 return;
550 }
551 else
552 {
553 // There are captures to process
554 emit newLog(i18n("Job requires %1-second %2 images, has %3/%4 frames captured and will be processed.",
555 QString("%L1").arg(job->getCoreProperty(SequenceJob::SJ_Exposure).toDouble(), 0, 'f', 3),
556 job->getCoreProperty(SequenceJob::SJ_Filter).toString(),
557 activeJob()->getCompleted(),
558 activeJob()->getCoreProperty(SequenceJob::SJ_Count).toInt()));
559
560 // Emit progress update - done a few lines below
561 // emit newImage(nullptr, activeJob());
562
563 activeCamera()->setNextSequenceID(state()->nextSequenceID());
564 }
565 }
566
567 if (activeCamera()->isBLOBEnabled() == false)
568 {
569 // FIXME: Move this warning pop-up elsewhere, it will interfere with automation.
570 // if (Options::guiderType() != Ekos::Guide::GUIDE_INTERNAL || KMessageBox::questionYesNo(nullptr, i18n("Image transfer is disabled for this camera. Would you like to enable it?")) ==
571 // KMessageBox::Yes)
572 if (Options::guiderType() != Guide::GUIDE_INTERNAL)
573 {
574 activeCamera()->setBLOBEnabled(true);
575 }
576 else
577 {
578 connect(KSMessageBox::Instance(), &KSMessageBox::accepted, this, [this]()
579 {
580 KSMessageBox::Instance()->disconnect(this);
581 activeCamera()->setBLOBEnabled(true);
582 prepareActiveJobStage1();
583
584 });
585 connect(KSMessageBox::Instance(), &KSMessageBox::rejected, this, [this]()
586 {
587 KSMessageBox::Instance()->disconnect(this);
588 activeCamera()->setBLOBEnabled(true);
589 state()->setBusy(false);
590 });
591
592 KSMessageBox::Instance()->questionYesNo(i18n("Image transfer is disabled for this camera. Would you like to enable it?"),
593 i18n("Image Transfer"), 15);
594
595 return;
596 }
597 }
598
599 emit jobPrepared(job);
600
601 prepareActiveJobStage1();
602
603}
604
605void CaptureProcess::prepareActiveJobStage1()
606{
607 if (activeJob() == nullptr)
608 {
609 qWarning(KSTARS_EKOS_CAPTURE) << "prepareActiveJobStage1 with null activeJob().";
610 }
611 else
612 {
613 // JM 2020-12-06: Check if we need to execute pre-job script first.
614 // Only run pre-job script for the first time and not after some images were captured but then stopped due to abort.
615 if (runCaptureScript(SCRIPT_PRE_JOB, activeJob()->getCompleted() == 0) == IPS_BUSY)
616 return;
617 }
618 prepareActiveJobStage2();
619}
620
621void CaptureProcess::prepareActiveJobStage2()
622{
623 // Just notification of active job stating up
624 if (activeJob() == nullptr)
625 {
626 qWarning(KSTARS_EKOS_CAPTURE) << "prepareActiveJobStage2 with null activeJob().";
627 }
628 else
629 emit newImage(activeJob(), state()->imageData());
630
631
632 /* Disable this restriction, let the sequence run even if focus did not run prior to the capture.
633 * Besides, this locks up the Scheduler when the Capture module starts a sequence without any prior focus procedure done.
634 * This is quite an old code block. The message "Manual scheduled" seems to even refer to some manual intervention?
635 * With the new HFR threshold, it might be interesting to prevent the execution because we actually need an HFR value to
636 * begin capturing, but even there, on one hand it makes sense for the end-user to know what HFR to put in the edit box,
637 * and on the other hand the focus procedure will deduce the next HFR automatically.
638 * But in the end, it's not entirely clear what the intent was. Note there is still a warning that a preliminary autofocus
639 * procedure is important to avoid any surprise that could make the whole schedule ineffective.
640 */
641 // JM 2020-12-06: Check if we need to execute pre-capture script first.
642 if (runCaptureScript(SCRIPT_PRE_CAPTURE) == IPS_BUSY)
643 return;
644
645 prepareJobExecution();
646}
647
648void CaptureProcess::executeJob()
649{
650 if (activeJob() == nullptr)
651 {
652 qWarning(KSTARS_EKOS_CAPTURE) << "executeJob with null activeJob().";
653 return;
654 }
655
656 // Double check all pointers are valid.
657 if (!activeCamera() || !devices()->getActiveChip())
658 {
659 checkCamera();
660 QTimer::singleShot(1000, this, &CaptureProcess::executeJob);
661 return;
662 }
663
665 if (Options::defaultObserver().isEmpty() == false)
666 FITSHeaders.append(FITSData::Record("Observer", Options::defaultObserver(), "Observer"));
667 if (activeJob()->getCoreProperty(SequenceJob::SJ_TargetName) != "")
668 FITSHeaders.append(FITSData::Record("Object", activeJob()->getCoreProperty(SequenceJob::SJ_TargetName).toString(),
669 "Object"));
670 FITSHeaders.append(FITSData::Record("TELESCOP", m_Scope, "Telescope"));
671
672 if (!FITSHeaders.isEmpty())
673 activeCamera()->setFITSHeaders(FITSHeaders);
674
675 // Update button status
676 state()->setBusy(true);
677 state()->setUseGuideHead((devices()->getActiveChip()->getType() == ISD::CameraChip::PRIMARY_CCD) ?
678 false : true);
679
680 emit syncGUIToJob(activeJob());
681
682 // If the job is a dark flat, let's find the optimal exposure from prior
683 // flat exposures.
684 if (activeJob()->jobType() == SequenceJob::JOBTYPE_DARKFLAT)
685 {
686 // If we found a prior exposure, and current upload more is not local, then update full prefix
687 if (state()->setDarkFlatExposure(activeJob())
688 && activeCamera()->getUploadMode() != ISD::Camera::UPLOAD_LOCAL)
689 {
690 auto placeholderPath = PlaceholderPath();
691 // Make sure to update Full Prefix as exposure value was changed
692 placeholderPath.processJobInfo(activeJob());
693 state()->setNextSequenceID(1);
694 }
695
696 }
697
698 updatePreCaptureCalibrationStatus();
699
700}
701
702void CaptureProcess::prepareJobExecution()
703{
704 if (activeJob() == nullptr)
705 {
706 qWarning(KSTARS_EKOS_CAPTURE) << "preparePreCaptureActions with null activeJob().";
707 // Everything below depends on activeJob(). Just return.
708 return;
709 }
710
711 state()->setBusy(true);
712
713 // Update guiderActive before prepareCapture.
714 activeJob()->setCoreProperty(SequenceJob::SJ_GuiderActive,
715 state()->isActivelyGuiding());
716
717 // signal that capture preparation steps should be executed
718 activeJob()->prepareCapture();
719
720 // update the UI
721 emit jobExecutionPreparationStarted();
722}
723
724void CaptureProcess::refreshOpticalTrain(QString name)
725{
726 auto mount = OpticalTrainManager::Instance()->getMount(name);
727 setMount(mount);
728
729 auto scope = OpticalTrainManager::Instance()->getScope(name);
730 setScope(scope["name"].toString());
731
732 auto camera = OpticalTrainManager::Instance()->getCamera(name);
733 setCamera(camera);
734
735 auto filterWheel = OpticalTrainManager::Instance()->getFilterWheel(name);
736 setFilterWheel(filterWheel);
737
738 auto rotator = OpticalTrainManager::Instance()->getRotator(name);
739 setRotator(rotator);
740
741 auto dustcap = OpticalTrainManager::Instance()->getDustCap(name);
742 setDustCap(dustcap);
743
744 auto lightbox = OpticalTrainManager::Instance()->getLightBox(name);
745 setLightBox(lightbox);
746}
747
748IPState CaptureProcess::checkLightFramePendingTasks()
749{
750 // step 1: did one of the pending jobs fail or has the user aborted the capture?
751 if (state()->getCaptureState() == CAPTURE_ABORTED)
752 return IPS_ALERT;
753
754 // step 2: check if pausing has been requested
755 if (checkPausing(CaptureModuleState::CONTINUE_ACTION_NEXT_EXPOSURE) == true)
756 return IPS_BUSY;
757
758 // step 3: check if a meridian flip is active
759 if (state()->checkMeridianFlipActive())
760 return IPS_BUSY;
761
762 // step 4: check guide deviation for non meridian flip stages if the initial guide limit is set.
763 // Wait until the guide deviation is reported to be below the limit (@see setGuideDeviation(double, double)).
764 if (state()->getCaptureState() == CAPTURE_PROGRESS &&
765 state()->getGuideState() == GUIDE_GUIDING &&
766 Options::enforceStartGuiderDrift())
767 return IPS_BUSY;
768
769 // step 5: check if dithering is required or running
770 if ((state()->getCaptureState() == CAPTURE_DITHERING && state()->getDitheringState() != IPS_OK)
771 || state()->checkDithering())
772 return IPS_BUSY;
773
774 // step 6: check if re-focusing is required
775 // Needs to be checked after dithering checks to avoid dithering in parallel
776 // to focusing, since @startFocusIfRequired() might change its value over time
777 if ((state()->getCaptureState() == CAPTURE_FOCUSING && state()->checkFocusRunning())
778 || state()->startFocusIfRequired())
779 return IPS_BUSY;
780
781 // step 7: resume guiding if it was suspended
782 // JM 2023.12.20: Must make to resume if we have a light frame.
783 if (state()->getGuideState() == GUIDE_SUSPENDED && activeJob()->getFrameType() == FRAME_LIGHT)
784 {
785 emit newLog(i18n("Autoguiding resumed."));
786 emit resumeGuiding();
787 // No need to return IPS_BUSY here, we can continue immediately.
788 // In the case that the capturing sequence has a guiding limit,
789 // capturing will be interrupted by setGuideDeviation().
790 }
791
792 // everything is ready for capturing light frames
793 return IPS_OK;
794
795}
796
797void CaptureProcess::captureStarted(CaptureModuleState::CAPTUREResult rc)
798{
799 switch (rc)
800 {
801 case CaptureModuleState::CAPTURE_OK:
802 {
803 state()->setCaptureState(CAPTURE_CAPTURING);
804 state()->getCaptureTimeout().start(static_cast<int>(activeJob()->getCoreProperty(
805 SequenceJob::SJ_Exposure).toDouble()) * 1000 +
806 CAPTURE_TIMEOUT_THRESHOLD);
807 // calculate remaining capture time for the current job
808 state()->imageCountDown().setHMS(0, 0, 0);
809 double ms_left = std::ceil(activeJob()->getExposeLeft() * 1000.0);
810 state()->imageCountDownAddMSecs(int(ms_left));
811 state()->setLastRemainingFrameTimeMS(ms_left);
812 state()->sequenceCountDown().setHMS(0, 0, 0);
813 state()->sequenceCountDownAddMSecs(activeJob()->getJobRemainingTime(state()->averageDownloadTime()) * 1000);
814 // ensure that the download time label is visible
815
816 if (activeJob()->jobType() != SequenceJob::JOBTYPE_PREVIEW)
817 {
818 auto index = state()->allJobs().indexOf(activeJob());
819 if (index >= 0 && index < state()->getSequence().count())
820 state()->changeSequenceValue(index, "Status", "In Progress");
821
822 emit updateJobTable(activeJob());
823 }
824 emit captureRunning();
825 }
826 break;
827
828 case CaptureModuleState::CAPTURE_FRAME_ERROR:
829 emit newLog(i18n("Failed to set sub frame."));
830 emit stopCapturing(CAPTURE_ABORTED);
831 break;
832
833 case CaptureModuleState::CAPTURE_BIN_ERROR:
834 emit newLog((i18n("Failed to set binning.")));
835 emit stopCapturing(CAPTURE_ABORTED);
836 break;
837
838 case CaptureModuleState::CAPTURE_FOCUS_ERROR:
839 emit newLog((i18n("Cannot capture while focus module is busy.")));
840 emit stopCapturing(CAPTURE_ABORTED);
841 break;
842 }
843}
844
845void CaptureProcess::checkNextExposure()
846{
847 IPState started = startNextExposure();
848 // if starting the next exposure did not succeed due to pending jobs running,
849 // we retry after 1 second
850 if (started == IPS_BUSY)
851 QTimer::singleShot(1000, this, &CaptureProcess::checkNextExposure);
852}
853
854IPState CaptureProcess::captureImageWithDelay()
855{
856 auto theJob = activeJob();
857
858 if (theJob == nullptr)
859 return IPS_IDLE;
860
861 const int seqDelay = theJob->getCoreProperty(SequenceJob::SJ_Delay).toInt();
862 // nothing pending, let's start the next exposure
863 if (seqDelay > 0)
864 {
865 state()->setCaptureState(CAPTURE_WAITING);
866 }
867 state()->getCaptureDelayTimer().start(seqDelay);
868 return IPS_OK;
869}
870
871IPState CaptureProcess::startNextExposure()
872{
873 // Since this function is looping while pending tasks are running in parallel
874 // it might happen that one of them leads to abort() which sets the #activeJob() to nullptr.
875 // In this case we terminate the loop by returning #IPS_IDLE without starting a new capture.
876 auto theJob = activeJob();
877
878 if (theJob == nullptr)
879 return IPS_IDLE;
880
881 // check pending jobs for light frames. All other frame types do not contain mid-sequence checks.
882 if (activeJob()->getFrameType() == FRAME_LIGHT)
883 {
884 IPState pending = checkLightFramePendingTasks();
885 if (pending != IPS_OK)
886 // there are still some jobs pending
887 return pending;
888 }
889
890 return captureImageWithDelay();
891
892 return IPS_OK;
893}
894
895IPState CaptureProcess::resumeSequence()
896{
897 // before we resume, we will check if pausing is requested
898 if (checkPausing(CaptureModuleState::CONTINUE_ACTION_CAPTURE_COMPLETE) == true)
899 return IPS_BUSY;
900
901 // If no job is active, we have to find if there are more pending jobs in the queue
902 if (!activeJob())
903 {
904 return startNextJob();
905 }
906 // Otherwise, let's prepare for next exposure.
907
908 // if we're done
909 else if (activeJob()->getCoreProperty(SequenceJob::SJ_Count).toInt() <=
910 activeJob()->getCompleted())
911 {
912 processJobCompletion1();
913 return IPS_OK;
914 }
915 // continue the current job
916 else
917 {
918 // If we suspended guiding due to primary chip download, resume guide chip guiding now - unless
919 // a meridian flip is ongoing
920 if (state()->getGuideState() == GUIDE_SUSPENDED && state()->suspendGuidingOnDownload() &&
921 state()->getMeridianFlipState()->checkMeridianFlipActive() == false)
922 {
923 qCInfo(KSTARS_EKOS_CAPTURE) << "Resuming guiding...";
924 emit resumeGuiding();
925 }
926
927 // If looping, we just increment the file system image count
928 if (activeCamera()->isFastExposureEnabled())
929 {
930 if (activeCamera()->getUploadMode() != ISD::Camera::UPLOAD_LOCAL)
931 {
932 state()->checkSeqBoundary();
933 activeCamera()->setNextSequenceID(state()->nextSequenceID());
934 }
935 }
936
937 // ensure state image received to recover properly after pausing
938 state()->setCaptureState(CAPTURE_IMAGE_RECEIVED);
939
940 // JM 2020-12-06: Check if we need to execute pre-capture script first.
941 if (runCaptureScript(SCRIPT_PRE_CAPTURE) == IPS_BUSY)
942 {
943 if (activeCamera()->isFastExposureEnabled())
944 {
945 state()->setRememberFastExposure(true);
946 activeCamera()->setFastExposureEnabled(false);
947 }
948 return IPS_BUSY;
949 }
950 else
951 {
952 // Check if we need to stop fast exposure to perform any
953 // pending tasks. If not continue as is.
954 if (activeCamera()->isFastExposureEnabled())
955 {
956 if (activeJob() &&
957 activeJob()->getFrameType() == FRAME_LIGHT &&
958 checkLightFramePendingTasks() == IPS_OK)
959 {
960 // Continue capturing seamlessly
961 state()->setCaptureState(CAPTURE_CAPTURING);
962 return IPS_OK;
963 }
964
965 // Stop fast exposure now.
966 state()->setRememberFastExposure(true);
967 activeCamera()->setFastExposureEnabled(false);
968 }
969
970 checkNextExposure();
971
972 }
973 }
974
975 return IPS_OK;
976
977}
978
979bool Ekos::CaptureProcess::checkSavingReceivedImage(const QSharedPointer<FITSData> &data, const QString &extension,
980 QString &filename)
981{
982 // trigger saving the FITS file for batch jobs that aren't calibrating
983 if (data && activeCamera() && activeCamera()->getUploadMode() != ISD::Camera::UPLOAD_LOCAL)
984 {
985 if (activeJob()->jobType() != SequenceJob::JOBTYPE_PREVIEW
986 && activeJob()->getCalibrationStage() != SequenceJobState::CAL_CALIBRATION)
987 {
988 if (state()->generateFilename(extension, &filename) && activeCamera()->saveCurrentImage(filename))
989 {
990 data->setFilename(filename);
991 KStars::Instance()->statusBar()->showMessage(i18n("file saved to %1", filename), 0);
992 return true;
993 }
994 else
995 {
996 qCWarning(KSTARS_EKOS_CAPTURE) << "Saving current image failed!";
997 connect(KSMessageBox::Instance(), &KSMessageBox::accepted, this, [ = ]()
998 {
999 KSMessageBox::Instance()->disconnect(this);
1000 });
1001 KSMessageBox::Instance()->error(i18n("Failed writing image to %1\nPlease check folder, filename & permissions.",
1002 filename),
1003 i18n("Image Write Failed"), 30);
1004 return false;
1005 }
1006 }
1007 }
1008 return true;
1009}
1010
1012{
1013 ISD::CameraChip * tChip = nullptr;
1014
1016 if (data)
1017 {
1018 state()->setImageData(data);
1019 blobInfo = QString("{Device: %1 Property: %2 Element: %3 Chip: %4}").arg(data->property("device").toString())
1020 .arg(data->property("blobVector").toString())
1021 .arg(data->property("blobElement").toString())
1022 .arg(data->property("chip").toInt());
1023 }
1024 else
1025 state()->imageData().reset();
1026
1027 const SequenceJob *job = activeJob();
1028 // If there is no active job, ignore
1029 if (job == nullptr)
1030 {
1031 if (data)
1032 qCWarning(KSTARS_EKOS_CAPTURE) << blobInfo << "Ignoring received FITS as active job is null.";
1033
1034 emit processingFITSfinished(false);
1035 return;
1036 }
1037
1038 if (state()->getMeridianFlipState()->getMeridianFlipStage() >= MeridianFlipState::MF_ALIGNING)
1039 {
1040 if (data)
1041 qCWarning(KSTARS_EKOS_CAPTURE) << blobInfo << "Ignoring Received FITS as meridian flip stage is" <<
1042 state()->getMeridianFlipState()->getMeridianFlipStage();
1043 emit processingFITSfinished(false);
1044 return;
1045 }
1046
1047 const SequenceJob::SequenceJobType currentJobType = activeJob()->jobType();
1048 // If image is client or both, let's process it.
1049 if (activeCamera() && activeCamera()->getUploadMode() != ISD::Camera::UPLOAD_LOCAL)
1050 {
1051
1052 if (state()->getCaptureState() == CAPTURE_IDLE || state()->getCaptureState() == CAPTURE_ABORTED)
1053 {
1054 qCWarning(KSTARS_EKOS_CAPTURE) << blobInfo << "Ignoring Received FITS as current capture state is not active" <<
1055 state()->getCaptureState();
1056
1057 emit processingFITSfinished(false);
1058 return;
1059 }
1060
1061 if (data)
1062 {
1063 tChip = activeCamera()->getChip(static_cast<ISD::CameraChip::ChipType>(data->property("chip").toInt()));
1064 if (tChip != devices()->getActiveChip())
1065 {
1066 if (state()->getGuideState() == GUIDE_IDLE)
1067 qCWarning(KSTARS_EKOS_CAPTURE) << blobInfo << "Ignoring Received FITS as it does not correspond to the target chip"
1068 << devices()->getActiveChip()->getType();
1069
1070 emit processingFITSfinished(false);
1071 return;
1072 }
1073 }
1074
1075 if (devices()->getActiveChip()->getCaptureMode() == FITS_FOCUS ||
1076 devices()->getActiveChip()->getCaptureMode() == FITS_GUIDE)
1077 {
1078 qCWarning(KSTARS_EKOS_CAPTURE) << blobInfo << "Ignoring Received FITS as it has the wrong capture mode" <<
1079 devices()->getActiveChip()->getCaptureMode();
1080
1081 emit processingFITSfinished(false);
1082 return;
1083 }
1084
1085 // If the FITS is not for our device, simply ignore
1086 if (data && data->property("device").toString() != activeCamera()->getDeviceName())
1087 {
1088 qCWarning(KSTARS_EKOS_CAPTURE) << blobInfo << "Ignoring Received FITS as the blob device name does not equal active camera"
1089 << activeCamera()->getDeviceName();
1090
1091 emit processingFITSfinished(false);
1092 return;
1093 }
1094
1095 if (currentJobType == SequenceJob::JOBTYPE_PREVIEW)
1096 {
1097 QString filename;
1098 if (checkSavingReceivedImage(data, extension, filename))
1099 {
1100 FITSMode captureMode = tChip->getCaptureMode();
1101 FITSScale captureFilter = tChip->getCaptureFilter();
1102 updateFITSViewer(data, captureMode, captureFilter, filename, data->property("device").toString());
1103 }
1104 }
1105
1106 // If dark is selected, perform dark substraction.
1107 if (data && Options::autoDark() && job->jobType() == SequenceJob::JOBTYPE_PREVIEW && state()->useGuideHead() == false)
1108 {
1109 QVariant trainID = ProfileSettings::Instance()->getOneSetting(ProfileSettings::CaptureOpticalTrain);
1110 if (trainID.isValid())
1111 {
1112 m_DarkProcessor.data()->denoise(trainID.toUInt(),
1113 devices()->getActiveChip(),
1114 state()->imageData(),
1115 job->getCoreProperty(SequenceJob::SJ_Exposure).toDouble(),
1116 job->getCoreProperty(SequenceJob::SJ_ROI).toRect().x(),
1117 job->getCoreProperty(SequenceJob::SJ_ROI).toRect().y());
1118 }
1119 else
1120 qWarning(KSTARS_EKOS_CAPTURE) << "Invalid train ID for darks substraction:" << trainID.toUInt();
1121
1122 }
1123 if (currentJobType == SequenceJob::JOBTYPE_PREVIEW)
1124 {
1125 // Set image metadata and emit captureComplete
1126 // Need to do this now for previews as the activeJob() will be set to null.
1127 updateImageMetadataAction(state()->imageData());
1128 }
1129 }
1130
1131 // image has been received and processed successfully.
1132 state()->setCaptureState(CAPTURE_IMAGE_RECEIVED);
1133 // processing finished successfully
1134 SequenceJob *thejob = activeJob();
1135
1136 if (thejob == nullptr)
1137 return;
1138
1139 // If fast exposure is off, disconnect exposure progress
1140 // otherwise, keep it going since it fires off from driver continuous capture process.
1141 if (activeCamera()->isFastExposureEnabled() == false && state()->isLooping() == false)
1142 {
1143 disconnect(activeCamera(), &ISD::Camera::newExposureValue, this,
1145 DarkLibrary::Instance()->disconnect(this);
1146 }
1147 // update counters
1148 // This will set activeJob to be a nullptr if it's a preview.
1150
1151 QString filename;
1152 bool alreadySaved = false;
1153 switch (thejob->getFrameType())
1154 {
1155 case FRAME_BIAS:
1156 case FRAME_DARK:
1157 thejob->setCalibrationStage(SequenceJobState::CAL_CALIBRATION_COMPLETE);
1158 break;
1159 case FRAME_FLAT:
1160 /* calibration not completed, adapt exposure time */
1161 if (thejob->getFlatFieldDuration() == DURATION_ADU
1162 && thejob->getCoreProperty(SequenceJob::SJ_TargetADU).toDouble() > 0 &&
1163 thejob->getCalibrationStage() == SequenceJobState::CAL_CALIBRATION)
1164 {
1165 if (checkFlatCalibration(state()->imageData(), state()->exposureRange().min, state()->exposureRange().max) == false)
1166 return; /* calibration not completed */
1167 thejob->setCalibrationStage(SequenceJobState::CAL_CALIBRATION_COMPLETE);
1168 // save current image since the image satisfies the calibration requirements
1169 if (checkSavingReceivedImage(data, extension, filename))
1170 {
1171 /* Increase the sequence's current capture count */
1172 updatedCaptureCompleted(activeJob()->getCompleted() + 1);
1173 alreadySaved = true;
1174 }
1175 }
1176 else
1177 {
1178 thejob->setCalibrationStage(SequenceJobState::CAL_CALIBRATION_COMPLETE);
1179 }
1180 break;
1181 case FRAME_LIGHT:
1182 // do nothing, continue
1183 break;
1184 case FRAME_NONE:
1185 // this should not happen!
1186 qWarning(KSTARS_EKOS_CAPTURE) << "Job completed with frametype NONE!";
1187 return;
1188 }
1189
1190 if (thejob->getCalibrationStage() == SequenceJobState::CAL_CALIBRATION_COMPLETE)
1191 thejob->setCalibrationStage(SequenceJobState::CAL_CAPTURING);
1192
1193 if (activeJob() && currentJobType != SequenceJob::JOBTYPE_PREVIEW &&
1194 activeCamera() && activeCamera()->getUploadMode() != ISD::Camera::UPLOAD_LOCAL)
1195 {
1196 // Check to save and show the new image in the FITS viewer
1197 if (alreadySaved || checkSavingReceivedImage(data, extension, filename))
1198 {
1199 FITSMode captureMode = tChip->getCaptureMode();
1200 FITSScale captureFilter = tChip->getCaptureFilter();
1201 updateFITSViewer(data, captureMode, captureFilter, filename, data->property("device").toString());
1202 }
1203
1204 // Set image metadata and emit captureComplete
1205 updateImageMetadataAction(state()->imageData());
1206 }
1207
1208 // JM 2020-06-17: Emit newImage for LOCAL images (stored on remote host)
1209 //if (m_Camera->getUploadMode() == ISD::Camera::UPLOAD_LOCAL)
1210 emit newImage(thejob, state()->imageData());
1211
1212 // Check if we need to execute post capture script first
1214 return;
1215
1216 // don't resume for preview jobs
1217 if (currentJobType != SequenceJob::JOBTYPE_PREVIEW)
1219
1220 // hand over to the capture module
1221 emit processingFITSfinished(true);
1222}
1223
1225{
1226 emit newLog(i18n("Remote image saved to %1", file));
1227 // call processing steps without image data if the image is stored only remotely
1228 QString nothing("");
1229 if (activeCamera() && activeCamera()->getUploadMode() == ISD::Camera::UPLOAD_LOCAL)
1230 {
1231 QString ext("");
1232 processFITSData(nullptr, ext);
1233 }
1234}
1235
1237{
1238 // in some rare cases it might happen that activeJob() has been cleared by a concurrent thread
1239 if (activeJob() == nullptr)
1240 {
1241 qCWarning(KSTARS_EKOS_CAPTURE) << "Processing pre capture calibration without active job, state = " <<
1242 getCaptureStatusString(state()->getCaptureState());
1243 return IPS_ALERT;
1244 }
1245
1246 // If we are currently guide and the frame is NOT a light frame, then we shopld suspend.
1247 // N.B. The guide camera could be on its own scope unaffected but it doesn't hurt to stop
1248 // guiding since it is no longer used anyway.
1249 if (activeJob()->getFrameType() != FRAME_LIGHT
1250 && state()->getGuideState() == GUIDE_GUIDING)
1251 {
1252 emit newLog(i18n("Autoguiding suspended."));
1253 emit suspendGuiding();
1254 }
1255
1256 // Run necessary tasks for each frame type
1257 switch (activeJob()->getFrameType())
1258 {
1259 case FRAME_LIGHT:
1261
1262 // FIXME Remote flats are not working since the files are saved remotely and no
1263 // preview is done locally first to calibrate the image.
1264 case FRAME_FLAT:
1265 case FRAME_BIAS:
1266 case FRAME_DARK:
1267 case FRAME_NONE:
1268 // no actions necessary
1269 break;
1270 }
1271
1272 return IPS_OK;
1273
1274}
1275
1277{
1278 // If process was aborted or stopped by the user
1279 if (state()->isBusy() == false)
1280 {
1281 emit newLog(i18n("Warning: Calibration process was prematurely terminated."));
1282 return;
1283 }
1284
1286
1287 if (rc == IPS_ALERT)
1288 return;
1289 else if (rc == IPS_BUSY)
1290 {
1292 return;
1293 }
1294
1295 captureImageWithDelay();
1296}
1297
1299{
1300 if (activeJob() == nullptr)
1301 {
1302 qWarning(KSTARS_EKOS_CAPTURE) << "procesJobCompletionStage1 with null activeJob().";
1303 }
1304 else
1305 {
1306 // JM 2020-12-06: Check if we need to execute post-job script first.
1308 return;
1309 }
1310
1312}
1313
1315{
1316 if (activeJob() == nullptr)
1317 {
1318 qWarning(KSTARS_EKOS_CAPTURE) << "procesJobCompletionStage2 with null activeJob().";
1319 }
1320 else
1321 {
1322 activeJob()->done();
1323
1324 if (activeJob()->jobType() != SequenceJob::JOBTYPE_PREVIEW)
1325 {
1326 int index = state()->allJobs().indexOf(activeJob());
1327 QJsonArray seqArray = state()->getSequence();
1328 QJsonObject oneSequence = seqArray[index].toObject();
1329 oneSequence["Status"] = "Complete";
1330 seqArray.replace(index, oneSequence);
1331 state()->setSequence(seqArray);
1332 emit sequenceChanged(seqArray);
1333 emit updateJobTable(activeJob());
1334 }
1335 }
1336 // stopping clears the planned state, therefore skip if pause planned
1337 if (state()->getCaptureState() != CAPTURE_PAUSE_PLANNED)
1338 emit stopCapture();
1339
1340 // Check if there are more pending jobs and execute them
1341 if (resumeSequence() == IPS_OK)
1342 return;
1343 // Otherwise, we're done. We park if required and resume guiding if no parking is done and autoguiding was engaged before.
1344 else
1345 {
1346 //KNotification::event(QLatin1String("CaptureSuccessful"), i18n("CCD capture sequence completed"));
1347 KSNotification::event(QLatin1String("CaptureSuccessful"), i18n("CCD capture sequence completed"),
1348 KSNotification::Capture);
1349
1350 emit stopCapture(CAPTURE_COMPLETE);
1351
1352 //Resume guiding if it was suspended before
1353 //if (isAutoGuiding && currentCCD->getChip(ISD::CameraChip::GUIDE_CCD) == guideChip)
1354 if (state()->getGuideState() == GUIDE_SUSPENDED && state()->suspendGuidingOnDownload())
1355 emit resumeGuiding();
1356 }
1357
1358}
1359
1361{
1362 SequenceJob * next_job = nullptr;
1363
1364 for (auto &oneJob : state()->allJobs())
1365 {
1366 if (oneJob->getStatus() == JOB_IDLE || oneJob->getStatus() == JOB_ABORTED)
1367 {
1368 next_job = oneJob;
1369 break;
1370 }
1371 }
1372
1373 if (next_job)
1374 {
1375
1377
1378 //Resume guiding if it was suspended before, except for an active meridian flip is running.
1379 //if (isAutoGuiding && currentCCD->getChip(ISD::CameraChip::GUIDE_CCD) == guideChip)
1380 if (state()->getGuideState() == GUIDE_SUSPENDED && state()->suspendGuidingOnDownload() &&
1381 state()->getMeridianFlipState()->checkMeridianFlipActive() == false)
1382 {
1383 qCDebug(KSTARS_EKOS_CAPTURE) << "Resuming guiding...";
1384 emit resumeGuiding();
1385 }
1386
1387 return IPS_OK;
1388 }
1389 else
1390 {
1391 qCDebug(KSTARS_EKOS_CAPTURE) << "All capture jobs complete.";
1392 return IPS_BUSY;
1393 }
1394}
1395
1397{
1398 if (activeJob() == nullptr)
1399 return;
1400
1401 // Bail out if we have no CCD anymore
1402 if (!activeCamera() || !activeCamera()->isConnected())
1403 {
1404 emit newLog(i18n("Error: Lost connection to CCD."));
1405 emit stopCapture(CAPTURE_ABORTED);
1406 return;
1407 }
1408
1409 state()->getCaptureTimeout().stop();
1410 state()->getCaptureDelayTimer().stop();
1411 if (activeCamera()->isFastExposureEnabled())
1412 {
1413 int remaining = state()->isLooping() ? 100000 : (activeJob()->getCoreProperty(
1414 SequenceJob::SJ_Count).toInt() -
1415 activeJob()->getCompleted());
1416 if (remaining > 1)
1417 activeCamera()->setFastCount(static_cast<uint>(remaining));
1418 }
1419
1420 setCamera(true);
1421
1422 if (activeJob()->getFrameType() == FRAME_FLAT)
1423 {
1424 // If we have to calibrate ADU levels, first capture must be preview and not in batch mode
1425 if (activeJob()->jobType() != SequenceJob::JOBTYPE_PREVIEW
1426 && activeJob()->getFlatFieldDuration() == DURATION_ADU &&
1427 activeJob()->getCalibrationStage() == SequenceJobState::CAL_NONE)
1428 {
1429 if (activeCamera()->getEncodingFormat() != "FITS" &&
1430 activeCamera()->getEncodingFormat() != "XISF")
1431 {
1432 emit newLog(i18n("Cannot calculate ADU levels in non-FITS images."));
1433 emit stopCapture(CAPTURE_ABORTED);
1434 return;
1435 }
1436
1437 activeJob()->setCalibrationStage(SequenceJobState::CAL_CALIBRATION);
1438 }
1439 }
1440
1441 // If preview, always set to UPLOAD_CLIENT if not already set.
1442 if (activeJob()->jobType() == SequenceJob::JOBTYPE_PREVIEW)
1443 {
1444 if (activeCamera()->getUploadMode() != ISD::Camera::UPLOAD_CLIENT)
1445 activeCamera()->setUploadMode(ISD::Camera::UPLOAD_CLIENT);
1446 }
1447 // If batch mode, ensure upload mode mathces the active job target.
1448 else
1449 {
1450 if (activeCamera()->getUploadMode() != activeJob()->getUploadMode())
1451 activeCamera()->setUploadMode(activeJob()->getUploadMode());
1452 }
1453
1454 if (activeCamera()->getUploadMode() != ISD::Camera::UPLOAD_LOCAL)
1455 {
1456 state()->checkSeqBoundary();
1457 activeCamera()->setNextSequenceID(state()->nextSequenceID());
1458 }
1459
1460 // Re-enable fast exposure if it was disabled before due to pending tasks
1461 if (state()->isRememberFastExposure())
1462 {
1463 state()->setRememberFastExposure(false);
1464 activeCamera()->setFastExposureEnabled(true);
1465 }
1466
1467 if (state()->frameSettings().contains(devices()->getActiveChip()))
1468 {
1469 const auto roi = activeJob()->getCoreProperty(SequenceJob::SJ_ROI).toRect();
1470 QVariantMap settings;
1471 settings["x"] = roi.x();
1472 settings["y"] = roi.y();
1473 settings["w"] = roi.width();
1474 settings["h"] = roi.height();
1475 settings["binx"] = activeJob()->getCoreProperty(SequenceJob::SJ_Binning).toPoint().x();
1476 settings["biny"] = activeJob()->getCoreProperty(SequenceJob::SJ_Binning).toPoint().y();
1477
1478 state()->frameSettings()[devices()->getActiveChip()] = settings;
1479 }
1480
1481 // If using DSLR, make sure it is set to correct transfer format
1482 activeCamera()->setEncodingFormat(activeJob()->getCoreProperty(
1483 SequenceJob::SJ_Encoding).toString());
1484
1485 state()->setStartingCapture(true);
1486 state()->placeholderPath().setGenerateFilenameSettings(*activeJob());
1487
1488 // update remote filename and directory filling all placeholders
1489 if (activeCamera()->getUploadMode() != ISD::Camera::UPLOAD_CLIENT)
1490 {
1491 auto remoteUpload = state()->placeholderPath().generateSequenceFilename(*activeJob(), false, true, 1, "", "", false,
1492 false);
1493
1494 auto lastSeparator = remoteUpload.lastIndexOf(QDir::separator());
1497 activeJob()->setCoreProperty(SequenceJob::SJ_RemoteFormatDirectory, remoteDirectory);
1498 activeJob()->setCoreProperty(SequenceJob::SJ_RemoteFormatFilename, remoteFilename);
1499 }
1500
1501 // now hand over the control of capturing to the sequence job. As soon as capturing
1502 // has started, the sequence job will report the result with the captureStarted() event
1503 // that will trigger Capture::captureStarted()
1504 activeJob()->startCapturing(state()->getRefocusState()->isAutoFocusReady(),
1505 activeJob()->getCalibrationStage() == SequenceJobState::CAL_CALIBRATION ? FITS_CALIBRATE :
1506 FITS_NORMAL);
1507
1508 // Re-enable fast exposure if it was disabled before due to pending tasks
1509 if (state()->isRememberFastExposure())
1510 {
1511 state()->setRememberFastExposure(false);
1512 activeCamera()->setFastExposureEnabled(true);
1513 }
1514
1515 emit captureTarget(activeJob()->getCoreProperty(SequenceJob::SJ_TargetName).toString());
1516 emit captureImageStarted();
1517}
1518
1520{
1521 devices()->setActiveChip(state()->useGuideHead() ?
1522 devices()->getActiveCamera()->getChip(
1523 ISD::CameraChip::GUIDE_CCD) :
1524 devices()->getActiveCamera()->getChip(ISD::CameraChip::PRIMARY_CCD));
1525 devices()->getActiveChip()->resetFrame();
1526 emit updateFrameProperties(1);
1527}
1528
1530{
1531 // ignore values if not capturing
1532 if (state()->checkCapturing() == false)
1533 return;
1534
1535 if (devices()->getActiveChip() != tChip ||
1536 devices()->getActiveChip()->getCaptureMode() != FITS_NORMAL
1537 || state()->getMeridianFlipState()->getMeridianFlipStage() >= MeridianFlipState::MF_ALIGNING)
1538 return;
1539
1540 double deltaMS = std::ceil(1000.0 * value - state()->lastRemainingFrameTimeMS());
1541 emit updateCaptureCountDown(int(deltaMS));
1542 state()->setLastRemainingFrameTimeMS(state()->lastRemainingFrameTimeMS() + deltaMS);
1543
1544 if (activeJob())
1545 {
1546 activeJob()->setExposeLeft(value);
1547
1548 emit newExposureProgress(activeJob());
1549 }
1550
1551 if (activeJob() && ipstate == IPS_ALERT)
1552 {
1553 int retries = activeJob()->getCaptureRetires() + 1;
1554
1555 activeJob()->setCaptureRetires(retries);
1556
1557 emit newLog(i18n("Capture failed. Check INDI Control Panel for details."));
1558
1559 if (retries >= 3)
1560 {
1561 activeJob()->abort();
1562 return;
1563 }
1564
1565 emit newLog((i18n("Restarting capture attempt #%1", retries)));
1566
1567 state()->setNextSequenceID(1);
1568
1569 captureImage();
1570 return;
1571 }
1572
1573 if (activeJob() != nullptr && ipstate == IPS_OK)
1574 {
1575 activeJob()->setCaptureRetires(0);
1576 activeJob()->setExposeLeft(0);
1577
1578 if (devices()->getActiveCamera()
1579 && devices()->getActiveCamera()->getUploadMode() == ISD::Camera::UPLOAD_LOCAL)
1580 {
1581 if (activeJob()->getStatus() == JOB_BUSY)
1582 {
1583 emit processingFITSfinished(false);
1584 return;
1585 }
1586 }
1587
1588 if (state()->getGuideState() == GUIDE_GUIDING && Options::guiderType() == 0
1589 && state()->suspendGuidingOnDownload())
1590 {
1591 qCDebug(KSTARS_EKOS_CAPTURE) << "Autoguiding suspended until primary CCD chip completes downloading...";
1592 emit suspendGuiding();
1593 }
1594
1595 emit downloadingFrame();
1596
1597 //This will start the clock to see how long the download takes.
1598 state()->downloadTimer().start();
1599 state()->downloadProgressTimer().start();
1600 }
1601}
1602
1604{
1605 if (activeJob())
1606 {
1607 double downloadTimeLeft = state()->averageDownloadTime() - state()->downloadTimer().elapsed() /
1608 1000.0;
1609 if(downloadTimeLeft >= 0)
1610 {
1611 state()->imageCountDown().setHMS(0, 0, 0);
1612 state()->imageCountDownAddMSecs(int(std::ceil(downloadTimeLeft * 1000)));
1613 emit newDownloadProgress(downloadTimeLeft);
1614 }
1615 }
1616
1617}
1618
1620{
1621 emit newImage(activeJob(), imageData);
1622 // If fast exposure is on, do not capture again, it will be captured by the driver.
1623 if (activeCamera()->isFastExposureEnabled() == false)
1624 {
1625 const int seqDelay = activeJob()->getCoreProperty(SequenceJob::SJ_Delay).toInt();
1626
1627 if (seqDelay > 0)
1628 {
1629 QTimer::singleShot(seqDelay, this, [this]()
1630 {
1631 activeJob()->startCapturing(state()->getRefocusState()->isAutoFocusReady(),
1632 FITS_NORMAL);
1633 });
1634 }
1635 else
1636 activeJob()->startCapturing(state()->getRefocusState()->isAutoFocusReady(),
1637 FITS_NORMAL);
1638 }
1639 return IPS_OK;
1640
1641}
1642
1644{
1645 // Do not calculate download time for images stored on server.
1646 // Only calculate for longer exposures.
1647 if (activeCamera()->getUploadMode() != ISD::Camera::UPLOAD_LOCAL
1648 && state()->downloadTimer().isValid())
1649 {
1650 //This determines the time since the image started downloading
1651 double currentDownloadTime = state()->downloadTimer().elapsed() / 1000.0;
1652 state()->addDownloadTime(currentDownloadTime);
1653 // Always invalidate timer as it must be explicitly started.
1654 state()->downloadTimer().invalidate();
1655
1657 QString estimatedTimeString = QString::number(state()->averageDownloadTime(), 'd', 2);
1658 emit newLog(i18n("Download Time: %1 s, New Download Time Estimate: %2 s.", dLTimeString, estimatedTimeString));
1659 }
1660 return IPS_OK;
1661}
1662
1664{
1665 if (activeJob()->jobType() == SequenceJob::JOBTYPE_PREVIEW)
1666 {
1667 // Reset upload mode if it was changed by preview
1668 activeCamera()->setUploadMode(activeJob()->getUploadMode());
1669 // Reset active job pointer
1670 state()->setActiveJob(nullptr);
1671 emit stopCapture(CAPTURE_COMPLETE);
1672 if (state()->getGuideState() == GUIDE_SUSPENDED && state()->suspendGuidingOnDownload())
1673 emit resumeGuiding();
1674 return IPS_OK;
1675 }
1676 else
1677 return IPS_IDLE;
1678
1679}
1680
1682{
1683 // stop timers
1684 state()->getCaptureTimeout().stop();
1685 state()->setCaptureTimeoutCounter(0);
1686
1687 state()->downloadProgressTimer().stop();
1688
1689 // In case we're framing, let's return quickly to continue the process.
1690 if (state()->isLooping())
1691 {
1692 continueFramingAction(state()->imageData());
1693 return;
1694 }
1695
1696 // Update download times.
1698
1699 // If it was initially set as pure preview job and NOT as preview for calibration
1701 return;
1702
1703 // do not update counters if in preview mode or calibrating
1704 if (activeJob()->jobType() == SequenceJob::JOBTYPE_PREVIEW
1705 || activeJob()->getCalibrationStage() == SequenceJobState::CAL_CALIBRATION)
1706 return;
1707
1708 /* Increase the sequence's current capture count */
1709 updatedCaptureCompleted(activeJob()->getCompleted() + 1);
1710 /* Decrease the counter for in-sequence focusing */
1711 state()->getRefocusState()->decreaseInSequenceFocusCounter();
1712 /* Reset adaptive focus flag */
1713 state()->getRefocusState()->setAdaptiveFocusDone(false);
1714
1715 /* Decrease the dithering counter except for directly after meridian flip */
1716 /* Hint: this isonly relevant when a meridian flip happened during a paused sequence when pressing "Start" afterwards. */
1717 if (state()->getMeridianFlipState()->getMeridianFlipStage() < MeridianFlipState::MF_FLIPPING)
1718 state()->decreaseDitherCounter();
1719
1720 /* If we were assigned a captured frame map, also increase the relevant counter for prepareJob */
1721 state()->addCapturedFrame(activeJob()->getSignature());
1722
1723 // report that the image has been received
1724 emit newLog(i18n("Received image %1 out of %2.", activeJob()->getCompleted(),
1725 activeJob()->getCoreProperty(SequenceJob::SJ_Count).toInt()));
1726}
1727
1729{
1730 double hfr = -1, eccentricity = -1;
1731 int numStars = -1, median = -1;
1732 QString filename;
1733 if (imageData)
1734 {
1735 QVariant frameType;
1736 if (Options::autoHFR() && imageData && !imageData->areStarsSearched() && imageData->getRecordValue("FRAME", frameType)
1737 && frameType.toString() == "Light")
1738 {
1739#ifdef HAVE_STELLARSOLVER
1740 // Don't use the StellarSolver defaults (which allow very small stars).
1741 // Use the HFR profile--which the user can modify.
1742 QVariantMap extractionSettings;
1743 extractionSettings["optionsProfileIndex"] = Options::hFROptionsProfile();
1744 extractionSettings["optionsProfileGroup"] = static_cast<int>(Ekos::HFRProfiles);
1745 imageData->setSourceExtractorSettings(extractionSettings);
1746#endif
1747 QFuture<bool> result = imageData->findStars(ALGORITHM_SEP);
1748 result.waitForFinished();
1749 }
1750 hfr = imageData->getHFR(HFR_AVERAGE);
1751 numStars = imageData->getSkyBackground().starsDetected;
1752 median = imageData->getMedian();
1753 eccentricity = imageData->getEccentricity();
1754 filename = imageData->filename();
1755
1756 // avoid logging that we captured a temporary file
1757 if (state()->isLooping() == false && activeJob()->jobType() != SequenceJob::JOBTYPE_PREVIEW)
1758 emit newLog(i18n("Captured %1", filename));
1759
1760 auto remainingPlaceholders = PlaceholderPath::remainingPlaceholders(filename);
1761 if (remainingPlaceholders.size() > 0)
1762 {
1763 emit newLog(
1764 i18n("WARNING: remaining and potentially unknown placeholders %1 in %2",
1765 remainingPlaceholders.join(", "), filename));
1766 }
1767 }
1768
1769 if (activeJob())
1770 {
1771 QVariantMap metadata;
1772 metadata["filename"] = filename;
1773 metadata["type"] = activeJob()->getFrameType();
1774 metadata["exposure"] = activeJob()->getCoreProperty(SequenceJob::SJ_Exposure).toDouble();
1775 metadata["filter"] = activeJob()->getCoreProperty(SequenceJob::SJ_Filter).toString();
1776 metadata["width"] = activeJob()->getCoreProperty(SequenceJob::SJ_ROI).toRect().width();
1777 metadata["height"] = activeJob()->getCoreProperty(SequenceJob::SJ_ROI).toRect().height();
1778 metadata["hfr"] = hfr;
1779 metadata["starCount"] = numStars;
1780 metadata["median"] = median;
1781 metadata["eccentricity"] = eccentricity;
1782 emit captureComplete(metadata);
1783 }
1784 return IPS_OK;
1785}
1786
1788{
1789 if (activeJob())
1790 {
1791 const QString captureScript = activeJob()->getScript(scriptType);
1792 if (captureScript.isEmpty() == false && precond)
1793 {
1794 state()->setCaptureScriptType(scriptType);
1795 m_CaptureScript.start(captureScript, generateScriptArguments());
1796 //m_CaptureScript.start("/bin/bash", QStringList() << captureScript);
1797 emit newLog(i18n("Executing capture script %1", captureScript));
1798 return IPS_BUSY;
1799 }
1800 }
1801 // no script execution started
1802 return IPS_OK;
1803}
1804
1806{
1807 Q_UNUSED(status)
1808
1809 switch (state()->captureScriptType())
1810 {
1811 case SCRIPT_PRE_CAPTURE:
1812 emit newLog(i18n("Pre capture script finished with code %1.", exitCode));
1813 if (activeJob() && activeJob()->getStatus() == JOB_IDLE)
1815 else
1817 break;
1818
1820 emit newLog(i18n("Post capture script finished with code %1.", exitCode));
1821
1822 // If we're done, proceed to completion.
1823 if (activeJob() == nullptr
1824 || activeJob()->getCoreProperty(SequenceJob::SJ_Count).toInt() <=
1825 activeJob()->getCompleted())
1826 {
1828 }
1829 // Else check if meridian condition is met.
1830 else if (state()->checkMeridianFlipReady())
1831 {
1832 emit newLog(i18n("Processing meridian flip..."));
1833 }
1834 // Then if nothing else, just resume sequence.
1835 else
1836 {
1838 }
1839 break;
1840
1841 case SCRIPT_PRE_JOB:
1842 emit newLog(i18n("Pre job script finished with code %1.", exitCode));
1844 break;
1845
1846 case SCRIPT_POST_JOB:
1847 emit newLog(i18n("Post job script finished with code %1.", exitCode));
1849 break;
1850
1851 default:
1852 // in all other cases do nothing
1853 break;
1854 }
1855
1856}
1857
1859{
1860 if (activeCamera() && activeCamera()->getDeviceName() == name)
1861 checkCamera();
1862
1863 emit refreshCamera();
1864}
1865
1867{
1868 // Do not update any camera settings while capture is in progress.
1869 if (state()->getCaptureState() == CAPTURE_CAPTURING)
1870 return;
1871
1872 // If camera is restarted, try again in 1 second
1873 if (!activeCamera())
1874 {
1876 return;
1877 }
1878
1879 devices()->setActiveChip(nullptr);
1880
1881 // FIXME TODO fix guide head detection
1882 if (activeCamera()->getDeviceName().contains("Guider"))
1883 {
1884 state()->setUseGuideHead(true);
1885 devices()->setActiveChip(activeCamera()->getChip(ISD::CameraChip::GUIDE_CCD));
1886 }
1887
1888 if (devices()->getActiveChip() == nullptr)
1889 {
1890 state()->setUseGuideHead(false);
1891 devices()->setActiveChip(activeCamera()->getChip(ISD::CameraChip::PRIMARY_CCD));
1892 }
1893
1894 emit refreshCameraSettings();
1895}
1896
1898{
1899 auto pos = std::find_if(state()->DSLRInfos().begin(),
1900 state()->DSLRInfos().end(), [model](const QMap<QString, QVariant> &oneDSLRInfo)
1901 {
1902 return (oneDSLRInfo["Model"] == model);
1903 });
1904
1905 // Sync Pixel Size
1906 if (pos != state()->DSLRInfos().end())
1907 {
1908 auto camera = *pos;
1909 devices()->getActiveChip()->setImageInfo(camera["Width"].toInt(),
1910 camera["Height"].toInt(),
1911 camera["PixelW"].toDouble(),
1912 camera["PixelH"].toDouble(),
1913 8);
1914 }
1915}
1916
1917void CaptureProcess::reconnectCameraDriver(const QString &camera, const QString &filterWheel)
1918{
1919 if (activeCamera() && activeCamera()->getDeviceName() == camera)
1920 {
1921 // Set camera again to the one we restarted
1922 auto rememberState = state()->getCaptureState();
1923 state()->setCaptureState(CAPTURE_IDLE);
1924 checkCamera();
1925 state()->setCaptureState(rememberState);
1926
1927 // restart capture
1928 state()->setCaptureTimeoutCounter(0);
1929
1930 if (activeJob())
1931 {
1932 devices()->setActiveChip(devices()->getActiveChip());
1933 captureImage();
1934 }
1935 return;
1936 }
1937
1938 QTimer::singleShot(5000, this, [ &, camera, filterWheel]()
1939 {
1940 reconnectCameraDriver(camera, filterWheel);
1941 });
1942}
1943
1945{
1946 auto name = device->getDeviceName();
1947 device->disconnect(this);
1948
1949 // Mounts
1950 if (devices()->mount() && devices()->mount()->getDeviceName() == device->getDeviceName())
1951 {
1952 devices()->mount()->disconnect(this);
1953 devices()->setMount(nullptr);
1954 if (activeJob() != nullptr)
1955 activeJob()->addMount(nullptr);
1956 }
1957
1958 // Domes
1959 if (devices()->dome() && devices()->dome()->getDeviceName() == device->getDeviceName())
1960 {
1961 devices()->dome()->disconnect(this);
1962 devices()->setDome(nullptr);
1963 }
1964
1965 // Rotators
1966 if (devices()->rotator() && devices()->rotator()->getDeviceName() == device->getDeviceName())
1967 {
1968 devices()->rotator()->disconnect(this);
1969 devices()->setRotator(nullptr);
1970 }
1971
1972 // Dust Caps
1973 if (devices()->dustCap() && devices()->dustCap()->getDeviceName() == device->getDeviceName())
1974 {
1975 devices()->dustCap()->disconnect(this);
1976 devices()->setDustCap(nullptr);
1977 state()->hasDustCap = false;
1978 state()->setDustCapState(CaptureModuleState::CAP_UNKNOWN);
1979 }
1980
1981 // Light Boxes
1982 if (devices()->lightBox() && devices()->lightBox()->getDeviceName() == device->getDeviceName())
1983 {
1984 devices()->lightBox()->disconnect(this);
1985 devices()->setLightBox(nullptr);
1986 state()->hasLightBox = false;
1987 state()->setLightBoxLightState(CaptureModuleState::CAP_LIGHT_UNKNOWN);
1988 }
1989
1990 // Cameras
1991 if (activeCamera() && activeCamera()->getDeviceName() == name)
1992 {
1993 activeCamera()->disconnect(this);
1994 devices()->setActiveCamera(nullptr);
1995 devices()->setActiveChip(nullptr);
1996
1998 if (INDIListener::findDevice(name, generic))
1999 DarkLibrary::Instance()->removeDevice(generic);
2000
2001 QTimer::singleShot(1000, this, [this]()
2002 {
2003 checkCamera();
2004 });
2005 }
2006
2007 // Filter Wheels
2008 if (devices()->filterWheel() && devices()->filterWheel()->getDeviceName() == name)
2009 {
2010 devices()->filterWheel()->disconnect(this);
2011 devices()->setFilterWheel(nullptr);
2012
2013 QTimer::singleShot(1000, this, [this]()
2014 {
2015 emit refreshFilterSettings();
2016 });
2017 }
2018}
2019
2021{
2022 state()->setCaptureTimeoutCounter(state()->captureTimeoutCounter() + 1);
2023
2024 if (state()->deviceRestartCounter() >= 3)
2025 {
2026 state()->setCaptureTimeoutCounter(0);
2027 state()->setDeviceRestartCounter(0);
2028 emit newLog(i18n("Exposure timeout. Aborting..."));
2029 emit stopCapture(CAPTURE_ABORTED);
2030 return;
2031 }
2032
2033 if (state()->captureTimeoutCounter() > 3 && activeCamera())
2034 {
2035 emit newLog(i18n("Exposure timeout. More than 3 have been detected, will restart driver."));
2036 QString camera = activeCamera()->getDeviceName();
2037 QString fw = (devices()->filterWheel() != nullptr) ?
2038 devices()->filterWheel()->getDeviceName() : "";
2039 emit driverTimedout(camera);
2040 QTimer::singleShot(5000, this, [ &, camera, fw]()
2041 {
2042 state()->setDeviceRestartCounter(state()->deviceRestartCounter() + 1);
2043 reconnectCameraDriver(camera, fw);
2044 });
2045 return;
2046 }
2047 else
2048 {
2049 // Double check that m_Camera is valid in case it was reset due to driver restart.
2050 if (activeCamera() && activeJob())
2051 {
2052 setCamera(true);
2053 emit newLog(i18n("Exposure timeout. Restarting exposure..."));
2054 activeCamera()->setEncodingFormat("FITS");
2055 auto rememberState = state()->getCaptureState();
2056 state()->setCaptureState(CAPTURE_IDLE);
2057 checkCamera();
2058 state()->setCaptureState(rememberState);
2059
2060 auto targetChip = activeCamera()->getChip(state()->useGuideHead() ?
2061 ISD::CameraChip::GUIDE_CCD :
2062 ISD::CameraChip::PRIMARY_CCD);
2063 targetChip->abortExposure();
2064 const double exptime = activeJob()->getCoreProperty(SequenceJob::SJ_Exposure).toDouble();
2065 targetChip->capture(exptime);
2066 state()->getCaptureTimeout().start(static_cast<int>((exptime) * 1000 + CAPTURE_TIMEOUT_THRESHOLD));
2067 }
2068 // Don't allow this to happen all night. We will repeat checking (most likely for activeCamera()
2069 // another 200s = 40 * 5s, but after that abort capture.
2070 else if (state()->captureTimeoutCounter() < 40)
2071 {
2072 qCDebug(KSTARS_EKOS_CAPTURE) << "Unable to restart exposure as camera is missing, trying again in 5 seconds...";
2074 }
2075 else
2076 {
2077 state()->setCaptureTimeoutCounter(0);
2078 state()->setDeviceRestartCounter(0);
2079 emit newLog(i18n("Exposure timeout. Too many. Aborting..."));
2080 emit stopCapture(CAPTURE_ABORTED);
2081 return;
2082 }
2083 }
2084
2085}
2086
2088{
2089 if (!activeJob())
2090 return;
2091
2092 if (type == ISD::Camera::ERROR_CAPTURE)
2093 {
2094 int retries = activeJob()->getCaptureRetires() + 1;
2095
2096 activeJob()->setCaptureRetires(retries);
2097
2098 emit newLog(i18n("Capture failed. Check INDI Control Panel for details."));
2099
2100 if (retries >= 3)
2101 {
2102 emit stopCapture(CAPTURE_ABORTED);
2103 return;
2104 }
2105
2106 emit newLog(i18n("Restarting capture attempt #%1", retries));
2107
2108 state()->setNextSequenceID(1);
2109
2110 captureImage();
2111 return;
2112 }
2113 else
2114 {
2115 emit stopCapture(CAPTURE_ABORTED);
2116 }
2117}
2118
2120{
2121 // nothing to do
2122 if (imageData.isNull())
2123 return true;
2124
2125 double currentADU = imageData->getADU();
2126 bool outOfRange = false, saturated = false;
2127
2128 switch (imageData->bpp())
2129 {
2130 case 8:
2131 if (activeJob()->getCoreProperty(SequenceJob::SJ_TargetADU).toDouble() > UINT8_MAX)
2132 outOfRange = true;
2133 else if (currentADU / UINT8_MAX > 0.95)
2134 saturated = true;
2135 break;
2136
2137 case 16:
2138 if (activeJob()->getCoreProperty(SequenceJob::SJ_TargetADU).toDouble() > UINT16_MAX)
2139 outOfRange = true;
2140 else if (currentADU / UINT16_MAX > 0.95)
2141 saturated = true;
2142 break;
2143
2144 case 32:
2145 if (activeJob()->getCoreProperty(SequenceJob::SJ_TargetADU).toDouble() > UINT32_MAX)
2146 outOfRange = true;
2147 else if (currentADU / UINT32_MAX > 0.95)
2148 saturated = true;
2149 break;
2150
2151 default:
2152 break;
2153 }
2154
2155 if (outOfRange)
2156 {
2157 emit newLog(i18n("Flat calibration failed. Captured image is only %1-bit while requested ADU is %2.",
2158 QString::number(imageData->bpp())
2159 , QString::number(activeJob()->getCoreProperty(SequenceJob::SJ_TargetADU).toDouble(), 'f', 2)));
2160 emit stopCapture(CAPTURE_ABORTED);
2161 return false;
2162 }
2163 else if (saturated)
2164 {
2165 double nextExposure = activeJob()->getCoreProperty(SequenceJob::SJ_Exposure).toDouble() * 0.1;
2167
2168 emit newLog(i18n("Current image is saturated (%1). Next exposure is %2 seconds.",
2169 QString::number(currentADU, 'f', 0), QString("%L1").arg(nextExposure, 0, 'f', 6)));
2170
2171 activeJob()->setCalibrationStage(SequenceJobState::CAL_CALIBRATION);
2172 activeJob()->setCoreProperty(SequenceJob::SJ_Exposure, nextExposure);
2173 if (activeCamera()->getUploadMode() != ISD::Camera::UPLOAD_CLIENT)
2174 {
2175 activeCamera()->setUploadMode(ISD::Camera::UPLOAD_CLIENT);
2176 }
2178 return false;
2179 }
2180
2181 double ADUDiff = fabs(currentADU - activeJob()->getCoreProperty(
2182 SequenceJob::SJ_TargetADU).toDouble());
2183
2184 // If it is within tolerance range of target ADU
2185 if (ADUDiff <= state()->targetADUTolerance())
2186 {
2187 if (activeJob()->getCalibrationStage() == SequenceJobState::CAL_CALIBRATION)
2188 {
2189 emit newLog(
2190 i18n("Current ADU %1 within target ADU tolerance range.", QString::number(currentADU, 'f', 0)));
2191 activeCamera()->setUploadMode(activeJob()->getUploadMode());
2192 auto placeholderPath = PlaceholderPath();
2193 // Make sure to update Full Prefix as exposure value was changed
2194 placeholderPath.processJobInfo(activeJob());
2195 // Mark calibration as complete
2196 activeJob()->setCalibrationStage(SequenceJobState::CAL_CALIBRATION_COMPLETE);
2197
2198 // Must update sequence prefix as this step is only done in prepareJob
2199 // but since the duration has now been updated, we must take care to update signature
2200 // since it may include a placeholder for duration which would affect it.
2201 if (activeCamera() && activeCamera()->getUploadMode() != ISD::Camera::UPLOAD_LOCAL)
2202 state()->checkSeqBoundary();
2203 }
2204
2205 return true;
2206 }
2207
2208 double nextExposure = -1;
2209
2210 // If value is saturated, try to reduce it to valid range first
2211 if (std::fabs(imageData->getMax(0) - imageData->getMin(0)) < 10)
2212 nextExposure = activeJob()->getCoreProperty(SequenceJob::SJ_Exposure).toDouble() * 0.5;
2213 else
2215
2216 if (nextExposure <= 0 || std::isnan(nextExposure))
2217 {
2218 emit newLog(
2219 i18n("Unable to calculate optimal exposure settings, please capture the flats manually."));
2220 emit stopCapture(CAPTURE_ABORTED);
2221 return false;
2222 }
2223
2224 // Limit to minimum and maximum values
2226
2227 emit newLog(i18n("Current ADU is %1 Next exposure is %2 seconds.", QString::number(currentADU, 'f', 0),
2228 QString("%L1").arg(nextExposure, 0, 'f', 6)));
2229
2230 activeJob()->setCalibrationStage(SequenceJobState::CAL_CALIBRATION);
2231 activeJob()->setCoreProperty(SequenceJob::SJ_Exposure, nextExposure);
2232 if (activeCamera()->getUploadMode() != ISD::Camera::UPLOAD_CLIENT)
2233 {
2234 activeCamera()->setUploadMode(ISD::Camera::UPLOAD_CLIENT);
2235 }
2236
2238 return false;
2239
2240
2241}
2242
2244{
2245 if (activeJob() == nullptr)
2246 {
2247 qWarning(KSTARS_EKOS_CAPTURE) << "setCurrentADU with null activeJob().";
2248 // Nothing good to do here. Just don't crash.
2249 return currentADU;
2250 }
2251
2252 double nextExposure = 0;
2253 double targetADU = activeJob()->getCoreProperty(SequenceJob::SJ_TargetADU).toDouble();
2254 std::vector<double> coeff;
2255
2256 // limit number of points to two so it can calibrate in intesity changing enviroment like shoting flats
2257 // at dawn/sunrise sky
2258 if(activeJob()->getCoreProperty(SequenceJob::SJ_SkyFlat).toBool() && ExpRaw.size() > 2)
2259 {
2260 int remove = ExpRaw.size() - 2;
2261 ExpRaw.remove(0, remove);
2262 ADURaw.remove(0, remove);
2263 }
2264
2265 // Check if saturated, then take shorter capture and discard value
2266 ExpRaw.append(activeJob()->getCoreProperty(SequenceJob::SJ_Exposure).toDouble());
2267 ADURaw.append(currentADU);
2268
2269 qCDebug(KSTARS_EKOS_CAPTURE) << "Capture: Current ADU = " << currentADU << " targetADU = " << targetADU
2270 << " Exposure Count: " << ExpRaw.count();
2271
2272 // Most CCDs are quite linear so 1st degree polynomial is quite sufficient
2273 // But DSLRs can exhibit non-linear response curve and so a 2nd degree polynomial is more appropriate
2274 if (ExpRaw.count() >= 2)
2275 {
2276 if (ExpRaw.count() >= 5)
2277 {
2278 double chisq = 0;
2279
2280 coeff = gsl_polynomial_fit(ADURaw.data(), ExpRaw.data(), ExpRaw.count(), 2, chisq);
2281 qCDebug(KSTARS_EKOS_CAPTURE) << "Running polynomial fitting. Found " << coeff.size() << " coefficients.";
2282 if (std::isnan(coeff[0]) || std::isinf(coeff[0]))
2283 {
2284 qCDebug(KSTARS_EKOS_CAPTURE) << "Coefficients are invalid.";
2285 targetADUAlgorithm = ADU_LEAST_SQUARES;
2286 }
2287 else
2288 {
2289 nextExposure = coeff[0] + (coeff[1] * targetADU) + (coeff[2] * pow(targetADU, 2));
2290 // If exposure is not valid or does not make sense, then we fall back to least squares
2291 if (nextExposure < 0 || (nextExposure > ExpRaw.last() || targetADU < ADURaw.last())
2292 || (nextExposure < ExpRaw.last() || targetADU > ADURaw.last()))
2293 {
2294 nextExposure = 0;
2295 targetADUAlgorithm = ADU_LEAST_SQUARES;
2296 }
2297 else
2298 {
2299 targetADUAlgorithm = ADU_POLYNOMIAL;
2300 for (size_t i = 0; i < coeff.size(); i++)
2301 qCDebug(KSTARS_EKOS_CAPTURE) << "Coeff #" << i << "=" << coeff[i];
2302 }
2303 }
2304 }
2305
2306 bool looping = false;
2307 if (ExpRaw.count() >= 10)
2308 {
2309 int size = ExpRaw.count();
2310 looping = (std::fabs(ExpRaw[size - 1] - ExpRaw[size - 2] < 0.01)) &&
2311 (std::fabs(ExpRaw[size - 2] - ExpRaw[size - 3] < 0.01));
2312 if (looping && targetADUAlgorithm == ADU_POLYNOMIAL)
2313 {
2314 qWarning(KSTARS_EKOS_CAPTURE) << "Detected looping in polynomial results. Falling back to llsqr.";
2315 targetADUAlgorithm = ADU_LEAST_SQUARES;
2316 }
2317 }
2318
2319 // If we get invalid data, let's fall back to llsq
2320 // Since polyfit can be unreliable at low counts, let's only use it at the 5th exposure
2321 // if we don't have results already.
2322 if (targetADUAlgorithm == ADU_LEAST_SQUARES)
2323 {
2324 double a = 0, b = 0;
2325 llsq(ExpRaw, ADURaw, a, b);
2326
2327 // If we have valid results, let's calculate next exposure
2328 if (a != 0.0)
2329 {
2330 nextExposure = (targetADU - b) / a;
2331 // If we get invalid value, let's just proceed iteratively
2332 if (nextExposure < 0)
2333 nextExposure = 0;
2334 }
2335 }
2336 }
2337
2338 // 2022.01.12 Put a hard limit to 180 seconds.
2339 // If it goes over this limit, the flat source is probably off.
2340 if (nextExposure == 0.0 || nextExposure > 180)
2341 {
2342 if (currentADU < targetADU)
2343 nextExposure = activeJob()->getCoreProperty(SequenceJob::SJ_Exposure).toDouble() * 1.25;
2344 else
2345 nextExposure = activeJob()->getCoreProperty(SequenceJob::SJ_Exposure).toDouble() * .75;
2346 }
2347
2348 qCDebug(KSTARS_EKOS_CAPTURE) << "next flat exposure is" << nextExposure;
2349
2350 return nextExposure;
2351
2352}
2353
2355{
2356 ADURaw.clear();
2357 ExpRaw.clear();
2358}
2359
2361{
2362 if (devices()->mount() && activeCamera() && devices()->mount()->isConnected())
2363 {
2364 // Camera to current telescope
2365 auto activeDevices = activeCamera()->getText("ACTIVE_DEVICES");
2366 if (activeDevices)
2367 {
2368 auto activeTelescope = activeDevices->findWidgetByName("ACTIVE_TELESCOPE");
2369 if (activeTelescope)
2370 {
2371 activeTelescope->setText(devices()->mount()->getDeviceName().toLatin1().constData());
2372 activeCamera()->sendNewProperty(activeDevices);
2373 }
2374 }
2375 }
2376
2377}
2378
2380{
2382 if (activeCamera())
2383 all_devices.append(activeCamera());
2384 if (devices()->dustCap())
2385 all_devices.append(devices()->dustCap());
2386
2387 for (auto &oneDevice : all_devices)
2388 {
2389 auto activeDevices = oneDevice->getText("ACTIVE_DEVICES");
2390 if (activeDevices)
2391 {
2392 auto activeFilter = activeDevices->findWidgetByName("ACTIVE_FILTER");
2393 if (activeFilter)
2394 {
2396 if (devices()->filterWheel())
2397 {
2398 if (activeFilterText != devices()->filterWheel()->getDeviceName())
2399 {
2400 activeFilter->setText(devices()->filterWheel()->getDeviceName().toLatin1().constData());
2401 oneDevice->sendNewProperty(activeDevices);
2402 }
2403 }
2404 // Reset filter name in CCD driver
2405 else if (activeFilterText.isEmpty())
2406 {
2407 // Add debug info since this issue is reported by users. Need to know when it happens.
2408 qCDebug(KSTARS_EKOS_CAPTURE) << "No active filter wheel. " << oneDevice->getDeviceName() << " ACTIVE_FILTER is reset.";
2409 activeFilter->setText("");
2410 oneDevice->sendNewProperty(activeDevices);
2411 }
2412 }
2413 }
2414 }
2415}
2416
2417void CaptureProcess::updateFITSViewer(const QSharedPointer<FITSData> data, const FITSMode &captureMode,
2418 const FITSScale &captureFilter, const QString &filename, const QString &deviceName)
2419{
2420 // do nothing in case of empty data
2421 if (data.isNull())
2422 return;
2423
2424 switch (captureMode)
2425 {
2426 case FITS_NORMAL:
2427 case FITS_CALIBRATE:
2428 {
2429 if (Options::useFITSViewer())
2430 {
2431 QUrl fileURL = QUrl::fromLocalFile(filename);
2432 bool success = false;
2433 int tabIndex = -1;
2434 int *tabID = &m_fitsvViewerTabIDs.normalTabID;
2435 const bool isPreview = (activeJob() && activeJob()->jobType() == SequenceJob::JOBTYPE_PREVIEW);
2436 if (*tabID == -1 || Options::singlePreviewFITS() == false)
2437 {
2438 // If image is preview and we should display all captured images in a
2439 // single tab called "Preview", then set the title to "Preview",
2440 // Otherwise, the title will be the captured image name
2442 if (isPreview && Options::singlePreviewFITS())
2443 {
2444 // If we are displaying all images from all cameras in a single FITS
2445 // Viewer window, then we prefix the camera name to the "Preview" string
2446 if (Options::singleWindowCapturedFITS())
2447 previewTitle = i18n("%1 Preview", deviceName);
2448 else
2449 // Otherwise, just use "Preview"
2450 previewTitle = i18n("Preview");
2451 }
2452
2453 success = getFITSViewer()->loadData(data, fileURL, &tabIndex, captureMode, captureFilter, previewTitle);
2454
2455 //Setup any necessary connections
2456 auto tabs = getFITSViewer()->tabs();
2457 if (tabIndex < tabs.size() && captureMode == FITS_NORMAL)
2458 {
2459 emit newView(tabs[tabIndex]->getView());
2460 tabs[tabIndex]->disconnect(this);
2461 connect(tabs[tabIndex].get(), &FITSTab::updated, this, [this]
2462 {
2463 auto tab = qobject_cast<FITSTab *>(sender());
2464 emit newView(tab->getView());
2465 });
2466 }
2467 }
2468 else
2469 {
2470 if (isPreview)
2471 fileURL = QUrl::fromLocalFile("Preview");
2472 success = getFITSViewer()->updateData(data, fileURL, *tabID, &tabIndex, captureFilter, captureMode);
2473 }
2474
2475 if (!success)
2476 {
2477 // If opening file fails, we treat it the same as exposure failure
2478 // and recapture again if possible
2479 qCCritical(KSTARS_EKOS_CAPTURE()) << "error adding/updating FITS";
2480 return;
2481 }
2482 *tabID = tabIndex;
2483 if (Options::focusFITSOnNewImage())
2484 getFITSViewer()->raise();
2485
2486 return;
2487 }
2488 }
2489 break;
2490 default:
2491 break;
2492 }
2493}
2494
2496 const QString &targetName, bool setOptions)
2497{
2498 state()->clearCapturedFramesMap();
2499 auto queue = state()->getSequenceQueue();
2500 if (!queue->load(fileURL, targetName, devices(), state()))
2501 {
2502 QString message = i18n("Unable to open file %1", fileURL);
2503 KSNotification::sorry(message, i18n("Could Not Open File"));
2504 return false;
2505 }
2506
2507 if (setOptions)
2508 {
2509 queue->setOptions();
2510 // Set the HFR Check value appropriately for the conditions, e.g. using Autofocus
2511 state()->updateHFRThreshold();
2512 }
2513
2514 for (auto j : state()->allJobs())
2515 emit addJob(j);
2516
2517 return true;
2518}
2519
2520bool CaptureProcess::saveSequenceQueue(const QString &path, bool loadOptions)
2521{
2522 if (loadOptions)
2523 state()->getSequenceQueue()->loadOptions();
2524 return state()->getSequenceQueue()->save(path, state()->observerName());
2525}
2526
2527void CaptureProcess::setCamera(bool connection)
2528{
2529 if (connection)
2530 {
2531 // TODO: do not simply forward the newExposureValue
2532 connect(activeCamera(), &ISD::Camera::newExposureValue, this, &CaptureProcess::setExposureProgress, Qt::UniqueConnection);
2533 connect(activeCamera(), &ISD::Camera::newImage, this, &CaptureProcess::processFITSData, Qt::UniqueConnection);
2534 connect(activeCamera(), &ISD::Camera::newRemoteFile, this, &CaptureProcess::processNewRemoteFile, Qt::UniqueConnection);
2535 connect(activeCamera(), &ISD::Camera::ready, this, &CaptureProcess::cameraReady, Qt::UniqueConnection);
2536 }
2537 else
2538 {
2539 // TODO: do not simply forward the newExposureValue
2540 disconnect(activeCamera(), &ISD::Camera::newExposureValue, this, &CaptureProcess::setExposureProgress);
2541 disconnect(activeCamera(), &ISD::Camera::newImage, this, &CaptureProcess::processFITSData);
2542 disconnect(activeCamera(), &ISD::Camera::newRemoteFile, this, &CaptureProcess::processNewRemoteFile);
2543 // disconnect(m_Camera, &ISD::Camera::previewFITSGenerated, this, &Capture::setGeneratedPreviewFITS);
2544 disconnect(activeCamera(), &ISD::Camera::ready, this, &CaptureProcess::cameraReady);
2545 }
2546
2547}
2548
2549bool CaptureProcess::setFilterWheel(ISD::FilterWheel * device)
2550{
2551 if (devices()->filterWheel() && devices()->filterWheel() == device)
2552 return false;
2553
2554 if (devices()->filterWheel())
2555 devices()->filterWheel()->disconnect(this);
2556
2557 devices()->setFilterWheel(device);
2558
2559 return (device != nullptr);
2560}
2561
2562bool CaptureProcess::checkPausing(CaptureModuleState::ContinueAction continueAction)
2563{
2564 if (state()->getCaptureState() == CAPTURE_PAUSE_PLANNED)
2565 {
2566 emit newLog(i18n("Sequence paused."));
2567 state()->setCaptureState(CAPTURE_PAUSED);
2568 // disconnect camera device
2569 setCamera(false);
2570 // save continue action
2571 state()->setContinueAction(continueAction);
2572 // pause
2573 return true;
2574 }
2575 // no pause
2576 return false;
2577}
2578
2580{
2581 SequenceJob * first_job = nullptr;
2582
2583 // search for idle or aborted jobs
2584 for (auto &job : state()->allJobs())
2585 {
2586 if (job->getStatus() == JOB_IDLE || job->getStatus() == JOB_ABORTED)
2587 {
2588 first_job = job;
2589 break;
2590 }
2591 }
2592
2593 // If there are no idle nor aborted jobs, question is whether to reset and restart
2594 // Scheduler will start a non-empty new job each time and doesn't use this execution path
2595 if (first_job == nullptr)
2596 {
2597 // If we have at least one job that are in error, bail out, even if ignoring job progress
2598 for (auto &job : state()->allJobs())
2599 {
2600 if (job->getStatus() != JOB_DONE)
2601 {
2602 // If we arrived here with a zero-delay timer, raise the interval before returning to avoid a cpu peak
2603 if (state()->getCaptureDelayTimer().isActive())
2604 {
2605 if (state()->getCaptureDelayTimer().interval() <= 0)
2606 state()->getCaptureDelayTimer().setInterval(1000);
2607 }
2608 return nullptr;
2609 }
2610 }
2611
2612 // If we only have completed jobs and we don't ignore job progress, ask the end-user what to do
2613 if (!state()->ignoreJobProgress())
2615 nullptr,
2616 i18n("All jobs are complete. Do you want to reset the status of all jobs and restart capturing?"),
2617 i18n("Reset job status"), KStandardGuiItem::cont(), KStandardGuiItem::cancel(),
2618 "reset_job_complete_status_warning") != KMessageBox::Continue)
2619 return nullptr;
2620
2621 // If the end-user accepted to reset, reset all jobs and restart
2622 resetAllJobs();
2623
2624 first_job = state()->allJobs().first();
2625 }
2626 // If we need to ignore job progress, systematically reset all jobs and restart
2627 // Scheduler will never ignore job progress and doesn't use this path
2628 else if (state()->ignoreJobProgress())
2629 {
2630 emit newLog(i18n("Warning: option \"Always Reset Sequence When Starting\" is enabled and resets the sequence counts."));
2631 resetAllJobs();
2632 }
2633
2634 return first_job;
2635}
2636
2637void CaptureProcess::resetJobStatus(JOBStatus newStatus)
2638{
2639 if (activeJob() != nullptr)
2640 {
2641 activeJob()->resetStatus(newStatus);
2642 emit updateJobTable(activeJob());
2643 }
2644}
2645
2646void CaptureProcess::resetAllJobs()
2647{
2648 for (auto &job : state()->allJobs())
2649 {
2650 job->resetStatus();
2651 }
2652 // clear existing job counts
2653 m_State->clearCapturedFramesMap();
2654 // update the entire job table
2655 emit updateJobTable(nullptr);
2656}
2657
2658void CaptureProcess::updatedCaptureCompleted(int count)
2659{
2660 activeJob()->setCompleted(count);
2661 emit updateJobTable(activeJob());
2662}
2663
2664void CaptureProcess::llsq(QVector<double> x, QVector<double> y, double &a, double &b)
2665{
2666 double bot;
2667 int i;
2668 double top;
2669 double xbar;
2670 double ybar;
2671 int n = x.count();
2672 //
2673 // Special case.
2674 //
2675 if (n == 1)
2676 {
2677 a = 0.0;
2678 b = y[0];
2679 return;
2680 }
2681 //
2682 // Average X and Y.
2683 //
2684 xbar = 0.0;
2685 ybar = 0.0;
2686 for (i = 0; i < n; i++)
2687 {
2688 xbar = xbar + x[i];
2689 ybar = ybar + y[i];
2690 }
2691 xbar = xbar / static_cast<double>(n);
2692 ybar = ybar / static_cast<double>(n);
2693 //
2694 // Compute Beta.
2695 //
2696 top = 0.0;
2697 bot = 0.0;
2698 for (i = 0; i < n; i++)
2699 {
2700 top = top + (x[i] - xbar) * (y[i] - ybar);
2701 bot = bot + (x[i] - xbar) * (x[i] - xbar);
2702 }
2703
2704 a = top / bot;
2705
2706 b = ybar - a * xbar;
2707
2708}
2709
2711{
2712 // TODO based on user feedback on what paramters are most useful to pass
2713 return QStringList();
2714}
2715
2717{
2718 if (devices()->getActiveCamera() && devices()->getActiveCamera()->hasCoolerControl())
2719 return true;
2720
2721 return false;
2722}
2723
2725{
2726 if (devices()->getActiveCamera() && devices()->getActiveCamera()->hasCoolerControl())
2727 return devices()->getActiveCamera()->setCoolerControl(enable);
2728
2729 return false;
2730}
2731
2733{
2734 connect(KSMessageBox::Instance(), &KSMessageBox::accepted, this, [this, name]()
2735 {
2736 KSMessageBox::Instance()->disconnect(this);
2738 emit driverTimedout(name);
2739 });
2740 connect(KSMessageBox::Instance(), &KSMessageBox::rejected, this, [this]()
2741 {
2742 KSMessageBox::Instance()->disconnect(this);
2743 });
2744
2745 KSMessageBox::Instance()->questionYesNo(i18n("Are you sure you want to restart %1 camera driver?", name),
2746 i18n("Driver Restart"), 5);
2747}
2748
2750{
2751 if (!activeCamera())
2752 return QStringList();
2753
2754 ISD::CameraChip *tChip = devices()->getActiveCamera()->getChip(ISD::CameraChip::PRIMARY_CCD);
2755
2756 return tChip->getFrameTypes();
2757}
2758
2760{
2761 if (devices()->getFilterManager().isNull())
2762 return QStringList();
2763
2764 return devices()->getFilterManager()->getFilterLabels();
2765}
2766
2768{
2769 if (devices()->getActiveCamera()->getProperty("CCD_GAIN"))
2770 {
2771 if (value >= 0)
2772 {
2774 ccdGain["GAIN"] = value;
2775 propertyMap["CCD_GAIN"] = ccdGain;
2776 }
2777 else
2778 {
2779 propertyMap["CCD_GAIN"].remove("GAIN");
2780 if (propertyMap["CCD_GAIN"].size() == 0)
2781 propertyMap.remove("CCD_GAIN");
2782 }
2783 }
2784 else if (devices()->getActiveCamera()->getProperty("CCD_CONTROLS"))
2785 {
2786 if (value >= 0)
2787 {
2788 QMap<QString, QVariant> ccdGain = propertyMap["CCD_CONTROLS"];
2789 ccdGain["Gain"] = value;
2790 propertyMap["CCD_CONTROLS"] = ccdGain;
2791 }
2792 else
2793 {
2794 propertyMap["CCD_CONTROLS"].remove("Gain");
2795 if (propertyMap["CCD_CONTROLS"].size() == 0)
2796 propertyMap.remove("CCD_CONTROLS");
2797 }
2798 }
2799}
2800
2802{
2803 if (devices()->getActiveCamera()->getProperty("CCD_OFFSET"))
2804 {
2805 if (value >= 0)
2806 {
2808 ccdOffset["OFFSET"] = value;
2809 propertyMap["CCD_OFFSET"] = ccdOffset;
2810 }
2811 else
2812 {
2813 propertyMap["CCD_OFFSET"].remove("OFFSET");
2814 if (propertyMap["CCD_OFFSET"].size() == 0)
2815 propertyMap.remove("CCD_OFFSET");
2816 }
2817 }
2818 else if (devices()->getActiveCamera()->getProperty("CCD_CONTROLS"))
2819 {
2820 if (value >= 0)
2821 {
2822 QMap<QString, QVariant> ccdOffset = propertyMap["CCD_CONTROLS"];
2823 ccdOffset["Offset"] = value;
2824 propertyMap["CCD_CONTROLS"] = ccdOffset;
2825 }
2826 else
2827 {
2828 propertyMap["CCD_CONTROLS"].remove("Offset");
2829 if (propertyMap["CCD_CONTROLS"].size() == 0)
2830 propertyMap.remove("CCD_CONTROLS");
2831 }
2832 }
2833}
2834
2835QSharedPointer<FITSViewer> CaptureProcess::getFITSViewer()
2836{
2837 // if the FITS viewer exists, return it
2838 if (!m_FITSViewerWindow.isNull() && ! m_FITSViewerWindow.isNull())
2839 return m_FITSViewerWindow;
2840
2841 // otherwise, create it
2842 m_fitsvViewerTabIDs = {-1, -1, -1, -1, -1};
2843
2844 m_FITSViewerWindow = KStars::Instance()->createFITSViewer();
2845
2846 // Check if ONE tab of the viewer was closed.
2847 connect(m_FITSViewerWindow.get(), &FITSViewer::closed, this, [this](int tabIndex)
2848 {
2849 if (tabIndex == m_fitsvViewerTabIDs.normalTabID)
2850 m_fitsvViewerTabIDs.normalTabID = -1;
2851 else if (tabIndex == m_fitsvViewerTabIDs.calibrationTabID)
2852 m_fitsvViewerTabIDs.calibrationTabID = -1;
2853 else if (tabIndex == m_fitsvViewerTabIDs.focusTabID)
2854 m_fitsvViewerTabIDs.focusTabID = -1;
2855 else if (tabIndex == m_fitsvViewerTabIDs.guideTabID)
2856 m_fitsvViewerTabIDs.guideTabID = -1;
2857 else if (tabIndex == m_fitsvViewerTabIDs.alignTabID)
2858 m_fitsvViewerTabIDs.alignTabID = -1;
2859 });
2860
2861 // If FITS viewer was completed closed. Reset everything
2862 connect(m_FITSViewerWindow.get(), &FITSViewer::terminated, this, [this]()
2863 {
2864 m_fitsvViewerTabIDs = {-1, -1, -1, -1, -1};
2865 m_FITSViewerWindow.clear();
2866 });
2867
2868 return m_FITSViewerWindow;
2869}
2870
2871ISD::Camera *CaptureProcess::activeCamera()
2872{
2873 return devices()->getActiveCamera();
2874}
2875} // Ekos namespace
void processCaptureError(ISD::Camera::ErrorType type)
processCaptureError Handle when image capture fails
void clearFlatCache()
clearFlatCache Clear the measured values for flat calibrations
QStringList generateScriptArguments() const
generateScriptArguments Generate argument list to pass to capture script
IPState processPreCaptureCalibrationStage()
processPreCaptureCalibrationStage Execute the tasks that need to be completed before capturing may st...
void processCaptureTimeout()
processCaptureTimeout If exposure timed out, let's handle it.
IPState resumeSequence()
resumeSequence Try to continue capturing.
void setExposureProgress(ISD::CameraChip *tChip, double value, IPState state)
setExposureProgress Manage exposure progress reported by the camera device.
QStringList frameTypes()
frameTypes Retrieve the frame types from the active camera's primary chip.
void updateFITSViewer(const QSharedPointer< FITSData > data, const FITSMode &captureMode, const FITSScale &captureFilter, const QString &filename, const QString &deviceName)
updateFITSViewer display new image in the configured FITSViewer tab.
void processFITSData(const QSharedPointer< FITSData > &data, const QString &extension)
newFITS process new FITS data received from camera.
bool saveSequenceQueue(const QString &path, bool loadOptions=true)
Saves the Sequence Queue to the Ekos Sequence Queue file.
void prepareJobExecution()
preparePreCaptureActions Trigger setting the filter, temperature, (if existing) the rotator angle and...
void checkCamera()
configureCamera Refreshes the CCD information in the capture module.
void updateCompletedCaptureCountersAction()
updateCompletedCaptureCounters Update counters if an image has been captured
void setDownloadProgress()
setDownloadProgress update the Capture Module and Summary Screen's estimate of how much time is left ...
void removeDevice(const QSharedPointer< ISD::GenericDevice > &device)
Generic method for removing any connected device.
bool setCamera(ISD::Camera *device)
setCamera Connect to the given camera device (and deconnect the old one if existing)
void syncDSLRToTargetChip(const QString &model)
syncDSLRToTargetChip Syncs INDI driver CCD_INFO property to the DSLR values.
IPState runCaptureScript(ScriptTypes scriptType, bool precond=true)
runCaptureScript Run the pre-/post capture/job script
QStringList filterLabels()
filterLabels list of currently available filter labels
void checkNextExposure()
checkNextExposure Try to start capturing the next exposure (
bool hasCoolerControl()
Does the CCD has a cooler control (On/Off) ?
double calculateFlatExpTime(double currentADU)
calculateFlatExpTime calculate the next flat exposure time from the measured ADU value
void prepareActiveJobStage2()
prepareActiveJobStage2 Reset #calibrationStage and continue with preparePreCaptureActions().
IPState startNextExposure()
startNextExposure Ensure that all pending preparation tasks are be completed (focusing,...
bool checkPausing(CaptureModuleState::ContinueAction continueAction)
checkPausing check if a pause has been planned and pause subsequently
IPState previewImageCompletedAction()
previewImageCompletedAction Activities required when a preview image has been captured.
void restartCamera(const QString &name)
restartCamera Restarts the INDI driver associated with a camera.
IPState continueFramingAction(const QSharedPointer< FITSData > &imageData)
continueFramingAction If framing is running, start the next capture sequence
IPState startNextJob()
startNextJob Select the next job that is either idle or aborted and call prepareJob(*SequenceJob) to ...
void updateGain(double value, QMap< QString, QMap< QString, QVariant > > &propertyMap)
getGain Update the gain value from the custom property value.
void scriptFinished(int exitCode, QProcess::ExitStatus status)
scriptFinished Slot managing the return status of pre/post capture/job scripts
void captureImage()
captureImage Initiates image capture in the active job.
void updatePreCaptureCalibrationStatus()
updatePreCaptureCalibrationStatus This is a wrapping loop for processPreCaptureCalibrationStage(),...
void updateTelescopeInfo()
updateTelescopeInfo Update the scope information in the camera's INDI driver.
void updateFilterInfo()
updateFilterInfo Update the filter information in the INDI drivers of the current camera and dust cap
void stopCapturing(CaptureState targetState)
stopCapturing Stopping the entire capturing state (envelope for aborting, suspending,...
Q_SCRIPTABLE void resetFrame()
resetFrame Reset frame settings of the camera
void processJobCompletion1()
processJobCompletionStage1 Process job completion.
void updateOffset(double value, QMap< QString, QMap< QString, QVariant > > &propertyMap)
getOffset Update the offset value from the custom property value.
void processNewRemoteFile(QString file)
setNewRemoteFile A new image has been stored as remote file
bool loadSequenceQueue(const QString &fileURL, const QString &targetName="", bool setOptions=true)
Loads the Ekos Sequence Queue file in the Sequence Queue.
bool setCoolerControl(bool enable)
Set the CCD cooler ON/OFF.
IPState updateDownloadTimesAction()
updateDownloadTimesAction Add the current download time to the list of already measured ones
void reconnectCameraDriver(const QString &camera, const QString &filterWheel)
reconnectDriver Reconnect the camera driver
SequenceJob * findNextPendingJob()
findExecutableJob find next job to be executed
bool checkFlatCalibration(QSharedPointer< FITSData > imageData, double exp_min, double exp_max)
checkFlatCalibration check the flat calibration
void prepareJob(SequenceJob *job)
prepareJob Update the counters of existing frames and continue with prepareActiveJob(),...
void selectCamera(QString name)
setCamera select camera device
IPState checkLightFramePendingTasks()
Check all tasks that might be pending before capturing may start.
void processJobCompletion2()
processJobCompletionStage2 Stop execution of the current sequence and check whether there exists a ne...
IPState updateImageMetadataAction(QSharedPointer< FITSData > imageData)
updateImageMetadataAction Update meta data of a captured image
bool setFilterWheel(ISD::FilterWheel *device)
setFilterWheel Connect to the given filter wheel device (and deconnect the old one if existing)
CameraChip class controls a particular chip in camera.
Camera class controls an INDI Camera device.
Definition indicamera.h:44
void sendNewProperty(INDI::Property prop)
Send new property command to server.
INDI::PropertyView< IText > * getText(const QString &name) const
Class handles control of INDI dome devices.
Definition indidome.h:23
Handles operation of a remotely controlled dust cover cap.
Definition indidustcap.h:23
Handles operation of a remotely controlled light box.
device handle controlling Mounts.
Definition indimount.h:27
void newTargetName(const QString &name)
The mount has finished the slew to a new target.
Rotator class handles control of INDI Rotator devices.
Definition indirotator.h:20
This is the main window for KStars.
Definition kstars.h:91
static KStars * Instance()
Definition kstars.h:123
QString i18n(const char *text, const TYPE &arg...)
Ekos is an advanced Astrophotography tool for Linux.
Definition align.cpp:79
CaptureState
Capture states.
Definition ekos.h:92
@ CAPTURE_DITHERING
Definition ekos.h:102
@ CAPTURE_PROGRESS
Definition ekos.h:94
@ CAPTURE_PAUSE_PLANNED
Definition ekos.h:96
@ CAPTURE_PAUSED
Definition ekos.h:97
@ CAPTURE_FOCUSING
Definition ekos.h:103
@ CAPTURE_IMAGE_RECEIVED
Definition ekos.h:101
@ CAPTURE_SUSPENDED
Definition ekos.h:98
@ CAPTURE_ABORTED
Definition ekos.h:99
@ CAPTURE_COMPLETE
Definition ekos.h:112
@ CAPTURE_CAPTURING
Definition ekos.h:95
@ CAPTURE_IDLE
Definition ekos.h:93
ScriptTypes
Definition ekos.h:173
@ SCRIPT_POST_CAPTURE
Script to run after a sequence capture is completed.
Definition ekos.h:176
@ SCRIPT_POST_JOB
Script to run after a sequence job is completed.
Definition ekos.h:177
@ SCRIPT_PRE_CAPTURE
Script to run before a sequence capture is started.
Definition ekos.h:175
@ SCRIPT_PRE_JOB
Script to run before a sequence job is started.
Definition ekos.h:174
ButtonCode warningContinueCancel(QWidget *parent, const QString &text, const QString &title=QString(), const KGuiItem &buttonContinue=KStandardGuiItem::cont(), const KGuiItem &buttonCancel=KStandardGuiItem::cancel(), const QString &dontAskAgainName=QString(), Options options=Notify)
KGuiItem cont()
KGuiItem cancel()
NETWORKMANAGERQT_EXPORT NetworkManager::Status status()
void accepted()
void rejected()
QChar separator()
Int toInt() const const
QMetaObject::Connection connect(const QObject *sender, PointerToMemberFunction signal, Functor functor)
bool disconnect(const QMetaObject::Connection &connection)
QObject * sender() const const
int x() const const
int y() const const
void errorOccurred(QProcess::ProcessError error)
void finished(int exitCode, QProcess::ExitStatus exitStatus)
void readyReadStandardError()
void readyReadStandardOutput()
void start(OpenMode mode)
int height() const const
int width() const const
int x() const const
int y() const const
T * get() const const
bool isNull() const const
QString arg(Args &&... args) const const
bool isEmpty() const const
QString number(double n, char format, int precision)
UniqueConnection
QFuture< ArgsType< Signal > > connect(Sender *sender, Signal signal)
void timeout()
QUrl fromLocalFile(const QString &localFile)
bool isValid() const const
double toDouble(bool *ok) const const
int toInt(bool *ok) const const
QPoint toPoint() const const
QRect toRect() const const
QString toString() const const
uint toUInt(bool *ok) const const
Object to hold FITS Header records.
Definition fitsdata.h:88
This file is part of the KDE documentation.
Documentation copyright © 1996-2024 The KDE developers.
Generated on Fri May 17 2024 11:48:25 by doxygen 1.10.0 written by Dimitri van Heesch, © 1997-2006

KDE's Doxygen guidelines are available online.