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

KDE's Doxygen guidelines are available online.