File indexing completed on 2024-04-28 03:43:16

0001 /*
0002     SPDX-FileCopyrightText: 2023 Wolfgang Reissenberger <sterne-jaeger@openfuture.de>
0003 
0004     SPDX-License-Identifier: GPL-2.0-or-later
0005 */
0006 #include "captureprocess.h"
0007 #include "capturedeviceadaptor.h"
0008 #include "refocusstate.h"
0009 #include "sequencejob.h"
0010 #include "sequencequeue.h"
0011 #include "ekos/manager.h"
0012 #include "ekos/auxiliary/darklibrary.h"
0013 #include "ekos/auxiliary/darkprocessor.h"
0014 #include "ekos/auxiliary/opticaltrainmanager.h"
0015 #include "ekos/auxiliary/profilesettings.h"
0016 #include "ekos/guide/guide.h"
0017 #include "indi/indilistener.h"
0018 #include "indi/indirotator.h"
0019 #include "indi/blobmanager.h"
0020 #include "indi/indilightbox.h"
0021 #include "ksmessagebox.h"
0022 
0023 #include "ksnotification.h"
0024 #include <ekos_capture_debug.h>
0025 
0026 #ifdef HAVE_STELLARSOLVER
0027 #include "ekos/auxiliary/stellarsolverprofileeditor.h"
0028 #endif
0029 
0030 namespace Ekos
0031 {
0032 CaptureProcess::CaptureProcess(QSharedPointer<CaptureModuleState> newModuleState,
0033                                QSharedPointer<CaptureDeviceAdaptor> newDeviceAdaptor) : QObject()
0034 {
0035     m_State = newModuleState;
0036     m_DeviceAdaptor = newDeviceAdaptor;
0037 
0038     // connect devices to processes
0039     connect(devices().data(), &CaptureDeviceAdaptor::newCamera, this, &CaptureProcess::selectCamera);
0040 
0041     //This Timer will update the Exposure time in the capture module to display the estimated download time left
0042     //It will also update the Exposure time left in the Summary Screen.
0043     //It fires every 100 ms while images are downloading.
0044     state()->downloadProgressTimer().setInterval(100);
0045     connect(&state()->downloadProgressTimer(), &QTimer::timeout, this, &CaptureProcess::setDownloadProgress);
0046 
0047     // configure dark processor
0048     m_DarkProcessor = new DarkProcessor(this);
0049     connect(m_DarkProcessor, &DarkProcessor::newLog, this, &CaptureProcess::newLog);
0050     connect(m_DarkProcessor, &DarkProcessor::darkFrameCompleted, this, &CaptureProcess::darkFrameCompleted);
0051 
0052     // Pre/post capture/job scripts
0053     connect(&m_CaptureScript,
0054             static_cast<void (QProcess::*)(int exitCode, QProcess::ExitStatus status)>(&QProcess::finished),
0055             this, &CaptureProcess::scriptFinished);
0056     connect(&m_CaptureScript, &QProcess::errorOccurred, this, [this](QProcess::ProcessError error)
0057     {
0058         Q_UNUSED(error)
0059         emit newLog(m_CaptureScript.errorString());
0060         scriptFinished(-1, QProcess::NormalExit);
0061     });
0062     connect(&m_CaptureScript, &QProcess::readyReadStandardError, this,
0063             [this]()
0064     {
0065         emit newLog(m_CaptureScript.readAllStandardError());
0066     });
0067     connect(&m_CaptureScript, &QProcess::readyReadStandardOutput, this,
0068             [this]()
0069     {
0070         emit newLog(m_CaptureScript.readAllStandardOutput());
0071     });
0072 }
0073 
0074 bool CaptureProcess::setMount(ISD::Mount *device)
0075 {
0076     if (devices()->mount() && devices()->mount() == device)
0077     {
0078         updateTelescopeInfo();
0079         return false;
0080     }
0081 
0082     if (devices()->mount())
0083         devices()->mount()->disconnect(state().data());
0084 
0085     devices()->setMount(device);
0086 
0087     if (!devices()->mount())
0088         return false;
0089 
0090     devices()->mount()->disconnect(this);
0091     connect(devices()->mount(), &ISD::Mount::newTargetName, this, &CaptureProcess::captureTarget);
0092 
0093     updateTelescopeInfo();
0094     return true;
0095 }
0096 
0097 bool CaptureProcess::setRotator(ISD::Rotator *device)
0098 {
0099     // do nothing if *real* rotator is already connected
0100     if ((devices()->rotator() == device) && (device != nullptr))
0101         return false;
0102 
0103     // real & manual rotator initializing depends on present mount process
0104     if (devices()->mount())
0105     {
0106         if (devices()->rotator())
0107             devices()->rotator()->disconnect(this);
0108 
0109         // clear initialisation.
0110         state()->isInitialized[CaptureModuleState::ACTION_ROTATOR] = false;
0111 
0112         if (device)
0113         {
0114             Manager::Instance()->createRotatorController(device);
0115             connect(devices().data(), &CaptureDeviceAdaptor::rotatorReverseToggled, this, &CaptureProcess::rotatorReverseToggled,
0116                     Qt::UniqueConnection);
0117         }
0118         devices()->setRotator(device);
0119         return true;
0120     }
0121     return false;
0122 }
0123 
0124 bool CaptureProcess::setDustCap(ISD::DustCap *device)
0125 {
0126     if (devices()->dustCap() && devices()->dustCap() == device)
0127         return false;
0128 
0129     devices()->setDustCap(device);
0130     state()->setDustCapState(CaptureModuleState::CAP_UNKNOWN);
0131 
0132     updateFilterInfo();
0133     return true;
0134 
0135 }
0136 
0137 bool CaptureProcess::setLightBox(ISD::LightBox *device)
0138 {
0139     if (devices()->lightBox() == device)
0140         return false;
0141 
0142     devices()->setLightBox(device);
0143     state()->setLightBoxLightState(CaptureModuleState::CAP_LIGHT_UNKNOWN);
0144 
0145     return true;
0146 }
0147 
0148 bool CaptureProcess::setDome(ISD::Dome *device)
0149 {
0150     if (devices()->dome() == device)
0151         return false;
0152 
0153     devices()->setDome(device);
0154 
0155     return true;
0156 }
0157 
0158 bool CaptureProcess::setCamera(ISD::Camera *device)
0159 {
0160     if (devices()->getActiveCamera() == device)
0161         return false;
0162 
0163     devices()->setActiveCamera(device);
0164 
0165     // If we capturing, then we need to process capture timeout immediately since this is a crash recovery
0166     if (state()->getCaptureTimeout().isActive() && state()->getCaptureState() == CAPTURE_CAPTURING)
0167         QTimer::singleShot(100, this, &CaptureProcess::processCaptureTimeout);
0168 
0169     return true;
0170 
0171 }
0172 
0173 void CaptureProcess::toggleVideo(bool enabled)
0174 {
0175     if (devices()->getActiveCamera() == nullptr)
0176         return;
0177 
0178     if (devices()->getActiveCamera()->isBLOBEnabled() == false)
0179     {
0180         if (Options::guiderType() != Guide::GUIDE_INTERNAL)
0181             devices()->getActiveCamera()->setBLOBEnabled(true);
0182         else
0183         {
0184             connect(KSMessageBox::Instance(), &KSMessageBox::accepted, this, [this, enabled]()
0185             {
0186                 KSMessageBox::Instance()->disconnect(this);
0187                 devices()->getActiveCamera()->setBLOBEnabled(true);
0188                 devices()->getActiveCamera()->setVideoStreamEnabled(enabled);
0189             });
0190 
0191             KSMessageBox::Instance()->questionYesNo(i18n("Image transfer is disabled for this camera. Would you like to enable it?"),
0192                                                     i18n("Image Transfer"), 15);
0193 
0194             return;
0195         }
0196     }
0197 
0198     devices()->getActiveCamera()->setVideoStreamEnabled(enabled);
0199 
0200 }
0201 
0202 void CaptureProcess::toggleSequence()
0203 {
0204     const CaptureState capturestate = state()->getCaptureState();
0205     if (capturestate == CAPTURE_PAUSE_PLANNED || capturestate == CAPTURE_PAUSED)
0206     {
0207         // change the state back to capturing only if planned pause is cleared
0208         if (capturestate == CAPTURE_PAUSE_PLANNED)
0209             state()->setCaptureState(CAPTURE_CAPTURING);
0210 
0211         emit newLog(i18n("Sequence resumed."));
0212 
0213         // Call from where ever we have left of when we paused
0214         switch (state()->getContinueAction())
0215         {
0216             case CaptureModuleState::CONTINUE_ACTION_CAPTURE_COMPLETE:
0217                 resumeSequence();
0218                 break;
0219             case CaptureModuleState::CONTINUE_ACTION_NEXT_EXPOSURE:
0220                 startNextExposure();
0221                 break;
0222             default:
0223                 break;
0224         }
0225     }
0226     else if (capturestate == CAPTURE_IDLE || capturestate == CAPTURE_ABORTED || capturestate == CAPTURE_COMPLETE)
0227     {
0228         startNextPendingJob();
0229     }
0230     else
0231     {
0232         emit stopCapture(CAPTURE_ABORTED);
0233     }
0234 }
0235 
0236 void CaptureProcess::startNextPendingJob()
0237 {
0238     if (state()->allJobs().count() > 0)
0239     {
0240         SequenceJob *nextJob = findNextPendingJob();
0241         if (nextJob != nullptr)
0242         {
0243             startJob(nextJob);
0244             emit jobStarting();
0245         }
0246         else // do nothing if no job is pending
0247             emit newLog(i18n("No pending jobs found. Please add a job to the sequence queue."));
0248     }
0249     else
0250     {
0251         // Add a new job from the current capture settings.
0252         // If this succeeds, Capture will call this function again.
0253         emit createJob();
0254     }
0255 }
0256 
0257 void CaptureProcess::jobCreated(SequenceJob *newJob)
0258 {
0259     if (newJob == nullptr)
0260     {
0261         emit newLog(i18n("No new job created."));
0262         return;
0263     }
0264     // a job has been created successfully
0265     switch (newJob->jobType())
0266     {
0267         case SequenceJob::JOBTYPE_BATCH:
0268             startNextPendingJob();
0269             break;
0270         case SequenceJob::JOBTYPE_PREVIEW:
0271             state()->setActiveJob(newJob);
0272             capturePreview();
0273             break;
0274         default:
0275             // do nothing
0276             break;
0277     }
0278 }
0279 
0280 void CaptureProcess::capturePreview(bool loop)
0281 {
0282     if (state()->getFocusState() >= FOCUS_PROGRESS)
0283     {
0284         emit newLog(i18n("Cannot capture while focus module is busy."));
0285     }
0286     else if (activeJob() == nullptr)
0287     {
0288         if (loop && !state()->isLooping())
0289         {
0290             state()->setLooping(true);
0291             emit newLog(i18n("Starting framing..."));
0292         }
0293         // create a preview job
0294         emit createJob(SequenceJob::JOBTYPE_PREVIEW);
0295     }
0296     else
0297     {
0298         // job created, start capture preparation
0299         prepareJob(activeJob());
0300     }
0301 }
0302 
0303 void CaptureProcess::stopCapturing(CaptureState targetState)
0304 {
0305     clearFlatCache();
0306 
0307     state()->resetAlignmentRetries();
0308     //seqTotalCount   = 0;
0309     //seqCurrentCount = 0;
0310 
0311     state()->getCaptureTimeout().stop();
0312     state()->getCaptureDelayTimer().stop();
0313     if (activeJob() != nullptr)
0314     {
0315         if (activeJob()->getStatus() == JOB_BUSY)
0316         {
0317             QString stopText;
0318             switch (targetState)
0319             {
0320                 case CAPTURE_SUSPENDED:
0321                     stopText = i18n("CCD capture suspended");
0322                     resetJobStatus(JOB_BUSY);
0323                     break;
0324 
0325                 case CAPTURE_COMPLETE:
0326                     stopText = i18n("CCD capture complete");
0327                     resetJobStatus(JOB_DONE);
0328                     break;
0329 
0330                 case CAPTURE_ABORTED:
0331                     stopText = i18n("CCD capture aborted");
0332                     resetJobStatus(JOB_ABORTED);
0333                     break;
0334 
0335                 default:
0336                     stopText = i18n("CCD capture stopped");
0337                     resetJobStatus(JOB_IDLE);
0338                     break;
0339             }
0340             emit captureAborted(activeJob()->getCoreProperty(SequenceJob::SJ_Exposure).toDouble());
0341             KSNotification::event(QLatin1String("CaptureFailed"), stopText, KSNotification::Capture, KSNotification::Alert);
0342             emit newLog(stopText);
0343             activeJob()->abort();
0344             if (activeJob()->jobType() != SequenceJob::JOBTYPE_PREVIEW)
0345             {
0346                 int index = state()->allJobs().indexOf(activeJob());
0347                 state()->changeSequenceValue(index, "Status", "Aborted");
0348                 emit updateJobTable(activeJob());
0349             }
0350         }
0351 
0352         // In case of batch job
0353         if (activeJob()->jobType() != SequenceJob::JOBTYPE_PREVIEW)
0354         {
0355         }
0356         // or preview job in calibration stage
0357         else if (activeJob()->getCalibrationStage() == SequenceJobState::CAL_CALIBRATION)
0358         {
0359         }
0360         // or regular preview job
0361         else
0362         {
0363             state()->allJobs().removeOne(activeJob());
0364             // Delete preview job
0365             activeJob()->deleteLater();
0366             // Clear active job
0367             state()->setActiveJob(nullptr);
0368         }
0369     }
0370 
0371     // stop focusing if capture is aborted
0372     if (state()->getCaptureState() == CAPTURE_FOCUSING && targetState == CAPTURE_ABORTED)
0373         emit abortFocus();
0374 
0375     state()->setCaptureState(targetState);
0376 
0377     state()->setLooping(false);
0378     state()->setBusy(false);
0379 
0380     state()->getSeqDelayTimer().stop();
0381 
0382     state()->setActiveJob(nullptr);
0383 
0384     // Turn off any calibration light, IF they were turned on by Capture module
0385     if (devices()->lightBox() && state()->lightBoxLightEnabled())
0386     {
0387         state()->setLightBoxLightEnabled(false);
0388         devices()->lightBox()->setLightEnabled(false);
0389     }
0390 
0391     // disconnect camera device
0392     setCamera(false);
0393 
0394     // In case of exposure looping, let's abort
0395     if (devices()->getActiveCamera() && devices()->getActiveChip()
0396             && devices()->getActiveCamera()->isFastExposureEnabled())
0397         devices()->getActiveChip()->abortExposure();
0398 
0399     // communicate successful stop
0400     emit captureStopped();
0401 }
0402 
0403 void CaptureProcess::pauseCapturing()
0404 {
0405     if (state()->getCaptureState() != CAPTURE_CAPTURING)
0406     {
0407         // Ensure that the pause function is only called during frame capturing
0408         // Handling it this way is by far easier than trying to enable/disable the pause button
0409         // Fixme: make pausing possible at all stages. This makes it necessary to separate the pausing states from CaptureState.
0410         emit newLog(i18n("Pausing only possible while frame capture is running."));
0411         qCInfo(KSTARS_EKOS_CAPTURE) << "Pause button pressed while not capturing.";
0412         return;
0413     }
0414     // we do not decide at this stage how to resume, since pause is only planned here
0415     state()->setContinueAction(CaptureModuleState::CONTINUE_ACTION_NONE);
0416     state()->setCaptureState(CAPTURE_PAUSE_PLANNED);
0417     emit newLog(i18n("Sequence shall be paused after current exposure is complete."));
0418 }
0419 
0420 void CaptureProcess::startJob(SequenceJob *job)
0421 {
0422     state()->initCapturePreparation();
0423     prepareJob(job);
0424 }
0425 
0426 void CaptureProcess::prepareJob(SequenceJob * job)
0427 {
0428     state()->setActiveJob(job);
0429 
0430     // If job is Preview and NO view is available, ask to enable it.
0431     // if job is batch job, then NO VIEW IS REQUIRED at all. It's optional.
0432     if (job->jobType() == SequenceJob::JOBTYPE_PREVIEW && Options::useFITSViewer() == false
0433             && Options::useSummaryPreview() == false)
0434     {
0435         // ask if FITS viewer usage should be enabled
0436         connect(KSMessageBox::Instance(), &KSMessageBox::accepted, this, [ = ]()
0437         {
0438             KSMessageBox::Instance()->disconnect(this);
0439             Options::setUseFITSViewer(true);
0440             // restart
0441             prepareJob(job);
0442         });
0443         connect(KSMessageBox::Instance(), &KSMessageBox::rejected, this, [&]()
0444         {
0445             KSMessageBox::Instance()->disconnect(this);
0446             activeJob()->abort();
0447         });
0448         KSMessageBox::Instance()->questionYesNo(i18n("No view available for previews. Enable FITS viewer?"),
0449                                                 i18n("Display preview"), 15);
0450         // do nothing because currently none of the previews is active.
0451         return;
0452     }
0453 
0454     if (state()->isLooping() == false)
0455         qCDebug(KSTARS_EKOS_CAPTURE) << "Preparing capture job" << job->getSignature() << "for execution.";
0456 
0457     if (activeJob()->jobType() != SequenceJob::JOBTYPE_PREVIEW)
0458     {
0459         // set the progress info
0460 
0461         if (activeCamera()->getUploadMode() != ISD::Camera::UPLOAD_LOCAL)
0462             state()->setNextSequenceID(1);
0463 
0464         // We check if the job is already fully or partially complete by checking how many files of its type exist on the file system
0465         // The signature is the unique identification path in the system for a particular job. Format is "<storage path>/<target>/<frame type>/<filter name>".
0466         // 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/...)
0467         // 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/...)
0468         QString signature = activeJob()->getSignature();
0469 
0470         // Now check on the file system ALL the files that exist with the above signature
0471         // If 29 files exist for example, then nextSequenceID would be the NEXT file number (30)
0472         // Therefore, we know how to number the next file.
0473         // However, we do not deduce the number of captures to process from this function.
0474         state()->checkSeqBoundary(state()->sequenceURL());
0475 
0476         // Captured Frames Map contains a list of signatures:count of _already_ captured files in the file system.
0477         // This map is set by the Scheduler in order to complete efficiently the required captures.
0478         // When the end-user requests a sequence to be processed, that map is empty.
0479         //
0480         // Example with a 5xL-5xR-5xG-5xB sequence
0481         //
0482         // When the end-user loads and runs this sequence, each filter gets to capture 5 frames, then the procedure stops.
0483         // When the Scheduler executes a job with this sequence, the procedure depends on what is in the storage.
0484         //
0485         // Let's consider the Scheduler has 3 instances of this job to run.
0486         //
0487         // When the first job completes the sequence, there are 20 images in the file system (5 for each filter).
0488         // 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.
0489         // 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.
0490         //
0491         // 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.
0492         // 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.
0493         // When the sequence runs, the procedure will bypass LRG and proceed to capture 4xB.
0494         int count = state()->capturedFramesCount(signature);
0495         if (count > 0)
0496         {
0497 
0498             // Count how many captures this job has to process, given that previous jobs may have done some work already
0499             for (auto &a_job : state()->allJobs())
0500                 if (a_job == activeJob())
0501                     break;
0502                 else if (a_job->getSignature() == activeJob()->getSignature())
0503                     count -= a_job->getCompleted();
0504 
0505             // This is the current completion count of the current job
0506             updatedCaptureCompleted(count);
0507         }
0508         // JM 2018-09-24: Only set completed jobs to 0 IF the scheduler set captured frames map to begin with
0509         // If the map is empty, then no scheduler is used and it should proceed as normal.
0510         else if (state()->hasCapturedFramesMap())
0511         {
0512             // No preliminary information, we reset the job count and run the job unconditionally to clarify the behavior
0513             updatedCaptureCompleted(0);
0514         }
0515         // JM 2018-09-24: In case ignoreJobProgress is enabled
0516         // We check if this particular job progress ignore flag is set. If not,
0517         // then we set it and reset completed to zero. Next time it is evaluated here again
0518         // It will maintain its count regardless
0519         else if (state()->ignoreJobProgress()
0520                  && activeJob()->getJobProgressIgnored() == false)
0521         {
0522             activeJob()->setJobProgressIgnored(true);
0523             updatedCaptureCompleted(0);
0524         }
0525         // 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
0526 
0527         // Check whether active job is complete by comparing required captures to what is already available
0528         if (activeJob()->getCoreProperty(SequenceJob::SJ_Count).toInt() <=
0529                 activeJob()->getCompleted())
0530         {
0531             updatedCaptureCompleted(activeJob()->getCoreProperty(
0532                                         SequenceJob::SJ_Count).toInt());
0533             emit newLog(i18n("Job requires %1-second %2 images, has already %3/%4 captures and does not need to run.",
0534                              QString("%L1").arg(job->getCoreProperty(SequenceJob::SJ_Exposure).toDouble(), 0, 'f', 3),
0535                              job->getCoreProperty(SequenceJob::SJ_Filter).toString(),
0536                              activeJob()->getCompleted(),
0537                              activeJob()->getCoreProperty(SequenceJob::SJ_Count).toInt()));
0538             processJobCompletion2();
0539 
0540             /* FIXME: find a clearer way to exit here */
0541             return;
0542         }
0543         else
0544         {
0545             // There are captures to process
0546             emit newLog(i18n("Job requires %1-second %2 images, has %3/%4 frames captured and will be processed.",
0547                              QString("%L1").arg(job->getCoreProperty(SequenceJob::SJ_Exposure).toDouble(), 0, 'f', 3),
0548                              job->getCoreProperty(SequenceJob::SJ_Filter).toString(),
0549                              activeJob()->getCompleted(),
0550                              activeJob()->getCoreProperty(SequenceJob::SJ_Count).toInt()));
0551 
0552             // Emit progress update - done a few lines below
0553             // emit newImage(nullptr, activeJob());
0554 
0555             activeCamera()->setNextSequenceID(state()->nextSequenceID());
0556         }
0557     }
0558 
0559     if (activeCamera()->isBLOBEnabled() == false)
0560     {
0561         // FIXME: Move this warning pop-up elsewhere, it will interfere with automation.
0562         //        if (Options::guiderType() != Ekos::Guide::GUIDE_INTERNAL || KMessageBox::questionYesNo(nullptr, i18n("Image transfer is disabled for this camera. Would you like to enable it?")) ==
0563         //                KMessageBox::Yes)
0564         if (Options::guiderType() != Guide::GUIDE_INTERNAL)
0565         {
0566             activeCamera()->setBLOBEnabled(true);
0567         }
0568         else
0569         {
0570             connect(KSMessageBox::Instance(), &KSMessageBox::accepted, this, [this]()
0571             {
0572                 KSMessageBox::Instance()->disconnect(this);
0573                 activeCamera()->setBLOBEnabled(true);
0574                 prepareActiveJobStage1();
0575 
0576             });
0577             connect(KSMessageBox::Instance(), &KSMessageBox::rejected, this, [this]()
0578             {
0579                 KSMessageBox::Instance()->disconnect(this);
0580                 activeCamera()->setBLOBEnabled(true);
0581                 state()->setBusy(false);
0582             });
0583 
0584             KSMessageBox::Instance()->questionYesNo(i18n("Image transfer is disabled for this camera. Would you like to enable it?"),
0585                                                     i18n("Image Transfer"), 15);
0586 
0587             return;
0588         }
0589     }
0590 
0591     emit jobPrepared(job);
0592 
0593     prepareActiveJobStage1();
0594 
0595 }
0596 
0597 void CaptureProcess::prepareActiveJobStage1()
0598 {
0599     if (activeJob() == nullptr)
0600     {
0601         qWarning(KSTARS_EKOS_CAPTURE) << "prepareActiveJobStage1 with null activeJob().";
0602     }
0603     else
0604     {
0605         // JM 2020-12-06: Check if we need to execute pre-job script first.
0606         // Only run pre-job script for the first time and not after some images were captured but then stopped due to abort.
0607         if (runCaptureScript(SCRIPT_PRE_JOB, activeJob()->getCompleted() == 0) == IPS_BUSY)
0608             return;
0609     }
0610     prepareActiveJobStage2();
0611 }
0612 
0613 void CaptureProcess::prepareActiveJobStage2()
0614 {
0615     // Just notification of active job stating up
0616     if (activeJob() == nullptr)
0617     {
0618         qWarning(KSTARS_EKOS_CAPTURE) << "prepareActiveJobStage2 with null activeJob().";
0619     }
0620     else
0621         emit newImage(activeJob(), state()->imageData());
0622 
0623 
0624     /* Disable this restriction, let the sequence run even if focus did not run prior to the capture.
0625      * Besides, this locks up the Scheduler when the Capture module starts a sequence without any prior focus procedure done.
0626      * This is quite an old code block. The message "Manual scheduled" seems to even refer to some manual intervention?
0627      * With the new HFR threshold, it might be interesting to prevent the execution because we actually need an HFR value to
0628      * 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,
0629      * and on the other hand the focus procedure will deduce the next HFR automatically.
0630      * But in the end, it's not entirely clear what the intent was. Note there is still a warning that a preliminary autofocus
0631      * procedure is important to avoid any surprise that could make the whole schedule ineffective.
0632      */
0633     // JM 2020-12-06: Check if we need to execute pre-capture script first.
0634     if (runCaptureScript(SCRIPT_PRE_CAPTURE) == IPS_BUSY)
0635         return;
0636 
0637     prepareJobExecution();
0638 }
0639 
0640 void CaptureProcess::executeJob()
0641 {
0642     if (activeJob() == nullptr)
0643     {
0644         qWarning(KSTARS_EKOS_CAPTURE) << "executeJob with null activeJob().";
0645         return;
0646     }
0647 
0648     // Double check all pointers are valid.
0649     if (!activeCamera() || !devices()->getActiveChip())
0650     {
0651         checkCamera();
0652         QTimer::singleShot(1000, this, &CaptureProcess::executeJob);
0653         return;
0654     }
0655 
0656     QList<FITSData::Record> FITSHeaders;
0657     if (Options::defaultObserver().isEmpty() == false)
0658         FITSHeaders.append(FITSData::Record("Observer", Options::defaultObserver(), "Observer"));
0659     if (activeJob()->getCoreProperty(SequenceJob::SJ_TargetName) != "")
0660         FITSHeaders.append(FITSData::Record("Object", activeJob()->getCoreProperty(SequenceJob::SJ_TargetName).toString(),
0661                                             "Object"));
0662     FITSHeaders.append(FITSData::Record("TELESCOP", m_Scope, "Telescope"));
0663 
0664     if (!FITSHeaders.isEmpty())
0665         activeCamera()->setFITSHeaders(FITSHeaders);
0666 
0667     // Update button status
0668     state()->setBusy(true);
0669     state()->setUseGuideHead((devices()->getActiveChip()->getType() == ISD::CameraChip::PRIMARY_CCD) ?
0670                              false : true);
0671 
0672     emit syncGUIToJob(activeJob());
0673 
0674     // If the job is a dark flat, let's find the optimal exposure from prior
0675     // flat exposures.
0676     if (activeJob()->jobType() == SequenceJob::JOBTYPE_DARKFLAT)
0677     {
0678         // If we found a prior exposure, and current upload more is not local, then update full prefix
0679         if (state()->setDarkFlatExposure(activeJob())
0680                 && activeCamera()->getUploadMode() != ISD::Camera::UPLOAD_LOCAL)
0681         {
0682             auto placeholderPath = PlaceholderPath();
0683             // Make sure to update Full Prefix as exposure value was changed
0684             placeholderPath.processJobInfo(activeJob());
0685             state()->setNextSequenceID(1);
0686         }
0687 
0688     }
0689 
0690     updatePreCaptureCalibrationStatus();
0691 
0692 }
0693 
0694 void CaptureProcess::prepareJobExecution()
0695 {
0696     if (activeJob() == nullptr)
0697     {
0698         qWarning(KSTARS_EKOS_CAPTURE) << "preparePreCaptureActions with null activeJob().";
0699         // Everything below depends on activeJob(). Just return.
0700         return;
0701     }
0702 
0703     state()->setBusy(true);
0704 
0705     // Update guiderActive before prepareCapture.
0706     activeJob()->setCoreProperty(SequenceJob::SJ_GuiderActive,
0707                                  state()->isActivelyGuiding());
0708 
0709     // signal that capture preparation steps should be executed
0710     activeJob()->prepareCapture();
0711 
0712     // update the UI
0713     emit jobExecutionPreparationStarted();
0714 }
0715 
0716 void CaptureProcess::refreshOpticalTrain(QString name)
0717 {
0718     auto mount = OpticalTrainManager::Instance()->getMount(name);
0719     setMount(mount);
0720 
0721     auto scope = OpticalTrainManager::Instance()->getScope(name);
0722     setScope(scope["name"].toString());
0723 
0724     auto camera = OpticalTrainManager::Instance()->getCamera(name);
0725     setCamera(camera);
0726 
0727     auto filterWheel = OpticalTrainManager::Instance()->getFilterWheel(name);
0728     setFilterWheel(filterWheel);
0729 
0730     auto rotator = OpticalTrainManager::Instance()->getRotator(name);
0731     setRotator(rotator);
0732 
0733     auto dustcap = OpticalTrainManager::Instance()->getDustCap(name);
0734     setDustCap(dustcap);
0735 
0736     auto lightbox = OpticalTrainManager::Instance()->getLightBox(name);
0737     setLightBox(lightbox);
0738 }
0739 
0740 IPState CaptureProcess::checkLightFramePendingTasks()
0741 {
0742     // step 1: did one of the pending jobs fail or has the user aborted the capture?
0743     if (state()->getCaptureState() == CAPTURE_ABORTED)
0744         return IPS_ALERT;
0745 
0746     // step 2: check if pausing has been requested
0747     if (checkPausing(CaptureModuleState::CONTINUE_ACTION_NEXT_EXPOSURE) == true)
0748         return IPS_BUSY;
0749 
0750     // step 3: check if a meridian flip is active
0751     if (state()->checkMeridianFlipActive())
0752         return IPS_BUSY;
0753 
0754     // step 4: check guide deviation for non meridian flip stages if the initial guide limit is set.
0755     //         Wait until the guide deviation is reported to be below the limit (@see setGuideDeviation(double, double)).
0756     if (state()->getCaptureState() == CAPTURE_PROGRESS &&
0757             state()->getGuideState() == GUIDE_GUIDING &&
0758             Options::enforceStartGuiderDrift())
0759         return IPS_BUSY;
0760 
0761     // step 5: check if dithering is required or running
0762     if ((state()->getCaptureState() == CAPTURE_DITHERING && state()->getDitheringState() != IPS_OK)
0763             || state()->checkDithering())
0764         return IPS_BUSY;
0765 
0766     // step 6: check if re-focusing is required
0767     //         Needs to be checked after dithering checks to avoid dithering in parallel
0768     //         to focusing, since @startFocusIfRequired() might change its value over time
0769     if ((state()->getCaptureState() == CAPTURE_FOCUSING && state()->checkFocusRunning())
0770             || state()->startFocusIfRequired())
0771         return IPS_BUSY;
0772 
0773     // step 7: resume guiding if it was suspended
0774     // JM 2023.12.20: Must make to resume if we have a light frame.
0775     if (state()->getGuideState() == GUIDE_SUSPENDED && activeJob()->getFrameType() == FRAME_LIGHT)
0776     {
0777         emit newLog(i18n("Autoguiding resumed."));
0778         emit resumeGuiding();
0779         // No need to return IPS_BUSY here, we can continue immediately.
0780         // In the case that the capturing sequence has a guiding limit,
0781         // capturing will be interrupted by setGuideDeviation().
0782     }
0783 
0784     // everything is ready for capturing light frames
0785     return IPS_OK;
0786 
0787 }
0788 
0789 void CaptureProcess::captureStarted(CaptureModuleState::CAPTUREResult rc)
0790 {
0791     switch (rc)
0792     {
0793         case CaptureModuleState::CAPTURE_OK:
0794         {
0795             state()->setCaptureState(CAPTURE_CAPTURING);
0796             state()->getCaptureTimeout().start(static_cast<int>(activeJob()->getCoreProperty(
0797                                                    SequenceJob::SJ_Exposure).toDouble()) * 1000 +
0798                                                CAPTURE_TIMEOUT_THRESHOLD);
0799             // calculate remaining capture time for the current job
0800             state()->imageCountDown().setHMS(0, 0, 0);
0801             double ms_left = std::ceil(activeJob()->getExposeLeft() * 1000.0);
0802             state()->imageCountDownAddMSecs(int(ms_left));
0803             state()->setLastRemainingFrameTimeMS(ms_left);
0804             state()->sequenceCountDown().setHMS(0, 0, 0);
0805             state()->sequenceCountDownAddMSecs(activeJob()->getJobRemainingTime(state()->averageDownloadTime()) * 1000);
0806             // ensure that the download time label is visible
0807 
0808             if (activeJob()->jobType() != SequenceJob::JOBTYPE_PREVIEW)
0809             {
0810                 auto index = state()->allJobs().indexOf(activeJob());
0811                 if (index >= 0 && index < state()->getSequence().count())
0812                     state()->changeSequenceValue(index, "Status", "In Progress");
0813 
0814                 emit updateJobTable(activeJob());
0815             }
0816             emit captureRunning();
0817         }
0818         break;
0819 
0820         case CaptureModuleState::CAPTURE_FRAME_ERROR:
0821             emit newLog(i18n("Failed to set sub frame."));
0822             emit stopCapturing(CAPTURE_ABORTED);
0823             break;
0824 
0825         case CaptureModuleState::CAPTURE_BIN_ERROR:
0826             emit newLog((i18n("Failed to set binning.")));
0827             emit stopCapturing(CAPTURE_ABORTED);
0828             break;
0829 
0830         case CaptureModuleState::CAPTURE_FOCUS_ERROR:
0831             emit newLog((i18n("Cannot capture while focus module is busy.")));
0832             emit stopCapturing(CAPTURE_ABORTED);
0833             break;
0834     }
0835 }
0836 
0837 void CaptureProcess::checkNextExposure()
0838 {
0839     IPState started = startNextExposure();
0840     // if starting the next exposure did not succeed due to pending jobs running,
0841     // we retry after 1 second
0842     if (started == IPS_BUSY)
0843         QTimer::singleShot(1000, this, &CaptureProcess::checkNextExposure);
0844 }
0845 
0846 IPState CaptureProcess::startNextExposure()
0847 {
0848     // Since this function is looping while pending tasks are running in parallel
0849     // it might happen that one of them leads to abort() which sets the #activeJob() to nullptr.
0850     // In this case we terminate the loop by returning #IPS_IDLE without starting a new capture.
0851     auto theJob = activeJob();
0852 
0853     if (theJob == nullptr)
0854         return IPS_IDLE;
0855 
0856     // check pending jobs for light frames. All other frame types do not contain mid-sequence checks.
0857     if (activeJob()->getFrameType() == FRAME_LIGHT)
0858     {
0859         IPState pending = checkLightFramePendingTasks();
0860         if (pending != IPS_OK)
0861             // there are still some jobs pending
0862             return pending;
0863     }
0864 
0865     const int seqDelay = theJob->getCoreProperty(SequenceJob::SJ_Delay).toInt();
0866     // nothing pending, let's start the next exposure
0867     if (seqDelay > 0)
0868     {
0869         state()->setCaptureState(CAPTURE_WAITING);
0870     }
0871     state()->getSeqDelayTimer().start(seqDelay);
0872 
0873     return IPS_OK;
0874 }
0875 
0876 IPState CaptureProcess::resumeSequence()
0877 {
0878     // before we resume, we will check if pausing is requested
0879     if (checkPausing(CaptureModuleState::CONTINUE_ACTION_CAPTURE_COMPLETE) == true)
0880         return IPS_BUSY;
0881 
0882     // If no job is active, we have to find if there are more pending jobs in the queue
0883     if (!activeJob())
0884     {
0885         return startNextJob();
0886     }
0887     // Otherwise, let's prepare for next exposure.
0888 
0889     // if we're done
0890     else if (activeJob()->getCoreProperty(SequenceJob::SJ_Count).toInt() <=
0891              activeJob()->getCompleted())
0892     {
0893         processJobCompletion1();
0894         return IPS_OK;
0895     }
0896     // continue the current job
0897     else
0898     {
0899         // If we suspended guiding due to primary chip download, resume guide chip guiding now - unless
0900         // a meridian flip is ongoing
0901         if (state()->getGuideState() == GUIDE_SUSPENDED && state()->suspendGuidingOnDownload() &&
0902                 state()->getMeridianFlipState()->checkMeridianFlipActive() == false)
0903         {
0904             qCInfo(KSTARS_EKOS_CAPTURE) << "Resuming guiding...";
0905             emit resumeGuiding();
0906         }
0907 
0908         // If looping, we just increment the file system image count
0909         if (activeCamera()->isFastExposureEnabled())
0910         {
0911             if (activeCamera()->getUploadMode() != ISD::Camera::UPLOAD_LOCAL)
0912             {
0913                 state()->checkSeqBoundary(state()->sequenceURL());
0914                 activeCamera()->setNextSequenceID(state()->nextSequenceID());
0915             }
0916         }
0917 
0918         // ensure state image received to recover properly after pausing
0919         state()->setCaptureState(CAPTURE_IMAGE_RECEIVED);
0920 
0921         // JM 2020-12-06: Check if we need to execute pre-capture script first.
0922         if (runCaptureScript(SCRIPT_PRE_CAPTURE) == IPS_BUSY)
0923         {
0924             if (activeCamera()->isFastExposureEnabled())
0925             {
0926                 state()->setRememberFastExposure(true);
0927                 activeCamera()->setFastExposureEnabled(false);
0928             }
0929             return IPS_BUSY;
0930         }
0931         else
0932         {
0933             // Check if we need to stop fast exposure to perform any
0934             // pending tasks. If not continue as is.
0935             if (activeCamera()->isFastExposureEnabled())
0936             {
0937                 if (activeJob() &&
0938                         activeJob()->getFrameType() == FRAME_LIGHT &&
0939                         checkLightFramePendingTasks() == IPS_OK)
0940                 {
0941                     // Continue capturing seamlessly
0942                     state()->setCaptureState(CAPTURE_CAPTURING);
0943                     return IPS_OK;
0944                 }
0945 
0946                 // Stop fast exposure now.
0947                 state()->setRememberFastExposure(true);
0948                 activeCamera()->setFastExposureEnabled(false);
0949             }
0950 
0951             checkNextExposure();
0952 
0953         }
0954     }
0955 
0956     return IPS_OK;
0957 
0958 }
0959 
0960 void CaptureProcess::processFITSData(const QSharedPointer<FITSData> &data)
0961 {
0962     ISD::CameraChip * tChip = nullptr;
0963 
0964     QString blobInfo;
0965     if (data)
0966     {
0967         state()->setImageData(data);
0968         blobInfo = QString("{Device: %1 Property: %2 Element: %3 Chip: %4}").arg(data->property("device").toString())
0969                    .arg(data->property("blobVector").toString())
0970                    .arg(data->property("blobElement").toString())
0971                    .arg(data->property("chip").toInt());
0972     }
0973     else
0974         state()->imageData().reset();
0975 
0976     const SequenceJob *job = activeJob();
0977     // If there is no active job, ignore
0978     if (job == nullptr)
0979     {
0980         if (data)
0981             qCWarning(KSTARS_EKOS_CAPTURE) << blobInfo << "Ignoring received FITS as active job is null.";
0982 
0983         emit processingFITSfinished(false);
0984         return;
0985     }
0986 
0987     if (state()->getMeridianFlipState()->getMeridianFlipStage() >= MeridianFlipState::MF_ALIGNING)
0988     {
0989         if (data)
0990             qCWarning(KSTARS_EKOS_CAPTURE) << blobInfo << "Ignoring Received FITS as meridian flip stage is" <<
0991                                            state()->getMeridianFlipState()->getMeridianFlipStage();
0992         emit processingFITSfinished(false);
0993         return;
0994     }
0995 
0996     // If image is client or both, let's process it.
0997     if (activeCamera() && activeCamera()->getUploadMode() != ISD::Camera::UPLOAD_LOCAL)
0998     {
0999 
1000         if (state()->getCaptureState() == CAPTURE_IDLE || state()->getCaptureState() == CAPTURE_ABORTED)
1001         {
1002             qCWarning(KSTARS_EKOS_CAPTURE) << blobInfo << "Ignoring Received FITS as current capture state is not active" <<
1003                                            state()->getCaptureState();
1004 
1005             emit processingFITSfinished(false);
1006             return;
1007         }
1008 
1009         if (data)
1010         {
1011             tChip = activeCamera()->getChip(static_cast<ISD::CameraChip::ChipType>
1012                                             (data->property("chip").toInt()));
1013             if (tChip != devices()->getActiveChip())
1014             {
1015                 if (state()->getGuideState() == GUIDE_IDLE)
1016                     qCWarning(KSTARS_EKOS_CAPTURE) << blobInfo << "Ignoring Received FITS as it does not correspond to the target chip"
1017                                                    << devices()->getActiveChip()->getType();
1018 
1019                 emit processingFITSfinished(false);
1020                 return;
1021             }
1022         }
1023 
1024         if (devices()->getActiveChip()->getCaptureMode() == FITS_FOCUS ||
1025                 devices()->getActiveChip()->getCaptureMode() == FITS_GUIDE)
1026         {
1027             qCWarning(KSTARS_EKOS_CAPTURE) << blobInfo << "Ignoring Received FITS as it has the wrong capture mode" <<
1028                                            devices()->getActiveChip()->getCaptureMode();
1029 
1030             emit processingFITSfinished(false);
1031             return;
1032         }
1033 
1034         // If the FITS is not for our device, simply ignore
1035 
1036         if (data && data->property("device").toString() != activeCamera()->getDeviceName())
1037         {
1038             qCWarning(KSTARS_EKOS_CAPTURE) << blobInfo << "Ignoring Received FITS as the blob device name does not equal active camera"
1039                                            << activeCamera()->getDeviceName();
1040 
1041             emit processingFITSfinished(false);
1042             return;
1043         }
1044 
1045         // If dark is selected, perform dark substraction.
1046         if (data && Options::autoDark() && job->jobType() == SequenceJob::JOBTYPE_PREVIEW && state()->useGuideHead() == false)
1047         {
1048             QVariant trainID = ProfileSettings::Instance()->getOneSetting(ProfileSettings::CaptureOpticalTrain);
1049             if (trainID.isValid())
1050             {
1051                 m_DarkProcessor.data()->denoise(trainID.toUInt(),
1052                                                 devices()->getActiveChip(),
1053                                                 state()->imageData(),
1054                                                 job->getCoreProperty(SequenceJob::SJ_Exposure).toDouble(),
1055                                                 job->getCoreProperty(SequenceJob::SJ_ROI).toRect().x(),
1056                                                 job->getCoreProperty(SequenceJob::SJ_ROI).toRect().y());
1057             }
1058             else
1059                 qWarning(KSTARS_EKOS_CAPTURE) << "Invalid train ID for darks substraction:" << trainID.toUInt();
1060 
1061         }
1062 
1063         // set image metadata
1064         updateImageMetadataAction(state()->imageData());
1065     }
1066 
1067     // image has been received and processed successfully.
1068     state()->setCaptureState(CAPTURE_IMAGE_RECEIVED);
1069     // processing finished successfully
1070     imageCapturingCompleted();
1071     // hand over to the capture module
1072     emit processingFITSfinished(true);
1073 }
1074 
1075 void CaptureProcess::processNewRemoteFile(QString file)
1076 {
1077     emit newLog(i18n("Remote image saved to %1", file));
1078     // call processing steps without image data if the image is stored only remotely
1079     if (activeCamera() && activeCamera()->getUploadMode() == ISD::Camera::UPLOAD_LOCAL)
1080         processFITSData(nullptr);
1081 }
1082 
1083 void CaptureProcess::imageCapturingCompleted()
1084 {
1085     SequenceJob *thejob = activeJob();
1086 
1087     if (thejob == nullptr)
1088         return;
1089 
1090     // If fast exposure is off, disconnect exposure progress
1091     // otherwise, keep it going since it fires off from driver continuous capture process.
1092     if (activeCamera()->isFastExposureEnabled() == false)
1093     {
1094         disconnect(activeCamera(), &ISD::Camera::newExposureValue, this,
1095                    &CaptureProcess::setExposureProgress);
1096         DarkLibrary::Instance()->disconnect(this);
1097     }
1098     // stop timers
1099     state()->getCaptureTimeout().stop();
1100     state()->setCaptureTimeoutCounter(0);
1101 
1102     state()->downloadProgressTimer().stop();
1103 
1104     // In case we're framing, let's return quickly to continue the process.
1105     if (state()->isLooping())
1106     {
1107         continueFramingAction(state()->imageData());
1108         return;
1109     }
1110 
1111     // Update download times.
1112     updateDownloadTimesAction();
1113 
1114     // If it was initially set as pure preview job and NOT as preview for calibration
1115     if (previewImageCompletedAction(state()->imageData()) == IPS_OK)
1116         return;
1117 
1118     // update counters
1119     updateCompletedCaptureCountersAction();
1120 
1121     switch (thejob->getFrameType())
1122     {
1123         case FRAME_BIAS:
1124         case FRAME_DARK:
1125             thejob->setCalibrationStage(SequenceJobState::CAL_CALIBRATION_COMPLETE);
1126             break;
1127         case FRAME_FLAT:
1128             /* calibration not completed, adapt exposure time */
1129             if (thejob->getFlatFieldDuration() == DURATION_ADU
1130                     && thejob->getCoreProperty(SequenceJob::SJ_TargetADU).toDouble() > 0 &&
1131                     checkFlatCalibration(state()->imageData(), state()->exposureRange().min,
1132                                          state()->exposureRange().max) == false)
1133                 return; /* calibration not completed */
1134             thejob->setCalibrationStage(SequenceJobState::CAL_CALIBRATION_COMPLETE);
1135             break;
1136         case FRAME_LIGHT:
1137             // don nothing, continue
1138             break;
1139         case FRAME_NONE:
1140             // this should not happen!
1141             qWarning(KSTARS_EKOS_CAPTURE) << "Job completed with frametype NONE!";
1142             return;
1143     }
1144 
1145     if (thejob->getCalibrationStage() == SequenceJobState::CAL_CALIBRATION_COMPLETE)
1146         thejob->setCalibrationStage(SequenceJobState::CAL_CAPTURING);
1147 
1148     // JM 2020-06-17: Emit newImage for LOCAL images (stored on remote host)
1149     //if (m_Camera->getUploadMode() == ISD::Camera::UPLOAD_LOCAL)
1150     emit newImage(thejob, state()->imageData());
1151 
1152 
1153     // Check if we need to execute post capture script first
1154     if (runCaptureScript(SCRIPT_POST_CAPTURE) == IPS_BUSY)
1155         return;
1156 
1157     resumeSequence();
1158 }
1159 
1160 IPState CaptureProcess::processPreCaptureCalibrationStage()
1161 {
1162     // in some rare cases it might happen that activeJob() has been cleared by a concurrent thread
1163     if (activeJob() == nullptr)
1164     {
1165         qCWarning(KSTARS_EKOS_CAPTURE) << "Processing pre capture calibration without active job, state = " <<
1166                                        getCaptureStatusString(state()->getCaptureState());
1167         return IPS_ALERT;
1168     }
1169 
1170     // If we are currently guide and the frame is NOT a light frame, then we shopld suspend.
1171     // N.B. The guide camera could be on its own scope unaffected but it doesn't hurt to stop
1172     // guiding since it is no longer used anyway.
1173     if (activeJob()->getFrameType() != FRAME_LIGHT
1174             && state()->getGuideState() == GUIDE_GUIDING)
1175     {
1176         emit newLog(i18n("Autoguiding suspended."));
1177         emit suspendGuiding();
1178     }
1179 
1180     // Run necessary tasks for each frame type
1181     switch (activeJob()->getFrameType())
1182     {
1183         case FRAME_LIGHT:
1184             return checkLightFramePendingTasks();
1185 
1186         // FIXME Remote flats are not working since the files are saved remotely and no
1187         // preview is done locally first to calibrate the image.
1188         case FRAME_FLAT:
1189         case FRAME_BIAS:
1190         case FRAME_DARK:
1191         case FRAME_NONE:
1192             // no actions necessary
1193             break;
1194     }
1195 
1196     return IPS_OK;
1197 
1198 }
1199 
1200 void CaptureProcess::updatePreCaptureCalibrationStatus()
1201 {
1202     // If process was aborted or stopped by the user
1203     if (state()->isBusy() == false)
1204     {
1205         emit newLog(i18n("Warning: Calibration process was prematurely terminated."));
1206         return;
1207     }
1208 
1209     IPState rc = processPreCaptureCalibrationStage();
1210 
1211     if (rc == IPS_ALERT)
1212         return;
1213     else if (rc == IPS_BUSY)
1214     {
1215         QTimer::singleShot(1000, this, &CaptureProcess::updatePreCaptureCalibrationStatus);
1216         return;
1217     }
1218 
1219     captureImage();
1220 }
1221 
1222 void CaptureProcess::processJobCompletion1()
1223 {
1224     if (activeJob() == nullptr)
1225     {
1226         qWarning(KSTARS_EKOS_CAPTURE) << "procesJobCompletionStage1 with null activeJob().";
1227     }
1228     else
1229     {
1230         // JM 2020-12-06: Check if we need to execute post-job script first.
1231         if (runCaptureScript(SCRIPT_POST_JOB) == IPS_BUSY)
1232             return;
1233     }
1234 
1235     processJobCompletion2();
1236 }
1237 
1238 void CaptureProcess::processJobCompletion2()
1239 {
1240     if (activeJob() == nullptr)
1241     {
1242         qWarning(KSTARS_EKOS_CAPTURE) << "procesJobCompletionStage2 with null activeJob().";
1243     }
1244     else
1245     {
1246         activeJob()->done();
1247 
1248         if (activeJob()->jobType() != SequenceJob::JOBTYPE_PREVIEW)
1249         {
1250             int index = state()->allJobs().indexOf(activeJob());
1251             QJsonArray seqArray = state()->getSequence();
1252             QJsonObject oneSequence = seqArray[index].toObject();
1253             oneSequence["Status"] = "Complete";
1254             seqArray.replace(index, oneSequence);
1255             state()->setSequence(seqArray);
1256             emit sequenceChanged(seqArray);
1257             emit updateJobTable(activeJob());
1258         }
1259     }
1260     // stopping clears the planned state, therefore skip if pause planned
1261     if (state()->getCaptureState() != CAPTURE_PAUSE_PLANNED)
1262         emit stopCapture();
1263 
1264     // Check if there are more pending jobs and execute them
1265     if (resumeSequence() == IPS_OK)
1266         return;
1267     // Otherwise, we're done. We park if required and resume guiding if no parking is done and autoguiding was engaged before.
1268     else
1269     {
1270         //KNotification::event(QLatin1String("CaptureSuccessful"), i18n("CCD capture sequence completed"));
1271         KSNotification::event(QLatin1String("CaptureSuccessful"), i18n("CCD capture sequence completed"),
1272                               KSNotification::Capture);
1273 
1274         emit stopCapture(CAPTURE_COMPLETE);
1275 
1276         //Resume guiding if it was suspended before
1277         //if (isAutoGuiding && currentCCD->getChip(ISD::CameraChip::GUIDE_CCD) == guideChip)
1278         if (state()->getGuideState() == GUIDE_SUSPENDED && state()->suspendGuidingOnDownload())
1279             emit resumeGuiding();
1280     }
1281 
1282 }
1283 
1284 IPState CaptureProcess::startNextJob()
1285 {
1286     SequenceJob * next_job = nullptr;
1287 
1288     for (auto &oneJob : state()->allJobs())
1289     {
1290         if (oneJob->getStatus() == JOB_IDLE || oneJob->getStatus() == JOB_ABORTED)
1291         {
1292             next_job = oneJob;
1293             break;
1294         }
1295     }
1296 
1297     if (next_job)
1298     {
1299 
1300         prepareJob(next_job);
1301 
1302         //Resume guiding if it was suspended before, except for an active meridian flip is running.
1303         //if (isAutoGuiding && currentCCD->getChip(ISD::CameraChip::GUIDE_CCD) == guideChip)
1304         if (state()->getGuideState() == GUIDE_SUSPENDED && state()->suspendGuidingOnDownload() &&
1305                 state()->getMeridianFlipState()->checkMeridianFlipActive() == false)
1306         {
1307             qCDebug(KSTARS_EKOS_CAPTURE) << "Resuming guiding...";
1308             emit resumeGuiding();
1309         }
1310 
1311         return IPS_OK;
1312     }
1313     else
1314     {
1315         qCDebug(KSTARS_EKOS_CAPTURE) << "All capture jobs complete.";
1316         return IPS_BUSY;
1317     }
1318 }
1319 
1320 void CaptureProcess::captureImage()
1321 {
1322     if (activeJob() == nullptr)
1323         return;
1324 
1325     // Bail out if we have no CCD anymore
1326     if (!activeCamera() || !activeCamera()->isConnected())
1327     {
1328         emit newLog(i18n("Error: Lost connection to CCD."));
1329         emit stopCapture(CAPTURE_ABORTED);
1330         return;
1331     }
1332 
1333     state()->getCaptureTimeout().stop();
1334     state()->getSeqDelayTimer().stop();
1335     state()->getCaptureDelayTimer().stop();
1336     if (activeCamera()->isFastExposureEnabled())
1337     {
1338         int remaining = state()->isLooping() ? 100000 : (activeJob()->getCoreProperty(
1339                             SequenceJob::SJ_Count).toInt() -
1340                         activeJob()->getCompleted());
1341         if (remaining > 1)
1342             activeCamera()->setFastCount(static_cast<uint>(remaining));
1343     }
1344 
1345     setCamera(true);
1346 
1347     if (activeJob()->getFrameType() == FRAME_FLAT)
1348     {
1349         // If we have to calibrate ADU levels, first capture must be preview and not in batch mode
1350         if (activeJob()->jobType() != SequenceJob::JOBTYPE_PREVIEW
1351                 && activeJob()->getFlatFieldDuration() == DURATION_ADU &&
1352                 activeJob()->getCalibrationStage() == SequenceJobState::CAL_NONE)
1353         {
1354             if (activeCamera()->getEncodingFormat() != "FITS" &&
1355                     activeCamera()->getEncodingFormat() != "XISF")
1356             {
1357                 emit newLog(i18n("Cannot calculate ADU levels in non-FITS images."));
1358                 emit stopCapture(CAPTURE_ABORTED);
1359                 return;
1360             }
1361 
1362             activeJob()->setCalibrationStage(SequenceJobState::CAL_CALIBRATION);
1363         }
1364     }
1365 
1366     // If preview, always set to UPLOAD_CLIENT if not already set.
1367     if (activeJob()->jobType() == SequenceJob::JOBTYPE_PREVIEW)
1368     {
1369         if (activeCamera()->getUploadMode() != ISD::Camera::UPLOAD_CLIENT)
1370             activeCamera()->setUploadMode(ISD::Camera::UPLOAD_CLIENT);
1371     }
1372     // If batch mode, ensure upload mode mathces the active job target.
1373     else
1374     {
1375         if (activeCamera()->getUploadMode() != activeJob()->getUploadMode())
1376             activeCamera()->setUploadMode(activeJob()->getUploadMode());
1377     }
1378 
1379     if (activeCamera()->getUploadMode() != ISD::Camera::UPLOAD_LOCAL)
1380     {
1381         state()->checkSeqBoundary(state()->sequenceURL());
1382         activeCamera()->setNextSequenceID(state()->nextSequenceID());
1383     }
1384 
1385     // Re-enable fast exposure if it was disabled before due to pending tasks
1386     if (state()->isRememberFastExposure())
1387     {
1388         state()->setRememberFastExposure(false);
1389         activeCamera()->setFastExposureEnabled(true);
1390     }
1391 
1392     if (state()->frameSettings().contains(devices()->getActiveChip()))
1393     {
1394         const auto roi = activeJob()->getCoreProperty(SequenceJob::SJ_ROI).toRect();
1395         QVariantMap settings;
1396         settings["x"]    = roi.x();
1397         settings["y"]    = roi.y();
1398         settings["w"]    = roi.width();
1399         settings["h"]    = roi.height();
1400         settings["binx"] = activeJob()->getCoreProperty(SequenceJob::SJ_Binning).toPoint().x();
1401         settings["biny"] = activeJob()->getCoreProperty(SequenceJob::SJ_Binning).toPoint().y();
1402 
1403         state()->frameSettings()[devices()->getActiveChip()] = settings;
1404     }
1405 
1406     // If using DSLR, make sure it is set to correct transfer format
1407     activeCamera()->setEncodingFormat(activeJob()->getCoreProperty(
1408                                           SequenceJob::SJ_Encoding).toString());
1409 
1410     state()->setStartingCapture(true);
1411     auto placeholderPath = PlaceholderPath(state()->sequenceURL().toLocalFile());
1412     placeholderPath.setGenerateFilenameSettings(*activeJob());
1413     activeCamera()->setPlaceholderPath(placeholderPath);
1414     // now hand over the control of capturing to the sequence job. As soon as capturing
1415     // has started, the sequence job will report the result with the captureStarted() event
1416     // that will trigger Capture::captureStarted()
1417     activeJob()->startCapturing(state()->getRefocusState()->isAutoFocusReady(),
1418                                 activeJob()->getCalibrationStage() == SequenceJobState::CAL_CALIBRATION ? FITS_CALIBRATE :
1419                                 FITS_NORMAL);
1420 
1421     // Re-enable fast exposure if it was disabled before due to pending tasks
1422     if (state()->isRememberFastExposure())
1423     {
1424         state()->setRememberFastExposure(false);
1425         activeCamera()->setFastExposureEnabled(true);
1426     }
1427 
1428     emit captureTarget(activeJob()->getCoreProperty(SequenceJob::SJ_TargetName).toString());
1429     emit captureImageStarted();
1430 }
1431 
1432 void CaptureProcess::resetFrame()
1433 {
1434     devices()->setActiveChip(state()->useGuideHead() ?
1435                              devices()->getActiveCamera()->getChip(
1436                                  ISD::CameraChip::GUIDE_CCD) :
1437                              devices()->getActiveCamera()->getChip(ISD::CameraChip::PRIMARY_CCD));
1438     devices()->getActiveChip()->resetFrame();
1439     emit updateFrameProperties(1);
1440 }
1441 
1442 void CaptureProcess::setExposureProgress(ISD::CameraChip *tChip, double value, IPState ipstate)
1443 {
1444     // ignore values if not capturing
1445     if (state()->checkCapturing() == false)
1446         return;
1447 
1448     if (devices()->getActiveChip() != tChip ||
1449             devices()->getActiveChip()->getCaptureMode() != FITS_NORMAL
1450             || state()->getMeridianFlipState()->getMeridianFlipStage() >= MeridianFlipState::MF_ALIGNING)
1451         return;
1452 
1453     double deltaMS = std::ceil(1000.0 * value - state()->lastRemainingFrameTimeMS());
1454     emit updateCaptureCountDown(int(deltaMS));
1455     state()->setLastRemainingFrameTimeMS(state()->lastRemainingFrameTimeMS() + deltaMS);
1456 
1457     if (activeJob())
1458     {
1459         activeJob()->setExposeLeft(value);
1460 
1461         emit newExposureProgress(activeJob());
1462     }
1463 
1464     if (activeJob() && ipstate == IPS_ALERT)
1465     {
1466         int retries = activeJob()->getCaptureRetires() + 1;
1467 
1468         activeJob()->setCaptureRetires(retries);
1469 
1470         emit newLog(i18n("Capture failed. Check INDI Control Panel for details."));
1471 
1472         if (retries >= 3)
1473         {
1474             activeJob()->abort();
1475             return;
1476         }
1477 
1478         emit newLog((i18n("Restarting capture attempt #%1", retries)));
1479 
1480         state()->setNextSequenceID(1);
1481 
1482         captureImage();
1483         return;
1484     }
1485 
1486     if (activeJob() != nullptr && ipstate == IPS_OK)
1487     {
1488         activeJob()->setCaptureRetires(0);
1489         activeJob()->setExposeLeft(0);
1490 
1491         if (devices()->getActiveCamera()
1492                 && devices()->getActiveCamera()->getUploadMode() == ISD::Camera::UPLOAD_LOCAL)
1493         {
1494             if (activeJob()->getStatus() == JOB_BUSY)
1495             {
1496                 emit processingFITSfinished(false);
1497                 return;
1498             }
1499         }
1500 
1501         if (state()->getGuideState() == GUIDE_GUIDING && Options::guiderType() == 0
1502                 && state()->suspendGuidingOnDownload())
1503         {
1504             qCDebug(KSTARS_EKOS_CAPTURE) << "Autoguiding suspended until primary CCD chip completes downloading...";
1505             emit suspendGuiding();
1506         }
1507 
1508         emit downloadingFrame();
1509 
1510         //This will start the clock to see how long the download takes.
1511         state()->downloadTimer().start();
1512         state()->downloadProgressTimer().start();
1513     }
1514 }
1515 
1516 void CaptureProcess::setDownloadProgress()
1517 {
1518     if (activeJob())
1519     {
1520         double downloadTimeLeft = state()->averageDownloadTime() - state()->downloadTimer().elapsed() /
1521                                   1000.0;
1522         if(downloadTimeLeft >= 0)
1523         {
1524             state()->imageCountDown().setHMS(0, 0, 0);
1525             state()->imageCountDownAddMSecs(int(std::ceil(downloadTimeLeft * 1000)));
1526             emit newDownloadProgress(downloadTimeLeft);
1527         }
1528     }
1529 
1530 }
1531 
1532 IPState CaptureProcess::continueFramingAction(const QSharedPointer<FITSData> &imageData)
1533 {
1534     emit newImage(activeJob(), imageData);
1535     // If fast exposure is on, do not capture again, it will be captured by the driver.
1536     if (activeCamera()->isFastExposureEnabled() == false)
1537     {
1538         const int seqDelay = activeJob()->getCoreProperty(SequenceJob::SJ_Delay).toInt();
1539 
1540         if (seqDelay > 0)
1541         {
1542             QTimer::singleShot(seqDelay, this, [this]()
1543             {
1544                 activeJob()->startCapturing(state()->getRefocusState()->isAutoFocusReady(),
1545                                             FITS_NORMAL);
1546             });
1547         }
1548         else
1549             activeJob()->startCapturing(state()->getRefocusState()->isAutoFocusReady(),
1550                                         FITS_NORMAL);
1551     }
1552     return IPS_OK;
1553 
1554 }
1555 
1556 IPState CaptureProcess::updateDownloadTimesAction()
1557 {
1558     // Do not calculate download time for images stored on server.
1559     // Only calculate for longer exposures.
1560     if (activeCamera()->getUploadMode() != ISD::Camera::UPLOAD_LOCAL
1561             && state()->downloadTimer().isValid())
1562     {
1563         //This determines the time since the image started downloading
1564         double currentDownloadTime = state()->downloadTimer().elapsed() / 1000.0;
1565         state()->addDownloadTime(currentDownloadTime);
1566         // Always invalidate timer as it must be explicitly started.
1567         state()->downloadTimer().invalidate();
1568 
1569         QString dLTimeString = QString::number(currentDownloadTime, 'd', 2);
1570         QString estimatedTimeString = QString::number(state()->averageDownloadTime(), 'd', 2);
1571         emit newLog(i18n("Download Time: %1 s, New Download Time Estimate: %2 s.", dLTimeString, estimatedTimeString));
1572     }
1573     return IPS_OK;
1574 }
1575 
1576 IPState CaptureProcess::previewImageCompletedAction(QSharedPointer<FITSData> imageData)
1577 {
1578     if (activeJob()->jobType() == SequenceJob::JOBTYPE_PREVIEW)
1579     {
1580         //sendNewImage(blobFilename, blobChip);
1581         emit newImage(activeJob(), imageData);
1582         // Reset upload mode if it was changed by preview
1583         activeCamera()->setUploadMode(activeJob()->getUploadMode());
1584         // Reset active job pointer
1585         state()->setActiveJob(nullptr);
1586         emit stopCapture(CAPTURE_COMPLETE);
1587         if (state()->getGuideState() == GUIDE_SUSPENDED && state()->suspendGuidingOnDownload())
1588             emit resumeGuiding();
1589         return IPS_OK;
1590     }
1591     else
1592         return IPS_IDLE;
1593 
1594 }
1595 
1596 IPState CaptureProcess::updateCompletedCaptureCountersAction()
1597 {
1598     // update counters if not in preview mode or calibrating
1599     if (activeJob()->jobType() != SequenceJob::JOBTYPE_PREVIEW
1600             && activeJob()->getCalibrationStage() != SequenceJobState::CAL_CALIBRATION)
1601     {
1602         /* Increase the sequence's current capture count */
1603         updatedCaptureCompleted(activeJob()->getCompleted() + 1);
1604         /* Decrease the counter for in-sequence focusing */
1605         state()->getRefocusState()->decreaseInSequenceFocusCounter();
1606         /* Reset adaptive focus flag */
1607         state()->getRefocusState()->setAdaptiveFocusDone(false);
1608     }
1609 
1610     /* Decrease the dithering counter except for directly after meridian flip                                              */
1611     /* Hint: this isonly relevant when a meridian flip happened during a paused sequence when pressing "Start" afterwards. */
1612     if (state()->getMeridianFlipState()->getMeridianFlipStage() < MeridianFlipState::MF_FLIPPING)
1613         state()->decreaseDitherCounter();
1614 
1615     /* If we were assigned a captured frame map, also increase the relevant counter for prepareJob */
1616     state()->addCapturedFrame(activeJob()->getSignature());
1617 
1618     // report that the image has been received
1619     emit newLog(i18n("Received image %1 out of %2.", activeJob()->getCompleted(),
1620                      activeJob()->getCoreProperty(SequenceJob::SJ_Count).toInt()));
1621 
1622     return IPS_OK;
1623 }
1624 
1625 IPState CaptureProcess::updateImageMetadataAction(QSharedPointer<FITSData> imageData)
1626 {
1627     double hfr = -1, eccentricity = -1;
1628     int numStars = -1, median = -1;
1629     QString filename;
1630     if (imageData)
1631     {
1632         QVariant frameType;
1633         if (Options::autoHFR() && imageData && !imageData->areStarsSearched() && imageData->getRecordValue("FRAME", frameType)
1634                 && frameType.toString() == "Light")
1635         {
1636 #ifdef HAVE_STELLARSOLVER
1637             // Don't use the StellarSolver defaults (which allow very small stars).
1638             // Use the HFR profile--which the user can modify.
1639             QVariantMap extractionSettings;
1640             extractionSettings["optionsProfileIndex"] = Options::hFROptionsProfile();
1641             extractionSettings["optionsProfileGroup"] = static_cast<int>(Ekos::HFRProfiles);
1642             imageData->setSourceExtractorSettings(extractionSettings);
1643 #endif
1644             QFuture<bool> result = imageData->findStars(ALGORITHM_SEP);
1645             result.waitForFinished();
1646         }
1647         hfr = imageData->getHFR(HFR_AVERAGE);
1648         numStars = imageData->getSkyBackground().starsDetected;
1649         median = imageData->getMedian();
1650         eccentricity = imageData->getEccentricity();
1651         filename = imageData->filename();
1652         emit newLog(i18n("Captured %1", filename));
1653         auto remainingPlaceholders = PlaceholderPath::remainingPlaceholders(filename);
1654         if (remainingPlaceholders.size() > 0)
1655         {
1656             emit newLog(
1657                 i18n("WARNING: remaining and potentially unknown placeholders %1 in %2",
1658                      remainingPlaceholders.join(", "), filename));
1659         }
1660     }
1661 
1662     if (activeJob())
1663     {
1664         QVariantMap metadata;
1665         metadata["filename"] = filename;
1666         metadata["type"] = activeJob()->getFrameType();
1667         metadata["exposure"] = activeJob()->getCoreProperty(SequenceJob::SJ_Exposure).toDouble();
1668         metadata["filter"] = activeJob()->getCoreProperty(SequenceJob::SJ_Filter).toString();
1669         metadata["width"] = activeJob()->getCoreProperty(SequenceJob::SJ_ROI).toRect().width();
1670         metadata["height"] = activeJob()->getCoreProperty(SequenceJob::SJ_ROI).toRect().height();
1671         metadata["hfr"] = hfr;
1672         metadata["starCount"] = numStars;
1673         metadata["median"] = median;
1674         metadata["eccentricity"] = eccentricity;
1675         emit captureComplete(metadata);
1676     }
1677     return IPS_OK;
1678 }
1679 
1680 IPState CaptureProcess::runCaptureScript(ScriptTypes scriptType, bool precond)
1681 {
1682     if (activeJob())
1683     {
1684         const QString captureScript = activeJob()->getScript(scriptType);
1685         if (captureScript.isEmpty() == false && precond)
1686         {
1687             state()->setCaptureScriptType(scriptType);
1688             m_CaptureScript.start(captureScript, generateScriptArguments());
1689             //m_CaptureScript.start("/bin/bash", QStringList() << captureScript);
1690             emit newLog(i18n("Executing capture script %1", captureScript));
1691             return IPS_BUSY;
1692         }
1693     }
1694     // no script execution started
1695     return IPS_OK;
1696 }
1697 
1698 void CaptureProcess::scriptFinished(int exitCode, QProcess::ExitStatus status)
1699 {
1700     Q_UNUSED(status)
1701 
1702     switch (state()->captureScriptType())
1703     {
1704         case SCRIPT_PRE_CAPTURE:
1705             emit newLog(i18n("Pre capture script finished with code %1.", exitCode));
1706             if (activeJob() && activeJob()->getStatus() == JOB_IDLE)
1707                 prepareJobExecution();
1708             else
1709                 checkNextExposure();
1710             break;
1711 
1712         case SCRIPT_POST_CAPTURE:
1713             emit newLog(i18n("Post capture script finished with code %1.", exitCode));
1714 
1715             // If we're done, proceed to completion.
1716             if (activeJob() == nullptr
1717                     || activeJob()->getCoreProperty(SequenceJob::SJ_Count).toInt() <=
1718                     activeJob()->getCompleted())
1719             {
1720                 resumeSequence();
1721             }
1722             // Else check if meridian condition is met.
1723             else if (state()->checkMeridianFlipReady())
1724             {
1725                 emit newLog(i18n("Processing meridian flip..."));
1726             }
1727             // Then if nothing else, just resume sequence.
1728             else
1729             {
1730                 resumeSequence();
1731             }
1732             break;
1733 
1734         case SCRIPT_PRE_JOB:
1735             emit newLog(i18n("Pre job script finished with code %1.", exitCode));
1736             prepareActiveJobStage2();
1737             break;
1738 
1739         case SCRIPT_POST_JOB:
1740             emit newLog(i18n("Post job script finished with code %1.", exitCode));
1741             processJobCompletion2();
1742             break;
1743 
1744         default:
1745             // in all other cases do nothing
1746             break;
1747     }
1748 
1749 }
1750 
1751 void CaptureProcess::selectCamera(QString name)
1752 {
1753     if (activeCamera() && activeCamera()->getDeviceName() == name)
1754         checkCamera();
1755 
1756     emit refreshCamera();
1757 }
1758 
1759 void CaptureProcess::checkCamera()
1760 {
1761     // Do not update any camera settings while capture is in progress.
1762     if (state()->getCaptureState() == CAPTURE_CAPTURING)
1763         return;
1764 
1765     // If camera is restarted, try again in 1 second
1766     if (!activeCamera())
1767     {
1768         QTimer::singleShot(1000, this, &CaptureProcess::checkCamera);
1769         return;
1770     }
1771 
1772     devices()->setActiveChip(nullptr);
1773 
1774     // FIXME TODO fix guide head detection
1775     if (activeCamera()->getDeviceName().contains("Guider"))
1776     {
1777         state()->setUseGuideHead(true);
1778         devices()->setActiveChip(activeCamera()->getChip(ISD::CameraChip::GUIDE_CCD));
1779     }
1780 
1781     if (devices()->getActiveChip() == nullptr)
1782     {
1783         state()->setUseGuideHead(false);
1784         devices()->setActiveChip(activeCamera()->getChip(ISD::CameraChip::PRIMARY_CCD));
1785     }
1786 
1787     emit refreshCameraSettings();
1788 }
1789 
1790 void CaptureProcess::syncDSLRToTargetChip(const QString &model)
1791 {
1792     auto pos = std::find_if(state()->DSLRInfos().begin(),
1793                             state()->DSLRInfos().end(), [model](const QMap<QString, QVariant> &oneDSLRInfo)
1794     {
1795         return (oneDSLRInfo["Model"] == model);
1796     });
1797 
1798     // Sync Pixel Size
1799     if (pos != state()->DSLRInfos().end())
1800     {
1801         auto camera = *pos;
1802         devices()->getActiveChip()->setImageInfo(camera["Width"].toInt(),
1803                 camera["Height"].toInt(),
1804                 camera["PixelW"].toDouble(),
1805                 camera["PixelH"].toDouble(),
1806                 8);
1807     }
1808 }
1809 
1810 void CaptureProcess::reconnectCameraDriver(const QString &camera, const QString &filterWheel)
1811 {
1812     if (activeCamera() && activeCamera()->getDeviceName() == camera)
1813     {
1814         // Set camera again to the one we restarted
1815         auto rememberState = state()->getCaptureState();
1816         state()->setCaptureState(CAPTURE_IDLE);
1817         checkCamera();
1818         state()->setCaptureState(rememberState);
1819 
1820         // restart capture
1821         state()->setCaptureTimeoutCounter(0);
1822 
1823         if (activeJob())
1824         {
1825             devices()->setActiveChip(devices()->getActiveChip());
1826             captureImage();
1827         }
1828         return;
1829     }
1830 
1831     QTimer::singleShot(5000, this, [ &, camera, filterWheel]()
1832     {
1833         reconnectCameraDriver(camera, filterWheel);
1834     });
1835 }
1836 
1837 void CaptureProcess::removeDevice(const QSharedPointer<ISD::GenericDevice> &device)
1838 {
1839     auto name = device->getDeviceName();
1840     device->disconnect(this);
1841 
1842     // Mounts
1843     if (devices()->mount() && devices()->mount()->getDeviceName() == device->getDeviceName())
1844     {
1845         devices()->mount()->disconnect(this);
1846         devices()->setMount(nullptr);
1847         if (activeJob() != nullptr)
1848             activeJob()->addMount(nullptr);
1849     }
1850 
1851     // Domes
1852     if (devices()->dome() && devices()->dome()->getDeviceName() == device->getDeviceName())
1853     {
1854         devices()->dome()->disconnect(this);
1855         devices()->setDome(nullptr);
1856     }
1857 
1858     // Rotators
1859     if (devices()->rotator() && devices()->rotator()->getDeviceName() == device->getDeviceName())
1860     {
1861         devices()->rotator()->disconnect(this);
1862         devices()->setRotator(nullptr);
1863     }
1864 
1865     // Dust Caps
1866     if (devices()->dustCap() && devices()->dustCap()->getDeviceName() == device->getDeviceName())
1867     {
1868         devices()->dustCap()->disconnect(this);
1869         devices()->setDustCap(nullptr);
1870         state()->hasDustCap = false;
1871         state()->setDustCapState(CaptureModuleState::CAP_UNKNOWN);
1872     }
1873 
1874     // Light Boxes
1875     if (devices()->lightBox() && devices()->lightBox()->getDeviceName() == device->getDeviceName())
1876     {
1877         devices()->lightBox()->disconnect(this);
1878         devices()->setLightBox(nullptr);
1879         state()->hasLightBox = false;
1880         state()->setLightBoxLightState(CaptureModuleState::CAP_LIGHT_UNKNOWN);
1881     }
1882 
1883     // Cameras
1884     if (activeCamera() && activeCamera()->getDeviceName() == name)
1885     {
1886         activeCamera()->disconnect(this);
1887         devices()->setActiveCamera(nullptr);
1888         devices()->setActiveChip(nullptr);
1889 
1890         QSharedPointer<ISD::GenericDevice> generic;
1891         if (INDIListener::findDevice(name, generic))
1892             DarkLibrary::Instance()->removeDevice(generic);
1893 
1894         QTimer::singleShot(1000, this, [this]()
1895         {
1896             checkCamera();
1897         });
1898     }
1899 
1900     // Filter Wheels
1901     if (devices()->filterWheel() && devices()->filterWheel()->getDeviceName() == name)
1902     {
1903         devices()->filterWheel()->disconnect(this);
1904         devices()->setFilterWheel(nullptr);
1905 
1906         QTimer::singleShot(1000, this, [this]()
1907         {
1908             emit refreshFilterSettings();
1909         });
1910     }
1911 }
1912 
1913 void CaptureProcess::processCaptureTimeout()
1914 {
1915     state()->setCaptureTimeoutCounter(state()->captureTimeoutCounter() + 1);
1916 
1917     if (state()->deviceRestartCounter() >= 3)
1918     {
1919         state()->setCaptureTimeoutCounter(0);
1920         state()->setDeviceRestartCounter(0);
1921         emit newLog(i18n("Exposure timeout. Aborting..."));
1922         emit stopCapture(CAPTURE_ABORTED);
1923         return;
1924     }
1925 
1926     if (state()->captureTimeoutCounter() > 1 && activeCamera())
1927     {
1928         QString camera = activeCamera()->getDeviceName();
1929         QString fw = (devices()->filterWheel() != nullptr) ?
1930                      devices()->filterWheel()->getDeviceName() : "";
1931         emit driverTimedout(camera);
1932         QTimer::singleShot(5000, this, [ &, camera, fw]()
1933         {
1934             state()->setDeviceRestartCounter(state()->deviceRestartCounter() + 1);
1935             reconnectCameraDriver(camera, fw);
1936         });
1937         return;
1938     }
1939     else
1940     {
1941         // Double check that m_Camera is valid in case it was reset due to driver restart.
1942         if (activeCamera() && activeJob())
1943         {
1944             setCamera(true);
1945             emit newLog(i18n("Exposure timeout. Restarting exposure..."));
1946             activeCamera()->setEncodingFormat("FITS");
1947             auto rememberState = state()->getCaptureState();
1948             state()->setCaptureState(CAPTURE_IDLE);
1949             checkCamera();
1950             state()->setCaptureState(rememberState);
1951 
1952             auto targetChip = activeCamera()->getChip(state()->useGuideHead() ?
1953                               ISD::CameraChip::GUIDE_CCD :
1954                               ISD::CameraChip::PRIMARY_CCD);
1955             targetChip->abortExposure();
1956             const double exptime = activeJob()->getCoreProperty(SequenceJob::SJ_Exposure).toDouble();
1957             targetChip->capture(exptime);
1958             state()->getCaptureTimeout().start(static_cast<int>((exptime) * 1000 + CAPTURE_TIMEOUT_THRESHOLD));
1959         }
1960         else
1961         {
1962             qCDebug(KSTARS_EKOS_CAPTURE) << "Unable to restart exposure as camera is missing, trying again in 5 seconds...";
1963             QTimer::singleShot(5000, this, &CaptureProcess::processCaptureTimeout);
1964         }
1965     }
1966 
1967 }
1968 
1969 void CaptureProcess::processCaptureError(ISD::Camera::ErrorType type)
1970 {
1971     if (!activeJob())
1972         return;
1973 
1974     if (type == ISD::Camera::ERROR_CAPTURE)
1975     {
1976         int retries = activeJob()->getCaptureRetires() + 1;
1977 
1978         activeJob()->setCaptureRetires(retries);
1979 
1980         emit newLog(i18n("Capture failed. Check INDI Control Panel for details."));
1981 
1982         if (retries >= 3)
1983         {
1984             emit stopCapture(CAPTURE_ABORTED);
1985             return;
1986         }
1987 
1988         emit newLog(i18n("Restarting capture attempt #%1", retries));
1989 
1990         state()->setNextSequenceID(1);
1991 
1992         captureImage();
1993         return;
1994     }
1995     else
1996     {
1997         emit stopCapture(CAPTURE_ABORTED);
1998     }
1999 }
2000 
2001 bool CaptureProcess::checkFlatCalibration(QSharedPointer<FITSData> imageData, double exp_min, double exp_max)
2002 {
2003     // nothing to do
2004     if (imageData.isNull())
2005         return true;
2006 
2007     double currentADU = imageData->getADU();
2008     bool outOfRange = false, saturated = false;
2009 
2010     switch (imageData->bpp())
2011     {
2012         case 8:
2013             if (activeJob()->getCoreProperty(SequenceJob::SJ_TargetADU).toDouble() > UINT8_MAX)
2014                 outOfRange = true;
2015             else if (currentADU / UINT8_MAX > 0.95)
2016                 saturated = true;
2017             break;
2018 
2019         case 16:
2020             if (activeJob()->getCoreProperty(SequenceJob::SJ_TargetADU).toDouble() > UINT16_MAX)
2021                 outOfRange = true;
2022             else if (currentADU / UINT16_MAX > 0.95)
2023                 saturated = true;
2024             break;
2025 
2026         case 32:
2027             if (activeJob()->getCoreProperty(SequenceJob::SJ_TargetADU).toDouble() > UINT32_MAX)
2028                 outOfRange = true;
2029             else if (currentADU / UINT32_MAX > 0.95)
2030                 saturated = true;
2031             break;
2032 
2033         default:
2034             break;
2035     }
2036 
2037     if (outOfRange)
2038     {
2039         emit newLog(i18n("Flat calibration failed. Captured image is only %1-bit while requested ADU is %2.",
2040                          QString::number(imageData->bpp())
2041                          , QString::number(activeJob()->getCoreProperty(SequenceJob::SJ_TargetADU).toDouble(), 'f', 2)));
2042         emit stopCapture(CAPTURE_ABORTED);
2043         return false;
2044     }
2045     else if (saturated)
2046     {
2047         double nextExposure = activeJob()->getCoreProperty(SequenceJob::SJ_Exposure).toDouble() * 0.1;
2048         nextExposure = qBound(exp_min, nextExposure, exp_max);
2049 
2050         emit newLog(i18n("Current image is saturated (%1). Next exposure is %2 seconds.",
2051                          QString::number(currentADU, 'f', 0), QString("%L1").arg(nextExposure, 0, 'f', 6)));
2052 
2053         activeJob()->setCalibrationStage(SequenceJobState::CAL_CALIBRATION);
2054         activeJob()->setCoreProperty(SequenceJob::SJ_Exposure, nextExposure);
2055         if (activeCamera()->getUploadMode() != ISD::Camera::UPLOAD_CLIENT)
2056         {
2057             activeCamera()->setUploadMode(ISD::Camera::UPLOAD_CLIENT);
2058         }
2059         startNextExposure();
2060         return false;
2061     }
2062 
2063     double ADUDiff = fabs(currentADU - activeJob()->getCoreProperty(
2064                               SequenceJob::SJ_TargetADU).toDouble());
2065 
2066     // If it is within tolerance range of target ADU
2067     if (ADUDiff <= state()->targetADUTolerance())
2068     {
2069         if (activeJob()->getCalibrationStage() == SequenceJobState::CAL_CALIBRATION)
2070         {
2071             emit newLog(
2072                 i18n("Current ADU %1 within target ADU tolerance range.", QString::number(currentADU, 'f', 0)));
2073             activeCamera()->setUploadMode(activeJob()->getUploadMode());
2074             auto placeholderPath = PlaceholderPath();
2075             // Make sure to update Full Prefix as exposure value was changed
2076             placeholderPath.processJobInfo(activeJob());
2077             // Mark calibration as complete
2078             activeJob()->setCalibrationStage(SequenceJobState::CAL_CALIBRATION_COMPLETE);
2079 
2080             // Must update sequence prefix as this step is only done in prepareJob
2081             // but since the duration has now been updated, we must take care to update signature
2082             // since it may include a placeholder for duration which would affect it.
2083             if (activeCamera()
2084                     && activeCamera()->getUploadMode() != ISD::Camera::UPLOAD_LOCAL)
2085                 state()->setNextSequenceID(1);
2086 
2087             startNextExposure();
2088             return false;
2089         }
2090 
2091         return true;
2092     }
2093 
2094     double nextExposure = -1;
2095 
2096     // If value is saturated, try to reduce it to valid range first
2097     if (std::fabs(imageData->getMax(0) - imageData->getMin(0)) < 10)
2098         nextExposure = activeJob()->getCoreProperty(SequenceJob::SJ_Exposure).toDouble() * 0.5;
2099     else
2100         nextExposure = calculateFlatExpTime(currentADU);
2101 
2102     if (nextExposure <= 0 || std::isnan(nextExposure))
2103     {
2104         emit newLog(
2105             i18n("Unable to calculate optimal exposure settings, please capture the flats manually."));
2106         emit stopCapture(CAPTURE_ABORTED);
2107         return false;
2108     }
2109 
2110     // Limit to minimum and maximum values
2111     nextExposure = qBound(exp_min, nextExposure, exp_max);
2112 
2113     emit newLog(i18n("Current ADU is %1 Next exposure is %2 seconds.", QString::number(currentADU, 'f', 0),
2114                      QString("%L1").arg(nextExposure, 0, 'f', 6)));
2115 
2116     activeJob()->setCalibrationStage(SequenceJobState::CAL_CALIBRATION);
2117     activeJob()->setCoreProperty(SequenceJob::SJ_Exposure, nextExposure);
2118     if (activeCamera()->getUploadMode() != ISD::Camera::UPLOAD_CLIENT)
2119     {
2120         activeCamera()->setUploadMode(ISD::Camera::UPLOAD_CLIENT);
2121     }
2122 
2123     startNextExposure();
2124     return false;
2125 
2126 
2127 }
2128 
2129 double CaptureProcess::calculateFlatExpTime(double currentADU)
2130 {
2131     if (activeJob() == nullptr)
2132     {
2133         qWarning(KSTARS_EKOS_CAPTURE) << "setCurrentADU with null activeJob().";
2134         // Nothing good to do here. Just don't crash.
2135         return currentADU;
2136     }
2137 
2138     double nextExposure = 0;
2139     double targetADU    = activeJob()->getCoreProperty(SequenceJob::SJ_TargetADU).toDouble();
2140     std::vector<double> coeff;
2141 
2142     // Check if saturated, then take shorter capture and discard value
2143     ExpRaw.append(activeJob()->getCoreProperty(SequenceJob::SJ_Exposure).toDouble());
2144     ADURaw.append(currentADU);
2145 
2146     qCDebug(KSTARS_EKOS_CAPTURE) << "Capture: Current ADU = " << currentADU << " targetADU = " << targetADU
2147                                  << " Exposure Count: " << ExpRaw.count();
2148 
2149     // Most CCDs are quite linear so 1st degree polynomial is quite sufficient
2150     // But DSLRs can exhibit non-linear response curve and so a 2nd degree polynomial is more appropriate
2151     if (ExpRaw.count() >= 2)
2152     {
2153         if (ExpRaw.count() >= 5)
2154         {
2155             double chisq = 0;
2156 
2157             coeff = gsl_polynomial_fit(ADURaw.data(), ExpRaw.data(), ExpRaw.count(), 2, chisq);
2158             qCDebug(KSTARS_EKOS_CAPTURE) << "Running polynomial fitting. Found " << coeff.size() << " coefficients.";
2159             if (std::isnan(coeff[0]) || std::isinf(coeff[0]))
2160             {
2161                 qCDebug(KSTARS_EKOS_CAPTURE) << "Coefficients are invalid.";
2162                 targetADUAlgorithm = ADU_LEAST_SQUARES;
2163             }
2164             else
2165             {
2166                 nextExposure = coeff[0] + (coeff[1] * targetADU) + (coeff[2] * pow(targetADU, 2));
2167                 // If exposure is not valid or does not make sense, then we fall back to least squares
2168                 if (nextExposure < 0 || (nextExposure > ExpRaw.last() || targetADU < ADURaw.last())
2169                         || (nextExposure < ExpRaw.last() || targetADU > ADURaw.last()))
2170                 {
2171                     nextExposure = 0;
2172                     targetADUAlgorithm = ADU_LEAST_SQUARES;
2173                 }
2174                 else
2175                 {
2176                     targetADUAlgorithm = ADU_POLYNOMIAL;
2177                     for (size_t i = 0; i < coeff.size(); i++)
2178                         qCDebug(KSTARS_EKOS_CAPTURE) << "Coeff #" << i << "=" << coeff[i];
2179                 }
2180             }
2181         }
2182 
2183         bool looping = false;
2184         if (ExpRaw.count() >= 10)
2185         {
2186             int size = ExpRaw.count();
2187             looping  = (std::fabs(ExpRaw[size - 1] - ExpRaw[size - 2] < 0.01)) &&
2188                        (std::fabs(ExpRaw[size - 2] - ExpRaw[size - 3] < 0.01));
2189             if (looping && targetADUAlgorithm == ADU_POLYNOMIAL)
2190             {
2191                 qWarning(KSTARS_EKOS_CAPTURE) << "Detected looping in polynomial results. Falling back to llsqr.";
2192                 targetADUAlgorithm = ADU_LEAST_SQUARES;
2193             }
2194         }
2195 
2196         // If we get invalid data, let's fall back to llsq
2197         // Since polyfit can be unreliable at low counts, let's only use it at the 5th exposure
2198         // if we don't have results already.
2199         if (targetADUAlgorithm == ADU_LEAST_SQUARES)
2200         {
2201             double a = 0, b = 0;
2202             llsq(ExpRaw, ADURaw, a, b);
2203 
2204             // If we have valid results, let's calculate next exposure
2205             if (a != 0.0)
2206             {
2207                 nextExposure = (targetADU - b) / a;
2208                 // If we get invalid value, let's just proceed iteratively
2209                 if (nextExposure < 0)
2210                     nextExposure = 0;
2211             }
2212         }
2213     }
2214 
2215     // 2022.01.12 Put a hard limit to 180 seconds.
2216     // If it goes over this limit, the flat source is probably off.
2217     if (nextExposure == 0.0 || nextExposure > 180)
2218     {
2219         if (currentADU < targetADU)
2220             nextExposure = activeJob()->getCoreProperty(SequenceJob::SJ_Exposure).toDouble() * 1.25;
2221         else
2222             nextExposure = activeJob()->getCoreProperty(SequenceJob::SJ_Exposure).toDouble() * .75;
2223     }
2224 
2225     qCDebug(KSTARS_EKOS_CAPTURE) << "next flat exposure is" << nextExposure;
2226 
2227     return nextExposure;
2228 
2229 }
2230 
2231 void CaptureProcess::clearFlatCache()
2232 {
2233     ADURaw.clear();
2234     ExpRaw.clear();
2235 }
2236 
2237 void CaptureProcess::updateTelescopeInfo()
2238 {
2239     if (devices()->mount() && activeCamera() && devices()->mount()->isConnected())
2240     {
2241         // Camera to current telescope
2242         auto activeDevices = activeCamera()->getText("ACTIVE_DEVICES");
2243         if (activeDevices)
2244         {
2245             auto activeTelescope = activeDevices->findWidgetByName("ACTIVE_TELESCOPE");
2246             if (activeTelescope)
2247             {
2248                 activeTelescope->setText(devices()->mount()->getDeviceName().toLatin1().constData());
2249                 activeCamera()->sendNewProperty(activeDevices);
2250             }
2251         }
2252     }
2253 
2254 }
2255 
2256 void CaptureProcess::updateFilterInfo()
2257 {
2258     QList<ISD::ConcreteDevice *> all_devices;
2259     if (activeCamera())
2260         all_devices.append(activeCamera());
2261     if (devices()->dustCap())
2262         all_devices.append(devices()->dustCap());
2263 
2264     for (auto &oneDevice : all_devices)
2265     {
2266         auto activeDevices = oneDevice->getText("ACTIVE_DEVICES");
2267         if (activeDevices)
2268         {
2269             auto activeFilter = activeDevices->findWidgetByName("ACTIVE_FILTER");
2270             if (activeFilter)
2271             {
2272                 QString activeFilterText = QString(activeFilter->getText());
2273                 if (devices()->filterWheel())
2274                 {
2275                     if (activeFilterText != devices()->filterWheel()->getDeviceName())
2276                     {
2277                         activeFilter->setText(devices()->filterWheel()->getDeviceName().toLatin1().constData());
2278                         oneDevice->sendNewProperty(activeDevices);
2279                     }
2280                 }
2281                 // Reset filter name in CCD driver
2282                 else if (activeFilterText.isEmpty())
2283                 {
2284                     // Add debug info since this issue is reported by users. Need to know when it happens.
2285                     qCDebug(KSTARS_EKOS_CAPTURE) << "No active filter wheel. " << oneDevice->getDeviceName() << " ACTIVE_FILTER is reset.";
2286                     activeFilter->setText("");
2287                     oneDevice->sendNewProperty(activeDevices);
2288                 }
2289             }
2290         }
2291     }
2292 }
2293 
2294 bool CaptureProcess::loadSequenceQueue(const QString &fileURL,
2295                                        const QString &targetName, bool setOptions)
2296 {
2297     state()->clearCapturedFramesMap();
2298     auto queue = state()->getSequenceQueue();
2299     if (!queue->load(fileURL, targetName, devices(), state()))
2300     {
2301         QString message = i18n("Unable to open file %1", fileURL);
2302         KSNotification::sorry(message, i18n("Could Not Open File"));
2303         return false;
2304     }
2305 
2306     if (setOptions)
2307     {
2308         queue->setOptions();
2309         // Set the HFR Check value appropriately for the conditions, e.g. using Autofocus
2310         state()->updateHFRThreshold();
2311     }
2312 
2313     for (auto j : state()->allJobs())
2314         emit addJob(j);
2315 
2316     return true;
2317 }
2318 
2319 bool CaptureProcess::saveSequenceQueue(const QString &path, bool loadOptions)
2320 {
2321     if (loadOptions)
2322         state()->getSequenceQueue()->loadOptions();
2323     return state()->getSequenceQueue()->save(path, state()->observerName());
2324 }
2325 
2326 void CaptureProcess::setCamera(bool connection)
2327 {
2328     if (connection)
2329     {
2330         // TODO: do not simply forward the newExposureValue
2331         connect(activeCamera(), &ISD::Camera::newExposureValue, this, &CaptureProcess::setExposureProgress, Qt::UniqueConnection);
2332         connect(activeCamera(), &ISD::Camera::newImage, this, &CaptureProcess::processFITSData, Qt::UniqueConnection);
2333         connect(activeCamera(), &ISD::Camera::newRemoteFile, this, &CaptureProcess::processNewRemoteFile, Qt::UniqueConnection);
2334         connect(activeCamera(), &ISD::Camera::ready, this, &CaptureProcess::cameraReady, Qt::UniqueConnection);
2335     }
2336     else
2337     {
2338         // TODO: do not simply forward the newExposureValue
2339         disconnect(activeCamera(), &ISD::Camera::newExposureValue, this, &CaptureProcess::setExposureProgress);
2340         disconnect(activeCamera(), &ISD::Camera::newImage, this, &CaptureProcess::processFITSData);
2341         disconnect(activeCamera(), &ISD::Camera::newRemoteFile, this, &CaptureProcess::processNewRemoteFile);
2342         //    disconnect(m_Camera, &ISD::Camera::previewFITSGenerated, this, &Capture::setGeneratedPreviewFITS);
2343         disconnect(activeCamera(), &ISD::Camera::ready, this, &CaptureProcess::cameraReady);
2344     }
2345 
2346 }
2347 
2348 bool CaptureProcess::setFilterWheel(ISD::FilterWheel * device)
2349 {
2350     if (devices()->filterWheel() && devices()->filterWheel() == device)
2351         return false;
2352 
2353     if (devices()->filterWheel())
2354         devices()->filterWheel()->disconnect(this);
2355 
2356     devices()->setFilterWheel(device);
2357 
2358     return (device != nullptr);
2359 }
2360 
2361 bool CaptureProcess::checkPausing(CaptureModuleState::ContinueAction continueAction)
2362 {
2363     if (state()->getCaptureState() == CAPTURE_PAUSE_PLANNED)
2364     {
2365         emit newLog(i18n("Sequence paused."));
2366         state()->setCaptureState(CAPTURE_PAUSED);
2367         // disconnect camera device
2368         setCamera(false);
2369         // save continue action
2370         state()->setContinueAction(continueAction);
2371         // pause
2372         return true;
2373     }
2374     // no pause
2375     return false;
2376 }
2377 
2378 SequenceJob *CaptureProcess::findNextPendingJob()
2379 {
2380     SequenceJob * first_job = nullptr;
2381 
2382     // search for idle or aborted jobs
2383     for (auto &job : state()->allJobs())
2384     {
2385         if (job->getStatus() == JOB_IDLE || job->getStatus() == JOB_ABORTED)
2386         {
2387             first_job = job;
2388             break;
2389         }
2390     }
2391 
2392     // If there are no idle nor aborted jobs, question is whether to reset and restart
2393     // Scheduler will start a non-empty new job each time and doesn't use this execution path
2394     if (first_job == nullptr)
2395     {
2396         // If we have at least one job that are in error, bail out, even if ignoring job progress
2397         for (auto &job : state()->allJobs())
2398         {
2399             if (job->getStatus() != JOB_DONE)
2400             {
2401                 // If we arrived here with a zero-delay timer, raise the interval before returning to avoid a cpu peak
2402                 if (state()->getCaptureDelayTimer().isActive())
2403                 {
2404                     if (state()->getCaptureDelayTimer().interval() <= 0)
2405                         state()->getCaptureDelayTimer().setInterval(1000);
2406                 }
2407                 return nullptr;
2408             }
2409         }
2410 
2411         // If we only have completed jobs and we don't ignore job progress, ask the end-user what to do
2412         if (!state()->ignoreJobProgress())
2413             if(KMessageBox::warningContinueCancel(
2414                         nullptr,
2415                         i18n("All jobs are complete. Do you want to reset the status of all jobs and restart capturing?"),
2416                         i18n("Reset job status"), KStandardGuiItem::cont(), KStandardGuiItem::cancel(),
2417                         "reset_job_complete_status_warning") != KMessageBox::Continue)
2418                 return nullptr;
2419 
2420         // If the end-user accepted to reset, reset all jobs and restart
2421         resetAllJobs();
2422 
2423         first_job = state()->allJobs().first();
2424     }
2425     // If we need to ignore job progress, systematically reset all jobs and restart
2426     // Scheduler will never ignore job progress and doesn't use this path
2427     else if (state()->ignoreJobProgress())
2428     {
2429         emit newLog(i18n("Warning: option \"Always Reset Sequence When Starting\" is enabled and resets the sequence counts."));
2430         resetAllJobs();
2431     }
2432 
2433     return first_job;
2434 }
2435 
2436 void Ekos::CaptureProcess::resetJobStatus(JOBStatus newStatus)
2437 {
2438     if (activeJob() != nullptr)
2439     {
2440         activeJob()->resetStatus(newStatus);
2441         emit updateJobTable(activeJob());
2442     }
2443 }
2444 
2445 void Ekos::CaptureProcess::resetAllJobs()
2446 {
2447     for (auto &job : state()->allJobs())
2448     {
2449         job->resetStatus();
2450     }
2451     // clear existing job counts
2452     m_State->clearCapturedFramesMap();
2453     // update the entire job table
2454     emit updateJobTable(nullptr);
2455 }
2456 
2457 void Ekos::CaptureProcess::updatedCaptureCompleted(int count)
2458 {
2459     activeJob()->setCompleted(count);
2460     emit updateJobTable(activeJob());
2461 }
2462 
2463 void CaptureProcess::llsq(QVector<double> x, QVector<double> y, double &a, double &b)
2464 {
2465     double bot;
2466     int i;
2467     double top;
2468     double xbar;
2469     double ybar;
2470     int n = x.count();
2471     //
2472     //  Special case.
2473     //
2474     if (n == 1)
2475     {
2476         a = 0.0;
2477         b = y[0];
2478         return;
2479     }
2480     //
2481     //  Average X and Y.
2482     //
2483     xbar = 0.0;
2484     ybar = 0.0;
2485     for (i = 0; i < n; i++)
2486     {
2487         xbar = xbar + x[i];
2488         ybar = ybar + y[i];
2489     }
2490     xbar = xbar / static_cast<double>(n);
2491     ybar = ybar / static_cast<double>(n);
2492     //
2493     //  Compute Beta.
2494     //
2495     top = 0.0;
2496     bot = 0.0;
2497     for (i = 0; i < n; i++)
2498     {
2499         top = top + (x[i] - xbar) * (y[i] - ybar);
2500         bot = bot + (x[i] - xbar) * (x[i] - xbar);
2501     }
2502 
2503     a = top / bot;
2504 
2505     b = ybar - a * xbar;
2506 
2507 }
2508 
2509 QStringList CaptureProcess::generateScriptArguments() const
2510 {
2511     // TODO based on user feedback on what paramters are most useful to pass
2512     return QStringList();
2513 }
2514 
2515 bool CaptureProcess::hasCoolerControl()
2516 {
2517     if (devices()->getActiveCamera() && devices()->getActiveCamera()->hasCoolerControl())
2518         return true;
2519 
2520     return false;
2521 }
2522 
2523 bool CaptureProcess::setCoolerControl(bool enable)
2524 {
2525     if (devices()->getActiveCamera() && devices()->getActiveCamera()->hasCoolerControl())
2526         return devices()->getActiveCamera()->setCoolerControl(enable);
2527 
2528     return false;
2529 }
2530 
2531 void CaptureProcess::restartCamera(const QString &name)
2532 {
2533     connect(KSMessageBox::Instance(), &KSMessageBox::accepted, this, [this, name]()
2534     {
2535         KSMessageBox::Instance()->disconnect(this);
2536         emit stopCapturing(CAPTURE_ABORTED);
2537         emit driverTimedout(name);
2538     });
2539     connect(KSMessageBox::Instance(), &KSMessageBox::rejected, this, [this]()
2540     {
2541         KSMessageBox::Instance()->disconnect(this);
2542     });
2543 
2544     KSMessageBox::Instance()->questionYesNo(i18n("Are you sure you want to restart %1 camera driver?", name),
2545                                             i18n("Driver Restart"), 5);
2546 }
2547 
2548 QStringList CaptureProcess::frameTypes()
2549 {
2550     if (!activeCamera())
2551         return QStringList();
2552 
2553     ISD::CameraChip *tChip = devices()->getActiveCamera()->getChip(ISD::CameraChip::PRIMARY_CCD);
2554 
2555     return tChip->getFrameTypes();
2556 }
2557 
2558 QStringList CaptureProcess::filterLabels()
2559 {
2560     if (devices()->getFilterManager().isNull())
2561         return QStringList();
2562 
2563     return devices()->getFilterManager()->getFilterLabels();
2564 }
2565 
2566 void CaptureProcess::updateGain(double value, QMap<QString, QMap<QString, QVariant> > &propertyMap)
2567 {
2568     if (devices()->getActiveCamera()->getProperty("CCD_GAIN"))
2569     {
2570         if (value >= 0)
2571         {
2572             QMap<QString, QVariant> ccdGain;
2573             ccdGain["GAIN"] = value;
2574             propertyMap["CCD_GAIN"] = ccdGain;
2575         }
2576         else
2577         {
2578             propertyMap["CCD_GAIN"].remove("GAIN");
2579             if (propertyMap["CCD_GAIN"].size() == 0)
2580                 propertyMap.remove("CCD_GAIN");
2581         }
2582     }
2583     else if (devices()->getActiveCamera()->getProperty("CCD_CONTROLS"))
2584     {
2585         if (value >= 0)
2586         {
2587             QMap<QString, QVariant> ccdGain = propertyMap["CCD_CONTROLS"];
2588             ccdGain["Gain"] = value;
2589             propertyMap["CCD_CONTROLS"] = ccdGain;
2590         }
2591         else
2592         {
2593             propertyMap["CCD_CONTROLS"].remove("Gain");
2594             if (propertyMap["CCD_CONTROLS"].size() == 0)
2595                 propertyMap.remove("CCD_CONTROLS");
2596         }
2597     }
2598 }
2599 
2600 void CaptureProcess::updateOffset(double value, QMap<QString, QMap<QString, QVariant> > &propertyMap)
2601 {
2602     if (devices()->getActiveCamera()->getProperty("CCD_OFFSET"))
2603     {
2604         if (value >= 0)
2605         {
2606             QMap<QString, QVariant> ccdOffset;
2607             ccdOffset["OFFSET"] = value;
2608             propertyMap["CCD_OFFSET"] = ccdOffset;
2609         }
2610         else
2611         {
2612             propertyMap["CCD_OFFSET"].remove("OFFSET");
2613             if (propertyMap["CCD_OFFSET"].size() == 0)
2614                 propertyMap.remove("CCD_OFFSET");
2615         }
2616     }
2617     else if (devices()->getActiveCamera()->getProperty("CCD_CONTROLS"))
2618     {
2619         if (value >= 0)
2620         {
2621             QMap<QString, QVariant> ccdOffset = propertyMap["CCD_CONTROLS"];
2622             ccdOffset["Offset"] = value;
2623             propertyMap["CCD_CONTROLS"] = ccdOffset;
2624         }
2625         else
2626         {
2627             propertyMap["CCD_CONTROLS"].remove("Offset");
2628             if (propertyMap["CCD_CONTROLS"].size() == 0)
2629                 propertyMap.remove("CCD_CONTROLS");
2630         }
2631     }
2632 }
2633 
2634 ISD::Camera *CaptureProcess::activeCamera()
2635 {
2636     return devices()->getActiveCamera();
2637 }
2638 } // Ekos namespace