File indexing completed on 2024-05-05 11:59:42
0001 /* 0002 SPDX-FileCopyrightText: 2023 Wolfgang Reissenberger <sterne-jaeger@openfuture.de> 0003 0004 SPDX-License-Identifier: GPL-2.0-or-later 0005 */ 0006 0007 #include "schedulerutils.h" 0008 #include "schedulerjob.h" 0009 #include "schedulermodulestate.h" 0010 #include "ekos/capture/sequencejob.h" 0011 #include "Options.h" 0012 #include "skypoint.h" 0013 #include "kstarsdata.h" 0014 #include <ekos_scheduler_debug.h> 0015 0016 namespace Ekos { 0017 0018 SchedulerUtils::SchedulerUtils() 0019 { 0020 0021 } 0022 0023 SchedulerJob *SchedulerUtils::createJob(XMLEle *root) 0024 { 0025 SchedulerJob *job = new SchedulerJob(); 0026 QString name, group; 0027 dms ra, dec; 0028 double rotation = 0.0, minimumAltitude = 0.0, minimumMoonSeparation = 0.0; 0029 QUrl sequenceURL, fitsURL; 0030 StartupCondition startup = START_ASAP; 0031 CompletionCondition completion = FINISH_SEQUENCE; 0032 QDateTime startupTime, completionTime; 0033 int completionRepeats = 0; 0034 bool enforceWeather = false, enforceTwilight = false, enforceArtificialHorizon = false, 0035 track = false, focus = false, align = false, guide = false; 0036 0037 0038 XMLEle *ep, *subEP; 0039 // We expect all data read from the XML to be in the C locale - QLocale::c() 0040 QLocale cLocale = QLocale::c(); 0041 0042 for (ep = nextXMLEle(root, 1); ep != nullptr; ep = nextXMLEle(root, 0)) 0043 { 0044 if (!strcmp(tagXMLEle(ep), "Name")) 0045 name = pcdataXMLEle(ep); 0046 else if (!strcmp(tagXMLEle(ep), "Group")) 0047 group = pcdataXMLEle(ep); 0048 else if (!strcmp(tagXMLEle(ep), "Coordinates")) 0049 { 0050 subEP = findXMLEle(ep, "J2000RA"); 0051 if (subEP) 0052 ra.setH(cLocale.toDouble(pcdataXMLEle(subEP))); 0053 0054 subEP = findXMLEle(ep, "J2000DE"); 0055 if (subEP) 0056 dec.setD(cLocale.toDouble(pcdataXMLEle(subEP))); 0057 } 0058 else if (!strcmp(tagXMLEle(ep), "Sequence")) 0059 { 0060 sequenceURL = QUrl::fromUserInput(pcdataXMLEle(ep)); 0061 } 0062 else if (!strcmp(tagXMLEle(ep), "FITS")) 0063 { 0064 fitsURL.setPath(pcdataXMLEle(ep)); 0065 } 0066 else if (!strcmp(tagXMLEle(ep), "PositionAngle")) 0067 { 0068 rotation = cLocale.toDouble(pcdataXMLEle(ep)); 0069 } 0070 else if (!strcmp(tagXMLEle(ep), "StartupCondition")) 0071 { 0072 for (subEP = nextXMLEle(ep, 1); subEP != nullptr; subEP = nextXMLEle(ep, 0)) 0073 { 0074 if (!strcmp("ASAP", pcdataXMLEle(subEP))) 0075 startup = START_ASAP; 0076 else if (!strcmp("At", pcdataXMLEle(subEP))) 0077 { 0078 startup = START_AT; 0079 startupTime = QDateTime::fromString(findXMLAttValu(subEP, "value"), Qt::ISODate); 0080 // Todo sterne-jaeger 2024-01-01: setting time spec from KStars necessary? 0081 } 0082 } 0083 } 0084 else if (!strcmp(tagXMLEle(ep), "Constraints")) 0085 { 0086 for (subEP = nextXMLEle(ep, 1); subEP != nullptr; subEP = nextXMLEle(ep, 0)) 0087 { 0088 if (!strcmp("MinimumAltitude", pcdataXMLEle(subEP))) 0089 { 0090 Options::setEnableAltitudeLimits(true); 0091 minimumAltitude = cLocale.toDouble(findXMLAttValu(subEP, "value")); 0092 } 0093 else if (!strcmp("MoonSeparation", pcdataXMLEle(subEP))) 0094 { 0095 Options::setSchedulerMoonSeparation(true); 0096 minimumMoonSeparation = cLocale.toDouble(findXMLAttValu(subEP, "value")); 0097 } 0098 else if (!strcmp("EnforceWeather", pcdataXMLEle(subEP))) 0099 enforceWeather = true; 0100 else if (!strcmp("EnforceTwilight", pcdataXMLEle(subEP))) 0101 enforceTwilight = true; 0102 else if (!strcmp("EnforceArtificialHorizon", pcdataXMLEle(subEP))) 0103 enforceArtificialHorizon = true; 0104 } 0105 } 0106 else if (!strcmp(tagXMLEle(ep), "CompletionCondition")) 0107 { 0108 for (subEP = nextXMLEle(ep, 1); subEP != nullptr; subEP = nextXMLEle(ep, 0)) 0109 { 0110 if (!strcmp("Sequence", pcdataXMLEle(subEP))) 0111 completion = FINISH_SEQUENCE; 0112 else if (!strcmp("Repeat", pcdataXMLEle(subEP))) 0113 { 0114 completion = FINISH_REPEAT; 0115 completionRepeats = cLocale.toInt(findXMLAttValu(subEP, "value")); 0116 } 0117 else if (!strcmp("Loop", pcdataXMLEle(subEP))) 0118 completion = FINISH_LOOP; 0119 else if (!strcmp("At", pcdataXMLEle(subEP))) 0120 { 0121 completion = FINISH_AT; 0122 completionTime = QDateTime::fromString(findXMLAttValu(subEP, "value"), Qt::ISODate); 0123 } 0124 } 0125 } 0126 else if (!strcmp(tagXMLEle(ep), "Steps")) 0127 { 0128 XMLEle *module; 0129 0130 for (module = nextXMLEle(ep, 1); module != nullptr; module = nextXMLEle(ep, 0)) 0131 { 0132 const char *proc = pcdataXMLEle(module); 0133 0134 if (!strcmp(proc, "Track")) 0135 track = true; 0136 else if (!strcmp(proc, "Focus")) 0137 focus = true; 0138 else if (!strcmp(proc, "Align")) 0139 align = true; 0140 else if (!strcmp(proc, "Guide")) 0141 guide = true; 0142 } 0143 } 0144 } 0145 SchedulerUtils::setupJob(*job, name, group, ra, dec, 0146 KStarsData::Instance()->ut().djd(), 0147 rotation, sequenceURL, fitsURL, 0148 0149 startup, startupTime, 0150 completion, completionTime, completionRepeats, 0151 0152 minimumAltitude, 0153 minimumMoonSeparation, 0154 enforceWeather, 0155 enforceTwilight, 0156 enforceArtificialHorizon, 0157 0158 track, 0159 focus, 0160 align, 0161 guide); 0162 0163 return job; 0164 } 0165 0166 void SchedulerUtils::setupJob(SchedulerJob &job, const QString &name, const QString &group, const dms &ra, const dms &dec, double djd, double rotation, const QUrl &sequenceUrl, const QUrl &fitsUrl, StartupCondition startup, const QDateTime &startupTime, CompletionCondition completion, const QDateTime &completionTime, int completionRepeats, double minimumAltitude, double minimumMoonSeparation, bool enforceWeather, bool enforceTwilight, bool enforceArtificialHorizon, bool track, bool focus, bool align, bool guide) 0167 { 0168 /* Configure or reconfigure the observation job */ 0169 0170 job.setName(name); 0171 job.setGroup(group); 0172 // djd should be ut.djd 0173 job.setTargetCoords(ra, dec, djd); 0174 job.setPositionAngle(rotation); 0175 0176 /* Consider sequence file is new, and clear captured frames map */ 0177 job.setCapturedFramesMap(CapturedFramesMap()); 0178 job.setSequenceFile(sequenceUrl); 0179 job.setFITSFile(fitsUrl); 0180 // #1 Startup conditions 0181 0182 job.setStartupCondition(startup); 0183 if (startup == START_AT) 0184 { 0185 job.setStartupTime(startupTime); 0186 } 0187 /* Store the original startup condition */ 0188 job.setFileStartupCondition(job.getStartupCondition()); 0189 job.setFileStartupTime(job.getStartupTime()); 0190 0191 // #2 Constraints 0192 0193 job.setMinAltitude(minimumAltitude); 0194 job.setMinMoonSeparation(minimumMoonSeparation); 0195 0196 // Check enforce weather constraints 0197 job.setEnforceWeather(enforceWeather); 0198 // twilight constraints 0199 job.setEnforceTwilight(enforceTwilight); 0200 job.setEnforceArtificialHorizon(enforceArtificialHorizon); 0201 0202 job.setCompletionCondition(completion); 0203 if (completion == FINISH_AT) 0204 job.setCompletionTime(completionTime); 0205 else if (completion == FINISH_REPEAT) 0206 { 0207 job.setRepeatsRequired(completionRepeats); 0208 job.setRepeatsRemaining(completionRepeats); 0209 } 0210 // Job steps 0211 job.setStepPipeline(SchedulerJob::USE_NONE); 0212 if (track) 0213 job.setStepPipeline(static_cast<SchedulerJob::StepPipeline>(job.getStepPipeline() | SchedulerJob::USE_TRACK)); 0214 if (focus) 0215 job.setStepPipeline(static_cast<SchedulerJob::StepPipeline>(job.getStepPipeline() | SchedulerJob::USE_FOCUS)); 0216 if (align) 0217 job.setStepPipeline(static_cast<SchedulerJob::StepPipeline>(job.getStepPipeline() | SchedulerJob::USE_ALIGN)); 0218 if (guide) 0219 job.setStepPipeline(static_cast<SchedulerJob::StepPipeline>(job.getStepPipeline() | SchedulerJob::USE_GUIDE)); 0220 0221 /* Store the original startup condition */ 0222 job.setFileStartupCondition(job.getStartupCondition()); 0223 job.setFileStartupTime(job.getStartupTime()); 0224 0225 /* Reset job state to evaluate the changes */ 0226 job.reset(); 0227 } 0228 0229 uint16_t SchedulerUtils::fillCapturedFramesMap(const QMap<QString, uint16_t> &expected, const CapturedFramesMap &capturedFramesCount, SchedulerJob &schedJob, CapturedFramesMap &capture_map, int &completedIterations) 0230 { 0231 uint16_t totalCompletedCount = 0; 0232 0233 // Figure out which repeat this is for the key with the least progress. 0234 int minIterationsCompleted = -1, currentIteration = 0; 0235 if (Options::rememberJobProgress()) 0236 { 0237 completedIterations = 0; 0238 for (const QString &key : expected.keys()) 0239 { 0240 const int iterationsCompleted = capturedFramesCount[key] / expected[key]; 0241 if (minIterationsCompleted == -1 || iterationsCompleted < minIterationsCompleted) 0242 minIterationsCompleted = iterationsCompleted; 0243 } 0244 // If this condition is FINISH_REPEAT, and we've already completed enough iterations 0245 // Then set the currentIteratiion as 1 more than required. No need to go higher. 0246 if (schedJob.getCompletionCondition() == FINISH_REPEAT 0247 && minIterationsCompleted >= schedJob.getRepeatsRequired()) 0248 currentIteration = schedJob.getRepeatsRequired() + 1; 0249 else 0250 // Otherwise set it to one more than the number completed (i.e. the one it'll be working on). 0251 currentIteration = minIterationsCompleted + 1; 0252 completedIterations = std::max(0, currentIteration - 1); 0253 } 0254 else 0255 // If we are not remembering progress, we'll only know the iterations completed 0256 // by the current job's run. 0257 completedIterations = schedJob.getCompletedIterations(); 0258 0259 for (const QString &key : expected.keys()) 0260 { 0261 if (Options::rememberJobProgress()) 0262 { 0263 // If we're remembering progress, then figure out how many captures have not yet been captured. 0264 const int diff = expected[key] * currentIteration - capturedFramesCount[key]; 0265 0266 // Already captured more than required? Then don't capture any this round. 0267 if (diff <= 0) 0268 capture_map[key] = expected[key]; 0269 // Need more captures than one cycle could capture? If so, capture the full amount. 0270 else if (diff >= expected[key]) 0271 capture_map[key] = 0; 0272 // Otherwise we know that 0 < diff < expected[key]. Capture just the number needed. 0273 else 0274 capture_map[key] = expected[key] - diff; 0275 } 0276 else 0277 // If we are not remembering progress, then the capture module, which reads this 0278 // Will capture all requirements in the .esq file. 0279 capture_map[key] = 0; 0280 0281 // collect all captured frames counts 0282 if (schedJob.getCompletionCondition() == FINISH_LOOP) 0283 totalCompletedCount += capturedFramesCount[key]; 0284 else 0285 totalCompletedCount += std::min(capturedFramesCount[key], 0286 static_cast<uint16_t>(expected[key] * schedJob.getRepeatsRequired())); 0287 } 0288 return totalCompletedCount; 0289 } 0290 0291 void SchedulerUtils::updateLightFramesRequired(SchedulerJob *oneJob, const QList<SequenceJob *> &seqjobs, const CapturedFramesMap &framesCount) 0292 { 0293 bool lightFramesRequired = false; 0294 QMap<QString, uint16_t> expected; 0295 switch (oneJob->getCompletionCondition()) 0296 { 0297 case FINISH_SEQUENCE: 0298 case FINISH_REPEAT: 0299 // Step 1: determine expected frames 0300 SchedulerUtils::calculateExpectedCapturesMap(seqjobs, expected); 0301 // Step 2: compare with already captured frames 0302 for (SequenceJob *oneSeqJob : seqjobs) 0303 { 0304 QString const signature = oneSeqJob->getSignature(); 0305 /* If frame is LIGHT, how many do we have left? */ 0306 if (oneSeqJob->getFrameType() == FRAME_LIGHT && expected[signature] * oneJob->getRepeatsRequired() > framesCount[signature]) 0307 { 0308 lightFramesRequired = true; 0309 // exit the loop, one found is sufficient 0310 break; 0311 } 0312 } 0313 break; 0314 default: 0315 // in all other cases it does not depend on the number of captured frames 0316 lightFramesRequired = true; 0317 } 0318 oneJob->setLightFramesRequired(lightFramesRequired); 0319 } 0320 0321 SequenceJob *SchedulerUtils::processSequenceJobInfo(XMLEle *root, SchedulerJob *schedJob) 0322 { 0323 SequenceJob *job = new SequenceJob(root, schedJob->getName()); 0324 if (FRAME_LIGHT == job->getFrameType() && nullptr != schedJob) 0325 schedJob->setLightFramesRequired(true); 0326 0327 auto placeholderPath = Ekos::PlaceholderPath(); 0328 placeholderPath.processJobInfo(job); 0329 0330 return job; 0331 } 0332 0333 bool SchedulerUtils::loadSequenceQueue(const QString &fileURL, SchedulerJob *schedJob, QList<SequenceJob *> &jobs, bool &hasAutoFocus, ModuleLogger *logger) 0334 { 0335 QFile sFile; 0336 sFile.setFileName(fileURL); 0337 0338 if (!sFile.open(QIODevice::ReadOnly)) 0339 { 0340 if (logger != nullptr) logger->appendLogText(i18n("Unable to open sequence queue file '%1'", fileURL)); 0341 return false; 0342 } 0343 0344 LilXML *xmlParser = newLilXML(); 0345 char errmsg[MAXRBUF]; 0346 XMLEle *root = nullptr; 0347 XMLEle *ep = nullptr; 0348 char c; 0349 0350 while (sFile.getChar(&c)) 0351 { 0352 root = readXMLEle(xmlParser, c, errmsg); 0353 0354 if (root) 0355 { 0356 for (ep = nextXMLEle(root, 1); ep != nullptr; ep = nextXMLEle(root, 0)) 0357 { 0358 if (!strcmp(tagXMLEle(ep), "Autofocus")) 0359 hasAutoFocus = (!strcmp(findXMLAttValu(ep, "enabled"), "true")); 0360 else if (!strcmp(tagXMLEle(ep), "Job")) 0361 { 0362 SequenceJob *thisJob = processSequenceJobInfo(ep, schedJob); 0363 jobs.append(thisJob); 0364 if (jobs.count() == 1) 0365 { 0366 auto &firstJob = jobs.first(); 0367 if (FRAME_LIGHT == firstJob->getFrameType() && nullptr != schedJob) 0368 { 0369 schedJob->setInitialFilter(firstJob->getCoreProperty(SequenceJob::SJ_Filter).toString()); 0370 } 0371 0372 } 0373 } 0374 } 0375 delXMLEle(root); 0376 } 0377 else if (errmsg[0]) 0378 { 0379 if (logger != nullptr) logger->appendLogText(QString(errmsg)); 0380 delLilXML(xmlParser); 0381 qDeleteAll(jobs); 0382 return false; 0383 } 0384 } 0385 0386 return true; 0387 } 0388 0389 bool SchedulerUtils::estimateJobTime(SchedulerJob *schedJob, const QMap<QString, uint16_t> &capturedFramesCount, ModuleLogger *logger) 0390 { 0391 static SchedulerJob *jobWarned = nullptr; 0392 0393 // Load the sequence job associated with the argument scheduler job. 0394 QList<SequenceJob *> seqJobs; 0395 bool hasAutoFocus = false; 0396 bool result = loadSequenceQueue(schedJob->getSequenceFile().toLocalFile(), schedJob, seqJobs, hasAutoFocus, logger); 0397 if (result == false) 0398 { 0399 qCWarning(KSTARS_EKOS_SCHEDULER) << 0400 QString("Warning: Failed estimating the duration of job '%1', its sequence file is invalid.").arg( 0401 schedJob->getSequenceFile().toLocalFile()); 0402 return result; 0403 } 0404 0405 // FIXME: setting in-sequence focus should be done in XML processing. 0406 schedJob->setInSequenceFocus(hasAutoFocus); 0407 0408 // Stop spam of log on re-evaluation. If we display the warning once, then that's it. 0409 if (schedJob != jobWarned && hasAutoFocus && !(schedJob->getStepPipeline() & SchedulerJob::USE_FOCUS)) 0410 { 0411 logger->appendLogText(i18n("Warning: Job '%1' has its focus step disabled, periodic and/or HFR procedures currently set in its sequence will not occur.", 0412 schedJob->getName())); 0413 jobWarned = schedJob; 0414 } 0415 0416 /* This is the map of captured frames for this scheduler job, keyed per storage signature. 0417 * It will be forwarded to the Capture module in order to capture only what frames are required. 0418 * If option "Remember Job Progress" is disabled, this map will be empty, and the Capture module will process all requested captures unconditionally. 0419 */ 0420 CapturedFramesMap capture_map; 0421 bool const rememberJobProgress = Options::rememberJobProgress(); 0422 0423 double totalImagingTime = 0; 0424 double imagingTimePerRepeat = 0, imagingTimeLeftThisRepeat = 0; 0425 0426 // Determine number of captures in the scheduler job 0427 QMap<QString, uint16_t> expected; 0428 uint16_t allCapturesPerRepeat = calculateExpectedCapturesMap(seqJobs, expected); 0429 0430 // fill the captured frames map 0431 int completedIterations; 0432 uint16_t totalCompletedCount = fillCapturedFramesMap(expected, capturedFramesCount, *schedJob, capture_map, completedIterations); 0433 schedJob->setCompletedIterations(completedIterations); 0434 // Loop through sequence jobs to calculate the number of required frames and estimate duration. 0435 foreach (SequenceJob *seqJob, seqJobs) 0436 { 0437 // FIXME: find a way to actually display the filter name. 0438 QString seqName = i18n("Job '%1' %2x%3\" %4", schedJob->getName(), seqJob->getCoreProperty(SequenceJob::SJ_Count).toInt(), 0439 seqJob->getCoreProperty(SequenceJob::SJ_Exposure).toDouble(), 0440 seqJob->getCoreProperty(SequenceJob::SJ_Filter).toString()); 0441 0442 if (seqJob->getUploadMode() == ISD::Camera::UPLOAD_LOCAL) 0443 { 0444 qCInfo(KSTARS_EKOS_SCHEDULER) << 0445 QString("%1 duration cannot be estimated time since the sequence saves the files remotely.").arg(seqName); 0446 schedJob->setEstimatedTime(-2); 0447 qDeleteAll(seqJobs); 0448 return true; 0449 } 0450 0451 // Note that looping jobs will have zero repeats required. 0452 QString const signature = seqJob->getSignature(); 0453 QString const signature_path = QFileInfo(signature).path(); 0454 int captures_required = seqJob->getCoreProperty(SequenceJob::SJ_Count).toInt() * schedJob->getRepeatsRequired(); 0455 int captures_completed = capturedFramesCount[signature]; 0456 const int capturesRequiredPerRepeat = std::max(1, seqJob->getCoreProperty(SequenceJob::SJ_Count).toInt()); 0457 int capturesLeftThisRepeat = std::max(0, capturesRequiredPerRepeat - (captures_completed % capturesRequiredPerRepeat)); 0458 if (captures_completed >= (1 + completedIterations) * capturesRequiredPerRepeat) 0459 { 0460 // Something else is causing this iteration to be incomplete. Nothing left to do for this seqJob. 0461 capturesLeftThisRepeat = 0; 0462 } 0463 0464 if (rememberJobProgress && schedJob->getCompletionCondition() != FINISH_LOOP) 0465 { 0466 /* Enumerate sequence jobs associated to this scheduler job, and assign them a completed count. 0467 * 0468 * The objective of this block is to fill the storage map of the scheduler job with completed counts for each capture storage. 0469 * 0470 * Sequence jobs capture to a storage folder, and are given a count of captures to store at that location. 0471 * The tricky part is to make sure the repeat count of the scheduler job is properly transferred to each sequence job. 0472 * 0473 * For instance, a scheduler job repeated three times must execute the full list of sequence jobs three times, thus 0474 * has to tell each sequence job it misses all captures, three times. It cannot tell the sequence job three captures are 0475 * missing, first because that's not how the sequence job is designed (completed count, not required count), and second 0476 * because this would make the single sequence job repeat three times, instead of repeating the full list of sequence 0477 * jobs three times. 0478 * 0479 * The consolidated storage map will be assigned to each sequence job based on their signature when the scheduler job executes them. 0480 * 0481 * For instance, consider a RGBL sequence of single captures. The map will store completed captures for R, G, B and L storages. 0482 * If R and G have 1 file each, and B and L have no files, map[storage(R)] = map[storage(G)] = 1 and map[storage(B)] = map[storage(L)] = 0. 0483 * When that scheduler job executes, only B and L captures will be processed. 0484 * 0485 * In the case of a RGBLRGB sequence of single captures, the second R, G and B map items will count one less capture than what is really in storage. 0486 * If R and G have 1 file each, and B and L have no files, map[storage(R1)] = map[storage(B1)] = 1, and all others will be 0. 0487 * When that scheduler job executes, B1, L, R2, G2 and B2 will be processed. 0488 * 0489 * This doesn't handle the case of duplicated scheduler jobs, that is, scheduler jobs with the same storage for capture sets. 0490 * Those scheduler jobs will all change state to completion at the same moment as they all target the same storage. 0491 * This is why it is important to manage the repeat count of the scheduler job, as stated earlier. 0492 */ 0493 0494 captures_required = expected[seqJob->getSignature()] * schedJob->getRepeatsRequired(); 0495 0496 qCInfo(KSTARS_EKOS_SCHEDULER) << QString("%1 sees %2 captures in output folder '%3'.").arg(seqName).arg( 0497 captures_completed).arg(QFileInfo(signature).path()); 0498 0499 // Enumerate sequence jobs to check how many captures are completed overall in the same storage as the current one 0500 foreach (SequenceJob *prevSeqJob, seqJobs) 0501 { 0502 // Enumerate seqJobs up to the current one 0503 if (seqJob == prevSeqJob) 0504 break; 0505 0506 // If the previous sequence signature matches the current, skip counting to take duplicates into account 0507 if (!signature.compare(prevSeqJob->getSignature())) 0508 captures_required = 0; 0509 0510 // And break if no captures remain, this job does not need to be executed 0511 if (captures_required == 0) 0512 break; 0513 } 0514 0515 qCDebug(KSTARS_EKOS_SCHEDULER) << QString("%1 has completed %2/%3 of its required captures in output folder '%4'.").arg( 0516 seqName).arg(captures_completed).arg(captures_required).arg(signature_path); 0517 0518 } 0519 // Else rely on the captures done during this session 0520 else if (0 < allCapturesPerRepeat) 0521 { 0522 captures_completed = schedJob->getCompletedCount() / allCapturesPerRepeat * seqJob->getCoreProperty( 0523 SequenceJob::SJ_Count).toInt(); 0524 } 0525 else 0526 { 0527 captures_completed = 0; 0528 } 0529 0530 // Check if we still need any light frames. Because light frames changes the flow of the observatory startup 0531 // Without light frames, there is no need to do focusing, alignment, guiding...etc 0532 // We check if the frame type is LIGHT and if either the number of captures_completed frames is less than required 0533 // OR if the completion condition is set to LOOP so it is never complete due to looping. 0534 // Note that looping jobs will have zero repeats required. 0535 // FIXME: As it is implemented now, FINISH_LOOP may loop over a capture-complete, therefore inoperant, scheduler job. 0536 bool const areJobCapturesComplete = (0 == captures_required || captures_completed >= captures_required); 0537 if (seqJob->getFrameType() == FRAME_LIGHT) 0538 { 0539 if(areJobCapturesComplete) 0540 { 0541 qCInfo(KSTARS_EKOS_SCHEDULER) << QString("%1 completed its sequence of %2 light frames.").arg(seqName).arg( 0542 captures_required); 0543 } 0544 } 0545 else 0546 { 0547 qCInfo(KSTARS_EKOS_SCHEDULER) << QString("%1 captures calibration frames.").arg(seqName); 0548 } 0549 0550 /* If captures are not complete, we have imaging time left */ 0551 if (!areJobCapturesComplete || schedJob->getCompletionCondition() == FINISH_LOOP) 0552 { 0553 unsigned int const captures_to_go = captures_required - captures_completed; 0554 const double secsPerCapture = (seqJob->getCoreProperty(SequenceJob::SJ_Exposure).toDouble() + 0555 (seqJob->getCoreProperty(SequenceJob::SJ_Delay).toInt() / 1000.0)); 0556 totalImagingTime += fabs(secsPerCapture * captures_to_go); 0557 imagingTimePerRepeat += fabs(secsPerCapture * seqJob->getCoreProperty(SequenceJob::SJ_Count).toInt()); 0558 imagingTimeLeftThisRepeat += fabs(secsPerCapture * capturesLeftThisRepeat); 0559 /* If we have light frames to process, add focus/dithering delay */ 0560 if (seqJob->getFrameType() == FRAME_LIGHT) 0561 { 0562 // If inSequenceFocus is true 0563 if (hasAutoFocus) 0564 { 0565 // Wild guess, 10s of autofocus for each capture required. Can vary a lot, but this is just a completion estimate. 0566 constexpr int afSecsPerCapture = 10; 0567 qCInfo(KSTARS_EKOS_SCHEDULER) << QString("%1 requires a focus procedure.").arg(seqName); 0568 totalImagingTime += captures_to_go * afSecsPerCapture; 0569 imagingTimePerRepeat += capturesRequiredPerRepeat * afSecsPerCapture; 0570 imagingTimeLeftThisRepeat += capturesLeftThisRepeat * afSecsPerCapture; 0571 } 0572 // If we're dithering after each exposure, that's another 10-20 seconds 0573 if (schedJob->getStepPipeline() & SchedulerJob::USE_GUIDE && Options::ditherEnabled()) 0574 { 0575 constexpr int ditherSecs = 15; 0576 qCInfo(KSTARS_EKOS_SCHEDULER) << QString("%1 requires a dither procedure.").arg(seqName); 0577 totalImagingTime += (captures_to_go * ditherSecs) / Options::ditherFrames(); 0578 imagingTimePerRepeat += (capturesRequiredPerRepeat * ditherSecs) / Options::ditherFrames(); 0579 imagingTimeLeftThisRepeat += (capturesLeftThisRepeat * ditherSecs) / Options::ditherFrames(); 0580 } 0581 } 0582 } 0583 } 0584 0585 schedJob->setCapturedFramesMap(capture_map); 0586 schedJob->setSequenceCount(allCapturesPerRepeat * schedJob->getRepeatsRequired()); 0587 0588 // only in case we remember the job progress, we change the completion count 0589 if (rememberJobProgress) 0590 schedJob->setCompletedCount(totalCompletedCount); 0591 0592 qDeleteAll(seqJobs); 0593 0594 schedJob->setEstimatedTimePerRepeat(imagingTimePerRepeat); 0595 schedJob->setEstimatedTimeLeftThisRepeat(imagingTimeLeftThisRepeat); 0596 if (schedJob->getLightFramesRequired()) 0597 schedJob->setEstimatedStartupTime(timeHeuristics(schedJob)); 0598 0599 // FIXME: Move those ifs away to the caller in order to avoid estimating in those situations! 0600 0601 // We can't estimate times that do not finish when sequence is done 0602 if (schedJob->getCompletionCondition() == FINISH_LOOP) 0603 { 0604 // We can't know estimated time if it is looping indefinitely 0605 schedJob->setEstimatedTime(-2); 0606 qCDebug(KSTARS_EKOS_SCHEDULER) << 0607 QString("Job '%1' is configured to loop until Scheduler is stopped manually, has undefined imaging time.") 0608 .arg(schedJob->getName()); 0609 } 0610 // If we know startup and finish times, we can estimate time right away 0611 else if (schedJob->getStartupCondition() == START_AT && 0612 schedJob->getCompletionCondition() == FINISH_AT) 0613 { 0614 // FIXME: SchedulerJob is probably doing this already 0615 qint64 const diff = schedJob->getStartupTime().secsTo(schedJob->getCompletionTime()); 0616 schedJob->setEstimatedTime(diff); 0617 0618 qCDebug(KSTARS_EKOS_SCHEDULER) << QString("Job '%1' has a startup time and fixed completion time, will run for %2.") 0619 .arg(schedJob->getName()) 0620 .arg(dms(diff * 15.0 / 3600.0f).toHMSString()); 0621 } 0622 // If we know finish time only, we can roughly estimate the time considering the job starts now 0623 else if (schedJob->getStartupCondition() != START_AT && 0624 schedJob->getCompletionCondition() == FINISH_AT) 0625 { 0626 qint64 const diff = SchedulerModuleState::getLocalTime().secsTo(schedJob->getCompletionTime()); 0627 schedJob->setEstimatedTime(diff); 0628 qCDebug(KSTARS_EKOS_SCHEDULER) << 0629 QString("Job '%1' has no startup time but fixed completion time, will run for %2 if started now.") 0630 .arg(schedJob->getName()) 0631 .arg(dms(diff * 15.0 / 3600.0f).toHMSString()); 0632 } 0633 // Rely on the estimated imaging time to determine whether this job is complete or not - this makes the estimated time null 0634 else if (totalImagingTime <= 0) 0635 { 0636 schedJob->setEstimatedTime(0); 0637 schedJob->setEstimatedTimePerRepeat(1); 0638 schedJob->setEstimatedTimeLeftThisRepeat(0); 0639 0640 qCDebug(KSTARS_EKOS_SCHEDULER) << QString("Job '%1' will not run, complete with %2/%3 captures.") 0641 .arg(schedJob->getName()).arg(schedJob->getCompletedCount()).arg(schedJob->getSequenceCount()); 0642 } 0643 // Else consolidate with step durations 0644 else 0645 { 0646 if (schedJob->getLightFramesRequired()) 0647 { 0648 totalImagingTime += timeHeuristics(schedJob); 0649 schedJob->setEstimatedStartupTime(timeHeuristics(schedJob)); 0650 } 0651 dms const estimatedTime(totalImagingTime * 15.0 / 3600.0); 0652 schedJob->setEstimatedTime(std::ceil(totalImagingTime)); 0653 0654 qCInfo(KSTARS_EKOS_SCHEDULER) << QString("Job '%1' estimated to take %2 to complete.").arg(schedJob->getName(), 0655 estimatedTime.toHMSString()); 0656 } 0657 0658 return true; 0659 } 0660 0661 int SchedulerUtils::timeHeuristics(const SchedulerJob *schedJob) 0662 { 0663 double imagingTime = 0; 0664 /* FIXME: estimation should base on actual measure of each step, eventually with preliminary data as what it used now */ 0665 // Are we doing tracking? It takes about 30 seconds 0666 if (schedJob->getStepPipeline() & SchedulerJob::USE_TRACK) 0667 imagingTime += 30; 0668 // Are we doing initial focusing? That can take about 2 minutes 0669 if (schedJob->getStepPipeline() & SchedulerJob::USE_FOCUS) 0670 imagingTime += 120; 0671 // Are we doing astrometry? That can take about 60 seconds 0672 if (schedJob->getStepPipeline() & SchedulerJob::USE_ALIGN) 0673 { 0674 imagingTime += 60; 0675 } 0676 // Are we doing guiding? 0677 if (schedJob->getStepPipeline() & SchedulerJob::USE_GUIDE) 0678 { 0679 // Looping, finding guide star, settling takes 15 sec 0680 imagingTime += 15; 0681 0682 // Add guiding settle time from dither setting (used by phd2::guide()) 0683 imagingTime += Options::ditherSettle(); 0684 // Add guiding settle time from ekos sccheduler setting 0685 imagingTime += Options::guidingSettle(); 0686 0687 // If calibration always cleared 0688 // then calibration process can take about 2 mins 0689 if(Options::resetGuideCalibration()) 0690 imagingTime += 120; 0691 } 0692 return imagingTime; 0693 0694 } 0695 0696 uint16_t SchedulerUtils::calculateExpectedCapturesMap(const QList<SequenceJob *> &seqJobs, QMap<QString, uint16_t> &expected) 0697 { 0698 uint16_t capturesPerRepeat = 0; 0699 for (auto &seqJob : seqJobs) 0700 { 0701 capturesPerRepeat += seqJob->getCoreProperty(SequenceJob::SJ_Count).toInt(); 0702 QString signature = seqJob->getCoreProperty(SequenceJob::SJ_Signature).toString(); 0703 expected[signature] = static_cast<uint16_t>(seqJob->getCoreProperty(SequenceJob::SJ_Count).toInt()) + (expected.contains( 0704 signature) ? expected[signature] : 0); 0705 } 0706 return capturesPerRepeat; 0707 } 0708 0709 double SchedulerUtils::findAltitude(const SkyPoint &target, const QDateTime &when, bool *is_setting, bool debug) 0710 { 0711 // FIXME: block calculating target coordinates at a particular time is duplicated in several places 0712 0713 // Retrieve the argument date/time, or fall back to current time - don't use QDateTime's timezone! 0714 KStarsDateTime ltWhen(when.isValid() ? 0715 Qt::UTC == when.timeSpec() ? SchedulerModuleState::getGeo()->UTtoLT(KStarsDateTime(when)) : when : 0716 SchedulerModuleState::getLocalTime()); 0717 0718 // Create a sky object with the target catalog coordinates 0719 SkyObject o; 0720 o.setRA0(target.ra0()); 0721 o.setDec0(target.dec0()); 0722 0723 // Update RA/DEC of the target for the current fraction of the day 0724 KSNumbers numbers(ltWhen.djd()); 0725 o.updateCoordsNow(&numbers); 0726 0727 // Calculate alt/az coordinates using KStars instance's geolocation 0728 CachingDms const LST = SchedulerModuleState::getGeo()->GSTtoLST(SchedulerModuleState::getGeo()->LTtoUT(ltWhen).gst()); 0729 o.EquatorialToHorizontal(&LST, SchedulerModuleState::getGeo()->lat()); 0730 0731 // Hours are reduced to [0,24[, meridian being at 0 0732 double offset = LST.Hours() - o.ra().Hours(); 0733 if (24.0 <= offset) 0734 offset -= 24.0; 0735 else if (offset < 0.0) 0736 offset += 24.0; 0737 bool const passed_meridian = 0.0 <= offset && offset < 12.0; 0738 0739 if (debug) 0740 qCDebug(KSTARS_EKOS_SCHEDULER) << QString("When:%9 LST:%8 RA:%1 RA0:%2 DEC:%3 DEC0:%4 alt:%5 setting:%6 HA:%7") 0741 .arg(o.ra().toHMSString()) 0742 .arg(o.ra0().toHMSString()) 0743 .arg(o.dec().toHMSString()) 0744 .arg(o.dec0().toHMSString()) 0745 .arg(o.alt().Degrees()) 0746 .arg(passed_meridian ? "yes" : "no") 0747 .arg(o.ra().Hours()) 0748 .arg(LST.toHMSString()) 0749 .arg(ltWhen.toString("HH:mm:ss")); 0750 0751 if (is_setting) 0752 *is_setting = passed_meridian; 0753 0754 return o.alt().Degrees(); 0755 } 0756 0757 } // namespace