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#endif
22
23#include <KNotifications/KNotification>
24#include "auxiliary/ksmessagebox.h"
25#include "ksnotification.h"
26#include <QImageReader>
27#include <QFileInfo>
28#include <QStatusBar>
29#include <QtConcurrent>
30
31#include <basedevice.h>
32
33const QStringList RAWFormats = { "cr2", "cr3", "crw", "nef", "raf", "dng", "arw", "orf" };
34
35const QString &getFITSModeStringString(FITSMode mode)
36{
37 return FITSModes[mode];
38}
39
40namespace ISD
41{
42
43Camera::Camera(GenericDevice *parent) : ConcreteDevice(parent)
44{
45 primaryChip.reset(new CameraChip(this, CameraChip::PRIMARY_CCD));
46
47 m_Media.reset(new WSMedia(this));
48 connect(m_Media.get(), &WSMedia::newFile, this, &Camera::setWSBLOB);
49
50 connect(m_Parent->getClientManager(), &ClientManager::newBLOBManager, this, &Camera::setBLOBManager, Qt::UniqueConnection);
51 m_LastNotificationTS = QDateTime::currentDateTime();
52}
53
54Camera::~Camera()
55{
56 if (m_ImageViewerWindow)
57 m_ImageViewerWindow->close();
58 if (fileWriteThread.isRunning())
59 fileWriteThread.waitForFinished();
60 if (fileWriteBuffer != nullptr)
61 delete [] fileWriteBuffer;
62}
63
64void Camera::setBLOBManager(const char *device, INDI::Property prop)
65{
66 if (!prop.getRegistered())
67 return;
68
69 if (getDeviceName() == device)
70 emit newBLOBManager(prop);
71}
72
73void Camera::registerProperty(INDI::Property prop)
74{
75 if (prop.isNameMatch("GUIDER_EXPOSURE"))
76 {
77 HasGuideHead = true;
78 guideChip.reset(new CameraChip(this, CameraChip::GUIDE_CCD));
79 }
80 else if (prop.isNameMatch("CCD_FRAME_TYPE"))
81 {
82 primaryChip->clearFrameTypes();
83
84 for (auto &it : *prop.getSwitch())
85 primaryChip->addFrameLabel(it.getLabel());
86 }
87 else if (prop.isNameMatch("CCD_FRAME"))
88 {
89 auto np = prop.getNumber();
90 if (np && np->getPermission() != IP_RO)
91 primaryChip->setCanSubframe(true);
92 }
93 else if (prop.isNameMatch("GUIDER_FRAME"))
94 {
95 auto np = prop.getNumber();
96 if (np && np->getPermission() != IP_RO)
97 guideChip->setCanSubframe(true);
98 }
99 else if (prop.isNameMatch("CCD_BINNING"))
100 {
101 auto np = prop.getNumber();
102 if (np && np->getPermission() != IP_RO)
103 primaryChip->setCanBin(true);
104 }
105 else if (prop.isNameMatch("GUIDER_BINNING"))
106 {
107 auto np = prop.getNumber();
108 if (np && np->getPermission() != IP_RO)
109 guideChip->setCanBin(true);
110 }
111 else if (prop.isNameMatch("CCD_ABORT_EXPOSURE"))
112 {
113 auto sp = prop.getSwitch();
114 if (sp && sp->getPermission() != IP_RO)
115 primaryChip->setCanAbort(true);
116 }
117 else if (prop.isNameMatch("GUIDER_ABORT_EXPOSURE"))
118 {
119 auto sp = prop.getSwitch();
120 if (sp && sp->getPermission() != IP_RO)
121 guideChip->setCanAbort(true);
122 }
123 else if (prop.isNameMatch("CCD_TEMPERATURE"))
124 {
125 auto np = prop.getNumber();
126 HasCooler = true;
127 CanCool = (np->getPermission() != IP_RO);
128 if (np)
129 emit newTemperatureValue(np->at(0)->getValue());
130 }
131 else if (prop.isNameMatch("CCD_COOLER"))
132 {
133 // Can turn cooling on/off
134 HasCoolerControl = true;
135 }
136 else if (prop.isNameMatch("CCD_VIDEO_STREAM"))
137 {
138 // Has Video Stream
139 HasVideoStream = true;
140 }
141 else if (prop.isNameMatch("CCD_CAPTURE_FORMAT"))
142 {
143 auto sp = prop.getSwitch();
144 if (sp)
145 {
146 m_CaptureFormats.clear();
147 for (const auto &oneSwitch : *sp)
148 m_CaptureFormats << oneSwitch.getLabel();
149
150 m_CaptureFormatIndex = sp->findOnSwitchIndex();
151 }
152 }
153 else if (prop.isNameMatch("CCD_TRANSFER_FORMAT"))
154 {
155 auto sp = prop.getSwitch();
156 if (sp)
157 {
158 m_EncodingFormats.clear();
159 for (const auto &oneSwitch : *sp)
160 m_EncodingFormats << oneSwitch.getLabel();
161
162 auto format = sp->findOnSwitch();
163 if (format)
164 m_EncodingFormat = format->label;
165 }
166 }
167 else if (prop.isNameMatch("CCD_EXPOSURE_PRESETS"))
168 {
169 auto svp = prop.getSwitch();
170 if (svp)
171 {
172 bool ok = false;
173 auto separator = QDir::separator();
174 for (const auto &it : *svp)
175 {
176 QString key = QString(it.getLabel());
177 double value = key.toDouble(&ok);
178 if (!ok)
179 {
180 QStringList parts = key.split(separator);
181 if (parts.count() == 2)
182 {
183 bool numOk = false, denOk = false;
184 double numerator = parts[0].toDouble(&numOk);
185 double denominator = parts[1].toDouble(&denOk);
186 if (numOk && denOk && denominator > 0)
187 {
188 ok = true;
189 value = numerator / denominator;
190 }
191 }
192 }
193 if (ok)
194 m_ExposurePresets.insert(key, value);
195
196 double min = 1e6, max = 1e-6;
197 for (auto oneValue : m_ExposurePresets.values())
198 {
199 if (oneValue < min)
200 min = oneValue;
201 if (oneValue > max)
202 max = oneValue;
203 }
204 m_ExposurePresetsMinMax = qMakePair<double, double>(min, max);
205 }
206 }
207 }
208 else if (prop.isNameMatch("CCD_FAST_TOGGLE"))
209 {
210 auto sp = prop.getSwitch();
211 if (sp)
212 m_FastExposureEnabled = sp->findOnSwitchIndex() == 0;
213 else
214 m_FastExposureEnabled = false;
215 }
216 else if (prop.isNameMatch("TELESCOPE_TYPE"))
217 {
218 auto sp = prop.getSwitch();
219 if (sp)
220 {
221 auto format = sp->findWidgetByName("TELESCOPE_PRIMARY");
222 if (format && format->getState() == ISS_ON)
223 telescopeType = TELESCOPE_PRIMARY;
224 else
225 telescopeType = TELESCOPE_GUIDE;
226 }
227 }
228 else if (prop.isNameMatch("CCD_WEBSOCKET_SETTINGS"))
229 {
230 auto np = prop.getNumber();
231 m_Media->setURL(QUrl(QString("ws://%1:%2").arg(m_Parent->getClientManager()->getHost()).arg(np->at(0)->getValue())));
232 m_Media->connectServer();
233 }
234 else if (prop.isNameMatch("CCD1"))
235 {
236 primaryCCDBLOB = prop;
237 }
238 // try to find gain and/or offset property, if any
239 else if ( (gainN == nullptr || offsetN == nullptr) && prop.getType() == INDI_NUMBER)
240 {
241 // Since gain is spread among multiple property depending on the camera providing it
242 // we need to search in all possible number properties
243 auto controlNP = prop.getNumber();
244 if (controlNP)
245 {
246 for (auto &it : *controlNP)
247 {
248 QString name = QString(it.getName()).toLower();
249 QString label = QString(it.getLabel()).toLower();
250
251 if (name == "gain" || label == "gain")
252 {
253 gainN = &it;
254 gainPerm = controlNP->getPermission();
255 }
256 else if (name == "offset" || label == "offset")
257 {
258 offsetN = &it;
259 offsetPerm = controlNP->getPermission();
260 }
261 }
262 }
263 }
264
265 ConcreteDevice::registerProperty(prop);
266}
267
268void Camera::removeProperty(INDI::Property prop)
269{
270 if (prop.isNameMatch("CCD_WEBSOCKET_SETTINGS"))
271 {
272 m_Media->disconnectServer();
273 }
274}
275
276void Camera::processNumber(INDI::Property prop)
277{
278 auto nvp = prop.getNumber();
279 if (nvp->isNameMatch("CCD_EXPOSURE"))
280 {
281 auto np = nvp->findWidgetByName("CCD_EXPOSURE_VALUE");
282 if (np)
283 emit newExposureValue(primaryChip.get(), np->getValue(), nvp->getState());
284 if (nvp->getState() == IPS_ALERT)
285 emit error(ERROR_CAPTURE);
286 }
287 else if (prop.isNameMatch("CCD_TEMPERATURE"))
288 {
289 HasCooler = true;
290 auto np = nvp->findWidgetByName("CCD_TEMPERATURE_VALUE");
291 if (np)
292 emit newTemperatureValue(np->getValue());
293 }
294 else if (prop.isNameMatch("GUIDER_EXPOSURE"))
295 {
296 auto np = nvp->findWidgetByName("GUIDER_EXPOSURE_VALUE");
297 if (np)
298 emit newExposureValue(guideChip.get(), np->getValue(), nvp->getState());
299 }
300 else if (prop.isNameMatch("FPS"))
301 {
302 emit newFPS(nvp->at(0)->getValue(), nvp->at(1)->getValue());
303 }
304 else if (prop.isNameMatch("CCD_RAPID_GUIDE_DATA"))
305 {
306 if (nvp->getState() == IPS_ALERT)
307 {
308 emit newGuideStarData(primaryChip.get(), -1, -1, -1);
309 }
310 else
311 {
312 double dx = -1, dy = -1, fit = -1;
313
314 auto np = nvp->findWidgetByName("GUIDESTAR_X");
315 if (np)
316 dx = np->getValue();
317 np = nvp->findWidgetByName("GUIDESTAR_Y");
318 if (np)
319 dy = np->getValue();
320 np = nvp->findWidgetByName("GUIDESTAR_FIT");
321 if (np)
322 fit = np->getValue();
323
324 if (dx >= 0 && dy >= 0 && fit >= 0)
325 emit newGuideStarData(primaryChip.get(), dx, dy, fit);
326 }
327 }
328 else if (prop.isNameMatch("GUIDER_RAPID_GUIDE_DATA"))
329 {
330 if (nvp->getState() == IPS_ALERT)
331 {
332 emit newGuideStarData(guideChip.get(), -1, -1, -1);
333 }
334 else
335 {
336 double dx = -1, dy = -1, fit = -1;
337 auto np = nvp->findWidgetByName("GUIDESTAR_X");
338 if (np)
339 dx = np->getValue();
340 np = nvp->findWidgetByName("GUIDESTAR_Y");
341 if (np)
342 dy = np->getValue();
343 np = nvp->findWidgetByName("GUIDESTAR_FIT");
344 if (np)
345 fit = np->getValue();
346
347 if (dx >= 0 && dy >= 0 && fit >= 0)
348 emit newGuideStarData(guideChip.get(), dx, dy, fit);
349 }
350 }
351}
352
353void Camera::processSwitch(INDI::Property prop)
354{
355 auto svp = prop.getSwitch();
356
357 if (svp->isNameMatch("CCD_COOLER"))
358 {
359 // Can turn cooling on/off
360 HasCoolerControl = true;
361 emit coolerToggled(svp->sp[0].s == ISS_ON);
362 }
363 else if (QString(svp->getName()).endsWith("VIDEO_STREAM"))
364 {
365 // If BLOB is not enabled for this camera, then ignore all VIDEO_STREAM calls.
366 if (isBLOBEnabled() == false || m_StreamingEnabled == false)
367 return;
368
369 HasVideoStream = true;
370
371 if (!streamWindow && svp->sp[0].s == ISS_ON)
372 {
373 streamWindow.reset(new StreamWG(this));
374
375 INumberVectorProperty *streamFrame = getNumber("CCD_STREAM_FRAME");
376 INumber *w = nullptr, *h = nullptr;
377
378 if (streamFrame)
379 {
380 w = IUFindNumber(streamFrame, "WIDTH");
381 h = IUFindNumber(streamFrame, "HEIGHT");
382 }
383
384 if (w && h)
385 {
386 streamW = w->value;
387 streamH = h->value;
388 }
389 else
390 {
391 // Only use CCD dimensions if we are receiving raw stream and not stream of images (i.e. mjpeg..etc)
392 auto rawBP = getBLOB("CCD1");
393 if (rawBP)
394 {
395 int x = 0, y = 0, w = 0, h = 0;
396 int binx = 0, biny = 0;
397
398 primaryChip->getFrame(&x, &y, &w, &h);
399 primaryChip->getBinning(&binx, &biny);
400 streamW = w / binx;
401 streamH = h / biny;
402 }
403 }
404
405 streamWindow->setSize(streamW, streamH);
406 }
407
408 if (streamWindow)
409 {
410 connect(streamWindow.get(), &StreamWG::hidden, this, &Camera::StreamWindowHidden, Qt::UniqueConnection);
411 connect(streamWindow.get(), &StreamWG::imageChanged, this, &Camera::newVideoFrame, Qt::UniqueConnection);
412
413 streamWindow->enableStream(svp->sp[0].s == ISS_ON);
414 emit videoStreamToggled(svp->sp[0].s == ISS_ON);
415 }
416 }
417 else if (svp->isNameMatch("CCD_CAPTURE_FORMAT"))
418 {
419 m_CaptureFormats.clear();
420 for (int i = 0; i < svp->nsp; i++)
421 {
422 m_CaptureFormats << svp->sp[i].label;
423 if (svp->sp[i].s == ISS_ON)
424 m_CaptureFormatIndex = i;
425 }
426 }
427 else if (svp->isNameMatch("CCD_TRANSFER_FORMAT"))
428 {
429 ISwitch *format = IUFindOnSwitch(svp);
430 if (format)
431 m_EncodingFormat = format->label;
432 }
433 else if (svp->isNameMatch("RECORD_STREAM"))
434 {
435 ISwitch *recordOFF = IUFindSwitch(svp, "RECORD_OFF");
436
437 if (recordOFF && recordOFF->s == ISS_ON)
438 {
439 emit videoRecordToggled(false);
440 KSNotification::event(QLatin1String("IndiServerMessage"), i18n("Video Recording Stopped"), KSNotification::INDI);
441 }
442 else
443 {
444 emit videoRecordToggled(true);
445 KSNotification::event(QLatin1String("IndiServerMessage"), i18n("Video Recording Started"), KSNotification::INDI);
446 }
447 }
448 else if (svp->isNameMatch("TELESCOPE_TYPE"))
449 {
450 ISwitch *format = IUFindSwitch(svp, "TELESCOPE_PRIMARY");
451 if (format && format->s == ISS_ON)
452 telescopeType = TELESCOPE_PRIMARY;
453 else
454 telescopeType = TELESCOPE_GUIDE;
455 }
456 else if (!strcmp(svp->name, "CCD_FAST_TOGGLE"))
457 {
458 m_FastExposureEnabled = IUFindOnSwitchIndex(svp) == 0;
459 }
460 else if (svp->isNameMatch("CONNECTION"))
461 {
462 auto dSwitch = svp->findWidgetByName("DISCONNECT");
463
464 if (dSwitch && dSwitch->getState() == ISS_ON)
465 {
466 if (streamWindow)
467 {
468 streamWindow->enableStream(false);
469 emit videoStreamToggled(false);
470 streamWindow->close();
471 streamWindow.reset();
472 }
473
474 // Clear the pointers on disconnect.
475 gainN = nullptr;
476 offsetN = nullptr;
477 primaryCCDBLOB = INDI::Property();
478 }
479 }
480}
481
482void Camera::processText(INDI::Property prop)
483{
484 auto tvp = prop.getText();
485 if (tvp->isNameMatch("CCD_FILE_PATH"))
486 {
487 auto filepath = tvp->findWidgetByName("FILE_PATH");
488 if (filepath)
489 emit newRemoteFile(QString(filepath->getText()));
490 }
491}
492
493void Camera::setWSBLOB(const QByteArray &message, const QString &extension)
494{
495 if (!primaryCCDBLOB)
496 return;
497
498 auto bvp = primaryCCDBLOB.getBLOB();
499 auto bp = bvp->at(0);
500
501 bp->setBlob(const_cast<char *>(message.data()));
502 bp->setSize(message.size());
503 bp->setFormat(extension.toLatin1().constData());
504 processBLOB(primaryCCDBLOB);
505
506 // Disassociate
507 bp->setBlob(nullptr);
508}
509
510void Camera::processStream(INDI::Property prop)
511{
512 if (!streamWindow || streamWindow->isStreamEnabled() == false)
513 return;
514
515 INumberVectorProperty *streamFrame = getNumber("CCD_STREAM_FRAME");
516 INumber *w = nullptr, *h = nullptr;
517
518 if (streamFrame)
519 {
520 w = IUFindNumber(streamFrame, "WIDTH");
521 h = IUFindNumber(streamFrame, "HEIGHT");
522 }
523
524 if (w && h)
525 {
526 streamW = w->value;
527 streamH = h->value;
528 }
529 else
530 {
531 int x = 0, y = 0, w = 0, h = 0;
532 int binx = 1, biny = 1;
533
534 primaryChip->getFrame(&x, &y, &w, &h);
535 primaryChip->getBinning(&binx, &biny);
536 streamW = w / binx;
537 streamH = h / biny;
538 }
539
540 streamWindow->setSize(streamW, streamH);
541
542 streamWindow->show();
543 streamWindow->newFrame(prop);
544}
545
546void ISD::Camera::updateFileBuffer(INDI::Property prop, bool is_fits)
547{
548 if (is_fits)
549 {
550 // Check if the last write is still ongoing, and if so wait.
551 // It is using the fileWriteBuffer.
552 if (fileWriteThread.isRunning())
553 {
554 fileWriteThread.waitForFinished();
555 }
556 }
557
558 // Will write blob data in a separate thread, and can't depend on the blob
559 // memory, so copy it first.
560
561 auto bp = prop.getBLOB()->at(0);
562 // Check buffer size.
563 if (fileWriteBufferSize != bp->getBlobLen())
564 {
565 if (fileWriteBuffer != nullptr)
566 delete [] fileWriteBuffer;
567 fileWriteBufferSize = bp->getBlobLen();
568 fileWriteBuffer = new char[fileWriteBufferSize];
569 }
570
571 // Copy memory, and write file on a separate thread.
572 // Probably too late to return an error if the file couldn't write.
573 memcpy(fileWriteBuffer, bp->getBlob(), bp->getBlobLen());
574}
575
577{
578 // TODO: Not yet threading the writes for non-fits files.
579 // Would need to deal with the raw conversion, etc.
580 if (BType == BLOB_FITS)
581 {
582 // Check if the last write is still ongoing, and if so wait.
583 // It is using the fileWriteBuffer.
584 if (fileWriteThread.isRunning())
585 {
586 fileWriteThread.waitForFinished();
587 }
588
589 // Wait until the file is written before overwritting the filename.
590 fileWriteThread = QtConcurrent::run(this, &ISD::Camera::WriteImageFileInternal, filename, fileWriteBuffer,
591 fileWriteBufferSize);
592 }
593 else if (!WriteImageFileInternal(filename, static_cast<char*>(fileWriteBuffer), fileWriteBufferSize))
594 return false;
595
596 return true;
597}
598
599bool Camera::processBLOB(INDI::Property prop)
600{
601 auto bvp = prop.getBLOB();
602 // Ignore write-only BLOBs since we only receive it for state-change
603 if (bvp->getPermission() == IP_WO || bvp->at(0)->getSize() == 0)
604 return false;
605
606 BType = BLOB_OTHER;
607
608 auto bp = bvp->at(0);
609
610 auto format = QString(bp->getFormat()).toLower();
611
612 // If stream, process it first
613 if (format.contains("stream"))
614 {
615 if (m_StreamingEnabled == false)
616 return true;
617 else if (streamWindow)
618 processStream(prop);
619 return true;
620 }
621
622 // Format without leading . (.jpg --> jpg)
623 QString shortFormat = format.mid(1);
624
625 // If it's not FITS or an image, don't process it.
626 if ((QImageReader::supportedImageFormats().contains(shortFormat.toLatin1())))
627 BType = BLOB_IMAGE;
628 else if (format.contains("fits"))
629 BType = BLOB_FITS;
630 else if (format.contains("xisf"))
631 BType = BLOB_XISF;
632 else if (RAWFormats.contains(shortFormat))
633 BType = BLOB_RAW;
634
635 if (BType == BLOB_OTHER)
636 return false;
637
638 CameraChip *targetChip = nullptr;
639
640 if (bvp->isNameMatch("CCD2"))
641 targetChip = guideChip.get();
642 else
643 {
644 targetChip = primaryChip.get();
645 qCDebug(KSTARS_INDI) << "Image received. Mode:" << getFITSModeStringString(targetChip->getCaptureMode()) << "Size:" <<
646 bp->getSize();
647 }
648
649 // Create temporary name if ANY of the following conditions are met:
650 // 1. file is preview or batch mode is not enabled
651 // 2. file type is not FITS_NORMAL (focus, guide..etc)
652 // create the file buffer only, saving the image file must be triggered from outside.
653 updateFileBuffer(prop, BType == BLOB_FITS);
654
655 // Don't spam, just one notification per 3 seconds
656 if (QDateTime::currentDateTime().secsTo(m_LastNotificationTS) <= -3)
657 {
658 KNotification::event(QLatin1String("FITSReceived"), i18n("Image file is received"));
659 m_LastNotificationTS = QDateTime::currentDateTime();
660 }
661
662 QByteArray buffer = QByteArray::fromRawData(reinterpret_cast<char *>(bp->getBlob()), bp->getSize());
663 QSharedPointer<FITSData> imageData;
664 imageData.reset(new FITSData(targetChip->getCaptureMode()), &QObject::deleteLater);
665 imageData->setExtension(format);
666 if (!imageData->loadFromBuffer(buffer))
667 {
668 emit error(ERROR_LOAD);
669 return true;
670 }
671
672 // Add metadata
673 imageData->setProperty("device", getDeviceName());
674 imageData->setProperty("blobVector", prop.getName());
675 imageData->setProperty("blobElement", bp->getName());
676 imageData->setProperty("chip", targetChip->getType());
677
678 // Retain a copy
679 targetChip->setImageData(imageData);
680 emit propertyUpdated(prop);
681 emit newImage(imageData, QString(bp->getFormat()).toLower());
682
683 return true;
684}
685
686void Camera::StreamWindowHidden()
687{
688 if (isConnected())
689 {
690 // We can have more than one *_VIDEO_STREAM property active so disable them all
691 auto streamSP = getSwitch("CCD_VIDEO_STREAM");
692 if (streamSP)
693 {
694 streamSP->reset();
695 streamSP->at(0)->setState(ISS_OFF);
696 streamSP->at(1)->setState(ISS_ON);
697 streamSP->setState(IPS_IDLE);
699 }
700
701 streamSP = getSwitch("VIDEO_STREAM");
702 if (streamSP)
703 {
704 streamSP->reset();
705 streamSP->at(0)->setState(ISS_OFF);
706 streamSP->at(1)->setState(ISS_ON);
707 streamSP->setState(IPS_IDLE);
709 }
710
711 streamSP = getSwitch("AUX_VIDEO_STREAM");
712 if (streamSP)
713 {
714 streamSP->reset();
715 streamSP->at(0)->setState(ISS_OFF);
716 streamSP->at(1)->setState(ISS_ON);
717 streamSP->setState(IPS_IDLE);
719 }
720 }
721
722 if (streamWindow)
723 streamWindow->disconnect();
724}
725
726bool Camera::hasGuideHead()
727{
728 return HasGuideHead;
729}
730
731bool Camera::hasCooler()
732{
733 return HasCooler;
734}
735
736bool Camera::hasCoolerControl()
737{
738 return HasCoolerControl;
739}
740
741bool Camera::setCoolerControl(bool enable)
742{
743 if (HasCoolerControl == false)
744 return false;
745
746 auto coolerSP = getSwitch("CCD_COOLER");
747
748 if (!coolerSP)
749 return false;
750
751 // Cooler ON/OFF
752 auto coolerON = coolerSP->findWidgetByName("COOLER_ON");
753 auto coolerOFF = coolerSP->findWidgetByName("COOLER_OFF");
754 if (!coolerON || !coolerOFF)
755 return false;
756
757 coolerON->setState(enable ? ISS_ON : ISS_OFF);
758 coolerOFF->setState(enable ? ISS_OFF : ISS_ON);
760
761 return true;
762}
763
764CameraChip *Camera::getChip(CameraChip::ChipType cType)
765{
766 switch (cType)
767 {
768 case CameraChip::PRIMARY_CCD:
769 return primaryChip.get();
770
771 case CameraChip::GUIDE_CCD:
772 return guideChip.get();
773 }
774
775 return nullptr;
776}
777
778bool Camera::setRapidGuide(CameraChip *targetChip, bool enable)
779{
781 ISwitch *enableS = nullptr;
782
783 if (targetChip == primaryChip.get())
784 rapidSP = getSwitch("CCD_RAPID_GUIDE");
785 else
786 rapidSP = getSwitch("GUIDER_RAPID_GUIDE");
787
788 if (rapidSP == nullptr)
789 return false;
790
791 enableS = IUFindSwitch(rapidSP, "ENABLE");
792
793 if (enableS == nullptr)
794 return false;
795
796 // Already updated, return OK
797 if ((enable && enableS->s == ISS_ON) || (!enable && enableS->s == ISS_OFF))
798 return true;
799
801 rapidSP->sp[0].s = enable ? ISS_ON : ISS_OFF;
802 rapidSP->sp[1].s = enable ? ISS_OFF : ISS_ON;
803
805
806 return true;
807}
808
809bool Camera::configureRapidGuide(CameraChip *targetChip, bool autoLoop, bool sendImage, bool showMarker)
810{
812 ISwitch *autoLoopS = nullptr, *sendImageS = nullptr, *showMarkerS = nullptr;
813
814 if (targetChip == primaryChip.get())
815 rapidSP = getSwitch("CCD_RAPID_GUIDE_SETUP");
816 else
817 rapidSP = getSwitch("GUIDER_RAPID_GUIDE_SETUP");
818
819 if (rapidSP == nullptr)
820 return false;
821
822 autoLoopS = IUFindSwitch(rapidSP, "AUTO_LOOP");
823 sendImageS = IUFindSwitch(rapidSP, "SEND_IMAGE");
824 showMarkerS = IUFindSwitch(rapidSP, "SHOW_MARKER");
825
826 if (!autoLoopS || !sendImageS || !showMarkerS)
827 return false;
828
829 // If everything is already set, let's return.
830 if (((autoLoop && autoLoopS->s == ISS_ON) || (!autoLoop && autoLoopS->s == ISS_OFF)) &&
831 ((sendImage && sendImageS->s == ISS_ON) || (!sendImage && sendImageS->s == ISS_OFF)) &&
832 ((showMarker && showMarkerS->s == ISS_ON) || (!showMarker && showMarkerS->s == ISS_OFF)))
833 return true;
834
836 sendImageS->s = sendImage ? ISS_ON : ISS_OFF;
838
840
841 return true;
842}
843
844void Camera::updateUploadSettings(const QString &uploadDirectory, const QString &uploadFile)
845{
847 IText *uploadT = nullptr;
848
849 uploadSettingsTP = getText("UPLOAD_SETTINGS");
851 {
852 uploadT = IUFindText(uploadSettingsTP, "UPLOAD_DIR");
853 if (uploadT && uploadDirectory.isEmpty() == false)
854 {
856 // N.B. Need to convert any Windows directory separators / to Posix separators /
857 posixDirectory.replace(QDir::separator(), "/");
858 IUSaveText(uploadT, posixDirectory.toLatin1().constData());
859 }
860
861 uploadT = IUFindText(uploadSettingsTP, "UPLOAD_PREFIX");
862 if (uploadT)
863 IUSaveText(uploadT, uploadFile.toLatin1().constData());
864
866 }
867}
868
869Camera::UploadMode Camera::getUploadMode()
870{
872
873 uploadModeSP = getSwitch("UPLOAD_MODE");
874
875 if (uploadModeSP == nullptr)
876 {
877 qWarning() << "No UPLOAD_MODE in CCD driver. Please update driver to INDI compliant CCD driver.";
878 return UPLOAD_CLIENT;
879 }
880
881 if (uploadModeSP)
882 {
883 ISwitch *modeS = nullptr;
884
885 modeS = IUFindSwitch(uploadModeSP, "UPLOAD_CLIENT");
886 if (modeS && modeS->s == ISS_ON)
887 return UPLOAD_CLIENT;
888 modeS = IUFindSwitch(uploadModeSP, "UPLOAD_LOCAL");
889 if (modeS && modeS->s == ISS_ON)
890 return UPLOAD_LOCAL;
891 modeS = IUFindSwitch(uploadModeSP, "UPLOAD_BOTH");
892 if (modeS && modeS->s == ISS_ON)
893 return UPLOAD_BOTH;
894 }
895
896 // Default
897 return UPLOAD_CLIENT;
898}
899
900bool Camera::setUploadMode(UploadMode mode)
901{
902 ISwitch *modeS = nullptr;
903
904 auto uploadModeSP = getSwitch("UPLOAD_MODE");
905
906 if (!uploadModeSP)
907 {
908 qWarning() << "No UPLOAD_MODE in CCD driver. Please update driver to INDI compliant CCD driver.";
909 return false;
910 }
911
912 switch (mode)
913 {
914 case UPLOAD_CLIENT:
915 modeS = uploadModeSP->findWidgetByName("UPLOAD_CLIENT");
916 if (!modeS)
917 return false;
918 if (modeS->s == ISS_ON)
919 return true;
920 break;
921
922 case UPLOAD_BOTH:
923 modeS = uploadModeSP->findWidgetByName("UPLOAD_BOTH");
924 if (!modeS)
925 return false;
926 if (modeS->s == ISS_ON)
927 return true;
928 break;
929
930 case UPLOAD_LOCAL:
931 modeS = uploadModeSP->findWidgetByName("UPLOAD_LOCAL");
932 if (!modeS)
933 return false;
934 if (modeS->s == ISS_ON)
935 return true;
936 break;
937 }
938
939 uploadModeSP->reset();
940 modeS->s = ISS_ON;
941
943
944 return true;
945}
946
947bool Camera::getTemperature(double *value)
948{
949 if (HasCooler == false)
950 return false;
951
952 auto temperatureNP = getNumber("CCD_TEMPERATURE");
953
954 if (!temperatureNP)
955 return false;
956
957 *value = temperatureNP->at(0)->getValue();
958
959 return true;
960}
961
962bool Camera::setTemperature(double value)
963{
964 auto nvp = getNumber("CCD_TEMPERATURE");
965
966 if (!nvp)
967 return false;
968
969 auto np = nvp->findWidgetByName("CCD_TEMPERATURE_VALUE");
970
971 if (!np)
972 return false;
973
974 np->setValue(value);
975
977
978 return true;
979}
980
981bool Camera::setEncodingFormat(const QString &value)
982{
983 if (value.isEmpty() || value == m_EncodingFormat)
984 return true;
985
986 auto svp = getSwitch("CCD_TRANSFER_FORMAT");
987
988 if (!svp)
989 return false;
990
991 svp->reset();
992 for (int i = 0; i < svp->nsp; i++)
993 {
994 if (svp->at(i)->getLabel() == value)
995 {
996 svp->at(i)->setState(ISS_ON);
997 break;
998 }
999 }
1000
1001 m_EncodingFormat = value;
1003 return true;
1004}
1005
1006bool Camera::setTelescopeType(TelescopeType type)
1007{
1008 if (type == telescopeType)
1009 return true;
1010
1011 auto svp = getSwitch("TELESCOPE_TYPE");
1012
1013 if (!svp)
1014 return false;
1015
1016 auto typePrimary = svp->findWidgetByName("TELESCOPE_PRIMARY");
1017 auto typeGuide = svp->findWidgetByName("TELESCOPE_GUIDE");
1018
1019 if (!typePrimary || !typeGuide)
1020 return false;
1021
1022 telescopeType = type;
1023
1024 if ( (telescopeType == TELESCOPE_PRIMARY && typePrimary->getState() == ISS_OFF) ||
1025 (telescopeType == TELESCOPE_GUIDE && typeGuide->getState() == ISS_OFF))
1026 {
1027 typePrimary->setState(telescopeType == TELESCOPE_PRIMARY ? ISS_ON : ISS_OFF);
1028 typeGuide->setState(telescopeType == TELESCOPE_PRIMARY ? ISS_OFF : ISS_ON);
1030 setConfig(SAVE_CONFIG);
1031 }
1032
1033 return true;
1034}
1035
1036bool Camera::setVideoStreamEnabled(bool enable)
1037{
1038 if (HasVideoStream == false)
1039 return false;
1040
1041 auto svp = getSwitch("CCD_VIDEO_STREAM");
1042
1043 if (!svp)
1044 return false;
1045
1046 // If already on and enable is set or vice versa no need to change anything we return true
1047 if ((enable && svp->at(0)->getState() == ISS_ON) || (!enable && svp->at(1)->getState() == ISS_ON))
1048 return true;
1049
1050 svp->at(0)->setState(enable ? ISS_ON : ISS_OFF);
1051 svp->at(1)->setState(enable ? ISS_OFF : ISS_ON);
1052
1054
1055 return true;
1056}
1057
1058bool Camera::resetStreamingFrame()
1059{
1060 auto frameProp = getNumber("CCD_STREAM_FRAME");
1061
1062 if (!frameProp)
1063 return false;
1064
1065 auto xarg = frameProp->findWidgetByName("X");
1066 auto yarg = frameProp->findWidgetByName("Y");
1067 auto warg = frameProp->findWidgetByName("WIDTH");
1068 auto harg = frameProp->findWidgetByName("HEIGHT");
1069
1070 if (xarg && yarg && warg && harg)
1071 {
1072 if (!std::fabs(xarg->getValue() - xarg->getMin()) &&
1073 !std::fabs(yarg->getValue() - yarg->getMin()) &&
1074 !std::fabs(warg->getValue() - warg->getMax()) &&
1075 !std::fabs(harg->getValue() - harg->getMax()))
1076 return false;
1077
1078 xarg->setValue(xarg->getMin());
1079 yarg->setValue(yarg->getMin());
1080 warg->setValue(warg->getMax());
1081 harg->setValue(harg->getMax());
1082
1084 return true;
1085 }
1086
1087 return false;
1088}
1089
1090bool Camera::setStreamLimits(uint16_t maxBufferSize, uint16_t maxPreviewFPS)
1091{
1092 auto limitsProp = getNumber("LIMITS");
1093
1094 if (!limitsProp)
1095 return false;
1096
1097 auto bufferMax = limitsProp->findWidgetByName("LIMITS_BUFFER_MAX");
1098 auto previewFPS = limitsProp->findWidgetByName("LIMITS_PREVIEW_FPS");
1099
1100 if (bufferMax && previewFPS)
1101 {
1102 if(std::fabs(bufferMax->getValue() - maxBufferSize) > 0 || std::fabs(previewFPS->getValue() - maxPreviewFPS) > 0)
1103 {
1104 bufferMax->setValue(maxBufferSize);
1105 previewFPS->setValue(maxPreviewFPS);
1107 }
1108
1109 return true;
1110 }
1111
1112 return false;
1113}
1114
1115bool Camera::setStreamingFrame(int x, int y, int w, int h)
1116{
1117 auto frameProp = getNumber("CCD_STREAM_FRAME");
1118
1119 if (!frameProp)
1120 return false;
1121
1122 auto xarg = frameProp->findWidgetByName("X");
1123 auto yarg = frameProp->findWidgetByName("Y");
1124 auto warg = frameProp->findWidgetByName("WIDTH");
1125 auto harg = frameProp->findWidgetByName("HEIGHT");
1126
1127 if (xarg && yarg && warg && harg)
1128 {
1129 if (!std::fabs(xarg->getValue() - x) &&
1130 !std::fabs(yarg->getValue() - y) &&
1131 !std::fabs(warg->getValue() - w) &&
1132 !std::fabs(harg->getValue() - h))
1133 return true;
1134
1135 // N.B. We add offset since the X, Y are relative to whatever streaming frame is currently active
1136 xarg->value = qBound(xarg->getMin(), static_cast<double>(x) + xarg->getValue(), xarg->getMax());
1137 yarg->value = qBound(yarg->getMin(), static_cast<double>(y) + yarg->getValue(), yarg->getMax());
1138 warg->value = qBound(warg->getMin(), static_cast<double>(w), warg->getMax());
1139 harg->value = qBound(harg->getMin(), static_cast<double>(h), harg->getMax());
1140
1142 return true;
1143 }
1144
1145 return false;
1146}
1147
1148bool Camera::isStreamingEnabled()
1149{
1150 if (HasVideoStream == false || !streamWindow)
1151 return false;
1152
1153 return streamWindow->isStreamEnabled();
1154}
1155
1156bool Camera::setSERNameDirectory(const QString &filename, const QString &directory)
1157{
1158 auto tvp = getText("RECORD_FILE");
1159
1160 if (!tvp)
1161 return false;
1162
1163 auto filenameT = tvp->findWidgetByName("RECORD_FILE_NAME");
1164 auto dirT = tvp->findWidgetByName("RECORD_FILE_DIR");
1165
1166 if (!filenameT || !dirT)
1167 return false;
1168
1169 filenameT->setText(filename.toLatin1().data());
1170 dirT->setText(directory.toLatin1().data());
1171
1173
1174 return true;
1175}
1176
1177bool Camera::getSERNameDirectory(QString &filename, QString &directory)
1178{
1179 auto tvp = getText("RECORD_FILE");
1180
1181 if (!tvp)
1182 return false;
1183
1184 auto filenameT = tvp->findWidgetByName("RECORD_FILE_NAME");
1185 auto dirT = tvp->findWidgetByName("RECORD_FILE_DIR");
1186
1187 if (!filenameT || !dirT)
1188 return false;
1189
1190 filename = QString(filenameT->getText());
1191 directory = QString(dirT->getText());
1192
1193 return true;
1194}
1195
1196bool Camera::startRecording()
1197{
1198 auto svp = getSwitch("RECORD_STREAM");
1199
1200 if (!svp)
1201 return false;
1202
1203 auto recordON = svp->findWidgetByName("RECORD_ON");
1204
1205 if (!recordON)
1206 return false;
1207
1208 if (recordON->getState() == ISS_ON)
1209 return true;
1210
1211 svp->reset();
1212 recordON->setState(ISS_ON);
1213
1215
1216 return true;
1217}
1218
1219bool Camera::startDurationRecording(double duration)
1220{
1221 auto nvp = getNumber("RECORD_OPTIONS");
1222
1223 if (!nvp)
1224 return false;
1225
1226 auto durationN = nvp->findWidgetByName("RECORD_DURATION");
1227
1228 if (!durationN)
1229 return false;
1230
1231 auto svp = getSwitch("RECORD_STREAM");
1232
1233 if (!svp)
1234 return false;
1235
1236 auto recordON = svp->findWidgetByName("RECORD_DURATION_ON");
1237
1238 if (!recordON)
1239 return false;
1240
1241 if (recordON->getState() == ISS_ON)
1242 return true;
1243
1244 durationN->setValue(duration);
1246
1247 svp->reset();
1248 recordON->setState(ISS_ON);
1249
1251
1252 return true;
1253}
1254
1255bool Camera::startFramesRecording(uint32_t frames)
1256{
1257 auto nvp = getNumber("RECORD_OPTIONS");
1258
1259 if (!nvp)
1260 return false;
1261
1262 auto frameN = nvp->findWidgetByName("RECORD_FRAME_TOTAL");
1263 auto svp = getSwitch("RECORD_STREAM");
1264
1265 if (!frameN || !svp)
1266 return false;
1267
1268 auto recordON = svp->findWidgetByName("RECORD_FRAME_ON");
1269
1270 if (!recordON)
1271 return false;
1272
1273 if (recordON->getState() == ISS_ON)
1274 return true;
1275
1276 frameN->setValue(frames);
1278
1279 svp->reset();
1280 recordON->setState(ISS_ON);
1281
1283
1284 return true;
1285}
1286
1287bool Camera::stopRecording()
1288{
1289 auto svp = getSwitch("RECORD_STREAM");
1290
1291 if (!svp)
1292 return false;
1293
1294 auto recordOFF = svp->findWidgetByName("RECORD_OFF");
1295
1296 if (!recordOFF)
1297 return false;
1298
1299 // If already set
1300 if (recordOFF->getState() == ISS_ON)
1301 return true;
1302
1303 svp->reset();
1304 recordOFF->setState(ISS_ON);
1305
1307
1308 return true;
1309}
1310
1311bool Camera::setFITSHeaders(const QList<FITSData::Record> &values)
1312{
1313 auto tvp = getText("FITS_HEADER");
1314
1315 // Only proceed if FITS header has 3 fields introduced with INDI v2.0.1
1316 if (!tvp || tvp->count() < 3)
1317 return false;
1318
1319 for (auto &record : values)
1320 {
1321 tvp->at(0)->setText(record.key.toLatin1().constData());
1322 tvp->at(1)->setText(record.value.toString().toLatin1().constData());
1323 tvp->at(2)->setText(record.comment.toLatin1().constData());
1324
1326 }
1327
1328 return true;
1329}
1330
1331bool Camera::setGain(double value)
1332{
1333 if (!gainN)
1334 return false;
1335
1336 gainN->value = value;
1337 sendNewProperty(gainN->nvp);
1338 return true;
1339}
1340
1341bool Camera::getGain(double *value)
1342{
1343 if (!gainN)
1344 return false;
1345
1346 *value = gainN->value;
1347
1348 return true;
1349}
1350
1351bool Camera::getGainMinMaxStep(double *min, double *max, double *step)
1352{
1353 if (!gainN)
1354 return false;
1355
1356 *min = gainN->min;
1357 *max = gainN->max;
1358 *step = gainN->step;
1359
1360 return true;
1361}
1362
1363bool Camera::setOffset(double value)
1364{
1365 if (!offsetN)
1366 return false;
1367
1368 offsetN->value = value;
1369 sendNewProperty(offsetN->nvp);
1370 return true;
1371}
1372
1373bool Camera::getOffset(double *value)
1374{
1375 if (!offsetN)
1376 return false;
1377
1378 *value = offsetN->value;
1379
1380 return true;
1381}
1382
1383bool Camera::getOffsetMinMaxStep(double *min, double *max, double *step)
1384{
1385 if (!offsetN)
1386 return false;
1387
1388 *min = offsetN->min;
1389 *max = offsetN->max;
1390 *step = offsetN->step;
1391
1392 return true;
1393}
1394
1395bool Camera::isBLOBEnabled()
1396{
1397 return (m_Parent->getClientManager()->isBLOBEnabled(getDeviceName(), "CCD1"));
1398}
1399
1400bool Camera::setBLOBEnabled(bool enable, const QString &prop)
1401{
1402 m_Parent->getClientManager()->setBLOBEnabled(enable, getDeviceName(), prop);
1403
1404 return true;
1405}
1406
1407bool Camera::setFastExposureEnabled(bool enable)
1408{
1409 // Set value immediately
1410 m_FastExposureEnabled = enable;
1411
1412 auto svp = getSwitch("CCD_FAST_TOGGLE");
1413
1414 if (!svp)
1415 return false;
1416
1417 svp->at(0)->setState(enable ? ISS_ON : ISS_OFF);
1418 svp->at(1)->setState(enable ? ISS_OFF : ISS_ON);
1420
1421 return true;
1422}
1423
1424bool Camera::setCaptureFormat(const QString &format)
1425{
1426 auto svp = getSwitch("CCD_CAPTURE_FORMAT");
1427 if (!svp)
1428 return false;
1429
1430 for (auto &oneSwitch : *svp)
1431 oneSwitch.setState(oneSwitch.label == format ? ISS_ON : ISS_OFF);
1432
1434 return true;
1435}
1436
1437bool Camera::setFastCount(uint32_t count)
1438{
1439 auto nvp = getNumber("CCD_FAST_COUNT");
1440
1441 if (!nvp)
1442 return false;
1443
1444 nvp->at(0)->setValue(count);
1445
1447
1448 return true;
1449}
1450
1451bool Camera::setStreamExposure(double duration)
1452{
1453 auto nvp = getNumber("STREAMING_EXPOSURE");
1454
1455 if (!nvp)
1456 return false;
1457
1458 nvp->at(0)->setValue(duration);
1459
1461
1462 return true;
1463}
1464
1465bool Camera::getStreamExposure(double *duration)
1466{
1467 auto nvp = getNumber("STREAMING_EXPOSURE");
1468
1469 if (!nvp)
1470 return false;
1471
1472 *duration = nvp->at(0)->getValue();
1473
1474 return true;
1475}
1476
1477bool Camera::isCoolerOn()
1478{
1479 auto svp = getSwitch("CCD_COOLER");
1480
1481 if (!svp)
1482 return false;
1483
1484 return (svp->at(0)->getState() == ISS_ON);
1485}
1486
1487bool Camera::getTemperatureRegulation(double &ramp, double &threshold)
1488{
1489 auto regulation = getProperty("CCD_TEMP_RAMP");
1490 if (!regulation.isValid())
1491 return false;
1492
1493 ramp = regulation.getNumber()->at(0)->getValue();
1494 threshold = regulation.getNumber()->at(1)->getValue();
1495 return true;
1496}
1497
1498bool Camera::setTemperatureRegulation(double ramp, double threshold)
1499{
1500 auto regulation = getProperty("CCD_TEMP_RAMP");
1501 if (!regulation.isValid())
1502 return false;
1503
1504 regulation.getNumber()->at(0)->setValue(ramp);
1505 regulation.getNumber()->at(1)->setValue(threshold);
1506 sendNewProperty(regulation.getNumber());
1507 return true;
1508}
1509
1510bool Camera::setScopeInfo(double focalLength, double aperture)
1511{
1512 auto scopeInfo = getProperty("SCOPE_INFO");
1513 if (!scopeInfo.isValid())
1514 return false;
1515
1516 auto nvp = scopeInfo.getNumber();
1517 nvp->at(0)->setValue(focalLength);
1518 nvp->at(1)->setValue(aperture);
1520 return true;
1521}
1522
1523// Internal function to write an image blob to disk.
1524bool Camera::WriteImageFileInternal(const QString &filename, char *buffer, const size_t size)
1525{
1526 QFile file(filename);
1527 if (!file.open(QIODevice::WriteOnly))
1528 {
1529 qCCritical(KSTARS_INDI) << "ISD:CCD Error: Unable to open write file: " <<
1530 filename;
1531 return false;
1532 }
1533 int n = 0;
1534 QDataStream out(&file);
1535 bool ok = true;
1536 for (size_t nr = 0; nr < size; nr += n)
1537 {
1538 n = out.writeRawData(buffer + nr, size - nr);
1539 if (n < 0)
1540 {
1541 ok = false;
1542 break;
1543 }
1544 }
1545 ok = file.flush() && ok;
1546 file.close();
1547 file.setPermissions(QFileDevice::ReadUser |
1551 return ok;
1552}
1553
1554QString Camera::getCaptureFormat() const
1555{
1556 if (m_CaptureFormatIndex < 0 || m_CaptureFormats.isEmpty() || m_CaptureFormatIndex >= m_CaptureFormats.size())
1557 return QLatin1String("NA");
1558
1559 return m_CaptureFormats[m_CaptureFormatIndex];
1560}
1561}
@ ERROR_LOAD
Saving to disk error.
Definition indicamera.h:66
bool saveCurrentImage(QString &filename)
saveCurrentImage save the image that is currently in the image data buffer
void sendNewProperty(INDI::Property prop)
Send new property command to server.
INDI::PropertyView< IText > * getText(const QString &name) const
INDI::PropertyView< ISwitch > * getSwitch(const QString &name) const
static KNotification * event(const QString &eventId, const QString &text=QString(), const QPixmap &pixmap=QPixmap(), const NotificationFlags &flags=CloseOnTimeout, const QString &componentName=QString())
QString i18n(const char *text, const TYPE &arg...)
Type type(const QSqlDatabase &db)
ISD is a collection of INDI Standard Devices.
void error(QWidget *parent, const QString &text, const QString &title, const KGuiItem &buttonOk, Options options=Notify)
QString name(StandardAction id)
QString label(StandardShortcut id)
const char * constData() const const
char * data()
QByteArray fromRawData(const char *data, qsizetype size)
qsizetype size() const const
QDateTime currentDateTime()
QChar separator()
bool isRunning() const const
void waitForFinished()
QList< QByteArray > supportedImageFormats()
qsizetype count() const const
bool isEmpty() const const
qsizetype size() 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)
This file is part of the KDE documentation.
Documentation copyright © 1996-2024 The KDE developers.
Generated on Fri May 17 2024 11:48:26 by doxygen 1.10.0 written by Dimitri van Heesch, © 1997-2006

KDE's Doxygen guidelines are available online.