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

KDE's Doxygen guidelines are available online.