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