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

KDE's Doxygen guidelines are available online.