Kstars

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

KDE's Doxygen guidelines are available online.