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 = ⁢ 0255 gainPerm = controlNP->getPermission(); 0256 } 0257 else if (name == "offset" || label == "offset") 0258 { 0259 offsetN = ⁢ 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 }