Kstars

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

KDE's Doxygen guidelines are available online.