File indexing completed on 2024-04-14 14:10:51

0001 /*
0002     SPDX-FileCopyrightText: 2012 Jasem Mutlaq <mutlaqja@ikarustech.com>
0003 
0004     SPDX-License-Identifier: GPL-2.0-or-later
0005 */
0006 
0007 #include "indicamera.h"
0008 #include "indicamerachip.h"
0009 
0010 #include "config-kstars.h"
0011 
0012 #include "indi_debug.h"
0013 
0014 #include "clientmanager.h"
0015 #include "kstars.h"
0016 #include "Options.h"
0017 #include "streamwg.h"
0018 //#include "ekos/manager.h"
0019 #ifdef HAVE_CFITSIO
0020 #include "fitsviewer/fitsdata.h"
0021 #include "fitsviewer/fitstab.h"
0022 #endif
0023 
0024 #include <KNotifications/KNotification>
0025 #include "auxiliary/ksmessagebox.h"
0026 #include "ksnotification.h"
0027 #include <QImageReader>
0028 #include <QFileInfo>
0029 #include <QStatusBar>
0030 #include <QtConcurrent>
0031 
0032 #include <basedevice.h>
0033 
0034 const QStringList RAWFormats = { "cr2", "cr3", "crw", "nef", "raf", "dng", "arw", "orf" };
0035 
0036 const QString &getFITSModeStringString(FITSMode mode)
0037 {
0038     return FITSModes[mode];
0039 }
0040 
0041 namespace ISD
0042 {
0043 
0044 Camera::Camera(GenericDevice *parent) : ConcreteDevice(parent)
0045 {
0046     primaryChip.reset(new CameraChip(this, CameraChip::PRIMARY_CCD));
0047 
0048     m_Media.reset(new WSMedia(this));
0049     connect(m_Media.get(), &WSMedia::newFile, this, &Camera::setWSBLOB);
0050 
0051     connect(m_Parent->getClientManager(), &ClientManager::newBLOBManager, this, &Camera::setBLOBManager, Qt::UniqueConnection);
0052     m_LastNotificationTS = QDateTime::currentDateTime();
0053 }
0054 
0055 Camera::~Camera()
0056 {
0057     if (m_ImageViewerWindow)
0058         m_ImageViewerWindow->close();
0059     if (fileWriteThread.isRunning())
0060         fileWriteThread.waitForFinished();
0061     if (fileWriteBuffer != nullptr)
0062         delete [] fileWriteBuffer;
0063 }
0064 
0065 void Camera::setBLOBManager(const char *device, INDI::Property prop)
0066 {
0067     if (!prop.getRegistered())
0068         return;
0069 
0070     if (getDeviceName() == device)
0071         emit newBLOBManager(prop);
0072 }
0073 
0074 void Camera::registerProperty(INDI::Property prop)
0075 {
0076     if (prop.isNameMatch("GUIDER_EXPOSURE"))
0077     {
0078         HasGuideHead = true;
0079         guideChip.reset(new CameraChip(this, CameraChip::GUIDE_CCD));
0080     }
0081     else if (prop.isNameMatch("CCD_FRAME_TYPE"))
0082     {
0083         primaryChip->clearFrameTypes();
0084 
0085         for (auto &it : *prop.getSwitch())
0086             primaryChip->addFrameLabel(it.getLabel());
0087     }
0088     else if (prop.isNameMatch("CCD_FRAME"))
0089     {
0090         auto np = prop.getNumber();
0091         if (np && np->getPermission() != IP_RO)
0092             primaryChip->setCanSubframe(true);
0093     }
0094     else if (prop.isNameMatch("GUIDER_FRAME"))
0095     {
0096         auto np = prop.getNumber();
0097         if (np && np->getPermission() != IP_RO)
0098             guideChip->setCanSubframe(true);
0099     }
0100     else if (prop.isNameMatch("CCD_BINNING"))
0101     {
0102         auto np = prop.getNumber();
0103         if (np && np->getPermission() != IP_RO)
0104             primaryChip->setCanBin(true);
0105     }
0106     else if (prop.isNameMatch("GUIDER_BINNING"))
0107     {
0108         auto np = prop.getNumber();
0109         if (np && np->getPermission() != IP_RO)
0110             guideChip->setCanBin(true);
0111     }
0112     else if (prop.isNameMatch("CCD_ABORT_EXPOSURE"))
0113     {
0114         auto sp = prop.getSwitch();
0115         if (sp && sp->getPermission() != IP_RO)
0116             primaryChip->setCanAbort(true);
0117     }
0118     else if (prop.isNameMatch("GUIDER_ABORT_EXPOSURE"))
0119     {
0120         auto sp = prop.getSwitch();
0121         if (sp && sp->getPermission() != IP_RO)
0122             guideChip->setCanAbort(true);
0123     }
0124     else if (prop.isNameMatch("CCD_TEMPERATURE"))
0125     {
0126         auto np = prop.getNumber();
0127         HasCooler = true;
0128         CanCool   = (np->getPermission() != IP_RO);
0129         if (np)
0130             emit newTemperatureValue(np->at(0)->getValue());
0131     }
0132     else if (prop.isNameMatch("CCD_COOLER"))
0133     {
0134         // Can turn cooling on/off
0135         HasCoolerControl = true;
0136     }
0137     else if (prop.isNameMatch("CCD_VIDEO_STREAM"))
0138     {
0139         // Has Video Stream
0140         HasVideoStream = true;
0141     }
0142     else if (prop.isNameMatch("CCD_CAPTURE_FORMAT"))
0143     {
0144         auto sp = prop.getSwitch();
0145         if (sp)
0146         {
0147             m_CaptureFormats.clear();
0148             for (const auto &oneSwitch : *sp)
0149                 m_CaptureFormats << oneSwitch.getLabel();
0150 
0151             m_CaptureFormatIndex = sp->findOnSwitchIndex();
0152         }
0153     }
0154     else if (prop.isNameMatch("CCD_TRANSFER_FORMAT"))
0155     {
0156         auto sp = prop.getSwitch();
0157         if (sp)
0158         {
0159             m_EncodingFormats.clear();
0160             for (const auto &oneSwitch : *sp)
0161                 m_EncodingFormats << oneSwitch.getLabel();
0162 
0163             auto format = sp->findOnSwitch();
0164             if (format)
0165                 m_EncodingFormat = format->label;
0166         }
0167     }
0168     else if (prop.isNameMatch("CCD_EXPOSURE_PRESETS"))
0169     {
0170         auto svp = prop.getSwitch();
0171         if (svp)
0172         {
0173             bool ok = false;
0174             auto separator = QDir::separator();
0175             for (const auto &it : *svp)
0176             {
0177                 QString key = QString(it.getLabel());
0178                 double value = key.toDouble(&ok);
0179                 if (!ok)
0180                 {
0181                     QStringList parts = key.split(separator);
0182                     if (parts.count() == 2)
0183                     {
0184                         bool numOk = false, denOk = false;
0185                         double numerator = parts[0].toDouble(&numOk);
0186                         double denominator = parts[1].toDouble(&denOk);
0187                         if (numOk && denOk && denominator > 0)
0188                         {
0189                             ok = true;
0190                             value = numerator / denominator;
0191                         }
0192                     }
0193                 }
0194                 if (ok)
0195                     m_ExposurePresets.insert(key, value);
0196 
0197                 double min = 1e6, max = 1e-6;
0198                 for (auto oneValue : m_ExposurePresets.values())
0199                 {
0200                     if (oneValue < min)
0201                         min = oneValue;
0202                     if (oneValue > max)
0203                         max = oneValue;
0204                 }
0205                 m_ExposurePresetsMinMax = qMakePair<double, double>(min, max);
0206             }
0207         }
0208     }
0209     else if (prop.isNameMatch("CCD_FAST_TOGGLE"))
0210     {
0211         auto sp = prop.getSwitch();
0212         if (sp)
0213             m_FastExposureEnabled = sp->findOnSwitchIndex() == 0;
0214         else
0215             m_FastExposureEnabled = false;
0216     }
0217     else if (prop.isNameMatch("TELESCOPE_TYPE"))
0218     {
0219         auto sp = prop.getSwitch();
0220         if (sp)
0221         {
0222             auto format = sp->findWidgetByName("TELESCOPE_PRIMARY");
0223             if (format && format->getState() == ISS_ON)
0224                 telescopeType = TELESCOPE_PRIMARY;
0225             else
0226                 telescopeType = TELESCOPE_GUIDE;
0227         }
0228     }
0229     else if (prop.isNameMatch("CCD_WEBSOCKET_SETTINGS"))
0230     {
0231         auto np = prop.getNumber();
0232         m_Media->setURL(QUrl(QString("ws://%1:%2").arg(m_Parent->getClientManager()->getHost()).arg(np->at(0)->getValue())));
0233         m_Media->connectServer();
0234     }
0235     else if (prop.isNameMatch("CCD1"))
0236     {
0237         primaryCCDBLOB = prop;
0238     }
0239     // try to find gain and/or offset property, if any
0240     else if ( (gainN == nullptr || offsetN == nullptr) && prop.getType() == INDI_NUMBER)
0241     {
0242         // Since gain is spread among multiple property depending on the camera providing it
0243         // we need to search in all possible number properties
0244         auto controlNP = prop.getNumber();
0245         if (controlNP)
0246         {
0247             for (auto &it : *controlNP)
0248             {
0249                 QString name  = QString(it.getName()).toLower();
0250                 QString label = QString(it.getLabel()).toLower();
0251 
0252                 if (name == "gain" || label == "gain")
0253                 {
0254                     gainN = &it;
0255                     gainPerm = controlNP->getPermission();
0256                 }
0257                 else if (name == "offset" || label == "offset")
0258                 {
0259                     offsetN = &it;
0260                     offsetPerm = controlNP->getPermission();
0261                 }
0262             }
0263         }
0264     }
0265 
0266     ConcreteDevice::registerProperty(prop);
0267 }
0268 
0269 void Camera::removeProperty(INDI::Property prop)
0270 {
0271     if (prop.isNameMatch("CCD_WEBSOCKET_SETTINGS"))
0272     {
0273         m_Media->disconnectServer();
0274     }
0275 }
0276 
0277 void Camera::processNumber(INDI::Property prop)
0278 {
0279     auto nvp = prop.getNumber();
0280     if (nvp->isNameMatch("CCD_EXPOSURE"))
0281     {
0282         auto np = nvp->findWidgetByName("CCD_EXPOSURE_VALUE");
0283         if (np)
0284             emit newExposureValue(primaryChip.get(), np->getValue(), nvp->getState());
0285         if (nvp->getState() == IPS_ALERT)
0286             emit error(ERROR_CAPTURE);
0287     }
0288     else if (prop.isNameMatch("CCD_TEMPERATURE"))
0289     {
0290         HasCooler   = true;
0291         auto np = nvp->findWidgetByName("CCD_TEMPERATURE_VALUE");
0292         if (np)
0293             emit newTemperatureValue(np->getValue());
0294     }
0295     else if (prop.isNameMatch("GUIDER_EXPOSURE"))
0296     {
0297         auto np = nvp->findWidgetByName("GUIDER_EXPOSURE_VALUE");
0298         if (np)
0299             emit newExposureValue(guideChip.get(), np->getValue(), nvp->getState());
0300     }
0301     else if (prop.isNameMatch("FPS"))
0302     {
0303         emit newFPS(nvp->at(0)->getValue(), nvp->at(1)->getValue());
0304     }
0305     else if (prop.isNameMatch("CCD_RAPID_GUIDE_DATA"))
0306     {
0307         if (nvp->getState() == IPS_ALERT)
0308         {
0309             emit newGuideStarData(primaryChip.get(), -1, -1, -1);
0310         }
0311         else
0312         {
0313             double dx = -1, dy = -1, fit = -1;
0314 
0315             auto np = nvp->findWidgetByName("GUIDESTAR_X");
0316             if (np)
0317                 dx = np->getValue();
0318             np = nvp->findWidgetByName("GUIDESTAR_Y");
0319             if (np)
0320                 dy = np->getValue();
0321             np = nvp->findWidgetByName("GUIDESTAR_FIT");
0322             if (np)
0323                 fit = np->getValue();
0324 
0325             if (dx >= 0 && dy >= 0 && fit >= 0)
0326                 emit newGuideStarData(primaryChip.get(), dx, dy, fit);
0327         }
0328     }
0329     else if (prop.isNameMatch("GUIDER_RAPID_GUIDE_DATA"))
0330     {
0331         if (nvp->getState() == IPS_ALERT)
0332         {
0333             emit newGuideStarData(guideChip.get(), -1, -1, -1);
0334         }
0335         else
0336         {
0337             double dx = -1, dy = -1, fit = -1;
0338             auto np = nvp->findWidgetByName("GUIDESTAR_X");
0339             if (np)
0340                 dx = np->getValue();
0341             np = nvp->findWidgetByName("GUIDESTAR_Y");
0342             if (np)
0343                 dy = np->getValue();
0344             np = nvp->findWidgetByName("GUIDESTAR_FIT");
0345             if (np)
0346                 fit = np->getValue();
0347 
0348             if (dx >= 0 && dy >= 0 && fit >= 0)
0349                 emit newGuideStarData(guideChip.get(), dx, dy, fit);
0350         }
0351     }
0352 }
0353 
0354 void Camera::processSwitch(INDI::Property prop)
0355 {
0356     auto svp = prop.getSwitch();
0357 
0358     if (svp->isNameMatch("CCD_COOLER"))
0359     {
0360         // Can turn cooling on/off
0361         HasCoolerControl = true;
0362         emit coolerToggled(svp->sp[0].s == ISS_ON);
0363     }
0364     else if (QString(svp->getName()).endsWith("VIDEO_STREAM"))
0365     {
0366         // If BLOB is not enabled for this camera, then ignore all VIDEO_STREAM calls.
0367         if (isBLOBEnabled() == false || m_StreamingEnabled == false)
0368             return;
0369 
0370         HasVideoStream = true;
0371 
0372         if (!streamWindow && svp->sp[0].s == ISS_ON)
0373         {
0374             streamWindow.reset(new StreamWG(this));
0375 
0376             INumberVectorProperty *streamFrame = getNumber("CCD_STREAM_FRAME");
0377             INumber *w = nullptr, *h = nullptr;
0378 
0379             if (streamFrame)
0380             {
0381                 w = IUFindNumber(streamFrame, "WIDTH");
0382                 h = IUFindNumber(streamFrame, "HEIGHT");
0383             }
0384 
0385             if (w && h)
0386             {
0387                 streamW = w->value;
0388                 streamH = h->value;
0389             }
0390             else
0391             {
0392                 // Only use CCD dimensions if we are receiving raw stream and not stream of images (i.e. mjpeg..etc)
0393                 auto rawBP = getBLOB("CCD1");
0394                 if (rawBP)
0395                 {
0396                     int x = 0, y = 0, w = 0, h = 0;
0397                     int binx = 0, biny = 0;
0398 
0399                     primaryChip->getFrame(&x, &y, &w, &h);
0400                     primaryChip->getBinning(&binx, &biny);
0401                     streamW = w / binx;
0402                     streamH = h / biny;
0403                 }
0404             }
0405 
0406             streamWindow->setSize(streamW, streamH);
0407         }
0408 
0409         if (streamWindow)
0410         {
0411             connect(streamWindow.get(), &StreamWG::hidden, this, &Camera::StreamWindowHidden, Qt::UniqueConnection);
0412             connect(streamWindow.get(), &StreamWG::imageChanged, this, &Camera::newVideoFrame, Qt::UniqueConnection);
0413 
0414             streamWindow->enableStream(svp->sp[0].s == ISS_ON);
0415             emit videoStreamToggled(svp->sp[0].s == ISS_ON);
0416         }
0417     }
0418     else if (svp->isNameMatch("CCD_CAPTURE_FORMAT"))
0419     {
0420         m_CaptureFormats.clear();
0421         for (int i = 0; i < svp->nsp; i++)
0422         {
0423             m_CaptureFormats << svp->sp[i].label;
0424             if (svp->sp[i].s == ISS_ON)
0425                 m_CaptureFormatIndex = i;
0426         }
0427     }
0428     else if (svp->isNameMatch("CCD_TRANSFER_FORMAT"))
0429     {
0430         ISwitch *format = IUFindOnSwitch(svp);
0431         if (format)
0432             m_EncodingFormat = format->label;
0433     }
0434     else if (svp->isNameMatch("RECORD_STREAM"))
0435     {
0436         ISwitch *recordOFF = IUFindSwitch(svp, "RECORD_OFF");
0437 
0438         if (recordOFF && recordOFF->s == ISS_ON)
0439         {
0440             emit videoRecordToggled(false);
0441             KSNotification::event(QLatin1String("IndiServerMessage"), i18n("Video Recording Stopped"), KSNotification::INDI);
0442         }
0443         else
0444         {
0445             emit videoRecordToggled(true);
0446             KSNotification::event(QLatin1String("IndiServerMessage"), i18n("Video Recording Started"), KSNotification::INDI);
0447         }
0448     }
0449     else if (svp->isNameMatch("TELESCOPE_TYPE"))
0450     {
0451         ISwitch *format = IUFindSwitch(svp, "TELESCOPE_PRIMARY");
0452         if (format && format->s == ISS_ON)
0453             telescopeType = TELESCOPE_PRIMARY;
0454         else
0455             telescopeType = TELESCOPE_GUIDE;
0456     }
0457     else if (!strcmp(svp->name, "CCD_FAST_TOGGLE"))
0458     {
0459         m_FastExposureEnabled = IUFindOnSwitchIndex(svp) == 0;
0460     }
0461     else if (svp->isNameMatch("CONNECTION"))
0462     {
0463         auto dSwitch = svp->findWidgetByName("DISCONNECT");
0464 
0465         if (dSwitch && dSwitch->getState() == ISS_ON)
0466         {
0467             if (streamWindow)
0468             {
0469                 streamWindow->enableStream(false);
0470                 emit videoStreamToggled(false);
0471                 streamWindow->close();
0472                 streamWindow.reset();
0473             }
0474 
0475             // Clear the pointers on disconnect.
0476             gainN = nullptr;
0477             offsetN = nullptr;
0478             primaryCCDBLOB = INDI::Property();
0479         }
0480     }
0481 }
0482 
0483 void Camera::processText(INDI::Property prop)
0484 {
0485     auto tvp = prop.getText();
0486     if (tvp->isNameMatch("CCD_FILE_PATH"))
0487     {
0488         auto filepath = tvp->findWidgetByName("FILE_PATH");
0489         if (filepath)
0490             emit newRemoteFile(QString(filepath->getText()));
0491     }
0492 }
0493 
0494 void Camera::setWSBLOB(const QByteArray &message, const QString &extension)
0495 {
0496     if (!primaryCCDBLOB)
0497         return;
0498 
0499     auto bvp = primaryCCDBLOB.getBLOB();
0500     auto bp = bvp->at(0);
0501 
0502     bp->setBlob(const_cast<char *>(message.data()));
0503     bp->setSize(message.size());
0504     bp->setFormat(extension.toLatin1().constData());
0505     processBLOB(primaryCCDBLOB);
0506 
0507     // Disassociate
0508     bp->setBlob(nullptr);
0509 }
0510 
0511 void Camera::processStream(INDI::Property prop)
0512 {
0513     if (!streamWindow || streamWindow->isStreamEnabled() == false)
0514         return;
0515 
0516     INumberVectorProperty *streamFrame = getNumber("CCD_STREAM_FRAME");
0517     INumber *w = nullptr, *h = nullptr;
0518 
0519     if (streamFrame)
0520     {
0521         w = IUFindNumber(streamFrame, "WIDTH");
0522         h = IUFindNumber(streamFrame, "HEIGHT");
0523     }
0524 
0525     if (w && h)
0526     {
0527         streamW = w->value;
0528         streamH = h->value;
0529     }
0530     else
0531     {
0532         int x = 0, y = 0, w = 0, h = 0;
0533         int binx = 1, biny = 1;
0534 
0535         primaryChip->getFrame(&x, &y, &w, &h);
0536         primaryChip->getBinning(&binx, &biny);
0537         streamW = w / binx;
0538         streamH = h / biny;
0539     }
0540 
0541     streamWindow->setSize(streamW, streamH);
0542 
0543     streamWindow->show();
0544     streamWindow->newFrame(prop);
0545 }
0546 
0547 bool Camera::generateFilename(bool batch_mode, const QString &extension, QString *filename)
0548 {
0549 
0550     *filename = placeholderPath.generateOutputFilename(true, batch_mode, nextSequenceID, extension, "");
0551 
0552     QDir currentDir = QFileInfo(*filename).dir();
0553     if (currentDir.exists() == false)
0554         QDir().mkpath(currentDir.path());
0555 
0556     // Check if the file exists. We try not to overwrite capture files.
0557     if (QFile::exists(*filename))
0558     {
0559         QString oldFilename = *filename;
0560         *filename = placeholderPath.repairFilename(*filename);
0561         if (filename != oldFilename)
0562             qCWarning(KSTARS_INDI) << "File over-write detected: changing" << oldFilename << "to" << *filename;
0563         else
0564             qCWarning(KSTARS_INDI) << "File over-write detected for" << oldFilename << "but could not correct filename";
0565     }
0566 
0567     QFile test_file(*filename);
0568     if (!test_file.open(QIODevice::WriteOnly))
0569         return false;
0570     test_file.flush();
0571     test_file.close();
0572     return true;
0573 }
0574 
0575 bool Camera::writeImageFile(const QString &filename, INDI::Property prop, bool is_fits)
0576 {
0577     // TODO: Not yet threading the writes for non-fits files.
0578     // Would need to deal with the raw conversion, etc.
0579     if (is_fits)
0580     {
0581         // Check if the last write is still ongoing, and if so wait.
0582         // It is using the fileWriteBuffer.
0583         if (fileWriteThread.isRunning())
0584         {
0585             fileWriteThread.waitForFinished();
0586         }
0587 
0588         // Wait until the file is written before overwritting the filename.
0589         fileWriteFilename = filename;
0590 
0591         // Will write blob data in a separate thread, and can't depend on the blob
0592         // memory, so copy it first.
0593 
0594         auto bp = prop.getBLOB()->at(0);
0595         // Check buffer size.
0596         if (fileWriteBufferSize != bp->getBlobLen())
0597         {
0598             if (fileWriteBuffer != nullptr)
0599                 delete [] fileWriteBuffer;
0600             fileWriteBufferSize = bp->getBlobLen();
0601             fileWriteBuffer = new char[fileWriteBufferSize];
0602         }
0603 
0604         // Copy memory, and write file on a separate thread.
0605         // Probably too late to return an error if the file couldn't write.
0606         memcpy(fileWriteBuffer, bp->getBlob(), bp->getBlobLen());
0607         fileWriteThread = QtConcurrent::run(this, &ISD::Camera::WriteImageFileInternal, fileWriteFilename, fileWriteBuffer,
0608                                             bp->getBlobLen());
0609     }
0610     else
0611     {
0612         auto bp = prop.getBLOB()->at(0);
0613         if (!WriteImageFileInternal(filename, static_cast<char*>(bp->getBlob()), bp->getBlobLen()))
0614             return false;
0615     }
0616     return true;
0617 }
0618 
0619 // Get or Create FITSViewer if we are using FITSViewer
0620 // or if capture mode is calibrate since for now we are forced to open the file in the viewer
0621 // this should be fixed in the future and should only use FITSData
0622 QSharedPointer<FITSViewer> Camera::getFITSViewer()
0623 {
0624     // if the FITS viewer exists, return it
0625     if (!m_FITSViewerWindow.isNull() && ! m_FITSViewerWindow.isNull())
0626         return m_FITSViewerWindow;
0627 
0628     // otherwise, create it
0629     normalTabID = calibrationTabID = focusTabID = guideTabID = alignTabID = -1;
0630 
0631     m_FITSViewerWindow = KStars::Instance()->createFITSViewer();
0632 
0633     // Check if ONE tab of the viewer was closed.
0634     connect(m_FITSViewerWindow.get(), &FITSViewer::closed, this, [this](int tabIndex)
0635     {
0636         if (tabIndex == normalTabID)
0637             normalTabID = -1;
0638         else if (tabIndex == calibrationTabID)
0639             calibrationTabID = -1;
0640         else if (tabIndex == focusTabID)
0641             focusTabID = -1;
0642         else if (tabIndex == guideTabID)
0643             guideTabID = -1;
0644         else if (tabIndex == alignTabID)
0645             alignTabID = -1;
0646     });
0647 
0648     // If FITS viewer was completed closed. Reset everything
0649     connect(m_FITSViewerWindow.get(), &FITSViewer::terminated, this, [this]()
0650     {
0651         normalTabID = -1;
0652         calibrationTabID = -1;
0653         focusTabID = -1;
0654         guideTabID = -1;
0655         alignTabID = -1;
0656         m_FITSViewerWindow.clear();
0657     });
0658 
0659     return m_FITSViewerWindow;
0660 }
0661 
0662 bool Camera::processBLOB(INDI::Property prop)
0663 {
0664     auto bvp = prop.getBLOB();
0665     // Ignore write-only BLOBs since we only receive it for state-change
0666     if (bvp->getPermission() == IP_WO || bvp->at(0)->getSize() == 0)
0667         return false;
0668 
0669     BType = BLOB_OTHER;
0670 
0671     auto bp = bvp->at(0);
0672 
0673     auto format = QString(bp->getFormat()).toLower();
0674 
0675     // If stream, process it first
0676     if (format.contains("stream"))
0677     {
0678         if (m_StreamingEnabled == false)
0679             return true;
0680         else if (streamWindow)
0681             processStream(prop);
0682         return true;
0683     }
0684 
0685     // Format without leading . (.jpg --> jpg)
0686     QString shortFormat = format.mid(1);
0687 
0688     // If it's not FITS or an image, don't process it.
0689     if ((QImageReader::supportedImageFormats().contains(shortFormat.toLatin1())))
0690         BType = BLOB_IMAGE;
0691     else if (format.contains("fits"))
0692         BType = BLOB_FITS;
0693     else if (format.contains("xisf"))
0694         BType = BLOB_XISF;
0695     else if (RAWFormats.contains(shortFormat))
0696         BType = BLOB_RAW;
0697 
0698     if (BType == BLOB_OTHER)
0699     {
0700         emit newImage(nullptr);
0701         return false;
0702     }
0703 
0704     CameraChip *targetChip = nullptr;
0705 
0706     if (bvp->isNameMatch("CCD2"))
0707         targetChip = guideChip.get();
0708     else
0709     {
0710         targetChip = primaryChip.get();
0711         qCDebug(KSTARS_INDI) << "Image received. Mode:" << getFITSModeStringString(targetChip->getCaptureMode()) << "Size:" <<
0712                              bp->getSize();
0713     }
0714 
0715     // Create temporary name if ANY of the following conditions are met:
0716     // 1. file is preview or batch mode is not enabled
0717     // 2. file type is not FITS_NORMAL (focus, guide..etc)
0718     QString filename;
0719 #if 0
0720 
0721     if (targetChip->isBatchMode() == false || targetChip->getCaptureMode() != FITS_NORMAL)
0722     {
0723         if (!writeTempImageFile(format, static_cast<char *>(bp->blob), bp->size, &filename))
0724         {
0725             emit BLOBUpdated(nullptr);
0726             return;
0727         }
0728         if (BType == BLOB_FITS)
0729             addFITSKeywords(filename, filter);
0730 
0731     }
0732 #endif
0733     // Create file name for sequences.
0734     if (targetChip->isBatchMode() && targetChip->getCaptureMode() != FITS_CALIBRATE)
0735     {
0736         // If either generating file name or writing the image file fails
0737         // then return
0738         if (!generateFilename(targetChip->isBatchMode(), format, &filename) ||
0739                 !writeImageFile(filename, prop, BType == BLOB_FITS))
0740         {
0741             connect(KSMessageBox::Instance(), &KSMessageBox::accepted, this, [ = ]()
0742             {
0743                 KSMessageBox::Instance()->disconnect(this);
0744                 emit error(ERROR_SAVE);
0745             });
0746             KSMessageBox::Instance()->error(i18n("Failed writing image to %1\nPlease check folder, filename & permissions.",
0747                                                  filename),
0748                                             i18n("Image Write Failed"), 30);
0749 
0750             emit propertyUpdated(prop);
0751             return true;
0752         }
0753     }
0754     else
0755         filename = QDir::tempPath() + QDir::separator() + "image" + format;
0756 
0757     if (targetChip->getCaptureMode() == FITS_NORMAL && targetChip->isBatchMode() == true)
0758     {
0759         KStars::Instance()->statusBar()->showMessage(i18n("%1 file saved to %2", shortFormat.toUpper(), filename), 0);
0760         qCInfo(KSTARS_INDI) << shortFormat.toUpper() << "file saved to" << filename;
0761     }
0762 
0763     // Don't spam, just one notification per 3 seconds
0764     if (QDateTime::currentDateTime().secsTo(m_LastNotificationTS) <= -3)
0765     {
0766         KNotification::event(QLatin1String("FITSReceived"), i18n("Image file is received"));
0767         m_LastNotificationTS = QDateTime::currentDateTime();
0768     }
0769 
0770     // Check if we need to process RAW or regular image. Anything but FITS.
0771 #if 0
0772     if (BType == BLOB_IMAGE || BType == BLOB_RAW)
0773     {
0774         bool useFITSViewer = Options::autoImageToFITS() &&
0775                              (Options::useFITSViewer() || (Options::useDSLRImageViewer() == false && targetChip->isBatchMode() == false));
0776         bool useDSLRViewer = (Options::useDSLRImageViewer() || targetChip->isBatchMode() == false);
0777         // For raw image, we only process them to JPG if we need to open them in the image viewer
0778         if (BType == BLOB_RAW && (useFITSViewer || useDSLRViewer))
0779         {
0780             QString rawFileName  = filename;
0781             rawFileName          = rawFileName.remove(0, rawFileName.lastIndexOf(QLatin1String("/")));
0782 
0783             QString templateName = QString("%1/%2.XXXXXX").arg(QDir::tempPath(), rawFileName);
0784             QTemporaryFile imgPreview(templateName);
0785 
0786             imgPreview.setAutoRemove(false);
0787             imgPreview.open();
0788             imgPreview.close();
0789             QString preview_filename = imgPreview.fileName();
0790             QString errorMessage;
0791 
0792             if (KSUtils::RAWToJPEG(filename, preview_filename, errorMessage) == false)
0793             {
0794                 KStars::Instance()->statusBar()->showMessage(errorMessage);
0795                 emit BLOBUpdated(bp);
0796                 return;
0797             }
0798 
0799             // Remove tempeorary CR2 files
0800             if (targetChip->isBatchMode() == false)
0801                 QFile::remove(filename);
0802 
0803             filename = preview_filename;
0804             format = ".jpg";
0805             shortFormat = "jpg";
0806         }
0807 
0808         // Convert to FITS if checked.
0809         QString output;
0810         if (useFITSViewer && (FITSData::ImageToFITS(filename, shortFormat, output)))
0811         {
0812             if (BType == BLOB_RAW || targetChip->isBatchMode() == false)
0813                 QFile::remove(filename);
0814 
0815             filename = output;
0816             BType = BLOB_FITS;
0817 
0818             emit previewFITSGenerated(output);
0819 
0820             FITSData *blob_fits_data = new FITSData(targetChip->getCaptureMode());
0821 
0822             QFuture<bool> fitsloader = blob_fits_data->loadFromFile(filename, false);
0823             fitsloader.waitForFinished();
0824             if (!fitsloader.result())
0825             {
0826                 // If reading the blob fails, we treat it the same as exposure failure
0827                 // and recapture again if possible
0828                 delete (blob_fits_data);
0829                 qCCritical(KSTARS_INDI) << "failed reading FITS memory buffer";
0830                 emit newExposureValue(targetChip, 0, IPS_ALERT);
0831                 return;
0832             }
0833             displayFits(targetChip, filename, bp, blob_fits_data);
0834             return;
0835         }
0836         else if (useDSLRViewer)
0837         {
0838             if (m_ImageViewerWindow.isNull())
0839                 m_ImageViewerWindow = new ImageViewer(getDeviceName(), KStars::Instance());
0840 
0841             m_ImageViewerWindow->loadImage(filename);
0842 
0843             emit previewJPEGGenerated(filename, m_ImageViewerWindow->metadata());
0844         }
0845     }
0846 #endif
0847 
0848     // Load FITS if either:
0849     // #1 FITS Viewer is set to enabled.
0850     // #2 This is a preview, so we MUST open FITS Viewer even if disabled.
0851     //    if (BType == BLOB_FITS)
0852     //    {
0853     // Don't display image if the following conditions are met:
0854     // 1. Mode is NORMAL or CALIBRATE; and
0855     // 2. FITS Viewer is disabled; and
0856     // 3. Batch mode is enabled.
0857     // 4. Summary view is false.
0858     if (targetChip->getCaptureMode() == FITS_NORMAL &&
0859             Options::useFITSViewer() == false &&
0860             Options::useSummaryPreview() == false &&
0861             targetChip->isBatchMode())
0862     {
0863         emit propertyUpdated(prop);
0864         emit newImage(nullptr);
0865         return true;
0866     }
0867 
0868     QByteArray buffer = QByteArray::fromRawData(reinterpret_cast<char *>(bp->getBlob()), bp->getSize());
0869     QSharedPointer<FITSData> imageData;
0870     imageData.reset(new FITSData(targetChip->getCaptureMode()), &QObject::deleteLater);
0871     if (!imageData->loadFromBuffer(buffer, shortFormat, filename))
0872     {
0873         emit error(ERROR_LOAD);
0874         return true;
0875     }
0876 
0877     handleImage(targetChip, filename, prop, imageData);
0878     return true;
0879 }
0880 
0881 void Camera::handleImage(CameraChip *targetChip, const QString &filename, INDI::Property prop,
0882                          QSharedPointer<FITSData> data)
0883 {
0884     FITSMode captureMode = targetChip->getCaptureMode();
0885     auto bp = prop.getBLOB()->at(0);
0886 
0887     // Add metadata
0888     data->setProperty("device", getDeviceName());
0889     data->setProperty("blobVector", prop.getName());
0890     data->setProperty("blobElement", bp->getName());
0891     data->setProperty("chip", targetChip->getType());
0892     // Retain a copy
0893     targetChip->setImageData(data);
0894 
0895     switch (captureMode)
0896     {
0897         case FITS_NORMAL:
0898         case FITS_CALIBRATE:
0899         {
0900             if (Options::useFITSViewer())
0901             {
0902                 // No need to wait until the image is loaded in the view, but emit AFTER checking
0903                 // batch mode, since newImage() may change it
0904                 emit propertyUpdated(prop);
0905                 emit newImage(data);
0906 
0907                 bool success = false;
0908                 int tabIndex = -1;
0909                 int *tabID = &normalTabID;
0910                 QUrl fileURL = QUrl::fromLocalFile(filename);
0911                 FITSScale captureFilter = targetChip->getCaptureFilter();
0912                 if (*tabID == -1 || Options::singlePreviewFITS() == false)
0913                 {
0914                     // If image is preview and we should display all captured images in a
0915                     // single tab called "Preview", then set the title to "Preview",
0916                     // Otherwise, the title will be the captured image name
0917                     QString previewTitle;
0918                     if (Options::singlePreviewFITS())
0919                     {
0920                         // If we are displaying all images from all cameras in a single FITS
0921                         // Viewer window, then we prefix the camera name to the "Preview" string
0922                         if (Options::singleWindowCapturedFITS())
0923                             previewTitle = i18n("%1 Preview", getDeviceName());
0924                         else
0925                             // Otherwise, just use "Preview"
0926                             previewTitle = i18n("Preview");
0927                     }
0928 
0929                     success = getFITSViewer()->loadData(data, fileURL, &tabIndex, captureMode, captureFilter, previewTitle);
0930 
0931                     //Setup any necessary connections
0932                     auto tabs = getFITSViewer()->tabs();
0933                     if (tabIndex < tabs.size() && captureMode == FITS_NORMAL)
0934                     {
0935                         emit newView(tabs[tabIndex]->getView());
0936                         tabs[tabIndex]->disconnect(this);
0937                         connect(tabs[tabIndex].get(), &FITSTab::updated, this, [this]
0938                         {
0939                             auto tab = qobject_cast<FITSTab *>(sender());
0940                             emit newView(tab->getView());
0941                         });
0942                     }
0943                 }
0944                 else
0945                     success = getFITSViewer()->updateData(data, fileURL, *tabID, &tabIndex, captureFilter, captureMode);
0946 
0947                 if (!success)
0948                 {
0949                     // If opening file fails, we treat it the same as exposure failure
0950                     // and recapture again if possible
0951                     qCCritical(KSTARS_INDI) << "error adding/updating FITS";
0952                     emit error(ERROR_VIEWER);
0953                     return;
0954                 }
0955                 *tabID = tabIndex;
0956                 if (Options::focusFITSOnNewImage())
0957                     getFITSViewer()->raise();
0958 
0959                 return;
0960             }
0961         }
0962         break;
0963         default:
0964             break;
0965     }
0966 
0967     emit propertyUpdated(prop);
0968     emit newImage(data);
0969 }
0970 
0971 void Camera::StreamWindowHidden()
0972 {
0973     if (isConnected())
0974     {
0975         // We can have more than one *_VIDEO_STREAM property active so disable them all
0976         auto streamSP = getSwitch("CCD_VIDEO_STREAM");
0977         if (streamSP)
0978         {
0979             streamSP->reset();
0980             streamSP->at(0)->setState(ISS_OFF);
0981             streamSP->at(1)->setState(ISS_ON);
0982             streamSP->setState(IPS_IDLE);
0983             sendNewProperty(streamSP);
0984         }
0985 
0986         streamSP = getSwitch("VIDEO_STREAM");
0987         if (streamSP)
0988         {
0989             streamSP->reset();
0990             streamSP->at(0)->setState(ISS_OFF);
0991             streamSP->at(1)->setState(ISS_ON);
0992             streamSP->setState(IPS_IDLE);
0993             sendNewProperty(streamSP);
0994         }
0995 
0996         streamSP = getSwitch("AUX_VIDEO_STREAM");
0997         if (streamSP)
0998         {
0999             streamSP->reset();
1000             streamSP->at(0)->setState(ISS_OFF);
1001             streamSP->at(1)->setState(ISS_ON);
1002             streamSP->setState(IPS_IDLE);
1003             sendNewProperty(streamSP);
1004         }
1005     }
1006 
1007     if (streamWindow)
1008         streamWindow->disconnect();
1009 }
1010 
1011 bool Camera::hasGuideHead()
1012 {
1013     return HasGuideHead;
1014 }
1015 
1016 bool Camera::hasCooler()
1017 {
1018     return HasCooler;
1019 }
1020 
1021 bool Camera::hasCoolerControl()
1022 {
1023     return HasCoolerControl;
1024 }
1025 
1026 bool Camera::setCoolerControl(bool enable)
1027 {
1028     if (HasCoolerControl == false)
1029         return false;
1030 
1031     auto coolerSP = getSwitch("CCD_COOLER");
1032 
1033     if (!coolerSP)
1034         return false;
1035 
1036     // Cooler ON/OFF
1037     auto coolerON  = coolerSP->findWidgetByName("COOLER_ON");
1038     auto coolerOFF = coolerSP->findWidgetByName("COOLER_OFF");
1039     if (!coolerON || !coolerOFF)
1040         return false;
1041 
1042     coolerON->setState(enable ? ISS_ON : ISS_OFF);
1043     coolerOFF->setState(enable ? ISS_OFF : ISS_ON);
1044     sendNewProperty(coolerSP);
1045 
1046     return true;
1047 }
1048 
1049 CameraChip *Camera::getChip(CameraChip::ChipType cType)
1050 {
1051     switch (cType)
1052     {
1053         case CameraChip::PRIMARY_CCD:
1054             return primaryChip.get();
1055 
1056         case CameraChip::GUIDE_CCD:
1057             return guideChip.get();
1058     }
1059 
1060     return nullptr;
1061 }
1062 
1063 bool Camera::setRapidGuide(CameraChip *targetChip, bool enable)
1064 {
1065     ISwitchVectorProperty *rapidSP = nullptr;
1066     ISwitch *enableS               = nullptr;
1067 
1068     if (targetChip == primaryChip.get())
1069         rapidSP = getSwitch("CCD_RAPID_GUIDE");
1070     else
1071         rapidSP = getSwitch("GUIDER_RAPID_GUIDE");
1072 
1073     if (rapidSP == nullptr)
1074         return false;
1075 
1076     enableS = IUFindSwitch(rapidSP, "ENABLE");
1077 
1078     if (enableS == nullptr)
1079         return false;
1080 
1081     // Already updated, return OK
1082     if ((enable && enableS->s == ISS_ON) || (!enable && enableS->s == ISS_OFF))
1083         return true;
1084 
1085     IUResetSwitch(rapidSP);
1086     rapidSP->sp[0].s = enable ? ISS_ON : ISS_OFF;
1087     rapidSP->sp[1].s = enable ? ISS_OFF : ISS_ON;
1088 
1089     sendNewProperty(rapidSP);
1090 
1091     return true;
1092 }
1093 
1094 bool Camera::configureRapidGuide(CameraChip *targetChip, bool autoLoop, bool sendImage, bool showMarker)
1095 {
1096     ISwitchVectorProperty *rapidSP = nullptr;
1097     ISwitch *autoLoopS = nullptr, *sendImageS = nullptr, *showMarkerS = nullptr;
1098 
1099     if (targetChip == primaryChip.get())
1100         rapidSP = getSwitch("CCD_RAPID_GUIDE_SETUP");
1101     else
1102         rapidSP = getSwitch("GUIDER_RAPID_GUIDE_SETUP");
1103 
1104     if (rapidSP == nullptr)
1105         return false;
1106 
1107     autoLoopS   = IUFindSwitch(rapidSP, "AUTO_LOOP");
1108     sendImageS  = IUFindSwitch(rapidSP, "SEND_IMAGE");
1109     showMarkerS = IUFindSwitch(rapidSP, "SHOW_MARKER");
1110 
1111     if (!autoLoopS || !sendImageS || !showMarkerS)
1112         return false;
1113 
1114     // If everything is already set, let's return.
1115     if (((autoLoop && autoLoopS->s == ISS_ON) || (!autoLoop && autoLoopS->s == ISS_OFF)) &&
1116             ((sendImage && sendImageS->s == ISS_ON) || (!sendImage && sendImageS->s == ISS_OFF)) &&
1117             ((showMarker && showMarkerS->s == ISS_ON) || (!showMarker && showMarkerS->s == ISS_OFF)))
1118         return true;
1119 
1120     autoLoopS->s   = autoLoop ? ISS_ON : ISS_OFF;
1121     sendImageS->s  = sendImage ? ISS_ON : ISS_OFF;
1122     showMarkerS->s = showMarker ? ISS_ON : ISS_OFF;
1123 
1124     sendNewProperty(rapidSP);
1125 
1126     return true;
1127 }
1128 
1129 void Camera::updateUploadSettings(const QString &uploadDirectory, const QString &uploadFile)
1130 {
1131     ITextVectorProperty *uploadSettingsTP = nullptr;
1132     IText *uploadT                        = nullptr;
1133 
1134     uploadSettingsTP = getText("UPLOAD_SETTINGS");
1135     if (uploadSettingsTP)
1136     {
1137         uploadT = IUFindText(uploadSettingsTP, "UPLOAD_DIR");
1138         if (uploadT && uploadDirectory.isEmpty() == false)
1139         {
1140             auto posixDirectory = uploadDirectory;
1141             // N.B. Need to convert any Windows directory separators / to Posix separators /
1142             posixDirectory.replace(QDir::separator(), "/");
1143             IUSaveText(uploadT, posixDirectory.toLatin1().constData());
1144         }
1145 
1146         uploadT = IUFindText(uploadSettingsTP, "UPLOAD_PREFIX");
1147         if (uploadT)
1148             IUSaveText(uploadT, uploadFile.toLatin1().constData());
1149 
1150         sendNewProperty(uploadSettingsTP);
1151     }
1152 }
1153 
1154 Camera::UploadMode Camera::getUploadMode()
1155 {
1156     ISwitchVectorProperty *uploadModeSP = nullptr;
1157 
1158     uploadModeSP = getSwitch("UPLOAD_MODE");
1159 
1160     if (uploadModeSP == nullptr)
1161     {
1162         qWarning() << "No UPLOAD_MODE in CCD driver. Please update driver to INDI compliant CCD driver.";
1163         return UPLOAD_CLIENT;
1164     }
1165 
1166     if (uploadModeSP)
1167     {
1168         ISwitch *modeS = nullptr;
1169 
1170         modeS = IUFindSwitch(uploadModeSP, "UPLOAD_CLIENT");
1171         if (modeS && modeS->s == ISS_ON)
1172             return UPLOAD_CLIENT;
1173         modeS = IUFindSwitch(uploadModeSP, "UPLOAD_LOCAL");
1174         if (modeS && modeS->s == ISS_ON)
1175             return UPLOAD_LOCAL;
1176         modeS = IUFindSwitch(uploadModeSP, "UPLOAD_BOTH");
1177         if (modeS && modeS->s == ISS_ON)
1178             return UPLOAD_BOTH;
1179     }
1180 
1181     // Default
1182     return UPLOAD_CLIENT;
1183 }
1184 
1185 bool Camera::setUploadMode(UploadMode mode)
1186 {
1187     ISwitch *modeS = nullptr;
1188 
1189     auto uploadModeSP = getSwitch("UPLOAD_MODE");
1190 
1191     if (!uploadModeSP)
1192     {
1193         qWarning() << "No UPLOAD_MODE in CCD driver. Please update driver to INDI compliant CCD driver.";
1194         return false;
1195     }
1196 
1197     switch (mode)
1198     {
1199         case UPLOAD_CLIENT:
1200             modeS = uploadModeSP->findWidgetByName("UPLOAD_CLIENT");
1201             if (!modeS)
1202                 return false;
1203             if (modeS->s == ISS_ON)
1204                 return true;
1205             break;
1206 
1207         case UPLOAD_BOTH:
1208             modeS = uploadModeSP->findWidgetByName("UPLOAD_BOTH");
1209             if (!modeS)
1210                 return false;
1211             if (modeS->s == ISS_ON)
1212                 return true;
1213             break;
1214 
1215         case UPLOAD_LOCAL:
1216             modeS = uploadModeSP->findWidgetByName("UPLOAD_LOCAL");
1217             if (!modeS)
1218                 return false;
1219             if (modeS->s == ISS_ON)
1220                 return true;
1221             break;
1222     }
1223 
1224     uploadModeSP->reset();
1225     modeS->s = ISS_ON;
1226 
1227     sendNewProperty(uploadModeSP);
1228 
1229     return true;
1230 }
1231 
1232 bool Camera::getTemperature(double *value)
1233 {
1234     if (HasCooler == false)
1235         return false;
1236 
1237     auto temperatureNP = getNumber("CCD_TEMPERATURE");
1238 
1239     if (!temperatureNP)
1240         return false;
1241 
1242     *value = temperatureNP->at(0)->getValue();
1243 
1244     return true;
1245 }
1246 
1247 bool Camera::setTemperature(double value)
1248 {
1249     auto nvp = getNumber("CCD_TEMPERATURE");
1250 
1251     if (!nvp)
1252         return false;
1253 
1254     auto np = nvp->findWidgetByName("CCD_TEMPERATURE_VALUE");
1255 
1256     if (!np)
1257         return false;
1258 
1259     np->setValue(value);
1260 
1261     sendNewProperty(nvp);
1262 
1263     return true;
1264 }
1265 
1266 bool Camera::setEncodingFormat(const QString &value)
1267 {
1268     if (value.isEmpty() || value == m_EncodingFormat)
1269         return true;
1270 
1271     auto svp = getSwitch("CCD_TRANSFER_FORMAT");
1272 
1273     if (!svp)
1274         return false;
1275 
1276     svp->reset();
1277     for (int i = 0; i < svp->nsp; i++)
1278     {
1279         if (svp->at(i)->getLabel() == value)
1280         {
1281             svp->at(i)->setState(ISS_ON);
1282             break;
1283         }
1284     }
1285 
1286     m_EncodingFormat = value;
1287     sendNewProperty(svp);
1288     return true;
1289 }
1290 
1291 bool Camera::setTelescopeType(TelescopeType type)
1292 {
1293     if (type == telescopeType)
1294         return true;
1295 
1296     auto svp = getSwitch("TELESCOPE_TYPE");
1297 
1298     if (!svp)
1299         return false;
1300 
1301     auto typePrimary = svp->findWidgetByName("TELESCOPE_PRIMARY");
1302     auto typeGuide   = svp->findWidgetByName("TELESCOPE_GUIDE");
1303 
1304     if (!typePrimary || !typeGuide)
1305         return false;
1306 
1307     telescopeType = type;
1308 
1309     if ( (telescopeType == TELESCOPE_PRIMARY && typePrimary->getState() == ISS_OFF) ||
1310             (telescopeType == TELESCOPE_GUIDE && typeGuide->getState() == ISS_OFF))
1311     {
1312         typePrimary->setState(telescopeType == TELESCOPE_PRIMARY ? ISS_ON : ISS_OFF);
1313         typeGuide->setState(telescopeType == TELESCOPE_PRIMARY ? ISS_OFF : ISS_ON);
1314         sendNewProperty(svp);
1315         setConfig(SAVE_CONFIG);
1316     }
1317 
1318     return true;
1319 }
1320 
1321 bool Camera::setVideoStreamEnabled(bool enable)
1322 {
1323     if (HasVideoStream == false)
1324         return false;
1325 
1326     auto svp = getSwitch("CCD_VIDEO_STREAM");
1327 
1328     if (!svp)
1329         return false;
1330 
1331     // If already on and enable is set or vice versa no need to change anything we return true
1332     if ((enable && svp->at(0)->getState() == ISS_ON) || (!enable && svp->at(1)->getState() == ISS_ON))
1333         return true;
1334 
1335     svp->at(0)->setState(enable ? ISS_ON : ISS_OFF);
1336     svp->at(1)->setState(enable ? ISS_OFF : ISS_ON);
1337 
1338     sendNewProperty(svp);
1339 
1340     return true;
1341 }
1342 
1343 bool Camera::resetStreamingFrame()
1344 {
1345     auto frameProp = getNumber("CCD_STREAM_FRAME");
1346 
1347     if (!frameProp)
1348         return false;
1349 
1350     auto xarg = frameProp->findWidgetByName("X");
1351     auto yarg = frameProp->findWidgetByName("Y");
1352     auto warg = frameProp->findWidgetByName("WIDTH");
1353     auto harg = frameProp->findWidgetByName("HEIGHT");
1354 
1355     if (xarg && yarg && warg && harg)
1356     {
1357         if (!std::fabs(xarg->getValue() - xarg->getMin()) &&
1358                 !std::fabs(yarg->getValue() - yarg->getMin()) &&
1359                 !std::fabs(warg->getValue() - warg->getMax()) &&
1360                 !std::fabs(harg->getValue() - harg->getMax()))
1361             return false;
1362 
1363         xarg->setValue(xarg->getMin());
1364         yarg->setValue(yarg->getMin());
1365         warg->setValue(warg->getMax());
1366         harg->setValue(harg->getMax());
1367 
1368         sendNewProperty(frameProp);
1369         return true;
1370     }
1371 
1372     return false;
1373 }
1374 
1375 bool Camera::setStreamLimits(uint16_t maxBufferSize, uint16_t maxPreviewFPS)
1376 {
1377     auto limitsProp = getNumber("LIMITS");
1378 
1379     if (!limitsProp)
1380         return false;
1381 
1382     auto bufferMax = limitsProp->findWidgetByName("LIMITS_BUFFER_MAX");
1383     auto previewFPS = limitsProp->findWidgetByName("LIMITS_PREVIEW_FPS");
1384 
1385     if (bufferMax && previewFPS)
1386     {
1387         if(std::fabs(bufferMax->getValue() - maxBufferSize) > 0 || std::fabs(previewFPS->getValue() - maxPreviewFPS) > 0)
1388         {
1389             bufferMax->setValue(maxBufferSize);
1390             previewFPS->setValue(maxPreviewFPS);
1391             sendNewProperty(limitsProp);
1392         }
1393 
1394         return true;
1395     }
1396 
1397     return false;
1398 }
1399 
1400 bool Camera::setStreamingFrame(int x, int y, int w, int h)
1401 {
1402     auto frameProp = getNumber("CCD_STREAM_FRAME");
1403 
1404     if (!frameProp)
1405         return false;
1406 
1407     auto xarg = frameProp->findWidgetByName("X");
1408     auto yarg = frameProp->findWidgetByName("Y");
1409     auto warg = frameProp->findWidgetByName("WIDTH");
1410     auto harg = frameProp->findWidgetByName("HEIGHT");
1411 
1412     if (xarg && yarg && warg && harg)
1413     {
1414         if (!std::fabs(xarg->getValue() - x) &&
1415                 !std::fabs(yarg->getValue() - y) &&
1416                 !std::fabs(warg->getValue() - w) &&
1417                 !std::fabs(harg->getValue() - h))
1418             return true;
1419 
1420         // N.B. We add offset since the X, Y are relative to whatever streaming frame is currently active
1421         xarg->value = qBound(xarg->getMin(), static_cast<double>(x) + xarg->getValue(), xarg->getMax());
1422         yarg->value = qBound(yarg->getMin(), static_cast<double>(y) + yarg->getValue(), yarg->getMax());
1423         warg->value = qBound(warg->getMin(), static_cast<double>(w), warg->getMax());
1424         harg->value = qBound(harg->getMin(), static_cast<double>(h), harg->getMax());
1425 
1426         sendNewProperty(frameProp);
1427         return true;
1428     }
1429 
1430     return false;
1431 }
1432 
1433 bool Camera::isStreamingEnabled()
1434 {
1435     if (HasVideoStream == false || !streamWindow)
1436         return false;
1437 
1438     return streamWindow->isStreamEnabled();
1439 }
1440 
1441 bool Camera::setSERNameDirectory(const QString &filename, const QString &directory)
1442 {
1443     auto tvp = getText("RECORD_FILE");
1444 
1445     if (!tvp)
1446         return false;
1447 
1448     auto filenameT = tvp->findWidgetByName("RECORD_FILE_NAME");
1449     auto dirT      = tvp->findWidgetByName("RECORD_FILE_DIR");
1450 
1451     if (!filenameT || !dirT)
1452         return false;
1453 
1454     filenameT->setText(filename.toLatin1().data());
1455     dirT->setText(directory.toLatin1().data());
1456 
1457     sendNewProperty(tvp);
1458 
1459     return true;
1460 }
1461 
1462 bool Camera::getSERNameDirectory(QString &filename, QString &directory)
1463 {
1464     auto tvp = getText("RECORD_FILE");
1465 
1466     if (!tvp)
1467         return false;
1468 
1469     auto filenameT = tvp->findWidgetByName("RECORD_FILE_NAME");
1470     auto dirT      = tvp->findWidgetByName("RECORD_FILE_DIR");
1471 
1472     if (!filenameT || !dirT)
1473         return false;
1474 
1475     filename  = QString(filenameT->getText());
1476     directory = QString(dirT->getText());
1477 
1478     return true;
1479 }
1480 
1481 bool Camera::startRecording()
1482 {
1483     auto svp = getSwitch("RECORD_STREAM");
1484 
1485     if (!svp)
1486         return false;
1487 
1488     auto recordON = svp->findWidgetByName("RECORD_ON");
1489 
1490     if (!recordON)
1491         return false;
1492 
1493     if (recordON->getState() == ISS_ON)
1494         return true;
1495 
1496     svp->reset();
1497     recordON->setState(ISS_ON);
1498 
1499     sendNewProperty(svp);
1500 
1501     return true;
1502 }
1503 
1504 bool Camera::startDurationRecording(double duration)
1505 {
1506     auto nvp = getNumber("RECORD_OPTIONS");
1507 
1508     if (!nvp)
1509         return false;
1510 
1511     auto durationN = nvp->findWidgetByName("RECORD_DURATION");
1512 
1513     if (!durationN)
1514         return false;
1515 
1516     auto svp = getSwitch("RECORD_STREAM");
1517 
1518     if (!svp)
1519         return false;
1520 
1521     auto recordON = svp->findWidgetByName("RECORD_DURATION_ON");
1522 
1523     if (!recordON)
1524         return false;
1525 
1526     if (recordON->getState() == ISS_ON)
1527         return true;
1528 
1529     durationN->setValue(duration);
1530     sendNewProperty(nvp);
1531 
1532     svp->reset();
1533     recordON->setState(ISS_ON);
1534 
1535     sendNewProperty(svp);
1536 
1537     return true;
1538 }
1539 
1540 bool Camera::startFramesRecording(uint32_t frames)
1541 {
1542     auto nvp = getNumber("RECORD_OPTIONS");
1543 
1544     if (!nvp)
1545         return false;
1546 
1547     auto frameN = nvp->findWidgetByName("RECORD_FRAME_TOTAL");
1548     auto svp = getSwitch("RECORD_STREAM");
1549 
1550     if (!frameN || !svp)
1551         return false;
1552 
1553     auto recordON = svp->findWidgetByName("RECORD_FRAME_ON");
1554 
1555     if (!recordON)
1556         return false;
1557 
1558     if (recordON->getState() == ISS_ON)
1559         return true;
1560 
1561     frameN->setValue(frames);
1562     sendNewProperty(nvp);
1563 
1564     svp->reset();
1565     recordON->setState(ISS_ON);
1566 
1567     sendNewProperty(svp);
1568 
1569     return true;
1570 }
1571 
1572 bool Camera::stopRecording()
1573 {
1574     auto svp = getSwitch("RECORD_STREAM");
1575 
1576     if (!svp)
1577         return false;
1578 
1579     auto recordOFF = svp->findWidgetByName("RECORD_OFF");
1580 
1581     if (!recordOFF)
1582         return false;
1583 
1584     // If already set
1585     if (recordOFF->getState() == ISS_ON)
1586         return true;
1587 
1588     svp->reset();
1589     recordOFF->setState(ISS_ON);
1590 
1591     sendNewProperty(svp);
1592 
1593     return true;
1594 }
1595 
1596 bool Camera::setFITSHeaders(const QList<FITSData::Record> &values)
1597 {
1598     auto tvp = getText("FITS_HEADER");
1599 
1600     // Only proceed if FITS header has 3 fields introduced with INDI v2.0.1
1601     if (!tvp || tvp->count() < 3)
1602         return false;
1603 
1604     for (auto &record : values)
1605     {
1606         tvp->at(0)->setText(record.key.toLatin1().constData());
1607         tvp->at(1)->setText(record.value.toString().toLatin1().constData());
1608         tvp->at(2)->setText(record.comment.toLatin1().constData());
1609 
1610         sendNewProperty(tvp);
1611     }
1612 
1613     return true;
1614 }
1615 
1616 bool Camera::setGain(double value)
1617 {
1618     if (!gainN)
1619         return false;
1620 
1621     gainN->value = value;
1622     sendNewProperty(gainN->nvp);
1623     return true;
1624 }
1625 
1626 bool Camera::getGain(double *value)
1627 {
1628     if (!gainN)
1629         return false;
1630 
1631     *value = gainN->value;
1632 
1633     return true;
1634 }
1635 
1636 bool Camera::getGainMinMaxStep(double *min, double *max, double *step)
1637 {
1638     if (!gainN)
1639         return false;
1640 
1641     *min  = gainN->min;
1642     *max  = gainN->max;
1643     *step = gainN->step;
1644 
1645     return true;
1646 }
1647 
1648 bool Camera::setOffset(double value)
1649 {
1650     if (!offsetN)
1651         return false;
1652 
1653     offsetN->value = value;
1654     sendNewProperty(offsetN->nvp);
1655     return true;
1656 }
1657 
1658 bool Camera::getOffset(double *value)
1659 {
1660     if (!offsetN)
1661         return false;
1662 
1663     *value = offsetN->value;
1664 
1665     return true;
1666 }
1667 
1668 bool Camera::getOffsetMinMaxStep(double *min, double *max, double *step)
1669 {
1670     if (!offsetN)
1671         return false;
1672 
1673     *min  = offsetN->min;
1674     *max  = offsetN->max;
1675     *step = offsetN->step;
1676 
1677     return true;
1678 }
1679 
1680 bool Camera::isBLOBEnabled()
1681 {
1682     return (m_Parent->getClientManager()->isBLOBEnabled(getDeviceName(), "CCD1"));
1683 }
1684 
1685 bool Camera::setBLOBEnabled(bool enable, const QString &prop)
1686 {
1687     m_Parent->getClientManager()->setBLOBEnabled(enable, getDeviceName(), prop);
1688 
1689     return true;
1690 }
1691 
1692 bool Camera::setFastExposureEnabled(bool enable)
1693 {
1694     // Set value immediately
1695     m_FastExposureEnabled = enable;
1696 
1697     auto svp = getSwitch("CCD_FAST_TOGGLE");
1698 
1699     if (!svp)
1700         return false;
1701 
1702     svp->at(0)->setState(enable ? ISS_ON : ISS_OFF);
1703     svp->at(1)->setState(enable ? ISS_OFF : ISS_ON);
1704     sendNewProperty(svp);
1705 
1706     return true;
1707 }
1708 
1709 bool Camera::setCaptureFormat(const QString &format)
1710 {
1711     auto svp = getSwitch("CCD_CAPTURE_FORMAT");
1712     if (!svp)
1713         return false;
1714 
1715     for (auto &oneSwitch : *svp)
1716         oneSwitch.setState(oneSwitch.label == format ? ISS_ON : ISS_OFF);
1717 
1718     sendNewProperty(svp);
1719     return true;
1720 }
1721 
1722 bool Camera::setFastCount(uint32_t count)
1723 {
1724     auto nvp = getNumber("CCD_FAST_COUNT");
1725 
1726     if (!nvp)
1727         return false;
1728 
1729     nvp->at(0)->setValue(count);
1730 
1731     sendNewProperty(nvp);
1732 
1733     return true;
1734 }
1735 
1736 bool Camera::setStreamExposure(double duration)
1737 {
1738     auto nvp = getNumber("STREAMING_EXPOSURE");
1739 
1740     if (!nvp)
1741         return false;
1742 
1743     nvp->at(0)->setValue(duration);
1744 
1745     sendNewProperty(nvp);
1746 
1747     return true;
1748 }
1749 
1750 bool Camera::getStreamExposure(double *duration)
1751 {
1752     auto nvp = getNumber("STREAMING_EXPOSURE");
1753 
1754     if (!nvp)
1755         return false;
1756 
1757     *duration = nvp->at(0)->getValue();
1758 
1759     return true;
1760 }
1761 
1762 bool Camera::isCoolerOn()
1763 {
1764     auto svp = getSwitch("CCD_COOLER");
1765 
1766     if (!svp)
1767         return false;
1768 
1769     return (svp->at(0)->getState() == ISS_ON);
1770 }
1771 
1772 bool Camera::getTemperatureRegulation(double &ramp, double &threshold)
1773 {
1774     auto regulation = getProperty("CCD_TEMP_RAMP");
1775     if (!regulation.isValid())
1776         return false;
1777 
1778     ramp = regulation.getNumber()->at(0)->getValue();
1779     threshold = regulation.getNumber()->at(1)->getValue();
1780     return true;
1781 }
1782 
1783 bool Camera::setTemperatureRegulation(double ramp, double threshold)
1784 {
1785     auto regulation = getProperty("CCD_TEMP_RAMP");
1786     if (!regulation.isValid())
1787         return false;
1788 
1789     regulation.getNumber()->at(0)->setValue(ramp);
1790     regulation.getNumber()->at(1)->setValue(threshold);
1791     sendNewProperty(regulation.getNumber());
1792     return true;
1793 }
1794 
1795 bool Camera::setScopeInfo(double focalLength, double aperture)
1796 {
1797     auto scopeInfo = getProperty("SCOPE_INFO");
1798     if (!scopeInfo.isValid())
1799         return false;
1800 
1801     auto nvp = scopeInfo.getNumber();
1802     nvp->at(0)->setValue(focalLength);
1803     nvp->at(1)->setValue(aperture);
1804     sendNewProperty(nvp);
1805     return true;
1806 }
1807 
1808 // Internal function to write an image blob to disk.
1809 bool Camera::WriteImageFileInternal(const QString &filename, char *buffer, const size_t size)
1810 {
1811     QFile file(filename);
1812     if (!file.open(QIODevice::WriteOnly))
1813     {
1814         qCCritical(KSTARS_INDI) << "ISD:CCD Error: Unable to open write file: " <<
1815                                 filename;
1816         return false;
1817     }
1818     int n = 0;
1819     QDataStream out(&file);
1820     bool ok = true;
1821     for (size_t nr = 0; nr < size; nr += n)
1822     {
1823         n = out.writeRawData(buffer + nr, size - nr);
1824         if (n < 0)
1825         {
1826             ok = false;
1827             break;
1828         }
1829     }
1830     ok = file.flush() && ok;
1831     file.close();
1832     file.setPermissions(QFileDevice::ReadUser |
1833                         QFileDevice::WriteUser |
1834                         QFileDevice::ReadGroup |
1835                         QFileDevice::ReadOther);
1836     return ok;
1837 }
1838 
1839 QString Camera::getCaptureFormat() const
1840 {
1841     if (m_CaptureFormatIndex < 0 || m_CaptureFormats.isEmpty() || m_CaptureFormatIndex >= m_CaptureFormats.size())
1842         return QLatin1String("NA");
1843 
1844     return m_CaptureFormats[m_CaptureFormatIndex];
1845 }
1846 
1847 void Camera::setStretchValues(double shadows, double midtones, double highlights)
1848 {
1849     if (Options::useFITSViewer() == false || normalTabID < 0)
1850         return;
1851 
1852     auto tab = getFITSViewer()->tabs().at(normalTabID);
1853 
1854     if (!tab)
1855         return;
1856 
1857     tab->setStretchValues(shadows, midtones, highlights);
1858 }
1859 
1860 void Camera::setAutoStretch()
1861 {
1862     if (Options::useFITSViewer() == false || normalTabID < 0)
1863         return;
1864 
1865     auto tab = getFITSViewer()->tabs().at(normalTabID);
1866 
1867     if (!tab)
1868         return;
1869 
1870     auto view = tab->getView();
1871 
1872     if (!view->getAutoStretch())
1873         view->setAutoStretchParams();
1874 }
1875 
1876 void Camera::toggleHiPSOverlay()
1877 {
1878     if (Options::useFITSViewer() == false || normalTabID < 0)
1879         return;
1880 
1881     auto tab = getFITSViewer()->tabs().at(normalTabID);
1882 
1883     if (!tab)
1884         return;
1885 
1886     auto view = tab->getView();
1887 
1888     view->toggleHiPSOverlay();
1889 }
1890 }