diff --git a/kstars/ekos/capture/capture.h b/kstars/ekos/capture/capture.h --- a/kstars/ekos/capture/capture.h +++ b/kstars/ekos/capture/capture.h @@ -19,6 +19,7 @@ #include "indi/indilightbox.h" #include "indi/inditelescope.h" #include "ekos/auxiliary/filtermanager.h" +#include "ekos/scheduler/schedulerjob.h" #include #include @@ -742,6 +743,6 @@ QList> DSLRInfos; // Captured Frames Map - QMap capturedFramesMap; + SchedulerJob::CapturedFramesMap capturedFramesMap; }; } diff --git a/kstars/ekos/capture/capture.cpp b/kstars/ekos/capture/capture.cpp --- a/kstars/ekos/capture/capture.cpp +++ b/kstars/ekos/capture/capture.cpp @@ -435,8 +435,8 @@ void Capture::stop(bool abort) { retries = 0; - seqTotalCount = 0; - seqCurrentCount = 0; + //seqTotalCount = 0; + //seqCurrentCount = 0; ADURaw.clear(); ExpRaw.clear(); @@ -532,6 +532,8 @@ //button->setEnabled(true); seqTimer->stop(); + + activeJob = nullptr; } void Capture::sendNewImage(QImage *image, ISD::CCDChip *myChip) @@ -1180,7 +1182,7 @@ KSNotification::event(QLatin1String("EkosCaptureImageReceived"), i18n("Captured image received"), KSNotification::EVENT_INFO); // If it was initially set as preview job - if (seqTotalCount <= 0) + if (activeJob->isPreview()) { jobs.removeOne(activeJob); // Reset upload mode if it was changed by preview @@ -1205,6 +1207,14 @@ return false; } + /* Increase the sequence's current capture count */ + activeJob->setCompleted(activeJob->getCompleted() + 1); + + /* If we were assigned a captured frame map, also increase the relevant counter for prepareJob */ + SchedulerJob::CapturedFramesMap::iterator frame_item = capturedFramesMap.find(activeJob->getSignature()); + if (capturedFramesMap.end() != frame_item) + frame_item.value()++; + if (activeJob->getFrameType() != FRAME_LIGHT) { if (processPostCaptureCalibrationStage() == false) @@ -1214,16 +1224,15 @@ calibrationStage = CAL_CAPTURING; } - seqCurrentCount++; - activeJob->setCompleted(seqCurrentCount); - imgProgress->setValue(seqCurrentCount); + /* The image progress has now one more capture */ + imgProgress->setValue(activeJob->getCompleted()); - appendLogText(i18n("Received image %1 out of %2.", seqCurrentCount, seqTotalCount)); + appendLogText(i18n("Received image %1 out of %2.", activeJob->getCompleted(), activeJob->getCount())); state = CAPTURE_IMAGE_RECEIVED; emit newStatus(Ekos::CAPTURE_IMAGE_RECEIVED); - currentImgCountOUT->setText(QString::number(seqCurrentCount)); + currentImgCountOUT->setText(QString::number(activeJob->getCompleted())); // Check if we need to execute post capture script first if (activeJob->getPostCaptureScript().isEmpty() == false) @@ -1234,7 +1243,7 @@ } // if we're done - if (seqCurrentCount >= seqTotalCount) + if (activeJob->getCount() <= activeJob->getCompleted()) { processJobCompletion(); return true; @@ -1298,8 +1307,8 @@ return false; } - // If seqTotalCount is zero, we have to find if there are more pending jobs in the queue - if (seqTotalCount == 0) + // If no job is active, we have to find if there are more pending jobs in the queue + if (!activeJob) { SequenceJob *next_job = nullptr; @@ -1593,7 +1602,7 @@ { case SequenceJob::CAPTURE_OK: { - appendLogText(i18n("Capturing image...")); + appendLogText(i18n("Capturing %1-second %2 image...", QString::number(activeJob->getExposure(),'g',3), activeJob->getFilterName())); if (activeJob->isPreview() == false) { int index = jobs.indexOf(activeJob); @@ -1694,7 +1703,7 @@ { int newFileIndex = -1; QString tempName; - seqFileCount = 0; + // seqFileCount = 0; // No updates during meridian flip if (meridianFlipStage >= MF_ALIGNING) @@ -1715,7 +1724,11 @@ if (tempName.startsWith(finalSeqPrefix, Qt::CaseInsensitive) == false) continue; - seqFileCount++; + /* Do not change the number of captures. + * - If the sequence is required by the end-user, unconditionally run what each sequence item is requiring. + * - If the sequence is required by the scheduler, use capturedFramesMap to determine when to stop capturing. + */ + //seqFileCount++; int lastUnderScoreIndex = tempName.lastIndexOf("_"); if (lastUnderScoreIndex > 0) @@ -2237,95 +2250,123 @@ { activeJob = job; - qCDebug(KSTARS_EKOS_CAPTURE) << "Preparing capture job" << job->getFullPrefix() << "for execution."; + qCDebug(KSTARS_EKOS_CAPTURE) << "Preparing capture job" << job->getSignature() << "for execution."; if (activeJob->getActiveCCD() != currentCCD) { setCCD(activeJob->getActiveCCD()->getDeviceName()); } - if (activeJob->isPreview()) + /*if (activeJob->isPreview()) seqTotalCount = -1; else - seqTotalCount = activeJob->getCount(); + seqTotalCount = activeJob->getCount();*/ seqDelay = activeJob->getDelay(); - seqCurrentCount = activeJob->getCompleted(); + // seqCurrentCount = activeJob->getCompleted(); if (activeJob->isPreview() == false) { - fullImgCountOUT->setText(QString::number(seqTotalCount)); - currentImgCountOUT->setText(QString::number(seqCurrentCount)); + int const completion_count = activeJob->isPreview() ? -1 : activeJob->getCompleted(); + fullImgCountOUT->setText(QString::number(activeJob->getCount())); + currentImgCountOUT->setText(QString::number(completion_count)); // set the progress info imgProgress->setEnabled(true); - imgProgress->setMaximum(seqTotalCount); - imgProgress->setValue(seqCurrentCount); + imgProgress->setMaximum(activeJob->getCount()); + imgProgress->setValue(completion_count); if (currentCCD->getUploadMode() != ISD::CCD::UPLOAD_LOCAL) updateSequencePrefix(activeJob->getFullPrefix(), activeJob->getSignature()); } - // We check if the job is already fully or partially complete by checking how many files of its type exist on the file system unless ignoreJobProgress is set to true - if (ignoreJobProgress == false && activeJob->isPreview() == false) + // We check if the job is already fully or partially complete by checking how many files of its type exist on the file system + if (activeJob->isPreview() == false) { - // The signature is the unique identification path in the system for a particular job - // e.g. /home/jasem/M45/Light_Red + // The signature is the unique identification path in the system for a particular job. Format is "///". + // 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/...) + // 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/...) QString signature = activeJob->getSignature(); // Now check on the file system ALL the files that exist with the above signature // If 29 files exist for example, then nextSequenceID would be the NEXT file number (30) // Therefore, we know how to number the next file. + // However, we do not deduce the number of captures to process from this function. checkSeqBoundary(signature); // Captured Frames Map contains a list of signatures:count of _already_ captured files in the file system. - // This is usually set by the scheduler in case we have duplicate signatures. - // Eg. Simple Sequence = LRGB - // Scheduler wants to run job 3 times, with each sequence capture 5 frames per filter. - // When the first scheduler job is complete. On the file system, we have 20 images (5 for each filter) - // When the SECOND job scheduler starts. It already finds 20 images on the filter system, so it sets - // the signatures of L,R,G, and B to ZERO (seqFileCount) even though seqFileCount is set to 5 by the - // checkSeqBoundary function since this is what exists on the filter system for this signature. - // Therefore, we continue to capture the SECOND scheduler job without issues. - // This is the purpose of capturedFramesMap + // This map is set by the Scheduler in order to complete efficiently the required captures. + // When the end-user requests a sequence to be processed, that map is empty. + // + // Example with a 5xL-5xR-5xG-5xB sequence + // + // When the end-user loads and runs this sequence, each filter gets to capture 5 frames, then the procedure stops. + // When the Scheduler executes a job with this sequence, the procedure depends on what is in the storage. + // + // Let's consider the Scheduler has 3 instances of this job to run. + // + // When the first job completes the sequence, there are 20 images in the file system (5 for each filter). + // 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. + // 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. + // + // 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. + // When Scheduler attempts to run the aborted job again, it will count captures in storage, substract previous job requirements, and set the frames map counters to 0 for LRG, and 4 for B. + // When the sequence runs, the procedure will bypass LRG and proceed to capture 4xB. if (capturedFramesMap.contains(signature)) - seqFileCount = capturedFramesMap[signature]; + { + // Get the current capture count from the map + int count = capturedFramesMap[signature]; - if (seqFileCount > 0) + // Count how many captures this job has to process, given that previous jobs may have done some work already + foreach (SequenceJob *a_job, jobs) + if (a_job == activeJob) + break; + else if (a_job->getSignature() == activeJob->getSignature()) + count -= a_job->getCompleted(); + + // This is the current completion count of the current job + activeJob->setCompleted(count); + } + else { - // Get the TOTAL count of frames for a particular signature - // e.g. Suppose Sequnce is LRGBRGB each 5 frames - // getTotalFramesCount("R") = 10 - int totalSignatureFrameCount = getTotalFramesCount(signature); + // No preliminary information, we reset the job count and run the job unconditionally to clarify the behavior + activeJob->setCompleted(0); + } - // Fully complete - if (seqFileCount >= totalSignatureFrameCount) - { - activeJob->setCompleted(seqFileCount); - imgProgress->setValue(totalSignatureFrameCount); - qCDebug(KSTARS_EKOS_CAPTURE) << "Job" << job->getFullPrefix() << "already complete."; - processJobCompletion(); - return; - } + //qCDebug(KSTARS_EKOS_CAPTURE) << "Job" << activeJob->getSignature() << "has" << activeJob->getCompleted() << "captures already processed for this run."; + imgProgress->setValue(getTotalFramesCount(signature)); - // Partially complete - seqCurrentCount = seqFileCount; - activeJob->setCompleted(seqCurrentCount); - currentImgCountOUT->setText(QString::number(seqCurrentCount)); - qCDebug(KSTARS_EKOS_CAPTURE) << "Job" << job->getFullPrefix() << seqCurrentCount << "out of" << seqTotalCount << "is complete."; - imgProgress->setValue(seqCurrentCount); + // Check whether active job is complete by comparing required captures to what is already available + if (activeJob->getCount() <= activeJob->getCompleted()) + { + activeJob->setCompleted(activeJob->getCount()); + appendLogText(i18n("Job requires %1-second %2 images, has already %3/%4 captures and does not need to run.", + QString::number(job->getExposure(),'g',3), job->getFilterName(), + activeJob->getCompleted(), activeJob->getCount())); + processJobCompletion(); - // Emit progress update - emit newImage(nullptr, activeJob); + /* FIXME: find a clearer way to exit here */ + return; } + else + { + // There are captures to process + currentImgCountOUT->setText(QString::number(activeJob->getCompleted())); + appendLogText(i18n("Job requires %1-second %2 images, has %3/%4 frames captured and will be processed.", + QString::number(job->getExposure(),'g',3), job->getFilterName(), + activeJob->getCompleted(), activeJob->getCount())); - currentCCD->setNextSequenceID(nextSequenceID); + // Emit progress update - done a few lines below + // emit newImage(nullptr, activeJob); + + currentCCD->setNextSequenceID(nextSequenceID); + } } if (currentCCD->isBLOBEnabled() == false) { - + // FIXME: Move this warning pop-up elsewhere, it will interfere with automation. if (Options::guiderType() != Ekos::Guide::GUIDE_INTERNAL || KMessageBox::questionYesNo(nullptr, i18n("Image transfer is disabled for this camera. Would you like to enable it?")) == KMessageBox::Yes) { @@ -2573,8 +2614,11 @@ return; } - appendLogText(i18n("Guiding deviation %1 exceeded limit value of %2 arcsecs, aborting exposure.", - deviationText, guideDeviation->value())); + appendLogText(i18n("Guiding deviation %1 exceeded limit value of %2 arcsecs, " + "aborting exposure and waiting for guider up to %3 seconds.", + deviationText, guideDeviation->value(), + QString::number((double)guideDeviationTimer.interval()/1000.0f,'g',3))); + abort(); spikeDetected = false; @@ -2597,19 +2641,21 @@ guideDeviationTimer.stop(); if (seqDelay == 0) - appendLogText( - i18n("Guiding deviation %1 is now lower than limit value of %2 arcsecs, resuming exposure.", - deviationText, guideDeviation->value())); + appendLogText(i18n("Guiding deviation %1 is now lower than limit value of %2 arcsecs, " + "resuming exposure.", + deviationText, guideDeviation->value())); else - appendLogText(i18n("Guiding deviation %1 is now lower than limit value of %2 arcsecs, resuming " - "exposure in %3 seconds.", + appendLogText(i18n("Guiding deviation %1 is now lower than limit value of %2 arcsecs, " + "resuming exposure in %3 seconds.", deviationText, guideDeviation->value(), seqDelay / 1000.0)); activeJob = nullptr; QTimer::singleShot(seqDelay, this, SLOT(start())); return; } + else appendLogText(i18n("Guiding deviation %1 is still higher than limit value of %2 arcsecs.", + deviationText, guideDeviation->value())); } } @@ -3412,6 +3458,7 @@ stop(); ignoreJobProgress = true; + capturedFramesMap.clear(); } void Capture::ignoreSequenceHistory() @@ -4720,10 +4767,9 @@ bool Capture::processPostCaptureCalibrationStage() { // If there are no more images to capture, do not bother calculating next exposure - if (calibrationStage == CAL_CALIBRATION_COMPLETE && (seqCurrentCount + 1) >= seqTotalCount) - { - return true; - } + if (calibrationStage == CAL_CALIBRATION_COMPLETE) + if (activeJob && activeJob->getCount() <= activeJob->getCompleted()) + return true; // Check if we need to do flat field slope calculation if the user specified a desired ADU value if (activeJob->getFrameType() == FRAME_FLAT && activeJob->getFlatFieldDuration() == DURATION_ADU && @@ -4852,21 +4898,22 @@ { appendLogText(i18n("Post capture script finished with code %1.", exitCode)); - // if we're done - // FIXME getTotalFramesCount is problematic elsewhere. Check here - if (seqCurrentCount >= getTotalFramesCount(activeJob->getSignature())) + // If we're done, proceed to completion. + if (activeJob->getCount() <= activeJob->getCompleted()) { processJobCompletion(); - return; } - - // Check if meridian condition is met - if (checkMeridianFlip()) - return; - - appendLogText(i18n("Resuming sequence...")); - // Then just resume sequence. - resumeSequence(); + // Else check if meridian condition is met. + else if (checkMeridianFlip()) + { + appendLogText(i18n("Processing meridian flip...")); + } + // Then if nothing else, just resume sequence. + else + { + appendLogText(i18n("Resuming sequence...")); + resumeSequence(); + } } // FIXME Migrate to Filter Manager @@ -5295,6 +5342,10 @@ void Capture::setCapturedFramesMap(const QString &signature, int count) { capturedFramesMap[signature] = count; + qCDebug(KSTARS_EKOS_CAPTURE) << QString("Client module indicates that storage '%1' has already %2 captures processed.").arg(signature).arg(count); + //capturedFramesMap = map; + //for (auto key: map.keys()) + // qCDebug(KSTARS_EKOS_CAPTURE) << QString("Captured frame '%1' already has %2 captures stored.").arg(key).arg(map[key]); } void Capture::setSettings(const QJsonObject &settings) diff --git a/kstars/ekos/capture/sequencejob.h b/kstars/ekos/capture/sequencejob.h --- a/kstars/ekos/capture/sequencejob.h +++ b/kstars/ekos/capture/sequencejob.h @@ -60,7 +60,7 @@ bool isPreview() { return preview; } int getDelay() { return delay; } int getCount() { return count; } - unsigned int getCompleted() { return completed; } + int getCompleted() { return completed; } const QString &getRawPrefix() { return rawPrefix; } double getExposure() const { return exposure; } @@ -123,7 +123,7 @@ void setCount(int in_count) { count = in_count; } void setExposure(double duration) { exposure = duration; } void setStatusCell(QTableWidgetItem *cell) { statusCell = cell; } - void setCompleted(unsigned int in_completed) { completed = in_completed; } + void setCompleted(int in_completed) { completed = in_completed; } int getISOIndex() const; void setISOIndex(int value); diff --git a/kstars/ekos/scheduler/scheduler.cpp b/kstars/ekos/scheduler/scheduler.cpp --- a/kstars/ekos/scheduler/scheduler.cpp +++ b/kstars/ekos/scheduler/scheduler.cpp @@ -514,6 +514,9 @@ job->setPriority(prioritySpin->value()); job->setTargetCoords(ra, dec); job->setDateTimeDisplayFormat(startupTimeEdit->displayFormat()); + + /* Consider sequence file is new, and clear captured frames map */ + job->setCapturedFramesMap(SchedulerJob::CapturedFramesMap()); job->setSequenceFile(sequenceURL); fitsURL = QUrl::fromLocalFile(fitsEdit->text()); @@ -610,6 +613,7 @@ job->reset(); // Warn user if a duplicated job is in the list - same target, same sequence + // FIXME: Those duplicated jobs are not necessarily processed in the order they appear in the list! foreach (SchedulerJob *a_job, jobs) { if(a_job == job) @@ -4311,7 +4315,7 @@ dbusargs.append(url); captureInterface->callWithArgumentList(QDBus::AutoDetect, "loadSequenceQueue", dbusargs); - QMap fMap = currentJob->getCapturedFramesMap(); + SchedulerJob::CapturedFramesMap fMap = currentJob->getCapturedFramesMap(); for (auto &e : fMap.keys()) { @@ -4408,7 +4412,7 @@ void Scheduler::updateCompletedJobsCount(bool forced) { /* Use a temporary map in order to limit the number of file searches */ - QMap newFramesCount; + SchedulerJob::CapturedFramesMap newFramesCount; /* If update is forced, clear the frame map */ if (forced) @@ -4464,26 +4468,46 @@ } capturedFramesCount = newFramesCount; + + //if (forced) + { + qCDebug(KSTARS_EKOS_SCHEDULER) << "Frame map summary:"; + QMap::const_iterator it = capturedFramesCount.constBegin(); + for (; it != capturedFramesCount.constEnd(); it++) + qCDebug(KSTARS_EKOS_SCHEDULER) << " " << it.key() << ':' << it.value(); + } } bool Scheduler::estimateJobTime(SchedulerJob *schedJob) { /* updateCompletedJobsCount(); */ + // Load the sequence job associated with the argument scheduler job. QList seqJobs; bool hasAutoFocus = false; - if (loadSequenceQueue(schedJob->getSequenceFile().toLocalFile(), schedJob, seqJobs, hasAutoFocus) == false) + { + qCWarning(KSTARS_EKOS_SCHEDULER) << QString("Warning: Failed estimating the duration of job '%1', its sequence file is invalid.").arg(schedJob->getSequenceFile().toLocalFile()); return false; + } + // FIXME: setting in-sequence focus should be done in XML processing. schedJob->setInSequenceFocus(hasAutoFocus); + /* This is the map of captured frames for this scheduler job, keyed per storage signature. + * It will be forwarded to the Capture module in order to capture only what frames are required. + * If option "Remember Job Progress" is disabled, this map will be empty, and the Capture module will process all requested captures unconditionally. + */ + SchedulerJob::CapturedFramesMap capture_map; + bool const rememberJobProgress = Options::rememberJobProgress(); + int totalSequenceCount = 0, totalCompletedCount = 0; double totalImagingTime = 0; - bool rememberJobProgress = Options::rememberJobProgress(); + + // Loop through sequence jobs to calculate the number of required frames and estimate duration. foreach (SequenceJob *seqJob, seqJobs) { - /* FIXME: find a way to actually display the filter name */ + // FIXME: find a way to actually display the filter name. QString seqName = i18n("Job '%1' %2x%3\" %4", schedJob->getName(), seqJob->getCount(), seqJob->getExposure(), seqJob->getFilterName()); if (seqJob->getUploadMode() == ISD::CCD::UPLOAD_LOCAL) @@ -4527,7 +4551,7 @@ * This is why it is important to manage the repeat count of the scheduler job, as stated earlier. */ - // Retrieve cached count of captures_completed captures for the output folder of this seqJob + // Retrieve cached count of completed captures for the output folder of this seqJob QString const signature = seqJob->getLocalDir() + seqJob->getDirectoryPostfix(); captures_completed = capturedFramesCount[signature]; @@ -4563,14 +4587,17 @@ qCInfo(KSTARS_EKOS_SCHEDULER) << QString("%1 has completed %2/%3 of its required captures in output folder '%4'.").arg(seqName).arg(captures_completed).arg(captures_required).arg(signature); - // Update the completion count for this signature if we still have captures to take - // FIXME: setting the whole capture map each time is not very optimal - QMap fMap = schedJob->getCapturedFramesMap(); - if (fMap[signature] != captures_completed) - { - fMap[signature] = captures_completed; - schedJob->setCapturedFramesMap(fMap); - } + // Update the completion count for this signature in the frame map if we still have captures to take. + // That frame map will be transferred to the Capture module, for which the sequence is a single batch of the scheduler job. + // For instance, consider a scheduler job repeated 3 times and using a 3xLum sequence, so we want 9xLum in the end. + // - If no captures are already processed, the frame map contains Lum=0 + // - If 1xLum are already processed, the frame map contains Lum=0 when the batch executes, so that 3xLum and 3xHa may be taken. + // - If 3xLum are already processed, the frame map contains Lum=0 when the batch executes, as we still need more than what the sequence provides. + // - If 7xLum are already processed, the frame map contains Lum=1 when the batch executes, because we now only need 2xLum to finish the job. + // Therefore we need to specify a number of existing captures only for the last batch of the scheduler job. + // In the last batch, we only need the remainder of frames to get to the required total. + if (captures_required - seqJob->getCount() < captures_completed) + capture_map[signature] = captures_completed % seqJob->getCount(); // From now on, 'captures_completed' is the number of frames completed for the *current* sequence job } @@ -4626,6 +4653,7 @@ } } + schedJob->setCapturedFramesMap(capture_map); schedJob->setSequenceCount(totalSequenceCount); schedJob->setCompletedCount(totalCompletedCount); diff --git a/kstars/ekos/scheduler/schedulerjob.h b/kstars/ekos/scheduler/schedulerjob.h --- a/kstars/ekos/scheduler/schedulerjob.h +++ b/kstars/ekos/scheduler/schedulerjob.h @@ -306,8 +306,9 @@ /** @brief The map of capture counts for this job, keyed by its capture storage signatures. */ /** @{ */ - QMap getCapturedFramesMap() const { return capturedFramesMap; } - void setCapturedFramesMap(const QMap &value); + typedef QMap CapturedFramesMap; + const CapturedFramesMap& getCapturedFramesMap() const { return capturedFramesMap; } + void setCapturedFramesMap(const CapturedFramesMap &value); /** @} */ /** @brief Refresh all cells connected to this SchedulerJob. */ diff --git a/kstars/ekos/scheduler/schedulerjob.cpp b/kstars/ekos/scheduler/schedulerjob.cpp --- a/kstars/ekos/scheduler/schedulerjob.cpp +++ b/kstars/ekos/scheduler/schedulerjob.cpp @@ -307,7 +307,7 @@ updateJobCell(); } -void SchedulerJob::setCapturedFramesMap(const QMap &value) +void SchedulerJob::setCapturedFramesMap(const CapturedFramesMap &value) { capturedFramesMap = value; }