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

KDE's Doxygen guidelines are available online.