Kstars

indicamera.cpp
1/*
2 SPDX-FileCopyrightText: 2012 Jasem Mutlaq <mutlaqja@ikarustech.com>
3
4 SPDX-License-Identifier: GPL-2.0-or-later
5*/
6
7#include "indicamera.h"
8#include "indicamerachip.h"
9
10#include "config-kstars.h"
11
12#include "indi_debug.h"
13
14#include "clientmanager.h"
15#include "kstars.h"
16#include "Options.h"
17#include "streamwg.h"
18//#include "ekos/manager.h"
19#ifdef HAVE_CFITSIO
20#include "fitsviewer/fitsdata.h"
21#include "fitsviewer/fitstab.h"
22#endif
23
24#include <KNotifications/KNotification>
25#include "auxiliary/ksmessagebox.h"
26#include "ksnotification.h"
27#include <QImageReader>
28#include <QFileInfo>
29#include <QStatusBar>
30#include <QtConcurrent>
31
32#include <basedevice.h>
33
34const QStringList RAWFormats = { "cr2", "cr3", "crw", "nef", "raf", "dng", "arw", "orf" };
35
36const QString &getFITSModeStringString(FITSMode mode)
37{
38 return FITSModes[mode];
39}
40
41namespace ISD
42{
43
44Camera::Camera(GenericDevice *parent) : ConcreteDevice(parent)
45{
46 primaryChip.reset(new CameraChip(this, CameraChip::PRIMARY_CCD));
47
48 m_Media.reset(new WSMedia(this));
49 connect(m_Media.get(), &WSMedia::newFile, this, &Camera::setWSBLOB);
50
51 connect(m_Parent->getClientManager(), &ClientManager::newBLOBManager, this, &Camera::setBLOBManager, Qt::UniqueConnection);
52 m_LastNotificationTS = QDateTime::currentDateTime();
53}
54
55Camera::~Camera()
56{
57 if (m_ImageViewerWindow)
58 m_ImageViewerWindow->close();
59 if (fileWriteThread.isRunning())
60 fileWriteThread.waitForFinished();
61 if (fileWriteBuffer != nullptr)
62 delete [] fileWriteBuffer;
63}
64
65void Camera::setBLOBManager(const char *device, INDI::Property prop)
66{
67 if (!prop.getRegistered())
68 return;
69
70 if (getDeviceName() == device)
71 emit newBLOBManager(prop);
72}
73
74void Camera::registerProperty(INDI::Property prop)
75{
76 if (prop.isNameMatch("GUIDER_EXPOSURE"))
77 {
78 HasGuideHead = true;
79 guideChip.reset(new CameraChip(this, CameraChip::GUIDE_CCD));
80 }
81 else if (prop.isNameMatch("CCD_FRAME_TYPE"))
82 {
83 primaryChip->clearFrameTypes();
84
85 for (auto &it : *prop.getSwitch())
86 primaryChip->addFrameLabel(it.getLabel());
87 }
88 else if (prop.isNameMatch("CCD_FRAME"))
89 {
90 auto np = prop.getNumber();
91 if (np && np->getPermission() != IP_RO)
92 primaryChip->setCanSubframe(true);
93 }
94 else if (prop.isNameMatch("GUIDER_FRAME"))
95 {
96 auto np = prop.getNumber();
97 if (np && np->getPermission() != IP_RO)
98 guideChip->setCanSubframe(true);
99 }
100 else if (prop.isNameMatch("CCD_BINNING"))
101 {
102 auto np = prop.getNumber();
103 if (np && np->getPermission() != IP_RO)
104 primaryChip->setCanBin(true);
105 }
106 else if (prop.isNameMatch("GUIDER_BINNING"))
107 {
108 auto np = prop.getNumber();
109 if (np && np->getPermission() != IP_RO)
110 guideChip->setCanBin(true);
111 }
112 else if (prop.isNameMatch("CCD_ABORT_EXPOSURE"))
113 {
114 auto sp = prop.getSwitch();
115 if (sp && sp->getPermission() != IP_RO)
116 primaryChip->setCanAbort(true);
117 }
118 else if (prop.isNameMatch("GUIDER_ABORT_EXPOSURE"))
119 {
120 auto sp = prop.getSwitch();
121 if (sp && sp->getPermission() != IP_RO)
122 guideChip->setCanAbort(true);
123 }
124 else if (prop.isNameMatch("CCD_TEMPERATURE"))
125 {
126 auto np = prop.getNumber();
127 HasCooler = true;
128 CanCool = (np->getPermission() != IP_RO);
129 if (np)
130 emit newTemperatureValue(np->at(0)->getValue());
131 }
132 else if (prop.isNameMatch("CCD_COOLER"))
133 {
134 // Can turn cooling on/off
135 HasCoolerControl = true;
136 }
137 else if (prop.isNameMatch("CCD_VIDEO_STREAM"))
138 {
139 // Has Video Stream
140 HasVideoStream = true;
141 }
142 else if (prop.isNameMatch("CCD_CAPTURE_FORMAT"))
143 {
144 auto sp = prop.getSwitch();
145 if (sp)
146 {
147 m_CaptureFormats.clear();
148 for (const auto &oneSwitch : *sp)
149 m_CaptureFormats << oneSwitch.getLabel();
150
151 m_CaptureFormatIndex = sp->findOnSwitchIndex();
152 }
153 }
154 else if (prop.isNameMatch("CCD_TRANSFER_FORMAT"))
155 {
156 auto sp = prop.getSwitch();
157 if (sp)
158 {
159 m_EncodingFormats.clear();
160 for (const auto &oneSwitch : *sp)
161 m_EncodingFormats << oneSwitch.getLabel();
162
163 auto format = sp->findOnSwitch();
164 if (format)
165 m_EncodingFormat = format->label;
166 }
167 }
168 else if (prop.isNameMatch("CCD_EXPOSURE_PRESETS"))
169 {
170 auto svp = prop.getSwitch();
171 if (svp)
172 {
173 bool ok = false;
174 auto separator = QDir::separator();
175 for (const auto &it : *svp)
176 {
177 QString key = QString(it.getLabel());
178 double value = key.toDouble(&ok);
179 if (!ok)
180 {
181 QStringList parts = key.split(separator);
182 if (parts.count() == 2)
183 {
184 bool numOk = false, denOk = false;
185 double numerator = parts[0].toDouble(&numOk);
186 double denominator = parts[1].toDouble(&denOk);
187 if (numOk && denOk && denominator > 0)
188 {
189 ok = true;
190 value = numerator / denominator;
191 }
192 }
193 }
194 if (ok)
195 m_ExposurePresets.insert(key, value);
196
197 double min = 1e6, max = 1e-6;
198 for (auto oneValue : m_ExposurePresets.values())
199 {
200 if (oneValue < min)
201 min = oneValue;
202 if (oneValue > max)
203 max = oneValue;
204 }
205 m_ExposurePresetsMinMax = qMakePair<double, double>(min, max);
206 }
207 }
208 }
209 else if (prop.isNameMatch("CCD_FAST_TOGGLE"))
210 {
211 auto sp = prop.getSwitch();
212 if (sp)
213 m_FastExposureEnabled = sp->findOnSwitchIndex() == 0;
214 else
215 m_FastExposureEnabled = false;
216 }
217 else if (prop.isNameMatch("TELESCOPE_TYPE"))
218 {
219 auto sp = prop.getSwitch();
220 if (sp)
221 {
222 auto format = sp->findWidgetByName("TELESCOPE_PRIMARY");
223 if (format && format->getState() == ISS_ON)
224 telescopeType = TELESCOPE_PRIMARY;
225 else
226 telescopeType = TELESCOPE_GUIDE;
227 }
228 }
229 else if (prop.isNameMatch("CCD_WEBSOCKET_SETTINGS"))
230 {
231 auto np = prop.getNumber();
232 m_Media->setURL(QUrl(QString("ws://%1:%2").arg(m_Parent->getClientManager()->getHost()).arg(np->at(0)->getValue())));
233 m_Media->connectServer();
234 }
235 else if (prop.isNameMatch("CCD1"))
236 {
237 primaryCCDBLOB = prop;
238 }
239 // try to find gain and/or offset property, if any
240 else if ( (gainN == nullptr || offsetN == nullptr) && prop.getType() == INDI_NUMBER)
241 {
242 // Since gain is spread among multiple property depending on the camera providing it
243 // we need to search in all possible number properties
244 auto controlNP = prop.getNumber();
245 if (controlNP)
246 {
247 for (auto &it : *controlNP)
248 {
249 QString name = QString(it.getName()).toLower();
250 QString label = QString(it.getLabel()).toLower();
251
252 if (name == "gain" || label == "gain")
253 {
254 gainN = &it;
255 gainPerm = controlNP->getPermission();
256 }
257 else if (name == "offset" || label == "offset")
258 {
259 offsetN = &it;
260 offsetPerm = controlNP->getPermission();
261 }
262 }
263 }
264 }
265
266 ConcreteDevice::registerProperty(prop);
267}
268
269void Camera::removeProperty(INDI::Property prop)
270{
271 if (prop.isNameMatch("CCD_WEBSOCKET_SETTINGS"))
272 {
273 m_Media->disconnectServer();
274 }
275}
276
277void Camera::processNumber(INDI::Property prop)
278{
279 auto nvp = prop.getNumber();
280 if (nvp->isNameMatch("CCD_EXPOSURE"))
281 {
282 auto np = nvp->findWidgetByName("CCD_EXPOSURE_VALUE");
283 if (np)
284 emit newExposureValue(primaryChip.get(), np->getValue(), nvp->getState());
285 if (nvp->getState() == IPS_ALERT)
286 emit error(ERROR_CAPTURE);
287 }
288 else if (prop.isNameMatch("CCD_TEMPERATURE"))
289 {
290 HasCooler = true;
291 auto np = nvp->findWidgetByName("CCD_TEMPERATURE_VALUE");
292 if (np)
293 emit newTemperatureValue(np->getValue());
294 }
295 else if (prop.isNameMatch("GUIDER_EXPOSURE"))
296 {
297 auto np = nvp->findWidgetByName("GUIDER_EXPOSURE_VALUE");
298 if (np)
299 emit newExposureValue(guideChip.get(), np->getValue(), nvp->getState());
300 }
301 else if (prop.isNameMatch("FPS"))
302 {
303 emit newFPS(nvp->at(0)->getValue(), nvp->at(1)->getValue());
304 }
305 else if (prop.isNameMatch("CCD_RAPID_GUIDE_DATA"))
306 {
307 if (nvp->getState() == IPS_ALERT)
308 {
309 emit newGuideStarData(primaryChip.get(), -1, -1, -1);
310 }
311 else
312 {
313 double dx = -1, dy = -1, fit = -1;
314
315 auto np = nvp->findWidgetByName("GUIDESTAR_X");
316 if (np)
317 dx = np->getValue();
318 np = nvp->findWidgetByName("GUIDESTAR_Y");
319 if (np)
320 dy = np->getValue();
321 np = nvp->findWidgetByName("GUIDESTAR_FIT");
322 if (np)
323 fit = np->getValue();
324
325 if (dx >= 0 && dy >= 0 && fit >= 0)
326 emit newGuideStarData(primaryChip.get(), dx, dy, fit);
327 }
328 }
329 else if (prop.isNameMatch("GUIDER_RAPID_GUIDE_DATA"))
330 {
331 if (nvp->getState() == IPS_ALERT)
332 {
333 emit newGuideStarData(guideChip.get(), -1, -1, -1);
334 }
335 else
336 {
337 double dx = -1, dy = -1, fit = -1;
338 auto np = nvp->findWidgetByName("GUIDESTAR_X");
339 if (np)
340 dx = np->getValue();
341 np = nvp->findWidgetByName("GUIDESTAR_Y");
342 if (np)
343 dy = np->getValue();
344 np = nvp->findWidgetByName("GUIDESTAR_FIT");
345 if (np)
346 fit = np->getValue();
347
348 if (dx >= 0 && dy >= 0 && fit >= 0)
349 emit newGuideStarData(guideChip.get(), dx, dy, fit);
350 }
351 }
352}
353
354void Camera::processSwitch(INDI::Property prop)
355{
356 auto svp = prop.getSwitch();
357
358 if (svp->isNameMatch("CCD_COOLER"))
359 {
360 // Can turn cooling on/off
361 HasCoolerControl = true;
362 emit coolerToggled(svp->sp[0].s == ISS_ON);
363 }
364 else if (QString(svp->getName()).endsWith("VIDEO_STREAM"))
365 {
366 // If BLOB is not enabled for this camera, then ignore all VIDEO_STREAM calls.
367 if (isBLOBEnabled() == false || m_StreamingEnabled == false)
368 return;
369
370 HasVideoStream = true;
371
372 if (!streamWindow && svp->sp[0].s == ISS_ON)
373 {
374 streamWindow.reset(new StreamWG(this));
375
376 INumberVectorProperty *streamFrame = getNumber("CCD_STREAM_FRAME");
377 INumber *w = nullptr, *h = nullptr;
378
379 if (streamFrame)
380 {
381 w = IUFindNumber(streamFrame, "WIDTH");
382 h = IUFindNumber(streamFrame, "HEIGHT");
383 }
384
385 if (w && h)
386 {
387 streamW = w->value;
388 streamH = h->value;
389 }
390 else
391 {
392 // Only use CCD dimensions if we are receiving raw stream and not stream of images (i.e. mjpeg..etc)
393 auto rawBP = getBLOB("CCD1");
394 if (rawBP)
395 {
396 int x = 0, y = 0, w = 0, h = 0;
397 int binx = 0, biny = 0;
398
399 primaryChip->getFrame(&x, &y, &w, &h);
400 primaryChip->getBinning(&binx, &biny);
401 streamW = w / binx;
402 streamH = h / biny;
403 }
404 }
405
406 streamWindow->setSize(streamW, streamH);
407 }
408
409 if (streamWindow)
410 {
411 connect(streamWindow.get(), &StreamWG::hidden, this, &Camera::StreamWindowHidden, Qt::UniqueConnection);
412 connect(streamWindow.get(), &StreamWG::imageChanged, this, &Camera::newVideoFrame, Qt::UniqueConnection);
413
414 streamWindow->enableStream(svp->sp[0].s == ISS_ON);
415 emit videoStreamToggled(svp->sp[0].s == ISS_ON);
416 }
417 }
418 else if (svp->isNameMatch("CCD_CAPTURE_FORMAT"))
419 {
420 m_CaptureFormats.clear();
421 for (int i = 0; i < svp->nsp; i++)
422 {
423 m_CaptureFormats << svp->sp[i].label;
424 if (svp->sp[i].s == ISS_ON)
425 m_CaptureFormatIndex = i;
426 }
427 }
428 else if (svp->isNameMatch("CCD_TRANSFER_FORMAT"))
429 {
430 ISwitch *format = IUFindOnSwitch(svp);
431 if (format)
432 m_EncodingFormat = format->label;
433 }
434 else if (svp->isNameMatch("RECORD_STREAM"))
435 {
436 ISwitch *recordOFF = IUFindSwitch(svp, "RECORD_OFF");
437
438 if (recordOFF && recordOFF->s == ISS_ON)
439 {
440 emit videoRecordToggled(false);
441 KSNotification::event(QLatin1String("IndiServerMessage"), i18n("Video Recording Stopped"), KSNotification::INDI);
442 }
443 else
444 {
445 emit videoRecordToggled(true);
446 KSNotification::event(QLatin1String("IndiServerMessage"), i18n("Video Recording Started"), KSNotification::INDI);
447 }
448 }
449 else if (svp->isNameMatch("TELESCOPE_TYPE"))
450 {
451 ISwitch *format = IUFindSwitch(svp, "TELESCOPE_PRIMARY");
452 if (format && format->s == ISS_ON)
453 telescopeType = TELESCOPE_PRIMARY;
454 else
455 telescopeType = TELESCOPE_GUIDE;
456 }
457 else if (!strcmp(svp->name, "CCD_FAST_TOGGLE"))
458 {
459 m_FastExposureEnabled = IUFindOnSwitchIndex(svp) == 0;
460 }
461 else if (svp->isNameMatch("CONNECTION"))
462 {
463 auto dSwitch = svp->findWidgetByName("DISCONNECT");
464
465 if (dSwitch && dSwitch->getState() == ISS_ON)
466 {
467 if (streamWindow)
468 {
469 streamWindow->enableStream(false);
470 emit videoStreamToggled(false);
471 streamWindow->close();
472 streamWindow.reset();
473 }
474
475 // Clear the pointers on disconnect.
476 gainN = nullptr;
477 offsetN = nullptr;
478 primaryCCDBLOB = INDI::Property();
479 }
480 }
481}
482
483void Camera::processText(INDI::Property prop)
484{
485 auto tvp = prop.getText();
486 if (tvp->isNameMatch("CCD_FILE_PATH"))
487 {
488 auto filepath = tvp->findWidgetByName("FILE_PATH");
489 if (filepath)
490 emit newRemoteFile(QString(filepath->getText()));
491 }
492}
493
494void Camera::setWSBLOB(const QByteArray &message, const QString &extension)
495{
496 if (!primaryCCDBLOB)
497 return;
498
499 auto bvp = primaryCCDBLOB.getBLOB();
500 auto bp = bvp->at(0);
501
502 bp->setBlob(const_cast<char *>(message.data()));
503 bp->setSize(message.size());
504 bp->setFormat(extension.toLatin1().constData());
505 processBLOB(primaryCCDBLOB);
506
507 // Disassociate
508 bp->setBlob(nullptr);
509}
510
511void Camera::processStream(INDI::Property prop)
512{
513 if (!streamWindow || streamWindow->isStreamEnabled() == false)
514 return;
515
516 INumberVectorProperty *streamFrame = getNumber("CCD_STREAM_FRAME");
517 INumber *w = nullptr, *h = nullptr;
518
519 if (streamFrame)
520 {
521 w = IUFindNumber(streamFrame, "WIDTH");
522 h = IUFindNumber(streamFrame, "HEIGHT");
523 }
524
525 if (w && h)
526 {
527 streamW = w->value;
528 streamH = h->value;
529 }
530 else
531 {
532 int x = 0, y = 0, w = 0, h = 0;
533 int binx = 1, biny = 1;
534
535 primaryChip->getFrame(&x, &y, &w, &h);
536 primaryChip->getBinning(&binx, &biny);
537 streamW = w / binx;
538 streamH = h / biny;
539 }
540
541 streamWindow->setSize(streamW, streamH);
542
543 streamWindow->show();
544 streamWindow->newFrame(prop);
545}
546
547bool Camera::generateFilename(bool batch_mode, const QString &extension, QString *filename)
548{
549
550 *filename = placeholderPath.generateOutputFilename(true, batch_mode, nextSequenceID, extension, "");
551
552 QDir currentDir = QFileInfo(*filename).dir();
553 if (currentDir.exists() == false)
554 QDir().mkpath(currentDir.path());
555
556 // Check if the file exists. We try not to overwrite capture files.
557 if (QFile::exists(*filename))
558 {
559 QString oldFilename = *filename;
560 *filename = placeholderPath.repairFilename(*filename);
561 if (filename != oldFilename)
562 qCWarning(KSTARS_INDI) << "File over-write detected: changing" << oldFilename << "to" << *filename;
563 else
564 qCWarning(KSTARS_INDI) << "File over-write detected for" << oldFilename << "but could not correct filename";
565 }
566
567 QFile test_file(*filename);
569 return false;
570 test_file.flush();
571 test_file.close();
572 return true;
573}
574
575bool Camera::writeImageFile(const QString &filename, INDI::Property prop, bool is_fits)
576{
577 // TODO: Not yet threading the writes for non-fits files.
578 // Would need to deal with the raw conversion, etc.
579 if (is_fits)
580 {
581 // Check if the last write is still ongoing, and if so wait.
582 // It is using the fileWriteBuffer.
583 if (fileWriteThread.isRunning())
584 {
585 fileWriteThread.waitForFinished();
586 }
587
588 // Wait until the file is written before overwritting the filename.
589 fileWriteFilename = filename;
590
591 // Will write blob data in a separate thread, and can't depend on the blob
592 // memory, so copy it first.
593
594 auto bp = prop.getBLOB()->at(0);
595 // Check buffer size.
596 if (fileWriteBufferSize != bp->getBlobLen())
597 {
598 if (fileWriteBuffer != nullptr)
599 delete [] fileWriteBuffer;
600 fileWriteBufferSize = bp->getBlobLen();
601 fileWriteBuffer = new char[fileWriteBufferSize];
602 }
603
604 // Copy memory, and write file on a separate thread.
605 // Probably too late to return an error if the file couldn't write.
606 memcpy(fileWriteBuffer, bp->getBlob(), bp->getBlobLen());
607 fileWriteThread = QtConcurrent::run(this, &ISD::Camera::WriteImageFileInternal, fileWriteFilename, fileWriteBuffer,
608 bp->getBlobLen());
609 }
610 else
611 {
612 auto bp = prop.getBLOB()->at(0);
613 if (!WriteImageFileInternal(filename, static_cast<char*>(bp->getBlob()), bp->getBlobLen()))
614 return false;
615 }
616 return true;
617}
618
619// Get or Create FITSViewer if we are using FITSViewer
620// or if capture mode is calibrate since for now we are forced to open the file in the viewer
621// this should be fixed in the future and should only use FITSData
622QSharedPointer<FITSViewer> Camera::getFITSViewer()
623{
624 // if the FITS viewer exists, return it
625 if (!m_FITSViewerWindow.isNull() && ! m_FITSViewerWindow.isNull())
626 return m_FITSViewerWindow;
627
628 // otherwise, create it
629 normalTabID = calibrationTabID = focusTabID = guideTabID = alignTabID = -1;
630
631 m_FITSViewerWindow = KStars::Instance()->createFITSViewer();
632
633 // Check if ONE tab of the viewer was closed.
634 connect(m_FITSViewerWindow.get(), &FITSViewer::closed, this, [this](int tabIndex)
635 {
636 if (tabIndex == normalTabID)
637 normalTabID = -1;
638 else if (tabIndex == calibrationTabID)
639 calibrationTabID = -1;
640 else if (tabIndex == focusTabID)
641 focusTabID = -1;
642 else if (tabIndex == guideTabID)
643 guideTabID = -1;
644 else if (tabIndex == alignTabID)
645 alignTabID = -1;
646 });
647
648 // If FITS viewer was completed closed. Reset everything
649 connect(m_FITSViewerWindow.get(), &FITSViewer::terminated, this, [this]()
650 {
651 normalTabID = -1;
652 calibrationTabID = -1;
653 focusTabID = -1;
654 guideTabID = -1;
655 alignTabID = -1;
656 m_FITSViewerWindow.clear();
657 });
658
659 return m_FITSViewerWindow;
660}
661
662bool Camera::processBLOB(INDI::Property prop)
663{
664 auto bvp = prop.getBLOB();
665 // Ignore write-only BLOBs since we only receive it for state-change
666 if (bvp->getPermission() == IP_WO || bvp->at(0)->getSize() == 0)
667 return false;
668
669 BType = BLOB_OTHER;
670
671 auto bp = bvp->at(0);
672
673 auto format = QString(bp->getFormat()).toLower();
674
675 // If stream, process it first
676 if (format.contains("stream"))
677 {
678 if (m_StreamingEnabled == false)
679 return true;
680 else if (streamWindow)
681 processStream(prop);
682 return true;
683 }
684
685 // Format without leading . (.jpg --> jpg)
686 QString shortFormat = format.mid(1);
687
688 // If it's not FITS or an image, don't process it.
689 if ((QImageReader::supportedImageFormats().contains(shortFormat.toLatin1())))
690 BType = BLOB_IMAGE;
691 else if (format.contains("fits"))
692 BType = BLOB_FITS;
693 else if (format.contains("xisf"))
694 BType = BLOB_XISF;
695 else if (RAWFormats.contains(shortFormat))
696 BType = BLOB_RAW;
697
698 if (BType == BLOB_OTHER)
699 {
700 emit newImage(nullptr);
701 return false;
702 }
703
704 CameraChip *targetChip = nullptr;
705
706 if (bvp->isNameMatch("CCD2"))
707 targetChip = guideChip.get();
708 else
709 {
710 targetChip = primaryChip.get();
711 qCDebug(KSTARS_INDI) << "Image received. Mode:" << getFITSModeStringString(targetChip->getCaptureMode()) << "Size:" <<
712 bp->getSize();
713 }
714
715 // Create temporary name if ANY of the following conditions are met:
716 // 1. file is preview or batch mode is not enabled
717 // 2. file type is not FITS_NORMAL (focus, guide..etc)
718 QString filename;
719#if 0
720
721 if (targetChip->isBatchMode() == false || targetChip->getCaptureMode() != FITS_NORMAL)
722 {
723 if (!writeTempImageFile(format, static_cast<char *>(bp->blob), bp->size, &filename))
724 {
725 emit BLOBUpdated(nullptr);
726 return;
727 }
728 if (BType == BLOB_FITS)
729 addFITSKeywords(filename, filter);
730
731 }
732#endif
733 // Create file name for sequences.
734 if (targetChip->isBatchMode() && targetChip->getCaptureMode() != FITS_CALIBRATE)
735 {
736 // If either generating file name or writing the image file fails
737 // then return
738 if (!generateFilename(targetChip->isBatchMode(), format, &filename) ||
739 !writeImageFile(filename, prop, BType == BLOB_FITS))
740 {
741 connect(KSMessageBox::Instance(), &KSMessageBox::accepted, this, [ = ]()
742 {
743 KSMessageBox::Instance()->disconnect(this);
744 emit error(ERROR_SAVE);
745 });
746 KSMessageBox::Instance()->error(i18n("Failed writing image to %1\nPlease check folder, filename & permissions.",
747 filename),
748 i18n("Image Write Failed"), 30);
749
750 emit propertyUpdated(prop);
751 return true;
752 }
753 }
754 else
755 filename = QDir::tempPath() + QDir::separator() + "image" + format;
756
757 if (targetChip->getCaptureMode() == FITS_NORMAL && targetChip->isBatchMode() == true)
758 {
759 KStars::Instance()->statusBar()->showMessage(i18n("%1 file saved to %2", shortFormat.toUpper(), filename), 0);
760 qCInfo(KSTARS_INDI) << shortFormat.toUpper() << "file saved to" << filename;
761 }
762
763 // Don't spam, just one notification per 3 seconds
764 if (QDateTime::currentDateTime().secsTo(m_LastNotificationTS) <= -3)
765 {
766 KNotification::event(QLatin1String("FITSReceived"), i18n("Image file is received"));
767 m_LastNotificationTS = QDateTime::currentDateTime();
768 }
769
770 // Check if we need to process RAW or regular image. Anything but FITS.
771#if 0
772 if (BType == BLOB_IMAGE || BType == BLOB_RAW)
773 {
774 bool useFITSViewer = Options::autoImageToFITS() &&
775 (Options::useFITSViewer() || (Options::useDSLRImageViewer() == false && targetChip->isBatchMode() == false));
776 bool useDSLRViewer = (Options::useDSLRImageViewer() || targetChip->isBatchMode() == false);
777 // For raw image, we only process them to JPG if we need to open them in the image viewer
778 if (BType == BLOB_RAW && (useFITSViewer || useDSLRViewer))
779 {
780 QString rawFileName = filename;
781 rawFileName = rawFileName.remove(0, rawFileName.lastIndexOf(QLatin1String("/")));
782
785
786 imgPreview.setAutoRemove(false);
787 imgPreview.open();
788 imgPreview.close();
791
792 if (KSUtils::RAWToJPEG(filename, preview_filename, errorMessage) == false)
793 {
794 KStars::Instance()->statusBar()->showMessage(errorMessage);
795 emit BLOBUpdated(bp);
796 return;
797 }
798
799 // Remove tempeorary CR2 files
800 if (targetChip->isBatchMode() == false)
801 QFile::remove(filename);
802
803 filename = preview_filename;
804 format = ".jpg";
805 shortFormat = "jpg";
806 }
807
808 // Convert to FITS if checked.
809 QString output;
810 if (useFITSViewer && (FITSData::ImageToFITS(filename, shortFormat, output)))
811 {
812 if (BType == BLOB_RAW || targetChip->isBatchMode() == false)
813 QFile::remove(filename);
814
815 filename = output;
816 BType = BLOB_FITS;
817
819
820 FITSData *blob_fits_data = new FITSData(targetChip->getCaptureMode());
821
822 QFuture<bool> fitsloader = blob_fits_data->loadFromFile(filename, false);
823 fitsloader.waitForFinished();
824 if (!fitsloader.result())
825 {
826 // If reading the blob fails, we treat it the same as exposure failure
827 // and recapture again if possible
828 delete (blob_fits_data);
829 qCCritical(KSTARS_INDI) << "failed reading FITS memory buffer";
830 emit newExposureValue(targetChip, 0, IPS_ALERT);
831 return;
832 }
833 displayFits(targetChip, filename, bp, blob_fits_data);
834 return;
835 }
836 else if (useDSLRViewer)
837 {
838 if (m_ImageViewerWindow.isNull())
839 m_ImageViewerWindow = new ImageViewer(getDeviceName(), KStars::Instance());
840
841 m_ImageViewerWindow->loadImage(filename);
842
843 emit previewJPEGGenerated(filename, m_ImageViewerWindow->metadata());
844 }
845 }
846#endif
847
848 // Load FITS if either:
849 // #1 FITS Viewer is set to enabled.
850 // #2 This is a preview, so we MUST open FITS Viewer even if disabled.
851 // if (BType == BLOB_FITS)
852 // {
853 // Don't display image if the following conditions are met:
854 // 1. Mode is NORMAL or CALIBRATE; and
855 // 2. FITS Viewer is disabled; and
856 // 3. Batch mode is enabled.
857 // 4. Summary view is false.
858 if (targetChip->getCaptureMode() == FITS_NORMAL &&
859 Options::useFITSViewer() == false &&
860 Options::useSummaryPreview() == false &&
861 targetChip->isBatchMode())
862 {
863 emit propertyUpdated(prop);
864 emit newImage(nullptr);
865 return true;
866 }
867
868 QByteArray buffer = QByteArray::fromRawData(reinterpret_cast<char *>(bp->getBlob()), bp->getSize());
869 QSharedPointer<FITSData> imageData;
870 imageData.reset(new FITSData(targetChip->getCaptureMode()), &QObject::deleteLater);
871 if (!imageData->loadFromBuffer(buffer, shortFormat, filename))
872 {
873 emit error(ERROR_LOAD);
874 return true;
875 }
876
877 handleImage(targetChip, filename, prop, imageData);
878 return true;
879}
880
881void Camera::handleImage(CameraChip *targetChip, const QString &filename, INDI::Property prop,
883{
884 FITSMode captureMode = targetChip->getCaptureMode();
885 auto bp = prop.getBLOB()->at(0);
886
887 // Add metadata
888 data->setProperty("device", getDeviceName());
889 data->setProperty("blobVector", prop.getName());
890 data->setProperty("blobElement", bp->getName());
891 data->setProperty("chip", targetChip->getType());
892 // Retain a copy
893 targetChip->setImageData(data);
894
895 switch (captureMode)
896 {
897 case FITS_NORMAL:
898 case FITS_CALIBRATE:
899 {
900 if (Options::useFITSViewer())
901 {
902 // No need to wait until the image is loaded in the view, but emit AFTER checking
903 // batch mode, since newImage() may change it
904 emit propertyUpdated(prop);
905 emit newImage(data);
906
907 bool success = false;
908 int tabIndex = -1;
909 int *tabID = &normalTabID;
910 QUrl fileURL = QUrl::fromLocalFile(filename);
911 FITSScale captureFilter = targetChip->getCaptureFilter();
912 if (*tabID == -1 || Options::singlePreviewFITS() == false)
913 {
914 // If image is preview and we should display all captured images in a
915 // single tab called "Preview", then set the title to "Preview",
916 // Otherwise, the title will be the captured image name
918 if (Options::singlePreviewFITS())
919 {
920 // If we are displaying all images from all cameras in a single FITS
921 // Viewer window, then we prefix the camera name to the "Preview" string
922 if (Options::singleWindowCapturedFITS())
923 previewTitle = i18n("%1 Preview", getDeviceName());
924 else
925 // Otherwise, just use "Preview"
926 previewTitle = i18n("Preview");
927 }
928
929 success = getFITSViewer()->loadData(data, fileURL, &tabIndex, captureMode, captureFilter, previewTitle);
930
931 //Setup any necessary connections
932 auto tabs = getFITSViewer()->tabs();
933 if (tabIndex < tabs.size() && captureMode == FITS_NORMAL)
934 {
935 emit newView(tabs[tabIndex]->getView());
936 tabs[tabIndex]->disconnect(this);
937 connect(tabs[tabIndex].get(), &FITSTab::updated, this, [this]
938 {
939 auto tab = qobject_cast<FITSTab *>(sender());
940 emit newView(tab->getView());
941 });
942 }
943 }
944 else
945 success = getFITSViewer()->updateData(data, fileURL, *tabID, &tabIndex, captureFilter, captureMode);
946
947 if (!success)
948 {
949 // If opening file fails, we treat it the same as exposure failure
950 // and recapture again if possible
951 qCCritical(KSTARS_INDI) << "error adding/updating FITS";
952 emit error(ERROR_VIEWER);
953 return;
954 }
955 *tabID = tabIndex;
956 if (Options::focusFITSOnNewImage())
957 getFITSViewer()->raise();
958
959 return;
960 }
961 }
962 break;
963 default:
964 break;
965 }
966
967 emit propertyUpdated(prop);
968 emit newImage(data);
969}
970
971void Camera::StreamWindowHidden()
972{
973 if (isConnected())
974 {
975 // We can have more than one *_VIDEO_STREAM property active so disable them all
976 auto streamSP = getSwitch("CCD_VIDEO_STREAM");
977 if (streamSP)
978 {
979 streamSP->reset();
980 streamSP->at(0)->setState(ISS_OFF);
981 streamSP->at(1)->setState(ISS_ON);
982 streamSP->setState(IPS_IDLE);
983 sendNewProperty(streamSP);
984 }
985
986 streamSP = getSwitch("VIDEO_STREAM");
987 if (streamSP)
988 {
989 streamSP->reset();
990 streamSP->at(0)->setState(ISS_OFF);
991 streamSP->at(1)->setState(ISS_ON);
992 streamSP->setState(IPS_IDLE);
993 sendNewProperty(streamSP);
994 }
995
996 streamSP = getSwitch("AUX_VIDEO_STREAM");
997 if (streamSP)
998 {
999 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
1011bool Camera::hasGuideHead()
1012{
1013 return HasGuideHead;
1014}
1015
1016bool Camera::hasCooler()
1017{
1018 return HasCooler;
1019}
1020
1021bool Camera::hasCoolerControl()
1022{
1023 return HasCoolerControl;
1024}
1025
1026bool 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
1049CameraChip *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
1063bool 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
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
1094bool 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
1121 sendImageS->s = sendImage ? ISS_ON : ISS_OFF;
1123
1124 sendNewProperty(rapidSP);
1125
1126 return true;
1127}
1128
1129void Camera::updateUploadSettings(const QString &uploadDirectory, const QString &uploadFile)
1130{
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 {
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
1154Camera::UploadMode Camera::getUploadMode()
1155{
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
1185bool 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
1232bool 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
1247bool 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
1266bool 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
1291bool 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
1321bool 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
1343bool 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
1375bool 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
1400bool 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
1433bool Camera::isStreamingEnabled()
1434{
1435 if (HasVideoStream == false || !streamWindow)
1436 return false;
1437
1438 return streamWindow->isStreamEnabled();
1439}
1440
1441bool 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
1462bool 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
1481bool 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
1504bool 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
1540bool 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
1572bool 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
1596bool 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
1616bool 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
1626bool Camera::getGain(double *value)
1627{
1628 if (!gainN)
1629 return false;
1630
1631 *value = gainN->value;
1632
1633 return true;
1634}
1635
1636bool 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
1648bool 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
1658bool Camera::getOffset(double *value)
1659{
1660 if (!offsetN)
1661 return false;
1662
1663 *value = offsetN->value;
1664
1665 return true;
1666}
1667
1668bool 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
1680bool Camera::isBLOBEnabled()
1681{
1682 return (m_Parent->getClientManager()->isBLOBEnabled(getDeviceName(), "CCD1"));
1683}
1684
1685bool Camera::setBLOBEnabled(bool enable, const QString &prop)
1686{
1687 m_Parent->getClientManager()->setBLOBEnabled(enable, getDeviceName(), prop);
1688
1689 return true;
1690}
1691
1692bool 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
1709bool 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
1722bool 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
1736bool 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
1750bool 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
1762bool 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
1772bool 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
1783bool 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
1795bool 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.
1809bool 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 |
1836 return ok;
1837}
1838
1839QString 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
1847void 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
1860void 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
1876void 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}
static KNotification * event(const QString &eventId, const QString &text=QString(), const QPixmap &pixmap=QPixmap(), const NotificationFlags &flags=CloseOnTimeout, const QString &componentName=QString())
static KStars * Instance()
Definition kstars.h:123
QString i18n(const char *text, const TYPE &arg...)
Type type(const QSqlDatabase &db)
ISD is a collection of INDI Standard Devices.
KCALUTILS_EXPORT QString errorMessage(const KCalendarCore::Exception &exception)
KIOCORE_EXPORT TransferJob * get(const QUrl &url, LoadType reload=NoReload, JobFlags flags=DefaultFlags)
void error(QWidget *parent, const QString &text, const QString &title, const KGuiItem &buttonOk, Options options=Notify)
QString label(StandardShortcut id)
QString name(StandardShortcut id)
QCA_EXPORT QVariant getProperty(const QString &name)
const char * constData() const const
char * data()
QByteArray fromRawData(const char *data, qsizetype size)
qsizetype size() const const
QDateTime currentDateTime()
void accepted()
bool exists() const const
bool mkpath(const QString &dirPath) const const
QString path() const const
QChar separator()
QString tempPath()
bool exists() const const
bool remove()
QDir dir() const const
QList< QByteArray > supportedImageFormats()
qsizetype count() const const
void deleteLater()
QString arg(Args &&... args) const const
bool endsWith(QChar c, Qt::CaseSensitivity cs) const const
bool isEmpty() const const
QStringList split(QChar sep, Qt::SplitBehavior behavior, Qt::CaseSensitivity cs) const const
double toDouble(bool *ok) const const
QByteArray toLatin1() const const
QString toLower() const const
UniqueConnection
QFuture< T > run(Function function,...)
QFuture< ArgsType< Signal > > connect(Sender *sender, Signal signal)
QUrl fromLocalFile(const QString &localFile)
This file is part of the KDE documentation.
Documentation copyright © 1996-2024 The KDE developers.
Generated on Tue Mar 26 2024 11:19:03 by doxygen 1.10.0 written by Dimitri van Heesch, © 1997-2006

KDE's Doxygen guidelines are available online.