Instrument Neutral Distributed Interface INDI 2.2.5
streammanager.cpp
Go to the documentation of this file.
1/*
2 Copyright (C) 2020 by Pawel Soja <[email protected]>
3 Copyright (C) 2015 by Jasem Mutlaq <[email protected]>
4 Copyright (C) 2014 by geehalel <[email protected]>
5
6 Stream Recorder
7
8 This library is free software; you can redistribute it and/or
9 modify it under the terms of the GNU Lesser General Public
10 License as published by the Free Software Foundation; either
11 version 2.1 of the License, or (at your option) any later version.
12
13 This library is distributed in the hope that it will be useful,
14 but WITHOUT ANY WARRANTY; without even the implied warranty of
15 MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
16 Lesser General Public License for more details.
17
18 You should have received a copy of the GNU Lesser General Public
19 License along with this library; if not, write to the Free Software
20 Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
21
22*/
23
24#include <config.h>
25#include "streammanager.h"
26#include "streammanager_p.h"
27#include "indiccd.h"
28#include "indisensorinterface.h"
29#include "indilogger.h"
30#include "indiutility.h"
32#include "indielapsedtimer.h"
33
34#include <cerrno>
35#include <sys/stat.h>
36
37#include <algorithm>
38
39static const char * STREAM_TAB = "Streaming";
40
41namespace INDI
42{
43
45 : currentDevice(defaultDevice)
46{
48#ifdef __arm__
50#else
52#endif
53
55
56 LOGF_DEBUG("Using default recorder (%s)", recorder->getName());
57
59
61
62 LOGF_DEBUG("Using default encoder (%s)", encoder->getName());
63
65}
66
68{
69 if (framesThread.joinable())
70 {
72 framesIncoming.abort();
73 framesThread.join();
74 }
75}
76
78 : d_ptr(new StreamManagerPrivate(mainDevice))
79{ }
80
83
85{
87}
88
89const char * StreamManager::getDeviceName() const
90{
91 D_PTR(const StreamManager);
92 return d->getDeviceName();
93}
94
96{
97 /* Video Stream */
98 // @INDI_STANDARD_PROPERTY@
99 StreamSP[0].fill("STREAM_ON", "Stream On", ISS_OFF);
100 StreamSP[1].fill("STREAM_OFF", "Stream Off", ISS_ON);
102 StreamSP.fill(getDeviceName(), "SENSOR_DATA_STREAM", "Video Stream",
103 STREAM_TAB, IP_RW, ISR_1OFMANY, 0, IPS_IDLE);
104 else
105 StreamSP.fill(getDeviceName(), "CCD_VIDEO_STREAM", "Video Stream",
106 STREAM_TAB, IP_RW, ISR_1OFMANY, 0, IPS_IDLE);
107
108 // Full Depth Streaming — right after Video Stream toggle
109 FullDepthSP[FULL_DEPTH_8BIT ].fill("FULL_DEPTH_8BIT", "8-bit", ISS_ON);
110 FullDepthSP[FULL_DEPTH_16BIT].fill("FULL_DEPTH_16BIT", "16-bit", ISS_OFF);
111 FullDepthSP.fill(getDeviceName(), "STREAM_FULL_DEPTH", "Stream Depth", STREAM_TAB, IP_RW, ISR_1OFMANY, 0, IPS_IDLE);
112
113 // @INDI_STANDARD_PROPERTY@
114 StreamTimeNP[0].fill("STREAM_DELAY_TIME", "Delay (s)", "%.3f", 0, 60, 0.001, 0);
115 StreamTimeNP.fill(getDeviceName(), "STREAM_DELAY", "Video Stream Delay", STREAM_TAB, IP_RO, 0, IPS_IDLE);
116
117 // @INDI_STANDARD_PROPERTY@
118 StreamExposureNP[STREAM_EXPOSURE].fill("STREAMING_EXPOSURE_VALUE", "Duration (s)", "%.6f", 0.000001, 60, 0.1, 0.1);
119 StreamExposureNP[STREAM_DIVISOR ].fill("STREAMING_DIVISOR_VALUE", "Divisor", "%.f", 1, 15, 1.0, 1.0);
120 StreamExposureNP.fill(getDeviceName(), "STREAMING_EXPOSURE", "Expose", STREAM_TAB, IP_RW, 60, IPS_IDLE);
121
122 /* Measured FPS */
123 // @INDI_STANDARD_PROPERTY@
124 FpsNP[FPS_INSTANT].fill("EST_FPS", "Instant.", "%.2f", 0.0, 999.0, 0.0, 30);
125 FpsNP[FPS_AVERAGE].fill("AVG_FPS", "Average (1 sec.)", "%.2f", 0.0, 999.0, 0.0, 30);
126 FpsNP.fill(getDeviceName(), "FPS", "FPS", STREAM_TAB, IP_RO, 60, IPS_IDLE);
127
128 /* Record Frames */
129 /* File */
130 // @INDI_STANDARD_PROPERTY@
131 std::string defaultDirectory = std::string(getenv("HOME")) + std::string("/Videos/indi__D_");
132 RecordFileTP[0].fill("RECORD_FILE_DIR", "Dir.", defaultDirectory.data());
133 RecordFileTP[1].fill("RECORD_FILE_NAME", "Name", "indi_record__T_");
134 RecordFileTP.fill(getDeviceName(), "RECORD_FILE", "Record File",
135 STREAM_TAB, IP_RW, 0, IPS_IDLE);
136
137 /* Record Options */
138 // @INDI_STANDARD_PROPERTY@
139 RecordOptionsNP[0].fill("RECORD_DURATION", "Duration (sec)", "%.3f", 0.001, 999999.0, 0.0, 1.0);
140 RecordOptionsNP[1].fill("RECORD_FRAME_TOTAL", "Frames", "%.f", 1.0, 999999999.0, 1.0, 30.0);
141 RecordOptionsNP.fill(getDeviceName(), "RECORD_OPTIONS",
142 "Record Options", STREAM_TAB, IP_RW, 60, IPS_IDLE);
143
144 /* Record Switch */
145 // @INDI_STANDARD_PROPERTY@
146 RecordStreamSP[RECORD_ON ].fill("RECORD_ON", "Record On", ISS_OFF);
147 RecordStreamSP[RECORD_TIME ].fill("RECORD_DURATION_ON", "Record (Duration)", ISS_OFF);
148 RecordStreamSP[RECORD_FRAME].fill("RECORD_FRAME_ON", "Record (Frames)", ISS_OFF);
149 RecordStreamSP[RECORD_OFF ].fill("RECORD_OFF", "Record Off", ISS_ON);
150 RecordStreamSP.fill(getDeviceName(), "RECORD_STREAM", "Video Record", STREAM_TAB, IP_RW, ISR_1OFMANY, 0, IPS_IDLE);
151
153 {
154 // CCD Streaming Frame
155 // @INDI_STANDARD_PROPERTY@
156 StreamFrameNP[0].fill("X", "Left", "%.f", 0, 0, 0, 0);
157 StreamFrameNP[1].fill("Y", "Top", "%.f", 0, 0, 0, 0);
158 StreamFrameNP[2].fill("WIDTH", "Width", "%.f", 0, 0, 0, 0);
159 StreamFrameNP[3].fill("HEIGHT", "Height", "%.f", 0, 0, 0, 0);
160 StreamFrameNP.fill(getDeviceName(), "CCD_STREAM_FRAME", "Frame", STREAM_TAB, IP_RW,
161 60, IPS_IDLE);
162 }
163
164 // Encoder Selection
165 // @INDI_STANDARD_PROPERTY@
166 EncoderSP[ENCODER_RAW ].fill("RAW", "RAW", ISS_ON);
167 EncoderSP[ENCODER_MJPEG].fill("MJPEG", "MJPEG", ISS_OFF);
169 EncoderSP.fill(getDeviceName(), "SENSOR_STREAM_ENCODER", "Encoder", STREAM_TAB, IP_RW, ISR_1OFMANY, 0, IPS_IDLE);
170 else
171 EncoderSP.fill(getDeviceName(), "CCD_STREAM_ENCODER", "Encoder", STREAM_TAB, IP_RW, ISR_1OFMANY, 0, IPS_IDLE);
172
173 // Recorder Selector
174 // @INDI_STANDARD_PROPERTY@
175 RecorderSP[RECORDER_RAW].fill("SER", "SER", ISS_ON);
176 RecorderSP[RECORDER_OGV].fill("OGV", "OGV", ISS_OFF);
178 RecorderSP.fill(getDeviceName(), "SENSOR_STREAM_RECORDER", "Recorder", STREAM_TAB, IP_RW, ISR_1OFMANY, 0, IPS_IDLE);
179 else
180 RecorderSP.fill(getDeviceName(), "CCD_STREAM_RECORDER", "Recorder", STREAM_TAB, IP_RW, ISR_1OFMANY, 0, IPS_IDLE);
181
182 // If we do not have theora installed, let's just define SER default recorder
183#ifndef HAVE_THEORA
185#endif
186
187 // Limits
188 // @INDI_STANDARD_PROPERTY@
189 LimitsNP[LIMITS_BUFFER_MAX ].fill("LIMITS_BUFFER_MAX", "Maximum Buffer Size (MB)", "%.0f", 1, 1024 * 64, 1, 512);
190 LimitsNP[LIMITS_PREVIEW_FPS].fill("LIMITS_PREVIEW_FPS", "Maximum Preview FPS", "%.0f", 1, 120, 1, 10);
191 LimitsNP.fill(getDeviceName(), "LIMITS", "Limits", STREAM_TAB, IP_RW, 0, IPS_IDLE);
192
193 return true;
194}
195
197{
198 D_PTR(StreamManager);
199 return d->initProperties();
200}
201
223
224void StreamManager::ISGetProperties(const char * dev)
225{
226 D_PTR(StreamManager);
227 d->ISGetProperties(dev);
228}
229
231{
233 {
235 {
236 imageBP = currentDevice->getBLOB("CCD1");
237 }
239 {
240 imageBP = currentDevice->getBLOB("SENSOR");
241 }
242
256 }
257 else
258 {
272 }
273
274 return true;
275}
276
278{
279 D_PTR(StreamManager);
280 return d->updateProperties();
281}
282
283/*
284 * The camera driver is expected to send the FULL FRAME of the Camera after BINNING without any subframing at all
285 * Subframing for streaming/recording is done in the stream manager.
286 * Therefore nbytes is expected to be SubW/BinX * SubH/BinY * Bytes_Per_Pixels * Number_Color_Components
287 * Binned frame must be sent from the camera driver for this to work consistentaly for all drivers.*/
288void StreamManagerPrivate::newFrame(const uint8_t * buffer, uint32_t nbytes, uint64_t timestamp)
289{
290 // close the data stream on the same thread as the data stream
291 // manually triggered to stop recording.
293 {
295 return;
296 }
297
298 // Discard every N frame.
299 // do not count it to fps statistics
300 // N is StreamExposureNP[STREAM_DIVISOR].getValue()
302 if (
303 (StreamExposureNP[STREAM_DIVISOR].getValue() > 1) &&
304 (frameCountDivider % static_cast<int>(StreamExposureNP[STREAM_DIVISOR].getValue())) == 0
305 )
306 {
307 return;
308 }
309
310 if (FPSAverage.newFrame())
311 {
312 FpsNP[1].setValue(FPSAverage.framesPerSecond());
313 }
314
315 if (FPSFast.newFrame())
316 {
317 FpsNP[0].setValue(FPSFast.framesPerSecond());
318 if (fastFPSUpdate.try_lock()) // don't block stream thread / record thread
319 std::thread([&]()
320 {
321 FpsNP.apply();
322 fastFPSUpdate.unlock();
323 }).detach();
324 }
325
327 {
328 size_t allocatedSize = nbytes * framesIncoming.size() / 1024 / 1024; // allocated size in MB
329 if (allocatedSize > LimitsNP[LIMITS_BUFFER_MAX].getValue())
330 {
331 LOG_WARN("Frame buffer is full, skipping frame...");
332 return;
333 }
334
335 std::vector<uint8_t> copyBuffer(buffer, buffer + nbytes); // copy the frame
336
337 framesIncoming.push(TimeFrame{FPSFast.deltaTime(), timestamp, std::move(copyBuffer)}); // push it into the queue
338 }
339
341 {
342 FPSRecorder.newFrame(); // count frames and total time
343
344 // captured all frames, stream should be close
345 if (
346 (RecordStreamSP[RECORD_FRAME].getState() == ISS_ON && FPSRecorder.totalFrames() >= (RecordOptionsNP[1].getValue())) ||
347 (RecordStreamSP[RECORD_TIME ].getState() == ISS_ON && FPSRecorder.totalTime() >= (RecordOptionsNP[0].getValue() * 1000.0))
348 )
349 {
350 LOG_INFO("Waiting for all buffered frames to be recorded");
351 framesIncoming.waitForEmpty();
352 // duplicated message
353#if 0
354 LOGF_INFO(
355 "Ending record after %g millisecs and %d frames",
358 );
359#endif
364
366 }
367 }
368}
369
370void StreamManager::newFrame(const uint8_t * buffer, uint32_t nbytes, uint64_t timestamp)
371{
372 D_PTR(StreamManager);
373 d->newFrame(buffer, nbytes, timestamp);
374}
375
376
378{
379 FrameInfo srcFrameInfo;
380
381 uint8_t components = (PixelFormat == INDI_RGB) ? 3 : 1;
382 uint8_t bytesPerComponent = (PixelDepth + 7) / 8;
383
384 dstFrameInfo.bytesPerColor = components * bytesPerComponent;
385
387 {
388 srcFrameInfo = FrameInfo(
389 dynamic_cast<const INDI::CCD*>(currentDevice)->PrimaryCCD,
390 components * bytesPerComponent
391 );
392 }
394 {
395 srcFrameInfo = FrameInfo(
396 *dynamic_cast<const INDI::SensorInterface*>(currentDevice),
397 components * bytesPerComponent
398 );
399 }
400
401 // If stream frame was not yet initialized, let's do that now
402 if (dstFrameInfo.pixels() == 0)
403 {
404 //if (dynamic_cast<INDI::CCD*>(currentDevice)->PrimaryCCD.getNAxis() == 2)
405 // binFactor = dynamic_cast<INDI::CCD*>(currentDevice)->PrimaryCCD.getBinX();
406 dstFrameInfo = srcFrameInfo;
410 }
411
412 return srcFrameInfo;
413}
414
416 const uint8_t *srcBuffer,
417 const FrameInfo &srcFrameInfo,
418 uint8_t *dstBuffer,
419 const FrameInfo &dstFrameInfo
420)
421{
422 size_t srcOffset = srcFrameInfo.bytesPerColor * (dstFrameInfo.y * srcFrameInfo.w + dstFrameInfo.x);
423 uint32_t srcStride = srcFrameInfo.lineSize();
424 uint32_t dstStride = dstFrameInfo.lineSize();
425
426 srcBuffer += srcOffset;
427
428 // Copy line-by-line
429 for (size_t i = 0; i < dstFrameInfo.h; ++i)
430 {
431 memcpy(dstBuffer, srcBuffer, dstStride);
432 dstBuffer += dstStride;
433 srcBuffer += srcStride;
434 }
435}
436
438{
439 TimeFrame sourceTimeFrame;
440 sourceTimeFrame.time = 0;
441
442 std::vector<uint8_t> subframeBuffer; // Subframe buffer for recording/streaming
443 std::vector<uint8_t> downscaleBuffer; // Downscale buffer for streaming
444
445 INDI::SingleThreadPool previewThreadPool;
446 INDI::ElapsedTimer previewElapsed;
447
449 {
450 if (framesIncoming.pop(sourceTimeFrame) == false)
451 continue;
452
453 FrameInfo srcFrameInfo = updateSourceFrameInfo();
454
455 std::vector<uint8_t> *sourceBuffer = &sourceTimeFrame.frame;
456
457 // Source buffer size may be equal or larger than frame info size
458 // as some driver still retain full unbinned window size even when binning the output
459 // frame
460 if (PixelFormat != INDI_JPG && sourceBuffer->size() < srcFrameInfo.totalSize())
461 {
462 LOGF_ERROR("Source buffer size %d is less than frame size %d, skipping frame...", sourceBuffer->size(),
463 srcFrameInfo.totalSize());
464 continue;
465 }
466
467 // Check if we need to subframe
468 if (
470 dstFrameInfo.pixels() != 0 &&
471 dstFrameInfo != srcFrameInfo
472 )
473 {
474 subframeBuffer.resize(dstFrameInfo.totalSize());
475 subframe(sourceBuffer->data(), srcFrameInfo, subframeBuffer.data(), dstFrameInfo);
476
477 sourceBuffer = &subframeBuffer;
478 }
479
480 // For recording, save immediately.
481 {
482 std::lock_guard<std::mutex> lock(recordMutex);
483 if (
485 recordStream(sourceBuffer->data(), sourceBuffer->size(), sourceTimeFrame.time, sourceTimeFrame.timestamp) == false
486 )
487 {
488 LOG_ERROR("Recording failed.");
490 }
491 }
492
493 // For streaming, downscale to 8bit if higher than 8bit to reduce bandwidth
494 // unless Full Depth mode is enabled.
495 // You can reduce the number of frames by setting a frame limit.
497 {
498 // Downscale to 8bit for streaming to reduce bandwidth, unless full depth is enabled
499 if (PixelFormat != INDI_JPG && PixelDepth > 8 && FullDepthSP[FULL_DEPTH_16BIT].getState() != ISS_ON)
500 {
501 // Allocale new buffer if size changes
502 downscaleBuffer.resize(dstFrameInfo.pixels());
503
504 // Apply gamma
506 reinterpret_cast<const uint16_t*>(sourceBuffer->data()),
507 downscaleBuffer.size(),
508 downscaleBuffer.data()
509 );
510
511 sourceBuffer = &downscaleBuffer;
512 }
513
514 //uploadStream(sourceBuffer->data(), sourceBuffer->size());
515 previewThreadPool.start(std::bind([this, &previewElapsed](const std::atomic_bool & isAboutToQuit,
516 std::vector<uint8_t> frame)
517 {
518 INDI_UNUSED(isAboutToQuit);
519 previewElapsed.start();
520 uploadStream(frame.data(), frame.size());
521 StreamTimeNP[0].setValue(previewElapsed.nsecsElapsed() / 1000000000.0);
523
524 }, std::placeholders::_1, std::move(*sourceBuffer)));
525 }
526 }
527}
528
529void StreamManagerPrivate::setSize(uint16_t width, uint16_t height)
530{
531 if (width != StreamFrameNP[CCDChip::FRAME_W].getValue() || height != StreamFrameNP[CCDChip::FRAME_H].getValue())
532 {
533 if (PixelFormat == INDI_JPG)
534 LOG_WARN("Cannot subframe JPEG streams.");
535
536 StreamFrameNP[CCDChip::FRAME_X].setValue(0);
537 StreamFrameNP[CCDChip::FRAME_X].setMax(width - 1);
538 StreamFrameNP[CCDChip::FRAME_Y].setValue(0);
539 StreamFrameNP[CCDChip::FRAME_Y].setMax(height - 1);
540 StreamFrameNP[CCDChip::FRAME_W].setValue(width);
542 StreamFrameNP[CCDChip::FRAME_W].setMax(width);
543 StreamFrameNP[CCDChip::FRAME_H].setValue(height);
545 StreamFrameNP[CCDChip::FRAME_H].setMax(height);
546
549 }
550
555
556 // Width & Height are BINNED dimensions.
557 // Since they're the final size to make it to encoders and recorders.
558 rawWidth = width;
559 rawHeight = height;
560
561 for (EncoderInterface * oneEncoder : encoderManager.getEncoderList())
562 oneEncoder->setSize(rawWidth, rawHeight);
564 oneRecorder->setSize(rawWidth, rawHeight);
565}
566
568{
569 D_PTR(StreamManager);
570 std::lock_guard<std::mutex> lock(d->recordMutex);
571 return d->recorder->close();
572}
573
574bool StreamManagerPrivate::setPixelFormat(INDI_PIXEL_FORMAT pixelFormat, uint8_t pixelDepth)
575{
576 if (pixelFormat == PixelFormat && pixelDepth == PixelDepth)
577 return true;
578
579 bool recorderOK = recorder->setPixelFormat(pixelFormat, pixelDepth);
580 if (recorderOK == false)
581 {
582 LOGF_ERROR("Pixel format %d is not supported by %s recorder.", pixelFormat, recorder->getName());
583 }
584 else
585 {
586 LOGF_DEBUG("Pixel format %d is supported by %s recorder.", pixelFormat, recorder->getName());
587 }
588 bool encoderOK = encoder->setPixelFormat(pixelFormat, pixelDepth);
589 if (encoderOK == false)
590 {
591 LOGF_ERROR("Pixel format %d is not supported by %s encoder.", pixelFormat, encoder->getName());
592 }
593 else
594 {
595 LOGF_DEBUG("Pixel format %d is supported by %s encoder.", pixelFormat, encoder->getName());
596 }
597
598 PixelFormat = pixelFormat;
599 PixelDepth = pixelDepth;
600 return true;
601}
602
603bool StreamManager::setPixelFormat(INDI_PIXEL_FORMAT pixelFormat, uint8_t pixelDepth)
604{
605 D_PTR(StreamManager);
606 return d->setPixelFormat(pixelFormat, pixelDepth);
607}
608
609
610void StreamManager::setSize(uint16_t width, uint16_t height)
611{
612 D_PTR(StreamManager);
613 d->setSize(width, height);
614}
615
616bool StreamManagerPrivate::recordStream(const uint8_t * buffer, uint32_t nbytes, double deltams, uint64_t timestamp)
617{
618 INDI_UNUSED(deltams);
619 if (!isRecording)
620 return false;
621
622 return recorder->writeFrame(buffer, nbytes, timestamp);
623}
624
625std::string StreamManagerPrivate::expand(const std::string &fname, const std::map<std::string, std::string> &patterns)
626{
627 std::string result = fname;
628
629 std::time_t t = std::time(nullptr);
630 std::tm tm = *std::gmtime(&t);
631
632 auto extendedPatterns = patterns;
633 extendedPatterns["_D_"] = format_time(tm, "%Y-%m-%d");
634 extendedPatterns["_H_"] = format_time(tm, "%H-%M-%S");
635 extendedPatterns["_T_"] = format_time(tm, "%Y-%m-%d" "@" "%H-%M-%S");
636
637 for(const auto &pattern : extendedPatterns)
638 {
639 replace_all(result, pattern.first, pattern.second);
640 }
641
642 // Replace all : to - to be valid filename on Windows
643 std::replace(result.begin(), result.end(), ':', '-'); // it's really needed now?
644
645 return result;
646}
647
649{
650 char errmsg[MAXRBUF];
651 std::string filename, expfilename, expfiledir;
652 std::string filtername;
653 std::map<std::string, std::string> patterns;
654 if (isRecording)
655 return true;
656
658 {
659 /* get filter name for pattern substitution */
660 if (dynamic_cast<INDI::CCD * >(currentDevice)->CurrentFilterSlot != -1
661 && dynamic_cast<INDI::CCD * >(currentDevice)->CurrentFilterSlot <= static_cast<int>(dynamic_cast<INDI::CCD * >
662 (currentDevice)->FilterNames.size()))
663 {
664 filtername = dynamic_cast<INDI::CCD*>(currentDevice)->FilterNames.at(dynamic_cast<INDI::CCD*>
666 patterns["_F_"] = filtername;
667 LOGF_DEBUG("Adding filter pattern %s", filtername.c_str());
668 }
669 }
670
671 recorder->setFPS(FpsNP[FPS_AVERAGE].getValue());
672
673 // _HOME_ expands to this machine's home directory. Clients (e.g. Ekos) cannot know
674 // in advance what the home directory of a remote INDI server is, so they send the
675 // _HOME_ token literally and let the driver resolve it locally.
676 if (const char * home = getenv("HOME"))
677 patterns["_HOME_"] = home;
678
679 /* pattern substitution */
680 recordfiledir.assign(RecordFileTP[0].getText());
681 expfiledir = expand(recordfiledir, patterns);
682 if (expfiledir.at(expfiledir.size() - 1) != '/')
683 expfiledir += '/';
684 recordfilename.assign(RecordFileTP[1].getText());
685 expfilename = expand(recordfilename, patterns);
686 if (expfilename.size() < 4 || expfilename.substr(expfilename.size() - 4, 4) != recorder->getExtension())
687 expfilename += recorder->getExtension();
688
689 filename = expfiledir + expfilename;
690 //LOGF_INFO("Expanded file is %s", filename.c_str());
691 //filename=recordfiledir+recordfilename;
692 LOGF_INFO("Record file is %s", filename.c_str());
693 /* Create/open file/dir */
694 if (mkpath(expfiledir, 0755))
695 {
696 LOGF_WARN("Can not create record directory %s: %s", expfiledir.c_str(),
697 strerror(errno));
698 return false;
699 }
700 if (!recorder->open(filename.c_str(), errmsg))
701 {
704 LOGF_WARN("Can not open record file: %s", errmsg);
705 return false;
706 }
707
708#if 0
709 /* start capture */
710 // TODO direct recording should this be part of StreamManager?
711 if (direct_record)
712 {
713 LOG_INFO("Using direct recording (no software cropping).");
714 //v4l_base->doDecode(false);
715 //v4l_base->doRecord(true);
716 }
717 else
718 {
719 //if (ImageColorS[IMAGE_GRAYSCALE].s == ISS_ON)
720 if (dynamic_cast<INDI::CCD * >(currentDevice)->PrimaryCCD.getNAxis() == 2)
721 recorder->setDefaultMono();
722 else
723 recorder->setDefaultColor();
724 }
725#endif
728
729 if (isStreaming == false)
730 {
732 FPSFast.reset();
733 }
734
736 {
737 if (isStreaming == false && dynamic_cast<INDI::CCD * >(currentDevice)->StartStreaming() == false)
738 {
739 LOG_ERROR("Failed to start recording.");
744 }
745 }
747 {
748 if (isStreaming == false && dynamic_cast<INDI::SensorInterface * >(currentDevice)->StartStreaming() == false)
749 {
750 LOG_ERROR("Failed to start recording.");
755 }
756 }
757 isRecording = true;
758 return true;
759}
760
762{
763 if (!isRecording && force == false)
764 return true;
765
767 {
768 if (!isStreaming)
769 dynamic_cast<INDI::CCD * >(currentDevice)->StopStreaming();
770 }
772 {
773 if (!isStreaming)
774 dynamic_cast<INDI::SensorInterface * >(currentDevice)->StopStreaming();
775
776 }
777
778 isRecording = false;
780
781 {
782 std::lock_guard<std::mutex> lock(recordMutex);
783 recorder->close();
784 }
785
786 if (force)
787 return false;
788
789 LOGF_INFO(
790 "Record Duration: %g millisec / %d frames",
793 );
794
795 return true;
796}
797
798bool StreamManagerPrivate::ISNewSwitch(const char * dev, const char * name, ISState * states, char * names[], int n)
799{
800 /* ignore if not ours */
801 if (dev != nullptr && strcmp(getDeviceName(), dev))
802 return false;
803
804 /* Video Stream */
805 if (StreamSP.isNameMatch(name))
806 {
807 for (int i = 0; i < n; i++)
808 {
809 if (!strcmp(names[i], "STREAM_ON") && states[i] == ISS_ON)
810 {
811 setStream(true);
812 break;
813 }
814 else if (!strcmp(names[i], "STREAM_OFF") && states[i] == ISS_ON)
815 {
816 setStream(false);
817 break;
818 }
819 }
820 return true;
821 }
822
823 // Record Stream
824 if (RecordStreamSP.isNameMatch(name))
825 {
826 int prevSwitch = RecordStreamSP.findOnSwitchIndex();
827 RecordStreamSP.update(states, names, n);
828
829 if (isRecording && RecordStreamSP[RECORD_OFF].getState() != ISS_ON)
830 {
832 RecordStreamSP[prevSwitch].setState(ISS_ON);
834 LOG_WARN("Recording device is busy.");
835 return true;
836 }
837
838 if (
839 RecordStreamSP[RECORD_ON ].getState() == ISS_ON ||
840 RecordStreamSP[RECORD_TIME ].getState() == ISS_ON ||
841 RecordStreamSP[RECORD_FRAME].getState() == ISS_ON
842 )
843 {
844 if (!isRecording)
845 {
847 if (RecordStreamSP[RECORD_TIME].getState() == ISS_ON)
848 LOGF_INFO("Starting video record (Duration): %g secs.", RecordOptionsNP[0].getValue());
849 else if (RecordStreamSP[RECORD_FRAME].getState() == ISS_ON)
850 LOGF_INFO("Starting video record (Frame count): %d.", static_cast<int>(RecordOptionsNP[1].getValue()));
851 else
852 LOG_INFO("Starting video record.");
853
854 if (!startRecording())
855 {
859 }
860 }
861 }
862 else
863 {
865 Format.clear();
866 FpsNP[FPS_INSTANT].setValue(0);
867 FpsNP[FPS_AVERAGE].setValue(0);
868 if (isRecording)
869 {
870 LOG_INFO("Recording stream has been disabled. Closing the stream...");
872 }
873 }
874
876 return true;
877 }
878
879 // Encoder Selection
880 if (EncoderSP.isNameMatch(name))
881 {
882 EncoderSP.update(states, names, n);
884
885 const char * selectedEncoder = EncoderSP.findOnSwitch()->getName();
886
887 for (EncoderInterface * oneEncoder : encoderManager.getEncoderList())
888 {
889 if (!strcmp(selectedEncoder, oneEncoder->getName()))
890 {
891 encoderManager.setEncoder(oneEncoder);
892
893 oneEncoder->setPixelFormat(PixelFormat, PixelDepth);
894
895 encoder = oneEncoder;
896
898 }
899 }
901 return true;
902 }
903
904 // Recorder Selection
905 if (RecorderSP.isNameMatch(name))
906 {
907 RecorderSP.update(states, names, n);
909
910 const char * selectedRecorder = RecorderSP.findOnSwitch()->getName();
911
913 {
914 if (!strcmp(selectedRecorder, oneRecorder->getName()))
915 {
916 recorderManager.setRecorder(oneRecorder);
917
918 oneRecorder->setPixelFormat(PixelFormat, PixelDepth);
919
920 recorder = oneRecorder;
921
923 }
924 }
926 return true;
927 }
928
929 // Full Depth Streaming
930 if (FullDepthSP.isNameMatch(name))
931 {
932 currentDevice->updateProperty(FullDepthSP, states, names, n, [this, names]()
933 {
934 LOGF_INFO("Stream depth set to %s.", FullDepthSP[FULL_DEPTH_16BIT].isNameMatch(names[0]) == ISS_ON ? "16-bit" : "8-bit");
935 return true;
936 }, true);
937 return true;
938 }
939
940 // No properties were processed
941 return false;
942}
943
944bool StreamManager::ISNewSwitch(const char * dev, const char * name, ISState * states, char * names[], int n)
945{
946 D_PTR(StreamManager);
947 return d->ISNewSwitch(dev, name, states, names, n);
948}
949
950bool StreamManagerPrivate::ISNewText(const char * dev, const char * name, char * texts[], char * names[], int n)
951{
952 /* ignore if not ours */
953 if (dev != nullptr && strcmp(getDeviceName(), dev))
954 return false;
955
956 if (RecordFileTP.isNameMatch(name))
957 {
958 auto tp = RecordFileTP.findWidgetByName("RECORD_FILE_NAME");
959 if (strchr(tp->getText(), '/'))
960 {
961 LOG_WARN("Dir. separator (/) not allowed in filename.");
962 return true;
963 }
964
965 RecordFileTP.update(texts, names, n);
967 return true;
968 }
969
970 // No Properties were processed.
971 return false;
972}
973
974bool StreamManager::ISNewText(const char * dev, const char * name, char * texts[], char * names[], int n)
975{
976 D_PTR(StreamManager);
977 return d->ISNewText(dev, name, texts, names, n);
978}
979
980bool StreamManagerPrivate::ISNewNumber(const char * dev, const char * name, double values[], char * names[], int n)
981{
982 /* ignore if not ours */
983 if (dev != nullptr && strcmp(getDeviceName(), dev))
984 return false;
985
987 {
988 StreamExposureNP.update(values, names, n);
991 return true;
992 }
993
994 /* Limits */
995 if (LimitsNP.isNameMatch(name))
996 {
997 LimitsNP.update(values, names, n);
998
1000 FPSPreview.reset();
1001
1003 LimitsNP.apply();
1004 return true;
1005 }
1006
1007 /* Record Options */
1008 if (RecordOptionsNP.isNameMatch(name))
1009 {
1010 if (isRecording)
1011 {
1012 LOG_WARN("Recording device is busy");
1013 return true;
1014 }
1015
1016 RecordOptionsNP.update(values, names, n);
1019 return true;
1020 }
1021
1022 /* Stream Frame */
1023 if (StreamFrameNP.isNameMatch(name))
1024 {
1025 if (isRecording)
1026 {
1027 LOG_WARN("Recording device is busy");
1028 return true;
1029 }
1030
1031 FrameInfo srcFrameInfo;
1032
1034 {
1035 srcFrameInfo = FrameInfo(dynamic_cast<const INDI::CCD*>(currentDevice)->PrimaryCCD);
1036 }
1038 {
1039 srcFrameInfo = FrameInfo(*dynamic_cast<const INDI::SensorInterface*>(currentDevice));
1040 }
1041
1042 StreamFrameNP.update(values, names, n);
1044
1045 double subW = srcFrameInfo.w - StreamFrameNP[CCDChip::FRAME_X].getValue();
1046 double subH = srcFrameInfo.h - StreamFrameNP[CCDChip::FRAME_Y].getValue();
1047
1048 StreamFrameNP[CCDChip::FRAME_W].setValue(std::min(StreamFrameNP[CCDChip::FRAME_W].getValue(), subW));
1049 StreamFrameNP[CCDChip::FRAME_H].setValue(std::min(StreamFrameNP[CCDChip::FRAME_H].getValue(), subH));
1050
1052
1054 return true;
1055 }
1056
1057 // No properties were processed
1058 return false;
1059}
1060
1061bool StreamManager::ISNewNumber(const char * dev, const char * name, double values[], char * names[], int n)
1062{
1063 D_PTR(StreamManager);
1064 return d->ISNewNumber(dev, name, values, names, n);
1065}
1066
1068{
1069 if (enable)
1070 {
1071 if (!isStreaming)
1072 {
1074#if 0
1075 if (StreamOptionsN[OPTION_RATE_DIVISOR].value > 0)
1077 "Starting the video stream with target FPS %.f and rate divisor of %.f",
1078 StreamOptionsN[OPTION_TARGET_FPS].value, StreamOptionsN[OPTION_RATE_DIVISOR].value);
1079 else
1080 LOGF_INFO("Starting the video stream with target FPS %.f", StreamOptionsN[OPTION_TARGET_FPS].value);
1081#endif
1082 LOGF_INFO("Starting the video stream with target exposure %.6f s (Max theoretical FPS %.f)",
1083 StreamExposureNP[0].getValue(), 1 / StreamExposureNP[0].getValue());
1084
1085 FPSAverage.reset();
1086 FPSFast.reset();
1087 FPSPreview.reset();
1090
1092 {
1093 if (dynamic_cast<INDI::CCD * >(currentDevice)->StartStreaming() == false)
1094 {
1095 StreamSP.reset();
1098 LOG_ERROR("Failed to start streaming.");
1099 StreamSP.apply();
1100 return false;
1101 }
1102 }
1104 {
1105 if (dynamic_cast<INDI::SensorInterface * >(currentDevice)->StartStreaming() == false)
1106 {
1107 StreamSP.reset();
1110 LOG_ERROR("Failed to start streaming.");
1111 StreamSP.apply();
1112 return false;
1113 }
1114 }
1115 isStreaming = true;
1116 Format.clear();
1117 FpsNP[FPS_INSTANT].setValue(0);
1118 FpsNP[FPS_AVERAGE].setValue(0);
1119 StreamSP.reset();
1122 }
1123 }
1124 else
1125 {
1127 Format.clear();
1128 FpsNP[FPS_INSTANT].setValue(0);
1129 FpsNP[FPS_AVERAGE].setValue(0);
1130 if (isStreaming)
1131 {
1132 if (!isRecording)
1133 {
1135 {
1136 if (dynamic_cast<INDI::CCD * >(currentDevice)->StopStreaming() == false)
1137 {
1139 LOG_ERROR("Failed to stop streaming.");
1140 StreamSP.apply();
1141 return false;
1142 }
1143 }
1145 {
1146 if (dynamic_cast<INDI::SensorInterface * >(currentDevice)->StopStreaming() == false)
1147 {
1149 LOG_ERROR("Failed to stop streaming.");
1150 StreamSP.apply();
1151 return false;
1152 }
1153 }
1154 }
1155
1156 StreamSP.reset();
1158 isStreaming = false;
1159 Format.clear();
1160 FpsNP[FPS_INSTANT].setValue(0);
1161 FpsNP[FPS_AVERAGE].setValue(0);
1162
1163 recorder->setStreamEnabled(false);
1164 }
1165 }
1166
1167 StreamSP.apply();
1168 return true;
1169}
1170
1172{
1173 D_PTR(StreamManager);
1174 return d->setStream(enable);
1175}
1176
1178{
1179 D_PTR(StreamManager);
1180 d->EncoderSP.save(fp);
1181 d->RecordFileTP.save(fp);
1182 d->RecordOptionsNP.save(fp);
1183 d->RecorderSP.save(fp);
1184 d->LimitsNP.save(fp);
1185 d->FullDepthSP.save(fp);
1186 return true;
1187}
1188
1189void StreamManagerPrivate::getStreamFrame(uint16_t * x, uint16_t * y, uint16_t * w, uint16_t * h) const
1190{
1191 *x = StreamFrameNP[CCDChip::FRAME_X].getValue();
1192 *y = StreamFrameNP[CCDChip::FRAME_Y].getValue();
1193 *w = StreamFrameNP[CCDChip::FRAME_W].getValue();
1194 *h = StreamFrameNP[CCDChip::FRAME_H].getValue();
1195}
1196
1197void StreamManagerPrivate::setStreamFrame(uint16_t x, uint16_t y, uint16_t w, uint16_t h)
1198{
1199 StreamFrameNP[CCDChip::FRAME_X].setValue(x);
1200 StreamFrameNP[CCDChip::FRAME_Y].setValue(y);
1201 StreamFrameNP[CCDChip::FRAME_W].setValue(w);
1202 StreamFrameNP[CCDChip::FRAME_H].setValue(h);
1203}
1204
1206{
1207 setStreamFrame(frameInfo.x, frameInfo.y, frameInfo.w, frameInfo.h);
1208}
1209
1210void StreamManager::getStreamFrame(uint16_t * x, uint16_t * y, uint16_t * w, uint16_t * h) const
1211{
1212 D_PTR(const StreamManager);
1213 d->getStreamFrame(x, y, w, h);
1214}
1215
1216bool StreamManagerPrivate::uploadStream(const uint8_t * buffer, uint32_t nbytes)
1217{
1218 // Send as is, already encoded.
1219 if (PixelFormat == INDI_JPG)
1220 {
1221 imageBP[0].setBlob(const_cast<uint8_t *>(buffer));
1222 imageBP[0].setBlobLen(nbytes);
1223 imageBP[0].setSize(nbytes);
1224 imageBP[0].setFormat(".stream_jpg");
1226 imageBP.apply();
1227 return true;
1228 }
1229
1230 // Binning for grayscale frames only for now - REMOVE ME
1231#if 0
1232 if (dynamic_cast<INDI::CCD * >(currentDevice)->PrimaryCCD.getNAxis() == 2)
1233 {
1234 dynamic_cast<INDI::CCD*>(currentDevice)->PrimaryCCD.binFrame();
1235 nbytes /= dynamic_cast<INDI::CCD*>(currentDevice)->PrimaryCCD.getBinX() * dynamic_cast<INDI::CCD*>
1236 (currentDevice)->PrimaryCCD.getBinY();
1237 }
1238#endif
1239
1241 {
1242 if (encoder->upload(&imageBP[0], buffer, nbytes, dynamic_cast<INDI::CCD * >(currentDevice)->PrimaryCCD.isCompressed()))
1243 {
1244 // Upload to client now
1246 imageBP.apply();
1247 return true;
1248 }
1249 }
1251 {
1252 if (encoder->upload(&imageBP[0], buffer, nbytes,
1253 false))//dynamic_cast<INDI::SensorInterface*>(currentDevice)->isCompressed()))
1254 {
1255 // Upload to client now
1257 imageBP.apply();
1258 return true;
1259 }
1260 }
1261
1262 return false;
1263}
1264
1266{
1267 D_PTR(const StreamManager);
1268 return d->recorder;
1269}
1270
1272{
1273 D_PTR(const StreamManager);
1274 return d->direct_record;
1275}
1276
1278{
1279 D_PTR(const StreamManager);
1280 return d->isStreaming;
1281}
1282
1284{
1285 D_PTR(const StreamManager);
1286 return d->isRecording && !d->isRecordingAboutToClose;
1287}
1288
1290{
1291 D_PTR(const StreamManager);
1292 return (d->isStreaming || d->isRecording);
1293}
1294
1296{
1297 D_PTR(const StreamManager);
1298 return 1.0 / d->StreamExposureNP[0].getValue();
1299}
1301{
1302 D_PTR(const StreamManager);
1303 return d->StreamExposureNP[0].getValue();
1304}
1305
1307{
1308 D_PTR(StreamManager);
1309 d->hasStreamingExposure = enable;
1310}
1311
1312}
void apply(const uint16_t *source, size_t count, uint8_t *destination) const
bool isConnected() const
const char * getDeviceName() const
INDI::PropertyBlob getBLOB(const char *name) const
bool isCompressed() const
isCompressed
Class to provide general functionality of CCD cameras with a single CCD sensor, or a primary CCD sens...
Definition indiccd.h:108
CCDChip PrimaryCCD
Definition indiccd.h:621
int CurrentFilterSlot
Definition indiccd.h:615
Class to provide extended functionality for devices in addition to the functionality provided by INDI...
bool updateProperty(PropertyType &property, ValueType *values, char *names[], int n, std::function< bool()> updater, bool saveConfig=false)
Generic helper function to update an INDI property based on an external operation.
virtual bool deleteProperty(const char *propertyName)
Delete a property and unregister it. It will also be deleted from all clients.
void defineProperty(INumberVectorProperty *property)
uint32_t getDriverInterface() const
The ElapsedTimer class provides a fast way to calculate elapsed times.
int64_t nsecsElapsed() const
Returns the number of nanoseconds since this ElapsedTimer was last started.
void start()
Starts this timer. Once started, a timer value can be checked with elapsed().
The EncoderInterface class is the base class for video streaming encoders.
virtual void init(INDI::DefaultDevice *mainDevice)
virtual bool setPixelFormat(INDI_PIXEL_FORMAT pixelFormat, uint8_t pixelDepth)
virtual bool upload(INDI::WidgetViewBlob *bp, const uint8_t *buffer, uint32_t nbytes, bool isCompressed=false)=0
void setEncoder(EncoderInterface *encoder)
EncoderInterface * getDefaultEncoder()
std::vector< EncoderInterface * > getEncoderList()
void reset()
Reset all frame information.
Definition fpsmeter.cpp:80
void setTimeWindow(double timeWindow)
Time window setup.
Definition fpsmeter.cpp:55
double deltaTime() const
Time in milliseconds between last frames.
Definition fpsmeter.cpp:65
double totalTime() const
Total time.
Definition fpsmeter.cpp:75
uint64_t totalFrames() const
Total frames.
Definition fpsmeter.cpp:70
double framesPerSecond() const
Number of frames per second counted in the time window.
Definition fpsmeter.cpp:60
bool newFrame()
When you get a frame, call the function to count.
Definition fpsmeter.cpp:31
void setState(IPState state)
WidgetView< T > * findWidgetByName(const char *name) const
void apply(const char *format,...) const ATTRIBUTE_FORMAT_PRINTF(2
void resize(size_t size)
const char * getName() const
bool isNameMatch(const char *otherName) const
bool update(const double values[], const char *const names[], int n)
void fill(const char *device, const char *name, const char *label, const char *group, IPerm permission, double timeout, IPState state)
bool update(const ISState states[], const char *const names[], int n)
INDI::WidgetViewSwitch * findOnSwitch() const
void fill(const char *device, const char *name, const char *label, const char *group, IPerm permission, ISRule rule, double timeout, IPState state)
bool update(const char *const texts[], const char *const names[], int n)
void fill(const char *device, const char *name, const char *label, const char *group, IPerm permission, double timeout, IPState state)
The RecorderInterface class is the base class for recorders.
virtual bool writeFrame(const uint8_t *frame, uint32_t nbytes, uint64_t timestamp)=0
virtual const char * getName()
virtual bool open(const char *filename, char *errmsg)=0
virtual bool close()=0
virtual bool setPixelFormat(INDI_PIXEL_FORMAT pixelFormat, uint8_t pixelDepth=8)=0
virtual bool setFPS(float FPS)
virtual void setStreamEnabled(bool enable)=0
virtual const char * getExtension()=0
std::vector< RecorderInterface * > getRecorderList()
RecorderInterface * getDefaultRecorder()
void setRecorder(RecorderInterface *recorder)
The SensorDevice class provides functionality of a Sensor Device within a Sensor.
void start(const std::function< void(const std::atomic_bool &isAboutToClose)> &functionToRun)
Reserves a thread and uses it to run functionToRun. A running function can check the 'isAboutToClose'...
bool ISNewSwitch(const char *dev, const char *name, ISState *states, char *names[], int n)
std::atomic< bool > isRecording
bool setPixelFormat(INDI_PIXEL_FORMAT pixelFormat, uint8_t pixelDepth)
INDI::PropertySwitch RecordStreamSP
static std::string expand(const std::string &fname, const std::map< std::string, std::string > &patterns)
void setStreamFrame(uint16_t x, uint16_t y, uint16_t w, uint16_t h)
INDI::PropertySwitch EncoderSP
std::atomic< bool > isStreaming
INDI::PropertySwitch StreamSP
static void subframe(const uint8_t *srcBuffer, const FrameInfo &srcFrameInfo, uint8_t *dstBuffer, const FrameInfo &dstFrameInfo)
void newFrame(const uint8_t *buffer, uint32_t nbytes, uint64_t timestamp)
std::atomic< bool > isRecordingAboutToClose
bool stopRecording(bool force=false)
UniqueQueue< TimeFrame > framesIncoming
INDI::PropertySwitch FullDepthSP
std::atomic< bool > framesThreadTerminate
INDI::PropertyNumber RecordOptionsNP
const char * getDeviceName() const
INDI::PropertyNumber StreamTimeNP
INDI::PropertyNumber FpsNP
void setSize(uint16_t width, uint16_t height)
bool ISNewText(const char *dev, const char *name, char *texts[], char *names[], int n)
bool uploadStream(const uint8_t *buffer, uint32_t nbytes)
uploadStream Upload frame to client using the selected encoder
INDI_PIXEL_FORMAT PixelFormat
bool recordStream(const uint8_t *buffer, uint32_t nbytes, double deltams, uint64_t timestamp)
recordStream Calls the backend recorder to record a single frame.
RecorderInterface * recorder
INDI::PropertyText RecordFileTP
INDI::PropertyNumber StreamFrameNP
INDI::PropertyNumber LimitsNP
StreamManagerPrivate(DefaultDevice *defaultDevice)
void getStreamFrame(uint16_t *x, uint16_t *y, uint16_t *w, uint16_t *h) const
void asyncStreamThread()
Thread processing frames and forwarding to recording and preview.
void ISGetProperties(const char *dev)
bool ISNewNumber(const char *dev, const char *name, double values[], char *names[], int n)
INDI::PropertyNumber StreamExposureNP
INDI::PropertySwitch RecorderSP
void setSize(uint16_t width, uint16_t height=1)
bool isDirectRecording() const
void getStreamFrame(uint16_t *x, uint16_t *y, uint16_t *w, uint16_t *h) const
void newFrame(const uint8_t *buffer, uint32_t nbytes, uint64_t timestamp=0)
newFrame CCD drivers call this function when a new frame is received. It is then streamed,...
void setStreamingExposureEnabled(bool enable)
setStreamingExposureEnabled Can stream exposure time be changed?
bool setPixelFormat(INDI_PIXEL_FORMAT pixelFormat, uint8_t pixelDepth=8)
double getTargetExposure() const
bool setStream(bool enable)
setStream Enables (starts) or disables (stops) streaming.
double getTargetFPS() const
virtual bool ISNewText(const char *dev, const char *name, char *texts[], char *names[], int n)
virtual bool ISNewSwitch(const char *dev, const char *name, ISState *states, char *names[], int n)
virtual void ISGetProperties(const char *dev)
virtual bool initProperties()
StreamManager(DefaultDevice *currentDevice)
virtual bool ISNewNumber(const char *dev, const char *name, double values[], char *names[], int n)
virtual bool saveConfigItems(FILE *fp)
RecorderInterface * getRecorder() const
virtual bool updateProperties()
const char * getDeviceName() const
int errno
ISState
Switch state.
Definition indiapi.h:148
@ ISS_OFF
Definition indiapi.h:149
@ ISS_ON
Definition indiapi.h:150
@ IP_RW
Definition indiapi.h:184
@ IP_RO
Definition indiapi.h:182
@ IPS_BUSY
Definition indiapi.h:161
@ IPS_ALERT
Definition indiapi.h:162
@ IPS_IDLE
Definition indiapi.h:159
@ IPS_OK
Definition indiapi.h:160
@ ISR_1OFMANY
Definition indiapi.h:171
INDI_PIXEL_FORMAT
@ INDI_RGB
@ INDI_JPG
#define INDI_UNUSED(x)
Definition indidevapi.h:132
#define MAXRBUF
Definition indidriver.c:52
#define LOGF_INFO(fmt,...)
Definition indilogger.h:82
#define LOGF_WARN(fmt,...)
Definition indilogger.h:81
#define LOG_WARN(txt)
Definition indilogger.h:73
#define LOGF_DEBUG(fmt,...)
Definition indilogger.h:83
#define LOG_ERROR(txt)
Shorter logging macros. In order to use these macros, the function (or method) "getDeviceName()" must...
Definition indilogger.h:72
#define LOGF_ERROR(fmt,...)
Definition indilogger.h:80
#define LOG_INFO(txt)
Definition indilogger.h:74
#define DEBUGF(priority, msg,...)
Definition indilogger.h:57
Namespace to encapsulate INDI client, drivers, and mediator classes.
std::string format_time(const std::tm &tm, const char *format)
void replace_all(std::string &subject, const std::string &search, const std::string &replace)
int mkpath(std::string s, mode_t mode)
const char * getName() const