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 {
264 SequenceJob *nextJob = findNextPendingJob();
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
281void CameraProcess::jobCreated(SequenceJob *newJob)
282{
283 if (newJob == nullptr)
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 // Delete preview job
397 activeJob()->deleteLater();
398 // Clear active job
399 state()->setActiveJob(nullptr);
400 }
401 }
402
403 // stop focusing if capture is aborted
404 if (state()->getCaptureState() == CAPTURE_FOCUSING && targetState == CAPTURE_ABORTED)
405 emit abortFocus();
406
407 state()->setCaptureState(targetState);
408
409 state()->setLooping(false);
410 state()->setBusy(false);
411
412 state()->getCaptureDelayTimer().stop();
413
414 state()->setActiveJob(nullptr);
415
416 // Turn off any calibration light, IF they were turned on by Capture module
417 if (devices()->lightBox() && state()->lightBoxLightEnabled())
418 {
419 state()->setLightBoxLightEnabled(false);
420 devices()->lightBox()->setLightEnabled(false);
421 }
422
423 // disconnect camera device
424 setCamera(false);
425
426 // In case of exposure looping, let's abort
427 if (devices()->getActiveCamera() && devices()->getActiveChip()
428 && devices()->getActiveCamera()->isFastExposureEnabled())
429 devices()->getActiveChip()->abortExposure();
430
431 // communicate successful stop
432 emit captureStopped();
433}
434
436{
437 if (state()->isCaptureRunning() == false)
438 {
439 // Ensure that the pause function is only called during frame capturing
440 // Handling it this way is by far easier than trying to enable/disable the pause button
441 // Fixme: make pausing possible at all stages. This makes it necessary to separate the pausing states from CaptureState.
442 emit newLog(i18n("Pausing only possible while frame capture is running."));
443 qCInfo(KSTARS_EKOS_CAPTURE) << "Pause button pressed while not capturing.";
444 return;
445 }
446 // we do not decide at this stage how to resume, since pause is only planned here
447 state()->setContinueAction(CAPTURE_CONTINUE_ACTION_NONE);
448 state()->setCaptureState(CAPTURE_PAUSE_PLANNED);
449 emit newLog(i18n("Sequence shall be paused after current exposure is complete."));
450}
451
452void CameraProcess::startJob(SequenceJob *job)
453{
454 state()->initCapturePreparation();
455 prepareJob(job);
456}
457
458void CameraProcess::prepareJob(SequenceJob * job)
459{
460 if (activeCamera() == nullptr || activeCamera()->isConnected() == false)
461 {
462 emit newLog(i18n("No camera detected. Check train configuration and connection settings."));
463 activeJob()->abort();
464 return;
465 }
466
467 state()->setActiveJob(job);
468
469 // If job is Preview and NO view is available, ask to enable it.
470 // if job is batch job, then NO VIEW IS REQUIRED at all. It's optional.
471 if (job->jobType() == SequenceJob::JOBTYPE_PREVIEW && Options::useFITSViewer() == false
472 && Options::useSummaryPreview() == false)
473 {
474 // ask if FITS viewer usage should be enabled
475 connect(KSMessageBox::Instance(), &KSMessageBox::accepted, this, [ = ]()
476 {
477 KSMessageBox::Instance()->disconnect(this);
478 Options::setUseFITSViewer(true);
479 // restart
480 prepareJob(job);
481 });
482 connect(KSMessageBox::Instance(), &KSMessageBox::rejected, this, [&]()
483 {
484 KSMessageBox::Instance()->disconnect(this);
485 activeJob()->abort();
486 });
487 KSMessageBox::Instance()->questionYesNo(i18n("No view available for previews. Enable FITS viewer?"),
488 i18n("Display preview"), 15);
489 // do nothing because currently none of the previews is active.
490 return;
491 }
492
493 if (state()->isLooping() == false)
494 qCDebug(KSTARS_EKOS_CAPTURE) << "Preparing capture job" << job->getSignature() << "for execution.";
495
496 if (activeJob()->jobType() != SequenceJob::JOBTYPE_PREVIEW)
497 {
498 // set the progress info
499
500 if (activeCamera()->getUploadMode() != ISD::Camera::UPLOAD_REMOTE)
501 state()->setNextSequenceID(1);
502
503 // We check if the job is already fully or partially complete by checking how many files of its type exist on the file system
504 // The signature is the unique identification path in the system for a particular job. Format is "<storage path>/<target>/<frame type>/<filter name>".
505 // 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/...)
506 // 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/...)
507 QString signature = activeJob()->getSignature();
508
509 // Now check on the file system ALL the files that exist with the above signature
510 // If 29 files exist for example, then nextSequenceID would be the NEXT file number (30)
511 // Therefore, we know how to number the next file.
512 // However, we do not deduce the number of captures to process from this function.
513 state()->checkSeqBoundary();
514
515 // Captured Frames Map contains a list of signatures:count of _already_ captured files in the file system.
516 // This map is set by the Scheduler in order to complete efficiently the required captures.
517 // When the end-user requests a sequence to be processed, that map is empty.
518 //
519 // Example with a 5xL-5xR-5xG-5xB sequence
520 //
521 // When the end-user loads and runs this sequence, each filter gets to capture 5 frames, then the procedure stops.
522 // When the Scheduler executes a job with this sequence, the procedure depends on what is in the storage.
523 //
524 // Let's consider the Scheduler has 3 instances of this job to run.
525 //
526 // When the first job completes the sequence, there are 20 images in the file system (5 for each filter).
527 // 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.
528 // 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.
529 //
530 // 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.
531 // 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.
532 // When the sequence runs, the procedure will bypass LRG and proceed to capture 4xB.
533 int count = state()->capturedFramesCount(signature);
534 if (count > 0)
535 {
536
537 // Count how many captures this job has to process, given that previous jobs may have done some work already
538 for (auto &a_job : state()->allJobs())
539 if (a_job == activeJob())
540 break;
541 else if (a_job->getSignature() == activeJob()->getSignature())
542 count -= a_job->getCompleted();
543
544 // This is the current completion count of the current job
545 updatedCaptureCompleted(count);
546 }
547 // JM 2018-09-24: Only set completed jobs to 0 IF the scheduler set captured frames map to begin with
548 // If the map is empty, then no scheduler is used and it should proceed as normal.
549 else if (state()->hasCapturedFramesMap())
550 {
551 // No preliminary information, we reset the job count and run the job unconditionally to clarify the behavior
552 updatedCaptureCompleted(0);
553 }
554 // JM 2018-09-24: In case ignoreJobProgress is enabled
555 // We check if this particular job progress ignore flag is set. If not,
556 // then we set it and reset completed to zero. Next time it is evaluated here again
557 // It will maintain its count regardless
558 else if (state()->ignoreJobProgress()
559 && activeJob()->getJobProgressIgnored() == false)
560 {
561 activeJob()->setJobProgressIgnored(true);
562 updatedCaptureCompleted(0);
563 }
564 // 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
565
566 // Check whether active job is complete by comparing required captures to what is already available
567 if (activeJob()->getCoreProperty(SequenceJob::SJ_Count).toInt() <=
568 activeJob()->getCompleted())
569 {
570 updatedCaptureCompleted(activeJob()->getCoreProperty(
571 SequenceJob::SJ_Count).toInt());
572 emit newLog(i18n("Job requires %1-second %2 images, has already %3/%4 captures and does not need to run.",
573 QString("%L1").arg(job->getCoreProperty(SequenceJob::SJ_Exposure).toDouble(), 0, 'f', 3),
574 job->getCoreProperty(SequenceJob::SJ_Filter).toString(),
575 activeJob()->getCompleted(),
576 activeJob()->getCoreProperty(SequenceJob::SJ_Count).toInt()));
578
579 /* FIXME: find a clearer way to exit here */
580 return;
581 }
582 else
583 {
584 if (activeJob()->getFrameType() != FRAME_VIDEO)
585 {
586 // There are captures to process
587 emit newLog(i18n("Job requires %1-second %2 images, has %3/%4 frames captured and will be processed.",
588 QString("%L1").arg(job->getCoreProperty(SequenceJob::SJ_Exposure).toDouble(), 0, 'f', 3),
589 job->getCoreProperty(SequenceJob::SJ_Filter).toString(),
590 activeJob()->getCompleted(),
591 activeJob()->getCoreProperty(SequenceJob::SJ_Count).toInt()));
592
593 // Emit progress update - done a few lines below
594 // emit newImage(nullptr, activeJob());
595
596 activeCamera()->setNextSequenceID(state()->nextSequenceID());
597 }
598 else
599 {
600 emit newLog(i18n("Job requires %1 x %2-second %3 video and will be processed.",
601 activeJob()->getCoreProperty(SequenceJob::SJ_Count).toInt(),
602 QString("%L1").arg(activeJob()->getCoreProperty(SequenceJob::SJ_Exposure).toDouble(), 0, 'f', 3),
603 activeJob()->getCoreProperty(SequenceJob::SJ_Filter).toString()));
604 }
605 }
606 }
607
608 if (activeCamera()->isBLOBEnabled() == false)
609 {
610 // FIXME: Move this warning pop-up elsewhere, it will interfere with automation.
611 // if (Options::guiderType() != Ekos::Guide::GUIDE_INTERNAL || KMessageBox::questionYesNo(nullptr, i18n("Image transfer is disabled for this camera. Would you like to enable it?")) ==
612 // KMessageBox::Yes)
613 if (Options::guiderType() != Guide::GUIDE_INTERNAL)
614 {
615 activeCamera()->setBLOBEnabled(true);
616 }
617 else
618 {
619 connect(KSMessageBox::Instance(), &KSMessageBox::accepted, this, [this]()
620 {
621 KSMessageBox::Instance()->disconnect(this);
622 activeCamera()->setBLOBEnabled(true);
624
625 });
626 connect(KSMessageBox::Instance(), &KSMessageBox::rejected, this, [this]()
627 {
628 KSMessageBox::Instance()->disconnect(this);
629 activeCamera()->setBLOBEnabled(true);
630 state()->setBusy(false);
631 });
632
633 KSMessageBox::Instance()->questionYesNo(i18n("Image transfer is disabled for this camera. Would you like to enable it?"),
634 i18n("Image Transfer"), 15);
635
636 return;
637 }
638 }
639
640 emit jobPrepared(job);
641
643
644}
645
647{
648 if (activeJob() == nullptr)
649 {
650 qWarning(KSTARS_EKOS_CAPTURE) << "prepareActiveJobStage1 with null activeJob().";
651 }
652 else
653 {
654 // JM 2020-12-06: Check if we need to execute pre-job script first.
655 // Only run pre-job script for the first time and not after some images were captured but then stopped due to abort.
656 if (runCaptureScript(SCRIPT_PRE_JOB, activeJob()->getCompleted() == 0) == IPS_BUSY)
657 return;
658 }
660}
661
663{
664 // Just notification of active job stating up
665 if (activeJob() == nullptr)
666 {
667 qWarning(KSTARS_EKOS_CAPTURE) << "prepareActiveJobStage2 with null activeJob().";
668 }
669 else
670 emit newImage(activeJob(), state()->imageData());
671
672
673 /* Disable this restriction, let the sequence run even if focus did not run prior to the capture.
674 * Besides, this locks up the Scheduler when the Capture module starts a sequence without any prior focus procedure done.
675 * This is quite an old code block. The message "Manual scheduled" seems to even refer to some manual intervention?
676 * With the new HFR threshold, it might be interesting to prevent the execution because we actually need an HFR value to
677 * 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,
678 * and on the other hand the focus procedure will deduce the next HFR automatically.
679 * But in the end, it's not entirely clear what the intent was. Note there is still a warning that a preliminary autofocus
680 * procedure is important to avoid any surprise that could make the whole schedule ineffective.
681 */
682 // JM 2020-12-06: Check if we need to execute pre-capture script first.
683 if (runCaptureScript(SCRIPT_PRE_CAPTURE) == IPS_BUSY)
684 return;
685
687}
688
690{
691 if (activeJob() == nullptr)
692 {
693 qWarning(KSTARS_EKOS_CAPTURE) << "executeJob with null activeJob().";
694 return;
695 }
696
697 // Double check all pointers are valid.
698 if (!activeCamera() || !devices()->getActiveChip())
699 {
700 checkCamera();
701 checkCaptureOperationsTimeout(std::bind(&CameraProcess::executeJob, this));
702 return;
703 }
704
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());
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 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 SequenceJob *thejob = activeJob();
1183
1184 if (thejob == nullptr)
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{
1413 SequenceJob * next_job = nullptr;
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 auto lastSeparator = remoteUpload.lastIndexOf(QDir::separator());
1546 auto remoteDirectory = remoteUpload.mid(0, lastSeparator);
1547 auto remoteFilename = remoteUpload.mid(lastSeparator + 1);
1548 activeJob()->setCoreProperty(SequenceJob::SJ_RemoteFormatDirectory, remoteDirectory);
1549 activeJob()->setCoreProperty(SequenceJob::SJ_RemoteFormatFilename, remoteFilename);
1550 }
1551
1552 // now hand over the control of capturing to the sequence job. As soon as capturing
1553 // has started, the sequence job will report the result with the captureStarted() event
1554 // that will trigger Capture::captureStarted()
1555 activeJob()->startCapturing(state()->getRefocusState()->isAutoFocusReady(),
1556 activeJob()->getCalibrationStage() == SequenceJobState::CAL_CALIBRATION ? FITS_CALIBRATE :
1557 FITS_NORMAL);
1558
1559 // Re-enable fast exposure if it was disabled before due to pending tasks
1560 if (state()->isRememberFastExposure())
1561 {
1562 state()->setRememberFastExposure(false);
1563 activeCamera()->setFastExposureEnabled(true);
1564 }
1565
1566 emit captureTarget(activeJob()->getCoreProperty(SequenceJob::SJ_TargetName).toString());
1567 emit captureImageStarted();
1568}
1569
1571{
1572 devices()->setActiveChip(state()->useGuideHead() ?
1573 devices()->getActiveCamera()->getChip(
1574 ISD::CameraChip::GUIDE_CCD) :
1575 devices()->getActiveCamera()->getChip(ISD::CameraChip::PRIMARY_CCD));
1576 devices()->getActiveChip()->resetFrame();
1577 emit updateFrameProperties(1);
1578}
1579
1580void CameraProcess::setExposureProgress(ISD::CameraChip *tChip, double value, IPState ipstate)
1581{
1582 // ignore values if not capturing
1583 if (state()->checkCapturing() == false)
1584 return;
1585
1586 if (devices()->getActiveChip() != tChip ||
1587 devices()->getActiveChip()->getCaptureMode() != FITS_NORMAL
1588 || state()->getMeridianFlipState()->getMeridianFlipStage() >= MeridianFlipState::MF_ALIGNING)
1589 return;
1590
1591 double deltaMS = std::ceil(1000.0 * value - state()->lastRemainingFrameTimeMS());
1592 emit updateCaptureCountDown(int(deltaMS));
1593 state()->setLastRemainingFrameTimeMS(state()->lastRemainingFrameTimeMS() + deltaMS);
1594
1595 if (activeJob())
1596 {
1597 activeJob()->setExposeLeft(value);
1598
1599 emit newExposureProgress(activeJob());
1600 }
1601
1602 if (activeJob() && ipstate == IPS_ALERT)
1603 {
1604 int retries = activeJob()->getCaptureRetires() + 1;
1605
1606 activeJob()->setCaptureRetires(retries);
1607
1608 emit newLog(i18n("Capture failed. Check INDI Control Panel for details."));
1609
1610 if (retries >= 3)
1611 {
1612 activeJob()->abort();
1613 return;
1614 }
1615
1616 emit newLog((i18n("Restarting capture attempt #%1", retries)));
1617
1618 state()->setNextSequenceID(1);
1619
1620 captureImage();
1621 return;
1622 }
1623
1624 if (activeJob() != nullptr && ipstate == IPS_OK)
1625 {
1626 activeJob()->setCaptureRetires(0);
1627 activeJob()->setExposeLeft(0);
1628
1629 if (devices()->getActiveCamera()
1630 && devices()->getActiveCamera()->getUploadMode() == ISD::Camera::UPLOAD_REMOTE)
1631 {
1632 if (activeJob()->getStatus() == JOB_BUSY)
1633 {
1634 emit processingFITSfinished(false);
1635 return;
1636 }
1637 }
1638
1639 if (state()->getGuideState() == GUIDE_GUIDING && Options::guiderType() == 0
1640 && state()->suspendGuidingOnDownload())
1641 {
1642 qCDebug(KSTARS_EKOS_CAPTURE) << "Autoguiding suspended until primary CCD chip completes downloading...";
1643 emit suspendGuiding();
1644 }
1645
1646 emit downloadingFrame();
1647
1648 //This will start the clock to see how long the download takes.
1649 state()->downloadTimer().start();
1650 state()->downloadProgressTimer().start();
1651 }
1652}
1653
1655{
1656 if (activeJob())
1657 {
1658 double downloadTimeLeft = state()->averageDownloadTime() - state()->downloadTimer().elapsed() /
1659 1000.0;
1660 if(downloadTimeLeft >= 0)
1661 {
1662 state()->imageCountDown().setHMS(0, 0, 0);
1663 state()->imageCountDownAddMSecs(int(std::ceil(downloadTimeLeft * 1000)));
1664 emit newDownloadProgress(downloadTimeLeft);
1665 }
1666 }
1667
1668}
1669
1671{
1672 emit newImage(activeJob(), imageData);
1673 // If fast exposure is on, do not capture again, it will be captured by the driver.
1674 if (activeCamera()->isFastExposureEnabled() == false)
1675 {
1676 const int seqDelay = activeJob()->getCoreProperty(SequenceJob::SJ_Delay).toInt();
1677
1678 if (seqDelay > 0)
1679 {
1680 QTimer::singleShot(seqDelay, this, [this]()
1681 {
1682 if (activeJob() != nullptr)
1683 activeJob()->startCapturing(state()->getRefocusState()->isAutoFocusReady(), FITS_NORMAL);
1684 });
1685 }
1686 else if (activeJob() != nullptr)
1687 activeJob()->startCapturing(state()->getRefocusState()->isAutoFocusReady(), FITS_NORMAL);
1688 }
1689 return IPS_OK;
1690
1691}
1692
1694{
1695 // Do not calculate download time for images stored on server.
1696 // Only calculate for longer exposures.
1697 if (activeCamera()->getUploadMode() != ISD::Camera::UPLOAD_REMOTE
1698 && state()->downloadTimer().isValid())
1699 {
1700 //This determines the time since the image started downloading
1701 double currentDownloadTime = state()->downloadTimer().elapsed() / 1000.0;
1702 state()->addDownloadTime(currentDownloadTime);
1703 // Always invalidate timer as it must be explicitly started.
1704 state()->downloadTimer().invalidate();
1705
1706 QString dLTimeString = QString::number(currentDownloadTime, 'd', 2);
1707 QString estimatedTimeString = QString::number(state()->averageDownloadTime(), 'd', 2);
1708 emit newLog(i18n("Download Time: %1 s, New Download Time Estimate: %2 s.", dLTimeString, estimatedTimeString));
1709 }
1710 return IPS_OK;
1711}
1712
1714{
1715 if (activeJob()->jobType() == SequenceJob::JOBTYPE_PREVIEW)
1716 {
1717 // Reset upload mode if it was changed by preview
1718 activeCamera()->setUploadMode(activeJob()->getUploadMode());
1719 // Reset active job pointer
1720 state()->setActiveJob(nullptr);
1721 emit stopCapture(CAPTURE_COMPLETE);
1722 if (state()->getGuideState() == GUIDE_SUSPENDED && state()->suspendGuidingOnDownload())
1723 emit resumeGuiding();
1724 return IPS_OK;
1725 }
1726 else
1727 return IPS_IDLE;
1728
1729}
1730
1732{
1733 // stop timers
1734 state()->getCaptureTimeout().stop();
1735 state()->setCaptureTimeoutCounter(0);
1736
1737 state()->downloadProgressTimer().stop();
1738
1739 // In case we're framing, let's return quickly to continue the process.
1740 if (state()->isLooping())
1741 {
1742 continueFramingAction(state()->imageData());
1743 return;
1744 }
1745
1746 // Update download times.
1748
1749 // If it was initially set as pure preview job and NOT as preview for calibration
1750 if (previewImageCompletedAction() == IPS_OK)
1751 return;
1752
1753 // do not update counters if in preview mode or calibrating
1754 if (activeJob()->jobType() == SequenceJob::JOBTYPE_PREVIEW
1755 || activeJob()->getCalibrationStage() == SequenceJobState::CAL_CALIBRATION)
1756 return;
1757
1758 /* Increase the sequence's current capture count */
1759 updatedCaptureCompleted(activeJob()->getCompleted() + 1);
1760 /* Decrease the counter for in-sequence focusing */
1761 state()->getRefocusState()->decreaseInSequenceFocusCounter();
1762 /* Reset adaptive focus flag */
1763 state()->getRefocusState()->setAdaptiveFocusDone(false);
1764
1765 /* Decrease the dithering counter except for directly after meridian flip */
1766 /* Hint: this isonly relevant when a meridian flip happened during a paused sequence when pressing "Start" afterwards. */
1767 if (state()->getMeridianFlipState()->getMeridianFlipStage() < MeridianFlipState::MF_FLIPPING)
1768 state()->decreaseDitherCounter();
1769
1770 /* If we were assigned a captured frame map, also increase the relevant counter for prepareJob */
1771 state()->addCapturedFrame(activeJob()->getSignature());
1772
1773 // report that the image has been received
1774 emit newLog(i18n("Received image %1 out of %2.", activeJob()->getCompleted(),
1775 activeJob()->getCoreProperty(SequenceJob::SJ_Count).toInt()));
1776
1777 // Invalidate the timer so that it would be restart next time it is triggered.
1778 m_CaptureOperationsTimer.invalidate();
1779}
1780
1782{
1783 double hfr = -1, eccentricity = -1;
1784 int numStars = -1, median = -1;
1785 QString filename;
1786 if (imageData)
1787 {
1788 QVariant frameType;
1789 if (Options::autoHFR() && imageData && !imageData->areStarsSearched() && imageData->getRecordValue("FRAME", frameType)
1790 && frameType.toString() == "Light")
1791 {
1792#ifdef HAVE_STELLARSOLVER
1793 // Don't use the StellarSolver defaults (which allow very small stars).
1794 // Use the HFR profile--which the user can modify.
1795 QVariantMap extractionSettings;
1796 extractionSettings["optionsProfileIndex"] = Options::hFROptionsProfile();
1797 extractionSettings["optionsProfileGroup"] = static_cast<int>(Ekos::HFRProfiles);
1798 imageData->setSourceExtractorSettings(extractionSettings);
1799#endif
1800 QFuture<bool> result = imageData->findStars(ALGORITHM_SEP);
1801 result.waitForFinished();
1802 }
1803 hfr = imageData->getHFR(HFR_AVERAGE);
1804 numStars = imageData->getSkyBackground().starsDetected;
1805 median = imageData->getMedian();
1806 eccentricity = imageData->getEccentricity();
1807 filename = imageData->filename();
1808
1809 // avoid logging that we captured a temporary file
1810 if (state()->isLooping() == false && activeJob() != nullptr && activeJob()->jobType() != SequenceJob::JOBTYPE_PREVIEW)
1811 emit newLog(i18n("Captured %1", filename));
1812
1813 auto remainingPlaceholders = PlaceholderPath::remainingPlaceholders(filename);
1814 if (remainingPlaceholders.size() > 0)
1815 {
1816 emit newLog(
1817 i18n("WARNING: remaining and potentially unknown placeholders %1 in %2",
1818 remainingPlaceholders.join(", "), filename));
1819 }
1820 }
1821
1822 if (activeJob())
1823 {
1824 QVariantMap metadata;
1825 metadata["filename"] = filename;
1826 metadata["type"] = activeJob()->getFrameType();
1827 metadata["exposure"] = activeJob()->getCoreProperty(SequenceJob::SJ_Exposure).toDouble();
1828 metadata["filter"] = activeJob()->getCoreProperty(SequenceJob::SJ_Filter).toString();
1829 metadata["width"] = activeJob()->getCoreProperty(SequenceJob::SJ_ROI).toRect().width();
1830 metadata["height"] = activeJob()->getCoreProperty(SequenceJob::SJ_ROI).toRect().height();
1831 metadata["binx"] = activeJob()->getCoreProperty(SequenceJob::SJ_Binning).toPoint().x();
1832 metadata["biny"] = activeJob()->getCoreProperty(SequenceJob::SJ_Binning).toPoint().y();
1833 metadata["hfr"] = hfr;
1834 metadata["starCount"] = numStars;
1835 metadata["median"] = median;
1836 metadata["eccentricity"] = eccentricity;
1837 qCDebug(KSTARS_EKOS_CAPTURE) << "Captured frame metadata: filename =" << filename << ", type =" << metadata["type"].toInt()
1838 << "exposure =" << metadata["exposure"].toDouble() << "filter =" << metadata["filter"].toString() << "width =" <<
1839 metadata["width"].toInt() << "height =" << metadata["height"].toInt() << "hfr =" << metadata["hfr"].toDouble() <<
1840 "starCount =" << metadata["starCount"].toInt() << "median =" << metadata["median"].toInt() << "eccentricity =" <<
1841 metadata["eccentricity"].toDouble();
1842
1843 emit captureComplete(metadata);
1844 }
1845 return IPS_OK;
1846}
1847
1848IPState CameraProcess::runCaptureScript(ScriptTypes scriptType, bool precond)
1849{
1850 if (activeJob())
1851 {
1852 const QString captureScript = activeJob()->getScript(scriptType);
1853 if (captureScript.isEmpty() == false && precond)
1854 {
1855 state()->setCaptureScriptType(scriptType);
1856 m_CaptureScript.start(captureScript, generateScriptArguments());
1857 //m_CaptureScript.start("/bin/bash", QStringList() << captureScript);
1858 emit newLog(i18n("Executing capture script %1", captureScript));
1859 return IPS_BUSY;
1860 }
1861 }
1862 // no script execution started
1863 return IPS_OK;
1864}
1865
1867{
1868 Q_UNUSED(status)
1869
1870 switch (state()->captureScriptType())
1871 {
1872 case SCRIPT_PRE_CAPTURE:
1873 emit newLog(i18n("Pre capture script finished with code %1.", exitCode));
1874 if (activeJob() && activeJob()->getStatus() == JOB_IDLE)
1876 else
1877 {
1878 m_CaptureOperationsTimer.invalidate();
1880 }
1881 break;
1882
1884 emit newLog(i18n("Post capture script finished with code %1.", exitCode));
1885
1886 // If we're done, proceed to completion.
1887 if (activeJob() == nullptr
1888 || activeJob()->getCoreProperty(SequenceJob::SJ_Count).toInt() <=
1889 activeJob()->getCompleted())
1890 {
1892 }
1893 // Else check if meridian condition is met.
1894 else if (state()->checkMeridianFlipReady())
1895 {
1896 emit newLog(i18n("Processing meridian flip..."));
1897 }
1898 // Then if nothing else, just resume sequence.
1899 else
1900 {
1902 }
1903 break;
1904
1905 case SCRIPT_PRE_JOB:
1906 emit newLog(i18n("Pre job script finished with code %1.", exitCode));
1908 break;
1909
1910 case SCRIPT_POST_JOB:
1911 emit newLog(i18n("Post job script finished with code %1.", exitCode));
1913 break;
1914
1915 default:
1916 // in all other cases do nothing
1917 break;
1918 }
1919
1920}
1921
1923{
1924
1925 QVariant trainID = ProfileSettings::Instance()->getOneSetting(ProfileSettings::CaptureOpticalTrain);
1926 if (activeCamera() && trainID.isValid())
1927 {
1928 if (activeCamera() && activeCamera()->getDeviceName() == name)
1929 checkCamera();
1930
1931 emit refreshCamera(true);
1932 }
1933 else
1934 emit refreshCamera(false);
1935
1936}
1937
1939{
1940 // Do not update any camera settings while capture is in progress.
1941 if (state()->getCaptureState() == CAPTURE_CAPTURING)
1942 return;
1943
1944 // If camera is restarted, try again in 1 second
1945 if (!activeCamera())
1946 {
1948 return;
1949 }
1950
1951 devices()->setActiveChip(nullptr);
1952
1953 // FIXME TODO fix guide head detection
1954 if (activeCamera()->getDeviceName().contains("Guider"))
1955 {
1956 state()->setUseGuideHead(true);
1957 devices()->setActiveChip(activeCamera()->getChip(ISD::CameraChip::GUIDE_CCD));
1958 }
1959
1960 if (devices()->getActiveChip() == nullptr)
1961 {
1962 state()->setUseGuideHead(false);
1963 devices()->setActiveChip(activeCamera()->getChip(ISD::CameraChip::PRIMARY_CCD));
1964 }
1965
1966 emit refreshCameraSettings();
1967}
1968
1970{
1971 auto pos = std::find_if(state()->DSLRInfos().begin(),
1972 state()->DSLRInfos().end(), [model](const QMap<QString, QVariant> &oneDSLRInfo)
1973 {
1974 return (oneDSLRInfo["Model"] == model);
1975 });
1976
1977 // Sync Pixel Size
1978 if (pos != state()->DSLRInfos().end())
1979 {
1980 auto camera = *pos;
1981 devices()->getActiveChip()->setImageInfo(camera["Width"].toInt(),
1982 camera["Height"].toInt(),
1983 camera["PixelW"].toDouble(),
1984 camera["PixelH"].toDouble(),
1985 8);
1986 }
1987}
1988
1989void CameraProcess::reconnectCameraDriver(const QString &camera, const QString &filterWheel)
1990{
1991 if (activeCamera() && activeCamera()->getDeviceName() == camera)
1992 {
1993 // Set camera again to the one we restarted
1994 auto rememberState = state()->getCaptureState();
1995 state()->setCaptureState(CAPTURE_IDLE);
1996 checkCamera();
1997 state()->setCaptureState(rememberState);
1998
1999 // restart capture
2000 state()->setCaptureTimeoutCounter(0);
2001
2002 if (activeJob())
2003 {
2004 devices()->setActiveChip(devices()->getActiveChip());
2005 captureImage();
2006 }
2007 return;
2008 }
2009
2010 QTimer::singleShot(5000, this, [ &, camera, filterWheel]()
2011 {
2012 reconnectCameraDriver(camera, filterWheel);
2013 });
2014}
2015
2017{
2018 auto name = device->getDeviceName();
2019 device->disconnect(this);
2020
2021 // Mounts
2022 if (devices()->mount() && devices()->mount()->getDeviceName() == device->getDeviceName())
2023 {
2024 devices()->mount()->disconnect(this);
2025 devices()->setMount(nullptr);
2026 if (activeJob() != nullptr)
2027 activeJob()->addMount(nullptr);
2028 }
2029
2030 // Domes
2031 if (devices()->dome() && devices()->dome()->getDeviceName() == device->getDeviceName())
2032 {
2033 devices()->dome()->disconnect(this);
2034 devices()->setDome(nullptr);
2035 }
2036
2037 // Rotators
2038 if (devices()->rotator() && devices()->rotator()->getDeviceName() == device->getDeviceName())
2039 {
2040 devices()->rotator()->disconnect(this);
2041 devices()->setRotator(nullptr);
2042 }
2043
2044 // Dust Caps
2045 if (devices()->dustCap() && devices()->dustCap()->getDeviceName() == device->getDeviceName())
2046 {
2047 devices()->dustCap()->disconnect(this);
2048 devices()->setDustCap(nullptr);
2049 state()->hasDustCap = false;
2050 state()->setDustCapState(CAP_UNKNOWN);
2051 }
2052
2053 // Light Boxes
2054 if (devices()->lightBox() && devices()->lightBox()->getDeviceName() == device->getDeviceName())
2055 {
2056 devices()->lightBox()->disconnect(this);
2057 devices()->setLightBox(nullptr);
2058 state()->hasLightBox = false;
2059 state()->setLightBoxLightState(CAP_LIGHT_UNKNOWN);
2060 }
2061
2062 // Cameras
2063 if (activeCamera() && activeCamera()->getDeviceName() == name)
2064 {
2065 activeCamera()->disconnect(this);
2066 devices()->setActiveCamera(nullptr);
2067 devices()->setActiveChip(nullptr);
2068
2070 if (INDIListener::findDevice(name, generic))
2071 DarkLibrary::Instance()->removeDevice(generic);
2072
2074 }
2075
2076 // Filter Wheels
2077 if (devices()->filterWheel() && devices()->filterWheel()->getDeviceName() == name)
2078 {
2079 devices()->filterWheel()->disconnect(this);
2080 devices()->setFilterWheel(nullptr);
2081
2082 QTimer::singleShot(1000, this, [this]()
2083 {
2084 emit refreshFilterSettings();
2085 });
2086 }
2087}
2088
2090{
2091 state()->setCaptureTimeoutCounter(state()->captureTimeoutCounter() + 1);
2092
2093 if (state()->deviceRestartCounter() >= 3)
2094 {
2095 state()->setCaptureTimeoutCounter(0);
2096 state()->setDeviceRestartCounter(0);
2097 emit newLog(i18n("Exposure timeout. Aborting..."));
2098 emit stopCapture(CAPTURE_ABORTED);
2099 return;
2100 }
2101
2102 if (state()->captureTimeoutCounter() > 3 && activeCamera())
2103 {
2104 emit newLog(i18n("Exposure timeout. More than 3 have been detected, will restart driver."));
2105 QString camera = activeCamera()->getDeviceName();
2106 QString fw = (devices()->filterWheel() != nullptr) ?
2107 devices()->filterWheel()->getDeviceName() : "";
2108 emit driverTimedout(camera);
2109 QTimer::singleShot(5000, this, [ &, camera, fw]()
2110 {
2111 state()->setDeviceRestartCounter(state()->deviceRestartCounter() + 1);
2112 reconnectCameraDriver(camera, fw);
2113 });
2114 return;
2115 }
2116 else
2117 {
2118 // Double check that m_Camera is valid in case it was reset due to driver restart.
2119 if (activeCamera() && activeJob())
2120 {
2121 setCamera(true);
2122 emit newLog(i18n("Exposure timeout. Restarting exposure..."));
2123 activeCamera()->setEncodingFormat("FITS");
2124 auto rememberState = state()->getCaptureState();
2125 state()->setCaptureState(CAPTURE_IDLE);
2126 checkCamera();
2127 state()->setCaptureState(rememberState);
2128
2129 auto targetChip = activeCamera()->getChip(state()->useGuideHead() ?
2130 ISD::CameraChip::GUIDE_CCD :
2131 ISD::CameraChip::PRIMARY_CCD);
2132 targetChip->abortExposure();
2133 const double exptime = activeJob()->getCoreProperty(SequenceJob::SJ_Exposure).toDouble();
2134 targetChip->capture(exptime);
2135 state()->getCaptureTimeout().start(static_cast<int>((exptime) * 1000 + CAPTURE_TIMEOUT_THRESHOLD));
2136 }
2137 // Don't allow this to happen all night. We will repeat checking (most likely for activeCamera()
2138 // another 200s = 40 * 5s, but after that abort capture.
2139 else if (state()->captureTimeoutCounter() < 40)
2140 {
2141 qCDebug(KSTARS_EKOS_CAPTURE) << "Unable to restart exposure as camera is missing, trying again in 5 seconds...";
2143 }
2144 else
2145 {
2146 state()->setCaptureTimeoutCounter(0);
2147 state()->setDeviceRestartCounter(0);
2148 emit newLog(i18n("Exposure timeout. Too many. Aborting..."));
2149 emit stopCapture(CAPTURE_ABORTED);
2150 return;
2151 }
2152 }
2153
2154}
2155
2157{
2158 if (!activeJob())
2159 return;
2160
2161 if (type == ISD::Camera::ERROR_CAPTURE)
2162 {
2163 int retries = activeJob()->getCaptureRetires() + 1;
2164
2165 activeJob()->setCaptureRetires(retries);
2166
2167 emit newLog(i18n("Capture failed. Check INDI Control Panel for details."));
2168
2169 if (retries >= 3)
2170 {
2171 emit stopCapture(CAPTURE_ABORTED);
2172 return;
2173 }
2174
2175 emit newLog(i18n("Restarting capture attempt #%1", retries));
2176
2177 state()->setNextSequenceID(1);
2178
2179 captureImage();
2180 return;
2181 }
2182 else
2183 {
2184 emit stopCapture(CAPTURE_ABORTED);
2185 }
2186}
2187
2188bool CameraProcess::checkFlatCalibration(QSharedPointer<FITSData> imageData, double exp_min, double exp_max)
2189{
2190 // nothing to do
2191 if (imageData.isNull())
2192 return true;
2193
2194 double currentADU = imageData->getADU();
2195 bool outOfRange = false, saturated = false;
2196
2197 switch (imageData->bpp())
2198 {
2199 case 8:
2200 if (activeJob()->getCoreProperty(SequenceJob::SJ_TargetADU).toDouble() > UINT8_MAX)
2201 outOfRange = true;
2202 else if (currentADU / UINT8_MAX > 0.95)
2203 saturated = true;
2204 break;
2205
2206 case 16:
2207 if (activeJob()->getCoreProperty(SequenceJob::SJ_TargetADU).toDouble() > UINT16_MAX)
2208 outOfRange = true;
2209 else if (currentADU / UINT16_MAX > 0.95)
2210 saturated = true;
2211 break;
2212
2213 case 32:
2214 if (activeJob()->getCoreProperty(SequenceJob::SJ_TargetADU).toDouble() > UINT32_MAX)
2215 outOfRange = true;
2216 else if (currentADU / UINT32_MAX > 0.95)
2217 saturated = true;
2218 break;
2219
2220 default:
2221 break;
2222 }
2223
2224 if (outOfRange)
2225 {
2226 emit newLog(i18n("Flat calibration failed. Captured image is only %1-bit while requested ADU is %2.",
2227 QString::number(imageData->bpp())
2228 , QString::number(activeJob()->getCoreProperty(SequenceJob::SJ_TargetADU).toDouble(), 'f', 2)));
2229 emit stopCapture(CAPTURE_ABORTED);
2230 return false;
2231 }
2232 else if (saturated)
2233 {
2234 double nextExposure = activeJob()->getCoreProperty(SequenceJob::SJ_Exposure).toDouble() * 0.1;
2235 nextExposure = qBound(exp_min, nextExposure, exp_max);
2236
2237 emit newLog(i18n("Current image is saturated (%1). Next exposure is %2 seconds.",
2238 QString::number(currentADU, 'f', 0), QString("%L1").arg(nextExposure, 0, 'f', 6)));
2239
2240 activeJob()->setCalibrationStage(SequenceJobState::CAL_CALIBRATION);
2241 activeJob()->setCoreProperty(SequenceJob::SJ_Exposure, nextExposure);
2242 if (activeCamera()->getUploadMode() != ISD::Camera::UPLOAD_CLIENT)
2243 {
2244 activeCamera()->setUploadMode(ISD::Camera::UPLOAD_CLIENT);
2245 }
2247 return false;
2248 }
2249
2250 double ADUDiff = fabs(currentADU - activeJob()->getCoreProperty(
2251 SequenceJob::SJ_TargetADU).toDouble());
2252
2253 // If it is within tolerance range of target ADU
2254 if (ADUDiff <= state()->targetADUTolerance())
2255 {
2256 if (activeJob()->getCalibrationStage() == SequenceJobState::CAL_CALIBRATION)
2257 {
2258 emit newLog(
2259 i18n("Current ADU %1 within target ADU tolerance range.", QString::number(currentADU, 'f', 0)));
2260 activeCamera()->setUploadMode(activeJob()->getUploadMode());
2261 auto placeholderPath = PlaceholderPath();
2262 // Make sure to update Full Prefix as exposure value was changed
2263 placeholderPath.processJobInfo(activeJob());
2264 // Mark calibration as complete
2265 activeJob()->setCalibrationStage(SequenceJobState::CAL_CALIBRATION_COMPLETE);
2266
2267 // Must update sequence prefix as this step is only done in prepareJob
2268 // but since the duration has now been updated, we must take care to update signature
2269 // since it may include a placeholder for duration which would affect it.
2270 if (activeCamera() && activeCamera()->getUploadMode() != ISD::Camera::UPLOAD_REMOTE)
2271 state()->checkSeqBoundary();
2272 }
2273
2274 return true;
2275 }
2276
2277 double nextExposure = -1;
2278
2279 // If value is saturated, try to reduce it to valid range first
2280 if (std::fabs(imageData->getMax(0) - imageData->getMin(0)) < 10)
2281 nextExposure = activeJob()->getCoreProperty(SequenceJob::SJ_Exposure).toDouble() * 0.5;
2282 else
2283 nextExposure = calculateFlatExpTime(currentADU);
2284
2285 if (nextExposure <= 0 || std::isnan(nextExposure))
2286 {
2287 emit newLog(
2288 i18n("Unable to calculate optimal exposure settings, please capture the flats manually."));
2289 emit stopCapture(CAPTURE_ABORTED);
2290 return false;
2291 }
2292
2293 // Limit to minimum and maximum values
2294 nextExposure = qBound(exp_min, nextExposure, exp_max);
2295
2296 emit newLog(i18n("Current ADU is %1 Next exposure is %2 seconds.", QString::number(currentADU, 'f', 0),
2297 QString("%L1").arg(nextExposure, 0, 'f', 6)));
2298
2299 activeJob()->setCalibrationStage(SequenceJobState::CAL_CALIBRATION);
2300 activeJob()->setCoreProperty(SequenceJob::SJ_Exposure, nextExposure);
2301 if (activeCamera()->getUploadMode() != ISD::Camera::UPLOAD_CLIENT)
2302 {
2303 activeCamera()->setUploadMode(ISD::Camera::UPLOAD_CLIENT);
2304 }
2305
2307 return false;
2308
2309
2310}
2311
2313{
2314 if (activeJob() == nullptr)
2315 {
2316 qWarning(KSTARS_EKOS_CAPTURE) << "setCurrentADU with null activeJob().";
2317 // Nothing good to do here. Just don't crash.
2318 return currentADU;
2319 }
2320
2321 double nextExposure = 0;
2322 double targetADU = activeJob()->getCoreProperty(SequenceJob::SJ_TargetADU).toDouble();
2323 std::vector<double> coeff;
2324
2325 // limit number of points to two so it can calibrate in intesity changing enviroment like shoting flats
2326 // at dawn/sunrise sky
2327 if(activeJob()->getCoreProperty(SequenceJob::SJ_SkyFlat).toBool() && ExpRaw.size() > 2)
2328 {
2329 int remove = ExpRaw.size() - 2;
2330 ExpRaw.remove(0, remove);
2331 ADURaw.remove(0, remove);
2332 }
2333
2334 // Check if saturated, then take shorter capture and discard value
2335 ExpRaw.append(activeJob()->getCoreProperty(SequenceJob::SJ_Exposure).toDouble());
2336 ADURaw.append(currentADU);
2337
2338 qCDebug(KSTARS_EKOS_CAPTURE) << "Capture: Current ADU = " << currentADU << " targetADU = " << targetADU
2339 << " Exposure Count: " << ExpRaw.count();
2340
2341 // Most CCDs are quite linear so 1st degree polynomial is quite sufficient
2342 // But DSLRs can exhibit non-linear response curve and so a 2nd degree polynomial is more appropriate
2343 if (ExpRaw.count() >= 2)
2344 {
2345 if (ExpRaw.count() >= 5)
2346 {
2347 double chisq = 0;
2348
2349 coeff = gsl_polynomial_fit(ADURaw.data(), ExpRaw.data(), ExpRaw.count(), 2, chisq);
2350 qCDebug(KSTARS_EKOS_CAPTURE) << "Running polynomial fitting. Found " << coeff.size() << " coefficients.";
2351 if (std::isnan(coeff[0]) || std::isinf(coeff[0]))
2352 {
2353 qCDebug(KSTARS_EKOS_CAPTURE) << "Coefficients are invalid.";
2354 targetADUAlgorithm = ADU_LEAST_SQUARES;
2355 }
2356 else
2357 {
2358 nextExposure = coeff[0] + (coeff[1] * targetADU) + (coeff[2] * pow(targetADU, 2));
2359 // If exposure is not valid or does not make sense, then we fall back to least squares
2360 if (nextExposure < 0 || (nextExposure > ExpRaw.last() || targetADU < ADURaw.last())
2361 || (nextExposure < ExpRaw.last() || targetADU > ADURaw.last()))
2362 {
2363 nextExposure = 0;
2364 targetADUAlgorithm = ADU_LEAST_SQUARES;
2365 }
2366 else
2367 {
2368 targetADUAlgorithm = ADU_POLYNOMIAL;
2369 for (size_t i = 0; i < coeff.size(); i++)
2370 qCDebug(KSTARS_EKOS_CAPTURE) << "Coeff #" << i << "=" << coeff[i];
2371 }
2372 }
2373 }
2374
2375 bool looping = false;
2376 if (ExpRaw.count() >= 10)
2377 {
2378 int size = ExpRaw.count();
2379 looping = (std::fabs(ExpRaw[size - 1] - ExpRaw[size - 2] < 0.01)) &&
2380 (std::fabs(ExpRaw[size - 2] - ExpRaw[size - 3] < 0.01));
2381 if (looping && targetADUAlgorithm == ADU_POLYNOMIAL)
2382 {
2383 qWarning(KSTARS_EKOS_CAPTURE) << "Detected looping in polynomial results. Falling back to llsqr.";
2384 targetADUAlgorithm = ADU_LEAST_SQUARES;
2385 }
2386 }
2387
2388 // If we get invalid data, let's fall back to llsq
2389 // Since polyfit can be unreliable at low counts, let's only use it at the 5th exposure
2390 // if we don't have results already.
2391 if (targetADUAlgorithm == ADU_LEAST_SQUARES)
2392 {
2393 double a = 0, b = 0;
2394 llsq(ExpRaw, ADURaw, a, b);
2395
2396 // If we have valid results, let's calculate next exposure
2397 if (a != 0.0)
2398 {
2399 nextExposure = (targetADU - b) / a;
2400 // If we get invalid value, let's just proceed iteratively
2401 if (nextExposure < 0)
2402 nextExposure = 0;
2403 }
2404 }
2405 }
2406
2407 // 2022.01.12 Put a hard limit to 180 seconds.
2408 // If it goes over this limit, the flat source is probably off.
2409 if (nextExposure == 0.0 || nextExposure > 180)
2410 {
2411 if (currentADU < targetADU)
2412 nextExposure = activeJob()->getCoreProperty(SequenceJob::SJ_Exposure).toDouble() * 1.25;
2413 else
2414 nextExposure = activeJob()->getCoreProperty(SequenceJob::SJ_Exposure).toDouble() * .75;
2415 }
2416
2417 qCDebug(KSTARS_EKOS_CAPTURE) << "next flat exposure is" << nextExposure;
2418
2419 return nextExposure;
2420
2421}
2422
2424{
2425 ADURaw.clear();
2426 ExpRaw.clear();
2427}
2428
2429QString Ekos::CameraProcess::createTabTitle(const FITSMode &captureMode, const QString &deviceName)
2430{
2431 const bool isPreview = (activeJob() == nullptr || (activeJob() && activeJob()->jobType() == SequenceJob::JOBTYPE_PREVIEW));
2432 if (isPreview && Options::singlePreviewFITS())
2433 {
2434 // If we are displaying all images from all cameras in a single FITS
2435 // Viewer window, then we prefix the camera name to the "Preview" string
2436 if (Options::singleWindowCapturedFITS())
2437 return (i18n("%1 Preview", deviceName));
2438 else
2439 // Otherwise, just use "Preview"
2440 return(i18n("Preview"));
2441 }
2442 else if (captureMode == FITS_CALIBRATE)
2443 {
2444 if (activeJob())
2445 {
2446 const QString filtername = activeJob()->getCoreProperty(SequenceJob::SJ_Filter).toString();
2447 if (filtername == "")
2448 return(QString(i18n("Flat Calibration")));
2449 else
2450 return(QString("%1 %2").arg(filtername).arg(i18n("Flat Calibration")));
2451 }
2452 else
2453 return(i18n("Calibration"));
2454 }
2455 return "";
2456}
2457
2458void CameraProcess::updateFITSViewer(const QSharedPointer<FITSData> data, const FITSMode &captureMode,
2459 const FITSScale &captureFilter, const QString &filename, const QString &deviceName)
2460{
2461 // do nothing in case of empty data
2462 if (data.isNull())
2463 return;
2464
2465 switch (captureMode)
2466 {
2467 case FITS_NORMAL:
2468 case FITS_CALIBRATE:
2469 {
2470 if (Options::useFITSViewer())
2471 {
2472 QUrl fileURL = QUrl::fromLocalFile(filename);
2473 bool success = false;
2474 // If image is preview and we should display all captured images in a
2475 // single tab called "Preview", then set the title to "Preview". Similar if we are calibrating flats.
2476 // Otherwise, the title will be the captured image name
2477 QString tabTitle = createTabTitle(captureMode, deviceName);
2478
2479 int tabIndex = -1;
2480 int *tabID = &m_fitsvViewerTabIDs.normalTabID;
2481 if (*tabID == -1 || Options::singlePreviewFITS() == false)
2482 {
2483
2484 success = getFITSViewer()->loadData(data, fileURL, &tabIndex, captureMode, captureFilter, tabTitle);
2485
2486 //Setup any necessary connections
2487 auto tabs = getFITSViewer()->tabs();
2488 if (tabIndex < tabs.size() && captureMode == FITS_NORMAL)
2489 {
2490 emit newView(tabs[tabIndex]->getView());
2491 tabs[tabIndex]->disconnect(this);
2492 connect(tabs[tabIndex].get(), &FITSTab::updated, this, [this]
2493 {
2494 auto tab = qobject_cast<FITSTab *>(sender());
2495 emit newView(tab->getView());
2496 });
2497 }
2498 }
2499 else
2500 {
2501 success = getFITSViewer()->updateData(data, fileURL, *tabID, &tabIndex, captureMode, captureFilter, tabTitle);
2502 }
2503
2504 if (!success)
2505 {
2506 // If opening file fails, we treat it the same as exposure failure
2507 // and recapture again if possible
2508 qCCritical(KSTARS_EKOS_CAPTURE()) << "error adding/updating FITS";
2509 return;
2510 }
2511 *tabID = tabIndex;
2512 if (Options::focusFITSOnNewImage())
2513 getFITSViewer()->raise();
2514
2515 return;
2516 }
2517 }
2518 break;
2519 default:
2520 break;
2521 }
2522}
2523
2525{
2526 FITSMode captureMode = tChip == nullptr ? FITS_UNKNOWN : tChip->getCaptureMode();
2527 FITSScale captureFilter = tChip == nullptr ? FITS_NONE : tChip->getCaptureFilter();
2528 updateFITSViewer(data, captureMode, captureFilter, filename, data->property("device").toString());
2529}
2530
2532{
2533 // lazy initialization
2534 if (m_VideoWindow.isNull() && activeCamera() != nullptr)
2535 {
2536 m_VideoWindow.reset(new StreamWG(activeCamera()));
2537
2538 connect(m_VideoWindow.get(), &StreamWG::hidden, activeCamera(), &ISD::Camera::StreamWindowHidden, Qt::UniqueConnection);
2539 connect(m_VideoWindow.get(), &StreamWG::imageChanged, activeCamera(), &ISD::Camera::newVideoFrame, Qt::UniqueConnection);
2540
2541 connect(activeCamera(), &ISD::Camera::videoRecordToggled, m_VideoWindow.get(), &StreamWG::enableStream,
2543 connect(activeCamera(), &ISD::Camera::showVideoFrame, this, &CameraProcess::showVideoFrame, Qt::UniqueConnection);
2544 connect(activeCamera(), &ISD::Camera::closeVideoWindow, this, &CameraProcess::closeVideoWindow, Qt::UniqueConnection);
2545 }
2546
2547 return m_VideoWindow;
2548}
2549
2550void CameraProcess::updateVideoWindow(int width, int height, bool streamEnabled)
2551{
2552 getVideoWindow()->enableStream(streamEnabled);
2553
2554 if (width > 0 && height > 0)
2555 getVideoWindow()->setSize(width, height);
2556
2557}
2558
2559void CameraProcess::closeVideoWindow()
2560{
2561 if (m_VideoWindow.isNull())
2562 return;
2563
2564 m_VideoWindow->close();
2565}
2566
2567void CameraProcess::showVideoFrame(INDI::Property prop, int width, int height)
2568{
2569 if (!getVideoWindow().isNull() && getVideoWindow()->isStreamEnabled())
2570 {
2571 getVideoWindow()->setSize(width, height);
2572 getVideoWindow()->show();
2573 getVideoWindow()->newFrame(prop);
2574 }
2575}
2576
2578 const QString &targetName, bool setOptions)
2579{
2580 state()->clearCapturedFramesMap();
2581 auto queue = state()->getSequenceQueue();
2582 if (!queue->load(fileURL, targetName, devices(), state()))
2583 {
2584 QString message = i18n("Unable to open file %1", fileURL);
2585 KSNotification::sorry(message, i18n("Could Not Open File"));
2586 return false;
2587 }
2588
2589 if (setOptions)
2590 {
2591 queue->setOptions();
2592 // Set the HFR Check value appropriately for the conditions, e.g. using Autofocus
2593 state()->updateHFRThreshold();
2594 }
2595
2596 for (auto j : state()->allJobs())
2597 emit addJob(j);
2598
2599 return true;
2600}
2601
2602bool CameraProcess::saveSequenceQueue(const QString &path, bool loadOptions)
2603{
2604 if (loadOptions)
2605 state()->getSequenceQueue()->loadOptions();
2606 return state()->getSequenceQueue()->save(path, state()->observerName());
2607}
2608
2609void CameraProcess::setCamera(bool connection)
2610{
2611 if (connection)
2612 {
2613 // TODO: do not simply forward the newExposureValue
2614 connect(activeCamera(), &ISD::Camera::newExposureValue, this, &CameraProcess::setExposureProgress, Qt::UniqueConnection);
2615 connect(activeCamera(), &ISD::Camera::newImage, this, &CameraProcess::processFITSData, Qt::UniqueConnection);
2616 connect(activeCamera(), &ISD::Camera::newRemoteFile, this, &CameraProcess::processNewRemoteFile, Qt::UniqueConnection);
2617 connect(activeCamera(), &ISD::Camera::ready, this, &CameraProcess::cameraReady, Qt::UniqueConnection);
2618 connect(activeCamera(), &ISD::Camera::videoRecordToggled, this, &CameraProcess::updateVideoRecordStatus,
2620 // disable passing through new frames to the FITS viewer
2621 disconnect(activeCamera(), &ISD::Camera::newImage, this, &CameraProcess::showFITSPreview);
2622 }
2623 else
2624 {
2625 // enable passing through new frames to the FITS viewer
2626 connect(activeCamera(), &ISD::Camera::newImage, this, &CameraProcess::showFITSPreview);
2627 // TODO: do not simply forward the newExposureValue
2628 disconnect(activeCamera(), &ISD::Camera::newExposureValue, this, &CameraProcess::setExposureProgress);
2629 disconnect(activeCamera(), &ISD::Camera::newImage, this, &CameraProcess::processFITSData);
2630 disconnect(activeCamera(), &ISD::Camera::newRemoteFile, this, &CameraProcess::processNewRemoteFile);
2631 // disconnect(m_Camera, &ISD::Camera::previewFITSGenerated, this, &Capture::setGeneratedPreviewFITS);
2632 disconnect(activeCamera(), &ISD::Camera::ready, this, &CameraProcess::cameraReady);
2633 }
2634
2635}
2636
2637bool CameraProcess::setFilterWheel(ISD::FilterWheel * device)
2638{
2639 if (devices()->filterWheel() && devices()->filterWheel() == device)
2640 return false;
2641
2642 if (devices()->filterWheel())
2643 devices()->filterWheel()->disconnect(this);
2644
2645 devices()->setFilterWheel(device);
2646
2647 return (device != nullptr);
2648}
2649
2650bool CameraProcess::checkPausing(CaptureContinueAction continueAction)
2651{
2652 if (state()->getCaptureState() == CAPTURE_PAUSE_PLANNED)
2653 {
2654 emit newLog(i18n("Sequence paused."));
2655 state()->setCaptureState(CAPTURE_PAUSED);
2656 // disconnect camera device
2657 setCamera(false);
2658 // save continue action
2659 state()->setContinueAction(continueAction);
2660 // pause
2661 return true;
2662 }
2663 // no pause
2664 return false;
2665}
2666
2668{
2669 SequenceJob * first_job = nullptr;
2670
2671 // search for idle or aborted jobs
2672 for (auto &job : state()->allJobs())
2673 {
2674 if (job->getStatus() == JOB_IDLE || job->getStatus() == JOB_ABORTED)
2675 {
2676 first_job = job;
2677 break;
2678 }
2679 }
2680
2681 // If there are no idle nor aborted jobs, question is whether to reset and restart
2682 // Scheduler will start a non-empty new job each time and doesn't use this execution path
2683 if (first_job == nullptr)
2684 {
2685 // If we have at least one job that are in error, bail out, even if ignoring job progress
2686 for (auto &job : state()->allJobs())
2687 {
2688 if (job->getStatus() != JOB_DONE)
2689 {
2690 // If we arrived here with a zero-delay timer, raise the interval before returning to avoid a cpu peak
2691 if (state()->getCaptureDelayTimer().isActive())
2692 {
2693 if (state()->getCaptureDelayTimer().interval() <= 0)
2694 state()->getCaptureDelayTimer().setInterval(1000);
2695 }
2696 return nullptr;
2697 }
2698 }
2699
2700 // If we only have completed jobs and we don't ignore job progress, ask the end-user what to do
2701 if (!state()->ignoreJobProgress())
2703 nullptr,
2704 i18n("All jobs are complete. Do you want to reset the status of all jobs and restart capturing?"),
2705 i18n("Reset job status"), KStandardGuiItem::cont(), KStandardGuiItem::cancel(),
2706 "reset_job_complete_status_warning") != KMessageBox::Continue)
2707 return nullptr;
2708
2709 // If the end-user accepted to reset, reset all jobs and restart
2710 resetAllJobs();
2711
2712 first_job = state()->allJobs().first();
2713 }
2714 // If we need to ignore job progress, systematically reset all jobs and restart
2715 // Scheduler will never ignore job progress and doesn't use this path
2716 else if (state()->ignoreJobProgress())
2717 {
2718 emit newLog(i18n("Warning: option \"Always Reset Sequence When Starting\" is enabled and resets the sequence counts."));
2719 resetAllJobs();
2720 }
2721
2722 return first_job;
2723}
2724
2725void CameraProcess::resetJobStatus(JOBStatus newStatus)
2726{
2727 if (activeJob() != nullptr)
2728 {
2729 activeJob()->resetStatus(newStatus);
2730 emit updateJobTable(activeJob());
2731 }
2732}
2733
2734void CameraProcess::resetAllJobs()
2735{
2736 for (auto &job : state()->allJobs())
2737 {
2738 job->resetStatus();
2739 }
2740 // clear existing job counts
2741 m_State->clearCapturedFramesMap();
2742 // update the entire job table
2743 emit updateJobTable(nullptr);
2744}
2745
2746void CameraProcess::updatedCaptureCompleted(int count)
2747{
2748 activeJob()->setCompleted(count);
2749 emit updateJobTable(activeJob());
2750}
2751
2752void CameraProcess::updateVideoRecordStatus(bool enabled)
2753{
2754 // do nothing if no active job is present
2755 if (activeJob() == nullptr)
2756 return;
2757
2758 qCInfo(KSTARS_EKOS_CAPTURE) << "Video recording" << (enabled ? "started." : "stopped.");
2759 // video capturing job is completed
2760 if (enabled == false)
2761 {
2762 updatedCaptureCompleted(activeJob()->getCoreProperty(SequenceJob::SJ_Count).toInt());
2764 }
2765}
2766
2767void CameraProcess::llsq(QVector<double> x, QVector<double> y, double &a, double &b)
2768{
2769 double bot;
2770 int i;
2771 double top;
2772 double xbar;
2773 double ybar;
2774 int n = x.count();
2775 //
2776 // Special case.
2777 //
2778 if (n == 1)
2779 {
2780 a = 0.0;
2781 b = y[0];
2782 return;
2783 }
2784 //
2785 // Average X and Y.
2786 //
2787 xbar = 0.0;
2788 ybar = 0.0;
2789 for (i = 0; i < n; i++)
2790 {
2791 xbar = xbar + x[i];
2792 ybar = ybar + y[i];
2793 }
2794 xbar = xbar / static_cast<double>(n);
2795 ybar = ybar / static_cast<double>(n);
2796 //
2797 // Compute Beta.
2798 //
2799 top = 0.0;
2800 bot = 0.0;
2801 for (i = 0; i < n; i++)
2802 {
2803 top = top + (x[i] - xbar) * (y[i] - ybar);
2804 bot = bot + (x[i] - xbar) * (x[i] - xbar);
2805 }
2806
2807 a = top / bot;
2808
2809 b = ybar - a * xbar;
2810
2811}
2812
2814{
2815 // TODO based on user feedback on what paramters are most useful to pass
2816 return QStringList();
2817}
2818
2820{
2821 if (devices()->getActiveCamera() && devices()->getActiveCamera()->hasCoolerControl())
2822 return true;
2823
2824 return false;
2825}
2826
2828{
2829 if (devices()->getActiveCamera() && devices()->getActiveCamera()->hasCoolerControl())
2830 return devices()->getActiveCamera()->setCoolerControl(enable);
2831
2832 return false;
2833}
2834
2836{
2837 connect(KSMessageBox::Instance(), &KSMessageBox::accepted, this, [this, name]()
2838 {
2839 KSMessageBox::Instance()->disconnect(this);
2841 emit driverTimedout(name);
2842 });
2843 connect(KSMessageBox::Instance(), &KSMessageBox::rejected, this, [this]()
2844 {
2845 KSMessageBox::Instance()->disconnect(this);
2846 });
2847
2848 KSMessageBox::Instance()->questionYesNo(i18n("Are you sure you want to restart %1 camera driver?", name),
2849 i18n("Driver Restart"), 5);
2850}
2851
2853{
2854 if (!activeCamera())
2855 return QStringList();
2856
2857 ISD::CameraChip *tChip = devices()->getActiveCamera()->getChip(ISD::CameraChip::PRIMARY_CCD);
2858
2859 QStringList types = tChip->getFrameTypes();
2860 if (devices()->getActiveCamera()->hasVideoStream())
2861 types.append(CAPTURE_TYPE_VIDEO);
2862
2863 return types;
2864}
2865
2867{
2868 if (devices()->getFilterManager().isNull())
2869 return QStringList();
2870
2871 return devices()->getFilterManager()->getFilterLabels();
2872}
2873
2875{
2876 if (devices()->getActiveCamera()->getProperty("CCD_GAIN"))
2877 {
2878 if (value >= 0)
2879 {
2881 ccdGain["GAIN"] = value;
2882 propertyMap["CCD_GAIN"] = ccdGain;
2883 }
2884 else
2885 {
2886 propertyMap["CCD_GAIN"].remove("GAIN");
2887 if (propertyMap["CCD_GAIN"].size() == 0)
2888 propertyMap.remove("CCD_GAIN");
2889 }
2890 }
2891 else if (devices()->getActiveCamera()->getProperty("CCD_CONTROLS"))
2892 {
2893 if (value >= 0)
2894 {
2895 QMap<QString, QVariant> ccdGain = propertyMap["CCD_CONTROLS"];
2896 ccdGain["Gain"] = value;
2897 propertyMap["CCD_CONTROLS"] = ccdGain;
2898 }
2899 else
2900 {
2901 propertyMap["CCD_CONTROLS"].remove("Gain");
2902 if (propertyMap["CCD_CONTROLS"].size() == 0)
2903 propertyMap.remove("CCD_CONTROLS");
2904 }
2905 }
2906}
2907
2909{
2910 if (devices()->getActiveCamera()->getProperty("CCD_OFFSET"))
2911 {
2912 if (value >= 0)
2913 {
2914 QMap<QString, QVariant> ccdOffset;
2915 ccdOffset["OFFSET"] = value;
2916 propertyMap["CCD_OFFSET"] = ccdOffset;
2917 }
2918 else
2919 {
2920 propertyMap["CCD_OFFSET"].remove("OFFSET");
2921 if (propertyMap["CCD_OFFSET"].size() == 0)
2922 propertyMap.remove("CCD_OFFSET");
2923 }
2924 }
2925 else if (devices()->getActiveCamera()->getProperty("CCD_CONTROLS"))
2926 {
2927 if (value >= 0)
2928 {
2929 QMap<QString, QVariant> ccdOffset = propertyMap["CCD_CONTROLS"];
2930 ccdOffset["Offset"] = value;
2931 propertyMap["CCD_CONTROLS"] = ccdOffset;
2932 }
2933 else
2934 {
2935 propertyMap["CCD_CONTROLS"].remove("Offset");
2936 if (propertyMap["CCD_CONTROLS"].size() == 0)
2937 propertyMap.remove("CCD_CONTROLS");
2938 }
2939 }
2940}
2941
2942QSharedPointer<FITSViewer> CameraProcess::getFITSViewer()
2943{
2944 // if the FITS viewer exists, return it
2945 if (!m_FITSViewerWindow.isNull())
2946 return m_FITSViewerWindow;
2947
2948 // otherwise, create it
2949 m_fitsvViewerTabIDs = {-1, -1, -1, -1, -1};
2950
2951 m_FITSViewerWindow = KStars::Instance()->createFITSViewer();
2952
2953 // Check if ONE tab of the viewer was closed.
2954 connect(m_FITSViewerWindow.get(), &FITSViewer::closed, this, [this](int tabIndex)
2955 {
2956 if (tabIndex == m_fitsvViewerTabIDs.normalTabID)
2957 m_fitsvViewerTabIDs.normalTabID = -1;
2958 else if (tabIndex == m_fitsvViewerTabIDs.calibrationTabID)
2959 m_fitsvViewerTabIDs.calibrationTabID = -1;
2960 else if (tabIndex == m_fitsvViewerTabIDs.focusTabID)
2961 m_fitsvViewerTabIDs.focusTabID = -1;
2962 else if (tabIndex == m_fitsvViewerTabIDs.guideTabID)
2963 m_fitsvViewerTabIDs.guideTabID = -1;
2964 else if (tabIndex == m_fitsvViewerTabIDs.alignTabID)
2965 m_fitsvViewerTabIDs.alignTabID = -1;
2966 });
2967
2968 // If FITS viewer was completed closed. Reset everything
2969 connect(m_FITSViewerWindow.get(), &FITSViewer::terminated, this, [this]()
2970 {
2971 m_fitsvViewerTabIDs = {-1, -1, -1, -1, -1};
2972 m_FITSViewerWindow.clear();
2973 });
2974
2975 return m_FITSViewerWindow;
2976}
2977
2978ISD::Camera *CameraProcess::activeCamera()
2979{
2980 return devices()->getActiveCamera();
2981}
2982
2983void CameraProcess::checkCaptureOperationsTimeout(const std::function<void()> &slot)
2984{
2985 // If invalid, validate it by starting the timer.
2986 if (m_CaptureOperationsTimer.isValid() == false)
2987 m_CaptureOperationsTimer.start();
2988
2989 // If we are paused, then restart time.
2990 if (state()->getCaptureState() == CAPTURE_PAUSED)
2991 m_CaptureOperationsTimer.restart();
2992
2993 // If capture operations timer exceeds timeout, then abort.
2994 if (m_CaptureOperationsTimer.elapsed() >= Options::captureOperationsTimeout() * 1000)
2995 {
2996 emit newLog(i18n("Capture operations timed out after %1 seconds.", Options::captureOperationsTimeout()));
2997 stopCapturing(CAPTURE_ABORTED);
2998 }
2999 else
3000 QTimer::singleShot(1000, this, slot);
3001}
3002
3003} // Ekos namespace
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
Q_SCRIPTABLE void executeJob()
executeJob Start the execution of activeJob by initiating updatePreCaptureCalibrationStatus().
SequenceJob * findNextPendingJob()
findExecutableJob find next job to be executed
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 prepareActiveJobStage2()
prepareActiveJobStage2 Reset #calibrationStage and continue with preparePreCaptureActions().
void showFITSPreview(const QSharedPointer< FITSData > &data)
showFITSPreview Directly show the FITS data as preview
void removeDevice(const QSharedPointer< ISD::GenericDevice > &device)
Generic method for removing any connected device.
IPState resumeSequence()
resumeSequence Try to continue capturing.
void startJob(SequenceJob *job)
startJob Start the execution of a selected sequence job:
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 jobCreated(SequenceJob *newJob)
Counterpart to the event {.
void prepareJob(SequenceJob *job)
prepareJob Update the counters of existing frames and continue with prepareActiveJob(),...
void processNewRemoteFile(QString file)
setNewRemoteFile A new image has been stored as remote file
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:121
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()
int x() const const
int y() const const
T * get() const const
bool isNull() const const
void showMessage(const QString &message, int timeout)
QString arg(Args &&... args) const const
QString number(double n, char format, int precision)
UniqueConnection
QFuture< ArgsType< Signal > > connect(Sender *sender, Signal signal)
void timeout()
QUrl fromLocalFile(const QString &localFile)
bool isValid() const const
double toDouble(bool *ok) const const
QRect toRect() 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 Jan 24 2025 11:53:00 by doxygen 1.13.2 written by Dimitri van Heesch, © 1997-2006

KDE's Doxygen guidelines are available online.