Kstars

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

KDE's Doxygen guidelines are available online.