Kstars

indicamera.cpp
1 /*
2  SPDX-FileCopyrightText: 2012 Jasem Mutlaq <[email protected]>
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 
34 const QStringList RAWFormats = { "cr2", "cr3", "crw", "nef", "raf", "dng", "arw", "orf" };
35 
36 const QString &getFITSModeStringString(FITSMode mode)
37 {
38  return FITSModes[mode];
39 }
40 
41 namespace ISD
42 {
43 
44 Camera::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 
55 Camera::~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 
65 void 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 
74 void 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 
269 void Camera::removeProperty(INDI::Property prop)
270 {
271  if (prop.isNameMatch("CCD_WEBSOCKET_SETTINGS"))
272  {
273  m_Media->disconnectServer();
274  }
275 }
276 
277 void 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 
354 void 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 
483 void 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 
494 void 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 
511 void 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 
547 bool 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);
568  if (!test_file.open(QIODevice::WriteOnly))
569  return false;
570  test_file.flush();
571  test_file.close();
572  return true;
573 }
574 
575 bool 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
622 QSharedPointer<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 
662 bool 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 
783  QString templateName = QString("%1/%2.XXXXXX").arg(QDir::tempPath(), rawFileName);
784  QTemporaryFile imgPreview(templateName);
785 
786  imgPreview.setAutoRemove(false);
787  imgPreview.open();
788  imgPreview.close();
789  QString preview_filename = imgPreview.fileName();
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 
818  emit previewFITSGenerated(output);
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 
881 void 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
917  QString previewTitle;
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 
971 void 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 
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 |
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 }
bool endsWith(const QString &s, Qt::CaseSensitivity cs) const const
bool exists() const const
QFuture< T > run(Function function,...)
QString toUpper() const const
bool disconnect(const QObject *sender, const char *signal, const QObject *receiver, const char *method)
int size() const const
bool isNull() const const
QByteArray fromRawData(const char *data, int size)
bool remove()
Type type(const QSqlDatabase &db)
QDateTime currentDateTime()
QStringList split(const QString &sep, QString::SplitBehavior behavior, Qt::CaseSensitivity cs) const const
int count(const T &value) const const
QChar separator()
bool contains(const QString &str, Qt::CaseSensitivity cs) const const
INDI::PropertyView< ISwitch > * getSwitch(const QString &name) const
QObject * sender() const const
QByteArray toLatin1() const const
int lastIndexOf(QChar ch, int from, Qt::CaseSensitivity cs) const const
bool exists() const const
QList< QByteArray > supportedImageFormats()
QMetaObject::Connection connect(const QObject *sender, const char *signal, const QObject *receiver, const char *method, Qt::ConnectionType type)
static KStars * Instance()
Definition: kstars.h:123
QString tempPath()
@ ERROR_VIEWER
Loading image buffer error.
Definition: indicamera.h:70
KCALUTILS_EXPORT QString errorMessage(const KCalendarCore::Exception &exception)
void showMessage(const QString &message, int timeout)
QMap::iterator insert(const Key &key, const T &value)
int size() const const
void deleteLater()
QString i18n(const char *text, const TYPE &arg...)
bool isNull() const const
bool isEmpty() const const
QUrl fromLocalFile(const QString &localFile)
bool mkpath(const QString &dirPath) const const
UniqueConnection
bool isEmpty() const const
QList< T > values() const const
void waitForFinished()
QString path() const const
double toDouble(bool *ok) const const
T * get() const const
QString & replace(int position, int n, QChar after)
QString & remove(int position, int n)
QStatusBar * statusBar() const const
QString label(StandardShortcut id)
QString toLower() const const
Image viewer window for KStars.
Definition: imageviewer.h:56
const char * constData() const const
QString arg(qlonglong a, int fieldWidth, int base, QChar fillChar) const const
@ ERROR_LOAD
Saving to disk error.
Definition: indicamera.h:69
void sendNewProperty(INDI::Property prop)
Send new property command to server.
const QChar at(int position) const const
void clear()
@ ERROR_SAVE
INDI Camera error.
Definition: indicamera.h:68
QChar * data()
INDI::PropertyView< IText > * getText(const QString &name) const
INDI::PropertyView< IBLOB > * getBLOB(const QString &name) const
static KNotification * event(const QString &eventId, const QString &text=QString(), const QPixmap &pixmap=QPixmap(), QWidget *widget=nullptr, const NotificationFlags &flags=CloseOnTimeout, const QString &componentName=QString())
QString mid(int position, int n) const const
bool isRunning() const const
QString message
T result() const const
char * data()
virtual QVariant get(ScriptableExtension *callerPrincipal, quint64 objId, const QString &propName)
QDir dir() const const
void accepted()
This file is part of the KDE documentation.
Documentation copyright © 1996-2023 The KDE developers.
Generated on Mon Dec 11 2023 04:03:19 by doxygen 1.8.17 written by Dimitri van Heesch, © 1997-2006

KDE's Doxygen guidelines are available online.