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

0001 /*
0002     SPDX-FileCopyrightText: 2012 Jasem Mutlaq <mutlaqja@ikarustech.com>
0003 
0004     SPDX-License-Identifier: GPL-2.0-or-later
0005 */
0006 
0007 #include "sequencejob.h"
0008 
0009 #include <KNotifications/KNotification>
0010 #include <ekos_capture_debug.h>
0011 #include "capturedeviceadaptor.h"
0012 #include "skyobjects/skypoint.h"
0013 #include "ksnotification.h"
0014 
0015 #define MF_TIMER_TIMEOUT    90000
0016 #define MF_RA_DIFF_LIMIT    4
0017 
0018 namespace Ekos
0019 {
0020 QString const &SequenceJob::ISOMarker("_ISO8601");
0021 
0022 const QStringList SequenceJob::StatusStrings()
0023 {
0024     static const QStringList names = {i18n("Idle"), i18n("In Progress"), i18n("Error"), i18n("Aborted"),
0025                                       i18n("Complete")
0026                                      };
0027     return names;
0028 }
0029 
0030 
0031 /**
0032  * @brief SequenceJob::SequenceJob Construct job from XML source
0033  * @param root pointer to valid job stored in XML format.
0034  */
0035 SequenceJob::SequenceJob(XMLEle * root, QString targetName)
0036 {
0037     // set own unconnected state machine
0038     QSharedPointer<CaptureModuleState> sharedState;
0039     sharedState.reset(new CaptureModuleState);
0040     state.reset(new SequenceJobState(sharedState));
0041     // set simple device adaptor
0042     devices.reset(new CaptureDeviceAdaptor());
0043 
0044     init(SequenceJob::JOBTYPE_BATCH, root, sharedState, targetName);
0045 }
0046 
0047 SequenceJob::SequenceJob(const QSharedPointer<CaptureDeviceAdaptor> cp,
0048                          const QSharedPointer<CaptureModuleState> sharedState,
0049                          SequenceJobType jobType, XMLEle *root, QString targetName)
0050 {
0051     devices = cp;
0052     init(jobType, root, sharedState, targetName);
0053 }
0054 
0055 void Ekos::SequenceJob::init(SequenceJobType jobType, XMLEle *root,
0056                              QSharedPointer<CaptureModuleState> sharedState,
0057                              const QString &targetName)
0058 {
0059     // initialize the state machine
0060     state.reset(new SequenceJobState(sharedState));
0061 
0062     loadFrom(root, targetName, jobType, sharedState);
0063 
0064     // signal forwarding between this and the state machine
0065     connect(state.data(), &SequenceJobState::prepareState, this, &SequenceJob::prepareState);
0066     connect(state.data(), &SequenceJobState::prepareComplete, this, &SequenceJob::processPrepareComplete);
0067     connect(state.data(), &SequenceJobState::abortCapture, this, &SequenceJob::processAbortCapture);
0068     connect(state.data(), &SequenceJobState::newLog, this, &SequenceJob::newLog);
0069     // start capturing as soon as the capture initialization is complete
0070     connect(state.data(), &SequenceJobState::initCaptureComplete, this, &SequenceJob::capture);
0071 
0072     // finish if XML document empty
0073     if (root == nullptr)
0074         return;
0075 
0076     // create signature with current target
0077     auto placeholderPath = Ekos::PlaceholderPath();
0078     placeholderPath.processJobInfo(this);
0079 }
0080 
0081 void SequenceJob::resetStatus(JOBStatus status)
0082 {
0083     setStatus(status);
0084     setCalibrationStage(SequenceJobState::CAL_NONE);
0085     switch (status)
0086     {
0087         case JOB_IDLE:
0088             setCompleted(0);
0089             // 2022.03.10: Keeps failing on Windows despite installing latest libindi
0090 #ifndef Q_OS_WIN
0091             INDI_FALLTHROUGH;
0092 #endif
0093         case JOB_ERROR:
0094         case JOB_ABORTED:
0095         case JOB_DONE:
0096             m_ExposeLeft     = 0;
0097             m_CaptureRetires = 0;
0098             m_JobProgressIgnored = false;
0099             break;
0100         case JOB_BUSY:
0101             // do nothing
0102             break;
0103     }
0104 }
0105 
0106 void SequenceJob::abort()
0107 {
0108     setStatus(JOB_ABORTED);
0109     if (devices.data()->getActiveChip())
0110     {
0111         if (devices.data()->getActiveChip()->canAbort())
0112             devices.data()->getActiveChip()->abortExposure();
0113         devices.data()->getActiveChip()->setBatchMode(false);
0114     }
0115 }
0116 
0117 void SequenceJob::done()
0118 {
0119     setStatus(JOB_DONE);
0120 }
0121 
0122 int SequenceJob::getJobRemainingTime(double estimatedDownloadTime)
0123 {
0124     double remaining = (getCoreProperty(SJ_Exposure).toDouble() +
0125                         estimatedDownloadTime +
0126                         getCoreProperty(SJ_Delay).toDouble() / 1000) *
0127                        (getCoreProperty(SJ_Count).toDouble() - getCompleted());
0128 
0129     if (getStatus() == JOB_BUSY)
0130     {
0131         if (getExposeLeft() > 0.0)
0132             remaining -= getCoreProperty(SJ_Exposure).toDouble() - getExposeLeft();
0133         else
0134             remaining += getExposeLeft() + estimatedDownloadTime;
0135     }
0136 
0137     return static_cast<int>(std::round(remaining));
0138 }
0139 
0140 void SequenceJob::setStatus(JOBStatus const in_status)
0141 {
0142     state->reset(in_status);
0143 }
0144 
0145 void SequenceJob::setISO(int index)
0146 {
0147     if (devices->getActiveChip())
0148     {
0149         setCoreProperty(SequenceJob::SJ_ISOIndex, index);
0150         const auto isolist = devices->getActiveChip()->getISOList();
0151         if (isolist.count() > index && index >= 0)
0152             setCoreProperty(SequenceJob::SJ_ISO, isolist[index]);
0153     }
0154 }
0155 
0156 QStringList SequenceJob::frameTypes() const
0157 {
0158     if (!devices->getActiveCamera())
0159         return QStringList({"Light", "Bias", "Dark", "Flat"});
0160 
0161     ISD::CameraChip *tChip = devices->getActiveCamera()->getChip(ISD::CameraChip::PRIMARY_CCD);
0162 
0163     return tChip->getFrameTypes();
0164 }
0165 
0166 QStringList SequenceJob::filterLabels() const
0167 {
0168     if (devices->getFilterManager().isNull())
0169         return QStringList();
0170 
0171     return devices->getFilterManager()->getFilterLabels();
0172 
0173 }
0174 
0175 void SequenceJob::connectDeviceAdaptor()
0176 {
0177     devices->setCurrentSequenceJobState(state);
0178     // connect state machine with device adaptor
0179     connect(state.data(), &SequenceJobState::readCurrentState, devices.data(),
0180             &CaptureDeviceAdaptor::readCurrentState);
0181     connect(state.data(), &SequenceJobState::flatSyncFocus, devices.data(),
0182             &CaptureDeviceAdaptor::flatSyncFocus);
0183     // connect device adaptor with state machine
0184     connect(devices.data(), &CaptureDeviceAdaptor::flatSyncFocusChanged, state.data(),
0185             &SequenceJobState::flatSyncFocusChanged);
0186 }
0187 
0188 void SequenceJob::disconnectDeviceAdaptor()
0189 {
0190     devices->disconnectDevices(state.data());
0191     disconnect(state.data(), &SequenceJobState::readCurrentState, devices.data(),
0192                &CaptureDeviceAdaptor::readCurrentState);
0193     disconnect(state.data(), &SequenceJobState::flatSyncFocus, devices.data(),
0194                &CaptureDeviceAdaptor::flatSyncFocus);
0195     disconnect(devices.data(), &CaptureDeviceAdaptor::flatSyncFocusChanged, state.data(),
0196                &SequenceJobState::flatSyncFocusChanged);
0197 }
0198 
0199 void SequenceJob::startCapturing(bool autofocusReady, FITSMode mode)
0200 {
0201     state->initCapture(getFrameType(), jobType() == SequenceJob::JOBTYPE_PREVIEW, autofocusReady, mode);
0202 }
0203 
0204 void SequenceJob::capture(FITSMode mode)
0205 {
0206     if (!devices.data()->getActiveCamera() || !devices.data()->getActiveChip())
0207         return;
0208 
0209     // initialize the log entry
0210     QString logentry = QString("Capture exposure = %1 sec, type = %2").arg(getCoreProperty(SJ_Exposure).toDouble()).arg(
0211                            CCDFrameTypeNames[getFrameType()]);
0212     logentry.append(QString(", filter = %1, upload mode = %2").arg(getCoreProperty(SJ_Filter).toString()).arg(getUploadMode()));
0213 
0214     devices.data()->getActiveChip()->setBatchMode(jobType() != SequenceJob::JOBTYPE_PREVIEW);
0215     devices.data()->getActiveCamera()->setSeqPrefix(getCoreProperty(SJ_FullPrefix).toString());
0216     logentry.append(QString(", batch mode = %1, seq prefix = %2").arg(jobType() != SequenceJob::JOBTYPE_PREVIEW ? "true" :
0217                     "false").arg(getCoreProperty(SJ_FullPrefix).toString()));
0218 
0219     if (jobType() == SequenceJob::JOBTYPE_PREVIEW)
0220     {
0221         if (devices.data()->getActiveCamera()->getUploadMode() != ISD::Camera::UPLOAD_CLIENT)
0222             devices.data()->getActiveCamera()->setUploadMode(ISD::Camera::UPLOAD_CLIENT);
0223     }
0224     else
0225         devices.data()->getActiveCamera()->setUploadMode(m_UploadMode);
0226 
0227     QMapIterator<QString, QMap<QString, QVariant>> i(m_CustomProperties);
0228     while (i.hasNext())
0229     {
0230         i.next();
0231         auto customProp = devices.data()->getActiveCamera()->getProperty(i.key());
0232         if (customProp)
0233         {
0234             QMap<QString, QVariant> elements = i.value();
0235             QMapIterator<QString, QVariant> j(elements);
0236 
0237             switch (customProp.getType())
0238             {
0239                 case INDI_SWITCH:
0240                 {
0241                     auto sp = customProp.getSwitch();
0242                     while (j.hasNext())
0243                     {
0244                         j.next();
0245                         auto oneSwitch = sp->findWidgetByName(j.key().toLatin1().data());
0246                         if (oneSwitch)
0247                             oneSwitch->setState(static_cast<ISState>(j.value().toInt()));
0248                     }
0249                     devices.data()->getActiveCamera()->sendNewProperty(sp);
0250                 }
0251                 break;
0252                 case INDI_TEXT:
0253                 {
0254                     auto tp = customProp.getText();
0255                     while (j.hasNext())
0256                     {
0257                         j.next();
0258                         auto oneText = tp->findWidgetByName(j.key().toLatin1().data());
0259                         if (oneText)
0260                             oneText->setText(j.value().toString().toLatin1().constData());
0261                     }
0262                     devices.data()->getActiveCamera()->sendNewProperty(tp);
0263                 }
0264                 break;
0265                 case INDI_NUMBER:
0266                 {
0267                     auto np = customProp.getNumber();
0268                     while (j.hasNext())
0269                     {
0270                         j.next();
0271                         auto oneNumber = np->findWidgetByName(j.key().toLatin1().data());
0272                         if (oneNumber)
0273                             oneNumber->setValue(j.value().toDouble());
0274                     }
0275                     devices.data()->getActiveCamera()->sendNewProperty(np);
0276                 }
0277                 break;
0278                 default:
0279                     continue;
0280             }
0281         }
0282     }
0283 
0284     const auto remoteFormatDirectory = getCoreProperty(SJ_RemoteFormatDirectory).toString();
0285     const auto remoteFormatFilename = getCoreProperty(SJ_RemoteFormatFilename).toString();
0286     if (devices.data()->getActiveChip()->isBatchMode() &&
0287             remoteFormatDirectory.isEmpty() == false &&
0288             remoteFormatFilename.isEmpty() == false)
0289     {
0290         devices.data()->getActiveCamera()->updateUploadSettings(remoteFormatDirectory, remoteFormatFilename);
0291         if (getUploadMode() != ISD::Camera::UPLOAD_CLIENT)
0292             logentry.append(QString(", remote dir = %1, remote format = %2").arg(remoteFormatDirectory).arg(remoteFormatFilename));
0293     }
0294 
0295     const int ISOIndex = getCoreProperty(SJ_ISOIndex).toInt();
0296     if (ISOIndex != -1)
0297     {
0298         logentry.append(QString(", ISO index = %1").arg(ISOIndex));
0299         if (ISOIndex != devices.data()->getActiveChip()->getISOIndex())
0300             devices.data()->getActiveChip()->setISOIndex(ISOIndex);
0301     }
0302 
0303     const auto gain = getCoreProperty(SJ_Gain).toDouble();
0304     if (gain >= 0)
0305     {
0306         logentry.append(QString(", gain = %1").arg(gain));
0307         devices.data()->getActiveCamera()->setGain(gain);
0308     }
0309 
0310     const auto offset = getCoreProperty(SJ_Offset).toDouble();
0311     if (offset >= 0)
0312     {
0313         logentry.append(QString(", offset = %1").arg(offset));
0314         devices.data()->getActiveCamera()->setOffset(offset);
0315     }
0316 
0317     devices.data()->getActiveCamera()->setCaptureFormat(getCoreProperty(SJ_Format).toString());
0318     devices.data()->getActiveCamera()->setEncodingFormat(getCoreProperty(SJ_Encoding).toString());
0319     devices.data()->getActiveChip()->setFrameType(getFrameType());
0320     logentry.append(QString(", format = %1, encoding = %2").arg(getCoreProperty(SJ_Format).toString()).arg(getCoreProperty(
0321                         SJ_Encoding).toString()));
0322 
0323     // Only attempt to set ROI and Binning if CCD transfer format is FITS or XISF
0324     int currentBinX = 1, currentBinY = 1;
0325     devices.data()->getActiveChip()->getBinning(&currentBinX, &currentBinY);
0326 
0327     const auto binning = getCoreProperty(SJ_Binning).toPoint();
0328     // N.B. Always set binning _before_ setting frame because if the subframed image
0329     // is problematic in 1x1 but works fine for 2x2, then it would fail it was set first
0330     // So setting binning first always ensures this will work.
0331     if (devices.data()->getActiveChip()->canBin())
0332     {
0333         if (devices.data()->getActiveChip()->setBinning(binning.x(), binning.y()) == false)
0334         {
0335             qCWarning(KSTARS_EKOS_CAPTURE()) << "Cannot set binning to " << "x =" << binning.x() << ", y =" << binning.y();
0336             setStatus(JOB_ERROR);
0337             emit captureStarted(CaptureModuleState::CAPTURE_BIN_ERROR);
0338         }
0339         else
0340             logentry.append(QString(", binning = %1x%2").arg(binning.x()).arg(binning.y()));
0341     }
0342     else
0343         logentry.append(QString(", Cannot bin"));
0344 
0345 
0346     const auto roi = getCoreProperty(SJ_ROI).toRect();
0347 
0348     if (devices.data()->getActiveChip()->canSubframe())
0349     {
0350         if ((roi.width() > 0 && roi.height() > 0) && devices.data()->getActiveChip()->setFrame(roi.x(),
0351                 roi.y(),
0352                 roi.width(),
0353                 roi.height(),
0354                 currentBinX != binning.x()) == false)
0355         {
0356             qCWarning(KSTARS_EKOS_CAPTURE()) << "Cannot set ROI to " << "x =" << roi.x() << ", y =" << roi.y() << ", widht =" <<
0357                                              roi.width() << "height =" << roi.height();
0358             setStatus(JOB_ERROR);
0359             emit captureStarted(CaptureModuleState::CAPTURE_FRAME_ERROR);
0360         }
0361         else
0362             logentry.append(QString(", ROI = (%1+%2, %3+%4)").arg(roi.x()).arg(roi.width()).arg(roi.y()).arg(roi.width()));
0363     }
0364     else
0365         logentry.append(", Cannot subframe");
0366 
0367     // In case FITS Viewer is not enabled. Then for flat frames, we still need to keep the data
0368     // otherwise INDI CCD would simply discard loading the data in batch mode as the data are already
0369     // saved to disk and since no extra processing is required, FITSData is not loaded up with the data.
0370     // But in case of automatically calculated flat frames, we need FITSData.
0371     // Therefore, we need to explicitly set mode to FITS_CALIBRATE so that FITSData is generated.
0372     devices.data()->getActiveChip()->setCaptureMode(mode);
0373     devices.data()->getActiveChip()->setCaptureFilter(FITS_NONE);
0374 
0375     setStatus(getStatus());
0376 
0377     const auto exposure = getCoreProperty(SJ_Exposure).toDouble();
0378     m_ExposeLeft = exposure;
0379     devices.data()->getActiveChip()->capture(exposure);
0380     // create log entry with settings
0381     qCInfo(KSTARS_EKOS_CAPTURE()) << logentry;
0382 
0383     emit captureStarted(CaptureModuleState::CAPTURE_OK);
0384 }
0385 
0386 void SequenceJob::setTargetFilter(int pos, const QString &name)
0387 {
0388     state->targetFilterID = pos;
0389     setCoreProperty(SJ_Filter, name);
0390 }
0391 
0392 double SequenceJob::getExposeLeft() const
0393 {
0394     return m_ExposeLeft;
0395 }
0396 
0397 void SequenceJob::setExposeLeft(double value)
0398 {
0399     m_ExposeLeft = value;
0400 }
0401 
0402 
0403 int SequenceJob::getCaptureRetires() const
0404 {
0405     return m_CaptureRetires;
0406 }
0407 
0408 void SequenceJob::setCaptureRetires(int value)
0409 {
0410     m_CaptureRetires = value;
0411 }
0412 
0413 int SequenceJob::getCurrentFilter() const
0414 {
0415     return state->m_CaptureModuleState->currentFilterID;
0416 }
0417 
0418 ISD::Mount::PierSide SequenceJob::getPierSide() const
0419 {
0420     return state->m_CaptureModuleState->getPierSide();
0421 }
0422 
0423 // Setter: Set upload mode
0424 void SequenceJob::setUploadMode(ISD::Camera::UploadMode value)
0425 {
0426     m_UploadMode = value;
0427 }
0428 // Getter: get upload mode
0429 ISD::Camera::UploadMode SequenceJob::getUploadMode() const
0430 {
0431     return m_UploadMode;
0432 }
0433 
0434 // Setter: Set flat field source
0435 void SequenceJob::setCalibrationPreAction(uint32_t value)
0436 {
0437     state->m_CalibrationPreAction = value;
0438 }
0439 // Getter: Get calibration pre action
0440 uint32_t SequenceJob::getCalibrationPreAction() const
0441 {
0442     return state->m_CalibrationPreAction;
0443 }
0444 
0445 void SequenceJob::setWallCoord(const SkyPoint &value)
0446 {
0447     state->wallCoord = value;
0448 }
0449 
0450 const SkyPoint &SequenceJob::getWallCoord() const
0451 {
0452     return state->wallCoord;
0453 }
0454 
0455 // Setter: Set flat field duration
0456 void SequenceJob::setFlatFieldDuration(FlatFieldDuration value)
0457 {
0458     m_FlatFieldDuration = value;
0459 }
0460 
0461 // Getter: Get flat field duration
0462 FlatFieldDuration SequenceJob::getFlatFieldDuration() const
0463 {
0464     return m_FlatFieldDuration;
0465 }
0466 
0467 void SequenceJob::setJobProgressIgnored(bool value)
0468 {
0469     m_JobProgressIgnored = value;
0470 }
0471 
0472 bool SequenceJob::getJobProgressIgnored() const
0473 {
0474     return m_JobProgressIgnored;
0475 }
0476 
0477 void SequenceJob::updateDeviceStates()
0478 {
0479     setLightBox(devices->lightBox());
0480     addMount(devices->mount());
0481     setDome(devices->dome());
0482     setDustCap(devices->dustCap());
0483 }
0484 
0485 void SequenceJob::setLightBox(ISD::LightBox * lightBox)
0486 {
0487     state->m_CaptureModuleState->hasLightBox = (lightBox != nullptr);
0488 }
0489 
0490 void SequenceJob::setDustCap(ISD::DustCap * dustCap)
0491 {
0492     state->m_CaptureModuleState->hasDustCap = (dustCap != nullptr);
0493 }
0494 
0495 void SequenceJob::addMount(ISD::Mount * scope)
0496 {
0497     state->m_CaptureModuleState->hasTelescope = (scope != nullptr);
0498 }
0499 
0500 void SequenceJob::setDome(ISD::Dome * dome)
0501 {
0502     state->m_CaptureModuleState->hasDome = (dome != nullptr);
0503 }
0504 
0505 double SequenceJob::currentTemperature() const
0506 {
0507     return devices->cameraTemperature();
0508 }
0509 
0510 double SequenceJob::currentGain() const
0511 {
0512     return devices->cameraGain();
0513 }
0514 
0515 double SequenceJob::currentOffset() const
0516 {
0517     return devices->cameraOffset();
0518 }
0519 
0520 void SequenceJob::prepareCapture()
0521 {
0522     // simply forward it to the state machine
0523     switch (getFrameType())
0524     {
0525         case FRAME_LIGHT:
0526             state->prepareLightFrameCapture(getCoreProperty(SJ_EnforceTemperature).toBool(),
0527                                             jobType() == SequenceJob::JOBTYPE_PREVIEW);
0528             break;
0529         case FRAME_FLAT:
0530             state->prepareFlatFrameCapture(getCoreProperty(SJ_EnforceTemperature).toBool(),
0531                                            jobType() == SequenceJob::JOBTYPE_PREVIEW);
0532             break;
0533         case FRAME_DARK:
0534             state->prepareDarkFrameCapture(getCoreProperty(SJ_EnforceTemperature).toBool(),
0535                                            jobType() == SequenceJob::JOBTYPE_PREVIEW);
0536             break;
0537         case FRAME_BIAS:
0538             state->prepareBiasFrameCapture(getCoreProperty(SJ_EnforceTemperature).toBool(),
0539                                            jobType() == SequenceJob::JOBTYPE_PREVIEW);
0540             break;
0541         default:
0542             // not refactored yet, immediately completed
0543             processPrepareComplete();
0544             break;
0545     }
0546 }
0547 
0548 void SequenceJob::processPrepareComplete(bool success)
0549 {
0550     emit prepareComplete(success);
0551 }
0552 
0553 void SequenceJob::processAbortCapture()
0554 {
0555     disconnectDeviceAdaptor();
0556     emit abortCapture();
0557 }
0558 
0559 IPState SequenceJob::checkFlatFramePendingTasksCompleted()
0560 {
0561     // no further checks necessary
0562     return IPS_OK;
0563 }
0564 
0565 void SequenceJob::setCoreProperty(PropertyID id, const QVariant &value)
0566 {
0567     // Handle special cases
0568     switch (id)
0569     {
0570         case SJ_RemoteDirectory:
0571         {
0572             auto remoteDir = value.toString();
0573             if (remoteDir.endsWith('/'))
0574             {
0575                 remoteDir.chop(1);
0576                 m_CoreProperties[id] = remoteDir;
0577             }
0578         }
0579         break;
0580 
0581         default:
0582             break;
0583     }
0584     // store value
0585     m_CoreProperties[id] = value;
0586 }
0587 
0588 QVariant SequenceJob::getCoreProperty(PropertyID id) const
0589 {
0590     return m_CoreProperties[id];
0591 }
0592 
0593 void SequenceJob::loadFrom(XMLEle *root, const QString &targetName, SequenceJobType jobType,
0594                            QSharedPointer<CaptureModuleState> sharedState)
0595 {
0596     setJobType(jobType);
0597 
0598     // Set default property values
0599     m_CoreProperties[SJ_Exposure] = -1;
0600     m_CoreProperties[SJ_Gain] = -1;
0601     m_CoreProperties[SJ_Offset] = -1;
0602     m_CoreProperties[SJ_ISOIndex] = -1;
0603     m_CoreProperties[SJ_Count] = -1;
0604     m_CoreProperties[SJ_Delay] = -1;
0605     m_CoreProperties[SJ_Binning] = QPoint(1, 1);
0606     m_CoreProperties[SJ_ROI] = QRect(0, 0, 0, 0);
0607     m_CoreProperties[SJ_EnforceTemperature] = false;
0608     m_CoreProperties[SJ_GuiderActive] = false;
0609     m_CoreProperties[SJ_DitherPerJobFrequency] = 0;
0610     m_CoreProperties[SJ_Encoding] = "FITS";
0611 
0612     // targetName overrides values from the XML document
0613     if (targetName != "")
0614         setCoreProperty(SequenceJob::SJ_TargetName, targetName);
0615 
0616     if (root == nullptr)
0617         return;
0618 
0619     bool isDarkFlat = false;
0620 
0621     QLocale cLocale = QLocale::c();
0622     XMLEle * ep;
0623     XMLEle * subEP;
0624     for (ep = nextXMLEle(root, 1); ep != nullptr; ep = nextXMLEle(root, 0))
0625     {
0626         if (!strcmp(tagXMLEle(ep), "Exposure"))
0627             setCoreProperty(SequenceJob::SJ_Exposure, cLocale.toDouble(pcdataXMLEle(ep)));
0628         else if (!strcmp(tagXMLEle(ep), "Format"))
0629             setCoreProperty(SequenceJob::SJ_Format, pcdataXMLEle(ep));
0630         else if (!strcmp(tagXMLEle(ep), "Encoding"))
0631         {
0632             setCoreProperty(SequenceJob::SJ_Encoding, pcdataXMLEle(ep));
0633         }
0634         else if (!strcmp(tagXMLEle(ep), "Binning"))
0635         {
0636             QPoint binning(1, 1);
0637             subEP = findXMLEle(ep, "X");
0638             if (subEP)
0639                 binning.setX(cLocale.toInt(pcdataXMLEle(subEP)));
0640             subEP = findXMLEle(ep, "Y");
0641             if (subEP)
0642                 binning.setY(cLocale.toInt(pcdataXMLEle(subEP)));
0643 
0644             setCoreProperty(SequenceJob::SJ_Binning, binning);
0645         }
0646         else if (!strcmp(tagXMLEle(ep), "Frame"))
0647         {
0648             QRect roi(0, 0, 0, 0);
0649             subEP = findXMLEle(ep, "X");
0650             if (subEP)
0651                 roi.setX(cLocale.toInt(pcdataXMLEle(subEP)));
0652             subEP = findXMLEle(ep, "Y");
0653             if (subEP)
0654                 roi.setY(cLocale.toInt(pcdataXMLEle(subEP)));
0655             subEP = findXMLEle(ep, "W");
0656             if (subEP)
0657                 roi.setWidth(cLocale.toInt(pcdataXMLEle(subEP)));
0658             subEP = findXMLEle(ep, "H");
0659             if (subEP)
0660                 roi.setHeight(cLocale.toInt(pcdataXMLEle(subEP)));
0661 
0662             setCoreProperty(SequenceJob::SJ_ROI, roi);
0663         }
0664         else if (!strcmp(tagXMLEle(ep), "Temperature"))
0665         {
0666             setTargetTemperature(cLocale.toDouble(pcdataXMLEle(ep)));
0667 
0668             // If force attribute exist, we change cameraTemperatureS, otherwise do nothing.
0669             if (!strcmp(findXMLAttValu(ep, "force"), "true"))
0670                 setCoreProperty(SequenceJob::SJ_EnforceTemperature, true);
0671             else if (!strcmp(findXMLAttValu(ep, "force"), "false"))
0672                 setCoreProperty(SequenceJob::SJ_EnforceTemperature, false);
0673         }
0674         else if (!strcmp(tagXMLEle(ep), "Filter"))
0675         {
0676             const auto name = pcdataXMLEle(ep);
0677             const auto index = std::max(1, filterLabels().indexOf(name) + 1);
0678             setTargetFilter(index, name);
0679         }
0680         else if (!strcmp(tagXMLEle(ep), "Type"))
0681         {
0682             int index = frameTypes().indexOf(pcdataXMLEle(ep));
0683             setFrameType(static_cast<CCDFrameType>(qMax(0, index)));
0684         }
0685         else if (!strcmp(tagXMLEle(ep), "TargetName"))
0686         {
0687             auto jobTarget = pcdataXMLEle(ep);
0688 
0689             if (targetName == "")
0690                 // use the target from the XML document
0691                 setCoreProperty(SequenceJob::SJ_TargetName, jobTarget);
0692             else if (strcmp(jobTarget, "") != 0)
0693                 // issue a warning that target from the XML document is ignored
0694                 qWarning(KSTARS_EKOS_CAPTURE) << QString("Sequence job target name %1 ignored.").arg(jobTarget);
0695         }
0696         else if (!strcmp(tagXMLEle(ep), "Prefix"))
0697         {
0698             // RawPrefix is outdated and will be ignored
0699             subEP = findXMLEle(ep, "RawPrefix");
0700             if (subEP)
0701             {
0702                 auto jobTarget = pcdataXMLEle(subEP);
0703 
0704                 if (targetName == "")
0705                     // use the target from the XML document
0706                     setCoreProperty(SequenceJob::SJ_TargetName, jobTarget);
0707                 else if (strcmp(jobTarget, "") != 0)
0708                     // issue a warning that target from the XML document is ignored
0709                     qWarning(KSTARS_EKOS_CAPTURE) << QString("Sequence job raw prefix %1 ignored.").arg(jobTarget);
0710             }
0711             bool filterEnabled = false, expEnabled = false, tsEnabled = false;
0712             subEP = findXMLEle(ep, "FilterEnabled");
0713             if (subEP)
0714                 filterEnabled = !strcmp("1", pcdataXMLEle(subEP));
0715             subEP = findXMLEle(ep, "ExpEnabled");
0716             if (subEP)
0717                 expEnabled = !strcmp("1", pcdataXMLEle(subEP));
0718             subEP = findXMLEle(ep, "TimeStampEnabled");
0719             if (subEP)
0720                 tsEnabled = !strcmp("1", pcdataXMLEle(subEP));
0721             // build default format
0722             setCoreProperty(SequenceJob::SJ_PlaceholderFormat,
0723                             PlaceholderPath::defaultFormat(filterEnabled, expEnabled, tsEnabled));
0724         }
0725         else if (!strcmp(tagXMLEle(ep), "Count"))
0726         {
0727             setCoreProperty(SequenceJob::SJ_Count, cLocale.toInt(pcdataXMLEle(ep)));
0728         }
0729         else if (!strcmp(tagXMLEle(ep), "Delay"))
0730         {
0731             setCoreProperty(SequenceJob::SJ_Delay, cLocale.toInt(pcdataXMLEle(ep)) * 1000);
0732         }
0733         else if (!strcmp(tagXMLEle(ep), "PostCaptureScript"))
0734         {
0735             m_Scripts[SCRIPT_POST_CAPTURE] = pcdataXMLEle(ep);
0736         }
0737         else if (!strcmp(tagXMLEle(ep), "PreCaptureScript"))
0738         {
0739             m_Scripts[SCRIPT_PRE_CAPTURE] = pcdataXMLEle(ep);
0740         }
0741         else if (!strcmp(tagXMLEle(ep), "PostJobScript"))
0742         {
0743             m_Scripts[SCRIPT_POST_JOB] = pcdataXMLEle(ep);
0744         }
0745         else if (!strcmp(tagXMLEle(ep), "PreJobScript"))
0746         {
0747             m_Scripts[SCRIPT_PRE_JOB] = pcdataXMLEle(ep);
0748         }
0749         else if (!strcmp(tagXMLEle(ep), "GuideDitherPerJob"))
0750         {
0751             setCoreProperty(SequenceJob::SJ_DitherPerJobFrequency, cLocale.toInt(pcdataXMLEle(ep)));
0752         }
0753         else if (!strcmp(tagXMLEle(ep), "FITSDirectory"))
0754         {
0755             setCoreProperty(SequenceJob::SJ_LocalDirectory, pcdataXMLEle(ep));
0756         }
0757         else if (!strcmp(tagXMLEle(ep), "PlaceholderFormat"))
0758         {
0759             setCoreProperty(SequenceJob::SJ_PlaceholderFormat, pcdataXMLEle(ep));
0760         }
0761         else if (!strcmp(tagXMLEle(ep), "PlaceholderSuffix"))
0762         {
0763             setCoreProperty(SequenceJob::SJ_PlaceholderSuffix, cLocale.toUInt(pcdataXMLEle(ep)));
0764         }
0765         else if (!strcmp(tagXMLEle(ep), "RemoteDirectory"))
0766         {
0767             setCoreProperty(SequenceJob::SJ_RemoteDirectory, pcdataXMLEle(ep));
0768         }
0769         else if (!strcmp(tagXMLEle(ep), "UploadMode"))
0770         {
0771             setUploadMode(static_cast<ISD::Camera::UploadMode>(cLocale.toInt(pcdataXMLEle(ep))));
0772         }
0773         else if (!strcmp(tagXMLEle(ep), "ISOIndex"))
0774         {
0775             setISO(cLocale.toInt(pcdataXMLEle(ep)));
0776         }
0777         else if (!strcmp(tagXMLEle(ep), "Rotation"))
0778         {
0779             setTargetRotation(cLocale.toDouble(pcdataXMLEle(ep)));
0780         }
0781         else if (!strcmp(tagXMLEle(ep), "Properties"))
0782         {
0783             QMap<QString, QMap<QString, QVariant>> propertyMap;
0784 
0785             for (subEP = nextXMLEle(ep, 1); subEP != nullptr; subEP = nextXMLEle(ep, 0))
0786             {
0787                 QMap<QString, QVariant> elements;
0788                 XMLEle * oneElement = nullptr;
0789                 for (oneElement = nextXMLEle(subEP, 1); oneElement != nullptr; oneElement = nextXMLEle(subEP, 0))
0790                 {
0791                     const char * name = findXMLAttValu(oneElement, "name");
0792                     bool ok = false;
0793                     // String
0794                     auto xmlValue = pcdataXMLEle(oneElement);
0795                     // Try to load it as double
0796                     auto value = cLocale.toDouble(xmlValue, &ok);
0797                     if (ok)
0798                         elements[name] = value;
0799                     else
0800                         elements[name] = xmlValue;
0801                 }
0802 
0803                 const char * name = findXMLAttValu(subEP, "name");
0804                 propertyMap[name] = elements;
0805             }
0806 
0807             setCustomProperties(propertyMap);
0808             // read the gain and offset values from the custom properties
0809             setCoreProperty(SequenceJob::SJ_Gain, devices->cameraGain(propertyMap));
0810             setCoreProperty(SequenceJob::SJ_Offset, devices->cameraOffset(propertyMap));
0811         }
0812         else if (!strcmp(tagXMLEle(ep), "Calibration"))
0813         {
0814             // SQ_FORMAT_VERSION >= 2.7
0815             subEP = findXMLEle(ep, "PreAction");
0816             if (subEP)
0817             {
0818                 XMLEle * typeEP = findXMLEle(subEP, "Type");
0819                 if (typeEP)
0820                 {
0821                     setCalibrationPreAction(cLocale.toUInt(pcdataXMLEle(typeEP)));
0822                     if (getCalibrationPreAction() & ACTION_WALL)
0823                     {
0824                         XMLEle * azEP  = findXMLEle(subEP, "Az");
0825                         XMLEle * altEP = findXMLEle(subEP, "Alt");
0826 
0827                         if (azEP && altEP)
0828                         {
0829                             setCalibrationPreAction((getCalibrationPreAction() & ~ACTION_PARK_MOUNT) | ACTION_WALL);
0830                             SkyPoint wallCoord;
0831                             wallCoord.setAz(cLocale.toDouble(pcdataXMLEle(azEP)));
0832                             wallCoord.setAlt(cLocale.toDouble(pcdataXMLEle(altEP)));
0833                             setWallCoord(wallCoord);
0834                         }
0835                         else
0836                         {
0837                             qCWarning(KSTARS_EKOS_CAPTURE) << "Wall position coordinates missing, disabling slew to wall position action.";
0838                             setCalibrationPreAction((getCalibrationPreAction() & ~ACTION_WALL) | ACTION_NONE);
0839                         }
0840                     }
0841                 }
0842             }
0843 
0844             // SQ_FORMAT_VERSION < 2.7
0845             subEP = findXMLEle(ep, "FlatSource");
0846             if (subEP)
0847             {
0848                 XMLEle * typeEP = findXMLEle(subEP, "Type");
0849                 if (typeEP)
0850                 {
0851                     // default
0852                     setCalibrationPreAction(ACTION_NONE);
0853                     if (!strcmp(pcdataXMLEle(typeEP), "Wall"))
0854                     {
0855                         XMLEle * azEP  = findXMLEle(subEP, "Az");
0856                         XMLEle * altEP = findXMLEle(subEP, "Alt");
0857 
0858                         if (azEP && altEP)
0859                         {
0860                             setCalibrationPreAction((getCalibrationPreAction() & ~ACTION_PARK_MOUNT) | ACTION_WALL);
0861                             SkyPoint wallCoord;
0862                             wallCoord.setAz(cLocale.toDouble(pcdataXMLEle(azEP)));
0863                             wallCoord.setAlt(cLocale.toDouble(pcdataXMLEle(altEP)));
0864                             setWallCoord(wallCoord);
0865                         }
0866                     }
0867                 }
0868             }
0869 
0870             // SQ_FORMAT_VERSION < 2.7
0871             subEP = findXMLEle(ep, "PreMountPark");
0872             if (subEP && !strcmp(pcdataXMLEle(subEP), "True"))
0873                 setCalibrationPreAction(getCalibrationPreAction() | ACTION_PARK_MOUNT);
0874 
0875             // SQ_FORMAT_VERSION < 2.7
0876             subEP = findXMLEle(ep, "PreDomePark");
0877             if (subEP && !strcmp(pcdataXMLEle(subEP), "True"))
0878                 setCalibrationPreAction(getCalibrationPreAction() | ACTION_PARK_DOME);
0879 
0880             subEP = findXMLEle(ep, "FlatDuration");
0881             if (subEP)
0882             {
0883                 const char * dark = findXMLAttValu(subEP, "dark");
0884                 isDarkFlat = !strcmp(dark, "true");
0885 
0886                 XMLEle * typeEP = findXMLEle(subEP, "Type");
0887                 if (typeEP)
0888                 {
0889                     if (!strcmp(pcdataXMLEle(typeEP), "Manual"))
0890                         setFlatFieldDuration(DURATION_MANUAL);
0891                 }
0892 
0893                 XMLEle * aduEP = findXMLEle(subEP, "Value");
0894                 if (aduEP)
0895                 {
0896                     setFlatFieldDuration(DURATION_ADU);
0897                     setCoreProperty(SequenceJob::SJ_TargetADU, QVariant(cLocale.toDouble(pcdataXMLEle(aduEP))));
0898                 }
0899 
0900                 aduEP = findXMLEle(subEP, "Tolerance");
0901                 if (aduEP)
0902                 {
0903                     setCoreProperty(SequenceJob::SJ_TargetADUTolerance, QVariant(cLocale.toDouble(pcdataXMLEle(aduEP))));
0904                 }
0905             }
0906         }
0907     }
0908     if(isDarkFlat)
0909         setJobType(SequenceJob::JOBTYPE_DARKFLAT);
0910 }
0911 
0912 void SequenceJob::saveTo(QTextStream &outstream, const QLocale &cLocale) const
0913 {
0914     auto roi = getCoreProperty(SequenceJob::SJ_ROI).toRect();
0915 
0916     outstream << "<Job>" << Qt::endl;
0917 
0918     outstream << "<Exposure>" << cLocale.toString(getCoreProperty(SequenceJob::SJ_Exposure).toDouble()) << "</Exposure>" <<
0919               Qt::endl;
0920     outstream << "<Format>" << getCoreProperty(SequenceJob::SJ_Format).toString() << "</Format>" << Qt::endl;
0921     outstream << "<Encoding>" << getCoreProperty(SequenceJob::SJ_Encoding).toString() << "</Encoding>" << Qt::endl;
0922     outstream << "<Binning>" << Qt::endl;
0923     outstream << "<X>" << cLocale.toString(getCoreProperty(SequenceJob::SJ_Binning).toPoint().x()) << "</X>" << Qt::endl;
0924     outstream << "<Y>" << cLocale.toString(getCoreProperty(SequenceJob::SJ_Binning).toPoint().y()) << "</Y>" << Qt::endl;
0925     outstream << "</Binning>" << Qt::endl;
0926     outstream << "<Frame>" << Qt::endl;
0927     outstream << "<X>" << cLocale.toString(roi.x()) << "</X>" << Qt::endl;
0928     outstream << "<Y>" << cLocale.toString(roi.y()) << "</Y>" << Qt::endl;
0929     outstream << "<W>" << cLocale.toString(roi.width()) << "</W>" << Qt::endl;
0930     outstream << "<H>" << cLocale.toString(roi.height()) << "</H>" << Qt::endl;
0931     outstream << "</Frame>" << Qt::endl;
0932     if (getTargetTemperature() != Ekos::INVALID_VALUE)
0933         outstream << "<Temperature force='" << (getCoreProperty(SequenceJob::SJ_EnforceTemperature).toBool() ? "true" :
0934                                                 "false") << "'>"
0935                   << cLocale.toString(getTargetTemperature()) << "</Temperature>" << Qt::endl;
0936     if (getTargetFilter() >= 0)
0937         outstream << "<Filter>" << getCoreProperty(SequenceJob::SJ_Filter).toString() << "</Filter>" << Qt::endl;
0938     outstream << "<Type>" << frameTypes()[getFrameType()] << "</Type>" << Qt::endl;
0939     outstream << "<Count>" << cLocale.toString(getCoreProperty(SequenceJob::SJ_Count).toInt()) << "</Count>" << Qt::endl;
0940     // ms to seconds
0941     outstream << "<Delay>" << cLocale.toString(getCoreProperty(SequenceJob::SJ_Delay).toInt() / 1000.0) << "</Delay>" <<
0942               Qt::endl;
0943     if (getCoreProperty(SequenceJob::SJ_TargetName) != "")
0944         outstream << "<TargetName>" << getCoreProperty(SequenceJob::SJ_TargetName).toString() << "</TargetName>" << Qt::endl;
0945     if (getScript(SCRIPT_PRE_CAPTURE).isEmpty() == false)
0946         outstream << "<PreCaptureScript>" << getScript(SCRIPT_PRE_CAPTURE) << "</PreCaptureScript>" << Qt::endl;
0947     if (getScript(SCRIPT_POST_CAPTURE).isEmpty() == false)
0948         outstream << "<PostCaptureScript>" << getScript(SCRIPT_POST_CAPTURE) << "</PostCaptureScript>" << Qt::endl;
0949     if (getScript(SCRIPT_PRE_JOB).isEmpty() == false)
0950         outstream << "<PreJobScript>" << getScript(SCRIPT_PRE_JOB) << "</PreJobScript>" << Qt::endl;
0951     if (getScript(SCRIPT_POST_JOB).isEmpty() == false)
0952         outstream << "<PostJobScript>" << getScript(SCRIPT_POST_JOB) << "</PostJobScript>" << Qt::endl;
0953     outstream << "<GuideDitherPerJob>"
0954               << cLocale.toString(getCoreProperty(SequenceJob::SJ_DitherPerJobFrequency).toInt()) << "</GuideDitherPerJob>" <<
0955               Qt::endl;
0956     outstream << "<FITSDirectory>" << getCoreProperty(SequenceJob::SJ_LocalDirectory).toString() << "</FITSDirectory>" <<
0957               Qt::endl;
0958     outstream << "<PlaceholderFormat>" << getCoreProperty(SequenceJob::SJ_PlaceholderFormat).toString() <<
0959               "</PlaceholderFormat>" <<
0960               Qt::endl;
0961     outstream << "<PlaceholderSuffix>" << getCoreProperty(SequenceJob::SJ_PlaceholderSuffix).toUInt() <<
0962               "</PlaceholderSuffix>" <<
0963               Qt::endl;
0964     outstream << "<UploadMode>" << getUploadMode() << "</UploadMode>" << Qt::endl;
0965     if (getCoreProperty(SequenceJob::SJ_RemoteDirectory).toString().isEmpty() == false)
0966         outstream << "<RemoteDirectory>" << getCoreProperty(SequenceJob::SJ_RemoteDirectory).toString() << "</RemoteDirectory>"
0967                   << Qt::endl;
0968     if (getCoreProperty(SequenceJob::SJ_ISOIndex).toInt() != -1)
0969         outstream << "<ISOIndex>" << (getCoreProperty(SequenceJob::SJ_ISOIndex).toInt()) << "</ISOIndex>" << Qt::endl;
0970     if (getTargetRotation() != Ekos::INVALID_VALUE)
0971         outstream << "<Rotation>" << (getTargetRotation()) << "</Rotation>" << Qt::endl;
0972     QMapIterator<QString, QMap<QString, QVariant>> customIter(getCustomProperties());
0973     outstream << "<Properties>" << Qt::endl;
0974     while (customIter.hasNext())
0975     {
0976         customIter.next();
0977         outstream << "<PropertyVector name='" << customIter.key() << "'>" << Qt::endl;
0978         QMap<QString, QVariant> elements = customIter.value();
0979         QMapIterator<QString, QVariant> iter(elements);
0980         while (iter.hasNext())
0981         {
0982             iter.next();
0983             if (iter.value().type() == QVariant::String)
0984             {
0985                 outstream << "<OneElement name='" << iter.key()
0986                           << "'>" << iter.value().toString() << "</OneElement>" << Qt::endl;
0987             }
0988             else
0989             {
0990                 outstream << "<OneElement name='" << iter.key()
0991                           << "'>" << iter.value().toDouble() << "</OneElement>" << Qt::endl;
0992             }
0993         }
0994         outstream << "</PropertyVector>" << Qt::endl;
0995     }
0996     outstream << "</Properties>" << Qt::endl;
0997 
0998     outstream << "<Calibration>" << Qt::endl;
0999     outstream << "<PreAction>" << Qt::endl;
1000     outstream << QString("<Type>%1</Type>").arg(getCalibrationPreAction()) << Qt::endl;
1001     if (getCalibrationPreAction() & ACTION_WALL)
1002     {
1003         outstream << "<Az>" << cLocale.toString(getWallCoord().az().Degrees()) << "</Az>" << Qt::endl;
1004         outstream << "<Alt>" << cLocale.toString(getWallCoord().alt().Degrees()) << "</Alt>" << Qt::endl;
1005     }
1006     outstream << "</PreAction>" << Qt::endl;
1007 
1008     outstream << "<FlatDuration dark='" << (jobType() == SequenceJob::JOBTYPE_DARKFLAT ? "true" : "false")
1009               << "'>" << Qt::endl;
1010     if (getFlatFieldDuration() == DURATION_MANUAL)
1011         outstream << "<Type>Manual</Type>" << Qt::endl;
1012     else
1013     {
1014         outstream << "<Type>ADU</Type>" << Qt::endl;
1015         outstream << "<Value>" << cLocale.toString(getCoreProperty(SequenceJob::SJ_TargetADU).toDouble()) << "</Value>" <<
1016                   Qt::endl;
1017         outstream << "<Tolerance>" << cLocale.toString(getCoreProperty(SequenceJob::SJ_TargetADUTolerance).toDouble()) <<
1018                   "</Tolerance>" << Qt::endl;
1019     }
1020     outstream << "</FlatDuration>" << Qt::endl;
1021     outstream << "</Calibration>" << Qt::endl;
1022     outstream << "</Job>" << Qt::endl;
1023 }
1024 }
1025