Line data Source code
1 : /** \file frameGrabber.hpp
2 : * \brief The MagAO-X generic frame grabber.
3 : *
4 : * \author Jared R. Males (jaredmales@gmail.com)
5 : *
6 : * \ingroup app_files
7 : */
8 :
9 : #ifndef frameGrabber_hpp
10 : #define frameGrabber_hpp
11 :
12 : #include <sys/syscall.h>
13 :
14 : #include <mx/sigproc/circularBuffer.hpp>
15 : #include <mx/math/vectorUtils.hpp>
16 : #include <mx/improc/imageUtils.hpp>
17 :
18 : #include "../../ImageStreamIO/ImageStruct.hpp"
19 : #include <ImageStreamIO/ImageStreamIO.h>
20 :
21 : #include "../../common/paths.hpp"
22 :
23 : namespace MagAOX
24 : {
25 : namespace app
26 : {
27 : namespace dev
28 : {
29 :
30 : /** MagAO-X generic frame grabber
31 : *
32 : *
33 : * The derived class `derivedT` has the following requirements:
34 : *
35 : * - Must be derived from MagAOXApp<true>
36 : *
37 : * - Must be derived from telemeter<derivedT> (see below for required interface)
38 : *
39 : * - Must contain the following friend declaration:
40 : * \code
41 : * friend class dev::frameGrabber<derivedT>; //replace derivedT
42 : * \endcode
43 : *
44 : * - Must declare the following typedef:
45 : * \code
46 : * typedef dev::frameGrabber<derivedT> frameGrabberT; //replace derivedT
47 : * \endcode
48 : *
49 : * - must expose the following interface
50 : * \code
51 : * //Configures the camera for acquistion, must also set m_width, m_height, and m_dataType
52 : * //so that the shared memory can be allocated
53 : * int derivedT::configureAcquisition();
54 : *
55 : * //Gets the frames-per-second readout rate
56 : * //used for the latency statistics
57 : * float derivedT::fps();
58 : *
59 : * //Start acquisition.
60 : * int derivedT::startAcquisition();
61 : *
62 : * //Acquires the data, and checks if it is valid.
63 : * //This should set m_currImageTimestamp to the image timestamp.
64 : * // returns 0 if valid, < 0 on error, > 0 on no data.
65 : * int derivedT::acquireAndCheckValid()
66 : *
67 : * //Loads the acquired image into the stream, copying it to the appropriate member of m_imageStream->array.
68 : * //This could simply be a memcpy.
69 : * int derivedT::loadImageIntoStream(void * dest);
70 : *
71 : * //Take any actions needed to reconfigure the system. Called if m_reconfig is set to true.
72 : * int derivedT::reconfig()
73 : * \endcode
74 : * Each of the above functions should return 0 on success, and -1 on an error (except fps).
75 : * For `acquireAndCheckValid` >0 will indicate no data but not an error. In most cases,
76 : * an appropriate state code, such as NOTCONNECTED, should be set as well.
77 : *
78 : * - A derived class may optionally expose
79 : * \code
80 : * int derivedT::frameGrabberPostPublish( IMAGE *imageStream );
81 : * \endcode
82 : * to perform additional publication work immediately after the main image stream semaphores are posted.
83 : *
84 : * - A static configuration variable must be defined in derivedT as
85 : * \code
86 : * static constexpr bool c_frameGrabber_flippable =true; //or: false
87 : * \endcode
88 : * which determines whether or not the images can be flipped programmatically.
89 : *
90 : * - Calls to this class's `setupConfig`, `loadConfig`, `appStartup`, `appLogic`, `updateINDI`, and `appShutdown`
91 : * functions must be placed in the derived class's functions of the same name. For convenience the
92 : * following macros are defined to provide error checking:
93 : * \code
94 : * FRAMEGRABBER_SETUP_CONFIG( cfig )
95 : * FRAMEGRABBER_LOAD_CONFIG( cfig )
96 : * FRAMEGRABBER_APP_STARTUP
97 : * FRAMEGRABBER_APP_LOGIC
98 : * FRAMEGRABBER_UPDATE_INDI
99 : * FRAMEGRABBER_APP_SHUTDOWN
100 : * \endcode
101 : *
102 : * \ingroup appdev
103 : */
104 : template <class derivedT>
105 : class frameGrabber
106 : {
107 : public:
108 : enum fgFlip
109 : {
110 : fgFlipNone,
111 : fgFlipUD,
112 : fgFlipLR,
113 : fgFlipUDLR
114 : };
115 :
116 : typedef int32_t cbIndexT;
117 :
118 : protected:
119 : /** \name Configurable Parameters
120 : * @{
121 : */
122 : std::string m_shmimName{ "" }; ///< The name of the shared memory image, is used in `/tmp/<shmimName>.im.shm`.
123 : ///< Derived classes should set a default.
124 :
125 : int m_fgThreadPrio{ 2 }; ///< Priority of the framegrabber thread, should normally be > 00.
126 : std::string m_fgCpuset; ///< The cpuset to assign the framegrabber thread to. Not used if empty, the default.
127 :
128 : uint32_t m_circBuffLength{ 1 }; ///< Length of the circular buffer, in frames
129 :
130 : cbIndexT m_latencyCircBuffMaxLength{ 100000 }; ///< Maximum length of the latency measurement circular buffers
131 :
132 : float m_latencyCircBuffMaxTime{ 5 }; ///< Maximum time of the latency meaurement circular buffers
133 :
134 : int m_defaultFlip{ fgFlipNone };
135 :
136 : ///@}
137 :
138 : uint32_t m_width{ 0 }; ///< The width of the image, once deinterlaced etc.
139 : uint32_t m_height{ 0 }; ///< The height of the image, once deinterlaced etc.
140 :
141 : uint8_t m_dataType{ 0 }; ///< The ImageStreamIO type code.
142 : size_t m_typeSize{ 0 }; ///< The size of the type, in bytes. Result of sizeof.
143 :
144 : int m_xbinning{ 0 }; ///< The x-binning according to the framegrabber
145 : int m_ybinning{ 0 }; ///< The y-binning according to the framegrabber
146 :
147 : timespec m_currImageTimestamp{ 0, 0 }; ///< The timestamp of the current image.
148 :
149 : bool m_reconfig{ false }; ///< Flag to set if a camera reconfiguration requires a framegrabber reset.
150 :
151 : IMAGE *m_imageStream{ nullptr }; ///< The ImageStreamIO shared memory buffer.
152 :
153 : bool m_ownShmim{ true }; ///< Flag controlling if the shmim is owned. If true it will be destroyed as needed.
154 :
155 : ino_t m_inode{ 0 }; ///< The inode of the image stream file
156 :
157 : float m_cbFPS{ 0 }; ///< The FPS used to configure the circular buffers
158 :
159 : mx::sigproc::circularBufferIndex<timespec, cbIndexT> m_atimes;
160 : mx::sigproc::circularBufferIndex<timespec, cbIndexT> m_wtimes;
161 :
162 : std::vector<double> m_atimesD;
163 : std::vector<double> m_wtimesD;
164 : std::vector<double> m_watimesD;
165 :
166 : timespec m_dummy_ts{ 0, 0 };
167 : uint64_t m_dummy_cnt{ 0 };
168 : char m_dummy_c{ 0 };
169 :
170 : double m_mna;
171 : double m_vara;
172 : double m_mina;
173 : double m_maxa;
174 :
175 : double m_mnw;
176 : double m_varw;
177 : double m_minw;
178 : double m_maxw;
179 :
180 : double m_mnwa;
181 : double m_varwa;
182 :
183 : public:
184 : /// Setup the configuration system
185 : /**
186 : * This should be called in `derivedT::setupConfig` as
187 : * \code
188 : framegrabber<derivedT>::setupConfig(config);
189 : \endcode
190 : * with appropriate error checking.
191 : */
192 : int setupConfig( mx::app::appConfigurator &config /**< [out] the derived classes configurator*/ );
193 :
194 : /// load the configuration system results
195 : /**
196 : * This should be called in `derivedT::loadConfig` as
197 : * \code
198 : framegrabber<derivedT>::loadConfig(config);
199 : \endcode
200 : * with appropriate error checking.
201 : */
202 : int loadConfig( mx::app::appConfigurator &config /**< [in] the derived classes configurator*/ );
203 :
204 : /// Startup function
205 : /** Starts the framegrabber thread
206 : * This should be called in `derivedT::appStartup` as
207 : * \code
208 : framegrabber<derivedT>::appStartup();
209 : \endcode
210 : * with appropriate error checking.
211 : *
212 : * \returns 0 on success
213 : * \returns -1 on error, which is logged.
214 : */
215 : int appStartup();
216 :
217 : /// Checks the framegrabber thread
218 : /** This should be called in `derivedT::appLogic` as
219 : * \code
220 : framegrabber<derivedT>::appLogic();
221 : \endcode
222 : * with appropriate error checking.
223 : *
224 : * \returns 0 on success
225 : * \returns -1 on error, which is logged.
226 : */
227 : int appLogic();
228 :
229 : /// On power off, sets m_reconfig to true.
230 : /** This should be called in `derivedT::onPowerOff` as
231 : * \code
232 : framegrabber<derivedT>::onPowerOff();
233 : \endcode
234 : * with appropriate error checking.
235 : *
236 : * \returns 0 on success
237 : * \returns -1 on error, which is logged.
238 : */
239 : int onPowerOff();
240 :
241 : /// Shuts down the framegrabber thread
242 : /** This should be called in `derivedT::appShutdown` as
243 : * \code
244 : framegrabber<derivedT>::appShutdown();
245 : \endcode
246 : * with appropriate error checking.
247 : *
248 : * \returns 0 on success
249 : * \returns -1 on error, which is logged.
250 : */
251 : int appShutdown();
252 :
253 : int configCircBuffs();
254 :
255 : protected:
256 : /** \name Framegrabber Thread
257 : * This thread actually manages the framegrabbing hardware
258 : * @{
259 : */
260 :
261 : bool m_fgThreadInit{ true }; ///< Synchronizer for thread startup, to allow priority setting to finish.
262 :
263 : pid_t m_fgThreadID{ 0 }; ///< The ID of the framegrabber thread.
264 :
265 : pcf::IndiProperty m_fgThreadProp; ///< The property to hold the f.g. thread details.
266 :
267 : std::thread m_fgThread; ///< A separate thread for the actual framegrabbings
268 :
269 : /// Thread starter, called by MagAOXApp::threadStart on thread construction. Calls fgThreadExec.
270 : static void fgThreadStart( frameGrabber *o /**< [in] a pointer to a frameGrabber instance (normally this) */ );
271 :
272 : /// Execute framegrabbing.
273 : void fgThreadExec();
274 :
275 : ///@}
276 :
277 : void *loadImageIntoStreamCopy( void *dest, void *src, size_t width, size_t height, size_t szof );
278 :
279 : int openShmim();
280 :
281 : /** \name INDI
282 : *
283 : *@{
284 : */
285 : protected:
286 : // declare our properties
287 :
288 : pcf::IndiProperty m_indiP_shmimName; ///< Property used to report the shmim buffer name
289 :
290 : pcf::IndiProperty m_indiP_frameSize; ///< Property used to report the current frame size
291 :
292 : pcf::IndiProperty m_indiP_timing;
293 :
294 : public:
295 : /// Update the INDI properties for this device controller
296 : /** You should call this once per main loop.
297 : * It is not called automatically.
298 : *
299 : * \returns 0 on success.
300 : * \returns -1 on error.
301 : */
302 : int updateINDI();
303 :
304 : ///@}
305 :
306 : /** \name Telemeter Interface
307 : * @{
308 : */
309 :
310 : int recordFGTimings( bool force = false );
311 :
312 : /// @}
313 :
314 : private:
315 : /// Call an optional derived-class hook after the main stream publication completes.
316 : template <class hookT>
317 : static auto postPublishHook( hookT &hookOwner, /**< [in] derived object that may expose the hook */
318 : IMAGE *imageStream, /**< [in] published image stream */
319 : int callPriorityTag /**< [in] overload selector preferring the hook */
320 : ) -> decltype( hookOwner.frameGrabberPostPublish( imageStream ) );
321 :
322 : /// Return success when the derived class does not expose a post-publication hook.
323 : static int postPublishHook( derivedT &hookOwner, /**< [in] derived object lacking the optional hook */
324 : IMAGE *imageStream, /**< [in] published image stream */
325 : long callPriorityTag /**< [in] fallback overload selector */
326 : );
327 :
328 71 : derivedT &derived()
329 : {
330 71 : return *static_cast<derivedT *>( this );
331 : }
332 : };
333 :
334 : template <class derivedT>
335 : template <class hookT>
336 0 : auto frameGrabber<derivedT>::postPublishHook( hookT &hookOwner, IMAGE *imageStream, int callPriorityTag )
337 : -> decltype( hookOwner.frameGrabberPostPublish( imageStream ) )
338 : {
339 : static_cast<void>( callPriorityTag );
340 :
341 0 : return hookOwner.frameGrabberPostPublish( imageStream );
342 : }
343 :
344 : template <class derivedT>
345 0 : int frameGrabber<derivedT>::postPublishHook( derivedT &hookOwner, IMAGE *imageStream, long callPriorityTag )
346 : {
347 : static_cast<void>( hookOwner );
348 : static_cast<void>( imageStream );
349 : static_cast<void>( callPriorityTag );
350 :
351 0 : return 0;
352 : }
353 :
354 : template <class derivedT>
355 16 : int frameGrabber<derivedT>::setupConfig( mx::app::appConfigurator &config )
356 : {
357 224 : config.add( "framegrabber.threadPrio",
358 : "",
359 : "framegrabber.threadPrio",
360 : argType::Required,
361 : "framegrabber",
362 : "threadPrio",
363 : false,
364 : "int",
365 : "The real-time priority of the framegrabber thread." );
366 :
367 224 : config.add( "framegrabber.cpuset",
368 : "",
369 : "framegrabber.cpuset",
370 : argType::Required,
371 : "framegrabber",
372 : "cpuset",
373 : false,
374 : "string",
375 : "The cpuset to assign the framegrabber thread to." );
376 :
377 224 : config.add( "framegrabber.shmimName",
378 : "",
379 : "framegrabber.shmimName",
380 : argType::Required,
381 : "framegrabber",
382 : "shmimName",
383 : false,
384 : "string",
385 : "The name of the ImageStreamIO shared memory image. Will be used as /milk/shm/<shmimName>.im.shm." );
386 :
387 224 : config.add( "framegrabber.circBuffLength",
388 : "",
389 : "framegrabber.circBuffLength",
390 : argType::Required,
391 : "framegrabber",
392 : "circBuffLength",
393 : false,
394 : "size_t",
395 : "The length of the circular buffer. Sets m_circBuffLength, default is 1." );
396 :
397 224 : config.add( "framegrabber.latencyTime",
398 : "",
399 : "framegrabber.latencyTime",
400 : argType::Required,
401 : "framegrabber",
402 : "latencyTime",
403 : false,
404 : "float",
405 : "The maximum length of time to measure latency timings. "
406 : "Sets m_latencyCircBuffMaxTime, default is 5." );
407 :
408 224 : config.add( "framegrabber.latencySize",
409 : "",
410 : "framegrabber.latencySize",
411 : argType::Required,
412 : "framegrabber",
413 : "latencySize",
414 : false,
415 : "float",
416 : "The maximum length of the buffer used to measure latency timings. Sets m_latencyCircBuffMaxLength, "
417 : "default is 3600." );
418 :
419 : if( derivedT::c_frameGrabber_flippable )
420 : {
421 156 : config.add( "framegrabber.defaultFlip",
422 : "",
423 : "framegrabber.defaultFlip",
424 : argType::Required,
425 : "framegrabber",
426 : "defaultFlip",
427 : false,
428 : "string",
429 : "The default flip of the image. Options are flipNone, flipUD, flipLR, flipUDLR. The default is "
430 : "flipNone." );
431 : }
432 :
433 16 : return 0;
434 : }
435 :
436 : template <class derivedT>
437 15 : int frameGrabber<derivedT>::loadConfig( mx::app::appConfigurator &config )
438 : {
439 30 : config( m_fgThreadPrio, "framegrabber.threadPrio" );
440 15 : config( m_fgCpuset, "framegrabber.cpuset" );
441 15 : if( m_shmimName == "" )
442 : {
443 0 : m_shmimName = derived().configName();
444 : }
445 :
446 15 : config( m_shmimName, "framegrabber.shmimName" );
447 :
448 15 : if( m_shmimName == "" )
449 : {
450 0 : return derivedT::template log<software_critical, -1>( { "framegrabber.shmimName can't be empty" } );
451 : }
452 :
453 15 : config( m_circBuffLength, "framegrabber.circBuffLength" );
454 :
455 15 : if( m_circBuffLength < 1 )
456 : {
457 0 : m_circBuffLength = 1;
458 0 : derivedT::template log<text_log>( "circBuffLength set to 1" );
459 : }
460 :
461 15 : config( m_latencyCircBuffMaxTime, "framegrabber.latencyTime" );
462 15 : if( m_latencyCircBuffMaxTime < 0 )
463 : {
464 0 : m_latencyCircBuffMaxTime = 0;
465 0 : derivedT::template log<text_log>( "latencyTime set to 0 (off)" );
466 : }
467 :
468 30 : config( m_latencyCircBuffMaxLength, "framegrabber.latencySize" );
469 :
470 : if( derivedT::c_frameGrabber_flippable )
471 : {
472 22 : std::string flip = "flipNone";
473 :
474 11 : config( flip, "framegrabber.defaultFlip" );
475 :
476 11 : if( flip == "flipNone" )
477 : {
478 11 : m_defaultFlip = fgFlipNone;
479 : }
480 0 : else if( flip == "flipUD" )
481 : {
482 0 : m_defaultFlip = fgFlipUD;
483 : }
484 0 : else if( flip == "flipLR" )
485 : {
486 0 : m_defaultFlip = fgFlipLR;
487 : }
488 0 : else if( flip == "flipUDLR" )
489 : {
490 0 : m_defaultFlip = fgFlipUDLR;
491 : }
492 : else
493 : {
494 0 : derivedT::template log<text_log>( { std::string( "invalid framegrabber flip"
495 0 : "specification (" ) +
496 : flip + "), setting flipNone" },
497 : logPrio::LOG_ERROR );
498 :
499 0 : m_defaultFlip = fgFlipNone;
500 : }
501 11 : }
502 :
503 15 : return 0;
504 : }
505 :
506 : template <class derivedT>
507 2 : int frameGrabber<derivedT>::appStartup()
508 : {
509 : // Register the shmimName INDI property
510 2 : m_indiP_shmimName = pcf::IndiProperty( pcf::IndiProperty::Text );
511 2 : m_indiP_shmimName.setDevice( derived().configName() );
512 4 : m_indiP_shmimName.setName( "fg_shmimName" );
513 2 : m_indiP_shmimName.setPerm( pcf::IndiProperty::ReadOnly );
514 2 : m_indiP_shmimName.setState( pcf::IndiProperty::Idle );
515 4 : m_indiP_shmimName.add( pcf::IndiElement( "name" ) );
516 4 : m_indiP_shmimName["name"] = m_shmimName;
517 :
518 2 : if( derived().registerIndiPropertyNew( m_indiP_shmimName, nullptr ) < 0 )
519 : {
520 : #ifndef FRAMEGRABBER_TEST_NOLOG
521 0 : derivedT::template log<software_error>( { std::source_location::current() } );
522 : #endif
523 :
524 0 : return -1;
525 : }
526 :
527 : // Register the frameSize INDI property
528 2 : m_indiP_frameSize = pcf::IndiProperty( pcf::IndiProperty::Number );
529 2 : m_indiP_frameSize.setDevice( derived().configName() );
530 4 : m_indiP_frameSize.setName( "fg_frameSize" );
531 2 : m_indiP_frameSize.setPerm( pcf::IndiProperty::ReadOnly );
532 2 : m_indiP_frameSize.setState( pcf::IndiProperty::Idle );
533 4 : m_indiP_frameSize.add( pcf::IndiElement( "width" ) );
534 4 : m_indiP_frameSize["width"] = 0;
535 4 : m_indiP_frameSize.add( pcf::IndiElement( "height" ) );
536 4 : m_indiP_frameSize["height"] = 0;
537 4 : m_indiP_frameSize.add( pcf::IndiElement( "depth" ) );
538 4 : m_indiP_frameSize["depth"] = 0;
539 :
540 2 : if( derived().registerIndiPropertyNew( m_indiP_frameSize, nullptr ) < 0 )
541 : {
542 : #ifndef FRAMEGRABBER_TEST_NOLOG
543 0 : derivedT::template log<software_error>( { std::source_location::current() } );
544 : #endif
545 :
546 0 : return -1;
547 : }
548 :
549 : // Register the timing INDI property
550 12 : derived().createROIndiNumber( m_indiP_timing, "fg_timing" );
551 4 : m_indiP_timing.add( pcf::IndiElement( "acq_fps" ) );
552 4 : m_indiP_timing.add( pcf::IndiElement( "acq_min" ) );
553 4 : m_indiP_timing.add( pcf::IndiElement( "acq_max" ) );
554 4 : m_indiP_timing.add( pcf::IndiElement( "acq_jitter" ) );
555 4 : m_indiP_timing.add( pcf::IndiElement( "write_fps" ) );
556 4 : m_indiP_timing.add( pcf::IndiElement( "write_min" ) );
557 4 : m_indiP_timing.add( pcf::IndiElement( "write_max" ) );
558 4 : m_indiP_timing.add( pcf::IndiElement( "write_jitter" ) );
559 4 : m_indiP_timing.add( pcf::IndiElement( "delta_aw" ) );
560 4 : m_indiP_timing.add( pcf::IndiElement( "delta_aw_jitter" ) );
561 :
562 2 : if( derived().registerIndiPropertyReadOnly( m_indiP_timing ) < 0 )
563 : {
564 : #ifndef FRAMEGRABBER_TEST_NOLOG
565 0 : derivedT::template log<software_error>( { std::source_location::current() } );
566 : #endif
567 :
568 0 : return -1;
569 : }
570 :
571 : // Start the f.g. thread
572 6 : if( derived().threadStart( m_fgThread,
573 2 : m_fgThreadInit,
574 2 : m_fgThreadID,
575 2 : m_fgThreadProp,
576 : m_fgThreadPrio,
577 2 : m_fgCpuset,
578 : "framegrabber",
579 : this,
580 2 : fgThreadStart ) < 0 )
581 : {
582 0 : derivedT::template log<software_error, -1>( { std::source_location::current() } );
583 0 : return -1;
584 : }
585 :
586 2 : return 0;
587 : }
588 :
589 : template <class derivedT>
590 43 : int frameGrabber<derivedT>::appLogic()
591 : {
592 : // do a join check to see if other threads have exited.
593 43 : if( pthread_tryjoin_np( m_fgThread.native_handle(), 0 ) == 0 )
594 : {
595 1 : derivedT::template log<software_error>( { "framegrabber thread has exited" } );
596 :
597 1 : return -1;
598 : }
599 :
600 : try
601 : {
602 42 : if( derived().state() == stateCodes::OPERATING && m_atimes.size() > 1 && derived().fps() > 0 )
603 : {
604 0 : cbIndexT latTime = m_latencyCircBuffMaxTime * m_cbFPS;
605 0 : if( latTime >= m_atimes.maxEntries() )
606 : {
607 0 : latTime = m_atimes.maxEntries() - 1;
608 : }
609 :
610 0 : cbIndexT usedEntries = std::min<cbIndexT>( latTime, m_atimes.size() );
611 :
612 0 : if( usedEntries >= 2 )
613 : {
614 0 : m_atimesD.resize( usedEntries - 1 );
615 0 : m_wtimesD.resize( usedEntries - 1 );
616 0 : m_watimesD.resize( usedEntries - 1 );
617 :
618 0 : cbIndexT refEntry = m_atimes.latest();
619 :
620 0 : if( refEntry >= usedEntries )
621 : {
622 0 : refEntry -= usedEntries;
623 : }
624 : else
625 : {
626 0 : refEntry = m_atimes.maxEntries() + refEntry - usedEntries;
627 : }
628 :
629 0 : timespec ts = m_atimes.at( refEntry, 0 );
630 0 : double a0 = ts.tv_sec + ( (double)ts.tv_nsec ) / 1e9;
631 :
632 0 : ts = m_wtimes.at( refEntry, 0 );
633 0 : double w0 = ts.tv_sec + ( (double)ts.tv_nsec ) / 1e9;
634 :
635 0 : double mina = 1e9;
636 0 : double maxa = -1e9;
637 0 : double minw = 1e9;
638 0 : double maxw = -1e9;
639 :
640 0 : for( size_t n = 1; n <= m_atimesD.size(); ++n )
641 : {
642 0 : ts = m_atimes.at( refEntry, n );
643 :
644 0 : double a = ts.tv_sec + ( (double)ts.tv_nsec ) / 1e9;
645 :
646 0 : ts = m_wtimes.at( refEntry, n );
647 :
648 0 : double w = ts.tv_sec + ( (double)ts.tv_nsec ) / 1e9;
649 :
650 0 : m_atimesD[n - 1] = a - a0;
651 0 : m_wtimesD[n - 1] = w - w0;
652 0 : m_watimesD[n - 1] = w - a;
653 0 : a0 = a;
654 0 : w0 = w;
655 :
656 0 : if( m_atimesD[n - 1] < mina )
657 : {
658 0 : mina = m_atimesD[n - 1];
659 : }
660 :
661 0 : if( m_atimesD[n - 1] > maxa )
662 : {
663 0 : maxa = m_atimesD[n - 1];
664 : }
665 :
666 0 : if( m_wtimesD[n - 1] < minw )
667 : {
668 0 : minw = m_wtimesD[n - 1];
669 : }
670 :
671 0 : if( m_wtimesD[n - 1] > maxw )
672 : {
673 0 : maxw = m_wtimesD[n - 1];
674 : }
675 :
676 0 : if( m_wtimesD[n - 1] < 0 )
677 : {
678 0 : std::cerr << "negative wtime: " << m_wtimesD[n - 1] << ' ' << n << ' ' << m_atimesD.size()
679 0 : << ' ' << refEntry << ' ' << m_atimes.maxEntries() << ' ' << latTime << '\n';
680 :
681 0 : return derivedT::template log<software_error, 0>( { "negative write time. latency "
682 0 : "circ buff is not long enought" } );
683 : }
684 : }
685 :
686 0 : m_mna = mx::math::vectorMean( m_atimesD );
687 0 : m_vara = mx::math::vectorVariance( m_atimesD, m_mna );
688 0 : m_mina = mina;
689 0 : m_maxa = maxa;
690 :
691 0 : m_mnw = mx::math::vectorMean( m_wtimesD );
692 0 : m_varw = mx::math::vectorVariance( m_wtimesD, m_mnw );
693 0 : m_minw = minw;
694 0 : m_maxw = maxw;
695 :
696 0 : m_mnwa = mx::math::vectorMean( m_watimesD );
697 0 : m_varwa = mx::math::vectorVariance( m_watimesD, m_mnwa );
698 :
699 0 : recordFGTimings();
700 : }
701 : else
702 : {
703 0 : m_mna = 0;
704 0 : m_vara = 0;
705 0 : m_mina = 0;
706 0 : m_maxa = 0;
707 0 : m_mnw = 0;
708 0 : m_varw = 0;
709 0 : m_minw = 0;
710 0 : m_maxw = 0;
711 0 : m_mnwa = 0;
712 0 : m_varwa = 0;
713 : }
714 : }
715 : else
716 : {
717 42 : m_mna = 0;
718 42 : m_vara = 0;
719 42 : m_mina = 0;
720 42 : m_maxa = 0;
721 42 : m_mnw = 0;
722 42 : m_varw = 0;
723 42 : m_minw = 0;
724 42 : m_maxw = 0;
725 42 : m_mnwa = 0;
726 42 : m_varwa = 0;
727 : }
728 : }
729 0 : catch( const std::exception &e )
730 : {
731 0 : std::cerr << e.what() << '\n';
732 : }
733 :
734 42 : return 0;
735 : }
736 :
737 : template <class derivedT>
738 2 : int frameGrabber<derivedT>::onPowerOff()
739 : {
740 2 : m_mna = 0;
741 2 : m_vara = 0;
742 2 : m_mina = 0;
743 2 : m_maxa = 0;
744 2 : m_mnw = 0;
745 2 : m_varw = 0;
746 2 : m_minw = 0;
747 2 : m_maxw = 0;
748 2 : m_mnwa = 0;
749 2 : m_varwa = 0;
750 :
751 2 : m_width = 0;
752 2 : m_height = 0;
753 2 : m_circBuffLength = 1;
754 :
755 2 : updateINDI();
756 :
757 2 : m_reconfig = true;
758 :
759 2 : return 0;
760 : }
761 :
762 : template <class derivedT>
763 4 : int frameGrabber<derivedT>::appShutdown()
764 : {
765 4 : if( m_fgThread.joinable() )
766 : {
767 : try
768 : {
769 2 : m_fgThread.join(); // this will throw if it was already joined
770 : }
771 0 : catch( ... )
772 : {
773 : }
774 : }
775 :
776 4 : return 0;
777 : }
778 :
779 : template <class derivedT>
780 0 : int frameGrabber<derivedT>::configCircBuffs()
781 : {
782 0 : m_cbFPS = derived().fps();
783 :
784 0 : if( m_latencyCircBuffMaxLength == 0 || m_latencyCircBuffMaxTime == 0 || m_cbFPS <= 0 )
785 : {
786 0 : m_atimes.maxEntries( 0 );
787 0 : m_wtimes.maxEntries( 0 );
788 :
789 0 : if( m_cbFPS < 0 )
790 : {
791 0 : return -1;
792 : }
793 : }
794 : else
795 : {
796 : // Set up the latency circ. buffs
797 0 : cbIndexT cbSz = 2 * m_latencyCircBuffMaxTime * m_cbFPS;
798 0 : if( cbSz > m_latencyCircBuffMaxLength )
799 : {
800 0 : cbSz = m_latencyCircBuffMaxLength;
801 : }
802 0 : if( cbSz < 3 )
803 : {
804 0 : cbSz = 3; // Make variance meaningful
805 : }
806 :
807 0 : m_atimes.maxEntries( cbSz );
808 0 : m_wtimes.maxEntries( cbSz );
809 : }
810 :
811 0 : return 0;
812 : }
813 :
814 : template <class derivedT>
815 2 : void frameGrabber<derivedT>::fgThreadStart( frameGrabber *o )
816 : {
817 2 : o->fgThreadExec();
818 2 : }
819 :
820 : template <class derivedT>
821 2 : void frameGrabber<derivedT>::fgThreadExec()
822 : {
823 : // Get the thread PID immediately so the caller can return.
824 2 : m_fgThreadID = syscall( SYS_gettid );
825 :
826 : // timespec writestart;
827 :
828 : // Wait fpr the thread starter to finish initializing this thread.
829 4 : while( m_fgThreadInit == true && derived().shutdown() == 0 )
830 : {
831 2 : sleep( 1 );
832 : }
833 :
834 2 : uint32_t imsize[3] = { 0, 0, 0 };
835 2 : bool cbuff = false;
836 2 : std::string shmimName;
837 :
838 : static bool logged_wrong_size = false;
839 :
840 2 : while( derived().shutdown() == 0 )
841 : {
842 : ///\todo this ought to wait until OPERATING, using READY as a sign of "not integrating"
843 0 : while( !derived().shutdown() &&
844 0 : ( !( derived().state() == stateCodes::READY || derived().state() == stateCodes::OPERATING ) ||
845 0 : derived().powerState() <= 0 ) )
846 : {
847 0 : sleep( 1 );
848 : }
849 :
850 0 : if( derived().shutdown() )
851 : {
852 0 : break;
853 : }
854 :
855 : // At the end of this, must have m_width, m_height, m_dataType set, and derived()->fps must be valid.
856 0 : if( derived().configureAcquisition() != 0 )
857 : {
858 0 : sleep( 1 );
859 0 : continue;
860 : }
861 :
862 0 : m_typeSize = ImageStreamIO_typesize( m_dataType );
863 :
864 0 : if( configCircBuffs() < 0 )
865 : {
866 0 : derivedT::template log<software_error>( { "error configuring latency circ. buffs" } );
867 : }
868 :
869 : /* Initialize ImageStreamIO
870 : */
871 :
872 : // Check if we are already connected by configureAcquisition
873 0 : if( m_imageStream != nullptr )
874 : {
875 0 : imsize[0] = m_imageStream->md[0].size[0];
876 :
877 0 : if( m_imageStream->md[0].naxis > 1 )
878 : {
879 0 : imsize[1] = m_imageStream->md[0].size[1];
880 : }
881 :
882 0 : if( m_imageStream->md[0].naxis > 2 )
883 : {
884 0 : imsize[2] = m_imageStream->md[0].size[2];
885 0 : cbuff = true;
886 : }
887 : else
888 : {
889 0 : imsize[2] = 1;
890 0 : cbuff = false;
891 : }
892 : }
893 :
894 0 : if( m_width != imsize[0] || m_height != imsize[1] || m_circBuffLength != imsize[2] || m_imageStream == nullptr )
895 : {
896 0 : if( m_imageStream != nullptr && m_ownShmim )
897 : {
898 0 : ImageStreamIO_destroyIm( m_imageStream );
899 0 : free( m_imageStream );
900 : }
901 0 : else if( m_imageStream != nullptr && !m_ownShmim )
902 : {
903 0 : if( !logged_wrong_size )
904 : {
905 0 : derivedT::template log<text_log>(
906 0 : std::format( "image stream {} is not expected size", m_shmimName ), logPrio::LOG_WARNING );
907 0 : logged_wrong_size = true;
908 : }
909 :
910 0 : sleep( 1 );
911 0 : continue; // we go around and try again, waiting for the shmim to get resized to our expectations
912 : }
913 :
914 0 : logged_wrong_size = false;
915 :
916 0 : m_imageStream = reinterpret_cast<IMAGE *>( malloc( sizeof( IMAGE ) ) );
917 :
918 0 : imsize[0] = m_width;
919 0 : imsize[1] = m_height;
920 0 : imsize[2] = m_circBuffLength;
921 :
922 0 : std::cerr << "Creating: " << m_shmimName << " " << m_width << " " << m_height << " " << m_circBuffLength
923 0 : << "\n";
924 :
925 0 : ImageStreamIO_createIm_gpu( m_imageStream,
926 : m_shmimName.c_str(),
927 : 3,
928 : imsize,
929 0 : m_dataType,
930 : -1,
931 : 1,
932 : IMAGE_NB_SEMAPHORE,
933 : 0,
934 : CIRCULAR_BUFFER | ZAXIS_TEMPORAL,
935 : 0 );
936 :
937 0 : m_imageStream->md->cnt1 = m_circBuffLength - 1;
938 :
939 0 : cbuff = true; // because we created it!
940 : }
941 :
942 : // This completes the reconfiguration.
943 0 : m_reconfig = false;
944 :
945 0 : if( derived().startAcquisition() < 0 )
946 : {
947 0 : continue;
948 : }
949 :
950 0 : uint64_t next_cnt1 = 0;
951 0 : char *next_dest = reinterpret_cast<char *>( m_imageStream->array.raw );
952 :
953 0 : timespec *next_wtimearr = nullptr;
954 0 : timespec *next_atimearr = nullptr;
955 0 : uint64_t *next_cntarr = nullptr;
956 :
957 0 : if( cbuff )
958 : {
959 0 : next_wtimearr = &m_imageStream->writetimearray[0];
960 0 : next_atimearr = &m_imageStream->atimearray[0];
961 0 : next_cntarr = &m_imageStream->cntarray[0];
962 : }
963 :
964 : // This is the main image grabbing loop.
965 0 : while( !derived().shutdown() && !m_reconfig && derived().powerState() > 0 )
966 : {
967 : //==================
968 : // Get next image, process validity.
969 : //====================
970 0 : int isValid = derived().acquireAndCheckValid();
971 0 : if( isValid != 0 )
972 : {
973 0 : if( isValid < 0 )
974 : {
975 0 : break;
976 : }
977 : else
978 : {
979 0 : continue;
980 : }
981 : }
982 :
983 : // Ok, no timeout, so we process the image and publish it.
984 0 : m_imageStream->md->write = 1;
985 :
986 0 : if( derived().loadImageIntoStream( next_dest ) < 0 )
987 : {
988 0 : break;
989 : }
990 :
991 : // Set the time of last write
992 : // clock_gettime(CLOCK_REALTIME, &m_imageStream->md->writetime);
993 0 : if( clock_gettime( CLOCK_REALTIME, &m_imageStream->md->writetime ) < 0 )
994 : {
995 0 : derivedT::template log<software_critical>( { errno, "clock_gettime" } );
996 : }
997 :
998 : // Set the image acquisition timestamp
999 0 : m_imageStream->md->atime = m_currImageTimestamp;
1000 :
1001 : // Update cnt1
1002 0 : m_imageStream->md->cnt1 = next_cnt1;
1003 :
1004 : // Update cnt0
1005 0 : m_imageStream->md->cnt0++;
1006 :
1007 0 : if( cbuff )
1008 : {
1009 0 : *next_wtimearr = m_imageStream->md->writetime;
1010 0 : *next_atimearr = m_currImageTimestamp;
1011 0 : *next_cntarr = m_imageStream->md->cnt0;
1012 : }
1013 :
1014 : // And post
1015 0 : m_imageStream->md->write = 0;
1016 0 : ImageStreamIO_sempost( m_imageStream, -1 );
1017 :
1018 0 : if( postPublishHook( derived(), m_imageStream, 0 ) < 0 )
1019 : {
1020 0 : break;
1021 : }
1022 :
1023 : // Update the latency circ. buffs
1024 0 : if( m_atimes.maxEntries() > 0 )
1025 : {
1026 0 : m_atimes.nextEntry( m_imageStream->md->atime );
1027 0 : m_wtimes.nextEntry( m_imageStream->md->writetime );
1028 : }
1029 :
1030 : // Now we increment pointers outside the time-critical part of the loop.
1031 0 : next_cnt1 = m_imageStream->md->cnt1 + 1;
1032 0 : if( next_cnt1 >= m_circBuffLength )
1033 : {
1034 0 : next_cnt1 = 0;
1035 : }
1036 :
1037 0 : next_dest =
1038 0 : reinterpret_cast<char *>( m_imageStream->array.raw ) + next_cnt1 * m_width * m_height * m_typeSize;
1039 :
1040 0 : if( cbuff )
1041 : {
1042 0 : next_wtimearr = &m_imageStream->writetimearray[next_cnt1];
1043 0 : next_atimearr = &m_imageStream->atimearray[next_cnt1];
1044 0 : next_cntarr = &m_imageStream->cntarray[next_cnt1];
1045 : }
1046 :
1047 : // Touch them to make sure we move
1048 0 : m_dummy_c = next_dest[0];
1049 :
1050 0 : if( cbuff )
1051 : {
1052 0 : m_dummy_ts.tv_sec = next_wtimearr[0].tv_sec + next_atimearr[0].tv_sec;
1053 0 : m_dummy_cnt = next_cntarr[0];
1054 : }
1055 :
1056 0 : if( m_cbFPS != derived().fps() )
1057 : {
1058 0 : std::cerr << "configuring due to mismatch m_cbFPS\n";
1059 0 : if( configCircBuffs() < 0 )
1060 : {
1061 0 : derivedT::template log<software_error>( { "error configuring latency circ. buffs" } );
1062 : }
1063 : }
1064 : }
1065 :
1066 0 : if( m_reconfig && !derived().shutdown() )
1067 : {
1068 0 : derived().reconfig();
1069 : }
1070 :
1071 : } // outer loop, will exit if m_shutdown==true
1072 :
1073 2 : if( m_imageStream != nullptr )
1074 : {
1075 0 : if( m_ownShmim )
1076 : {
1077 0 : ImageStreamIO_destroyIm( m_imageStream );
1078 : }
1079 : else
1080 : {
1081 0 : ImageStreamIO_closeIm( m_imageStream );
1082 : }
1083 :
1084 0 : free( m_imageStream );
1085 0 : m_imageStream = nullptr;
1086 : }
1087 2 : }
1088 :
1089 : template <class derivedT>
1090 2 : void *frameGrabber<derivedT>::loadImageIntoStreamCopy( void *dest, void *src, size_t width, size_t height, size_t szof )
1091 : {
1092 : if( !derivedT::c_frameGrabber_flippable )
1093 : {
1094 : return memcpy( dest, src, width * height * szof );
1095 : }
1096 : else
1097 : {
1098 2 : switch( m_defaultFlip )
1099 : {
1100 1 : case fgFlipNone:
1101 1 : return mx::improc::imcpy( dest, src, width, height, szof );
1102 0 : case fgFlipUD:
1103 0 : return mx::improc::imcpy_flipUD( dest, src, width, height, szof );
1104 0 : case fgFlipLR:
1105 0 : return mx::improc::imcpy_flipLR( dest, src, width, height, szof );
1106 0 : case fgFlipUDLR:
1107 0 : return mx::improc::imcpy_flipUDLR( dest, src, width, height, szof );
1108 1 : default:
1109 1 : return nullptr;
1110 : }
1111 : }
1112 : }
1113 :
1114 : template <class derivedT>
1115 : int frameGrabber<derivedT>::openShmim()
1116 : {
1117 : static bool logged = false;
1118 :
1119 : if( m_imageStream != nullptr )
1120 : {
1121 : ImageStreamIO_closeIm( m_imageStream );
1122 : free( m_imageStream );
1123 : m_imageStream = nullptr;
1124 : }
1125 :
1126 : // b/c ImageStreamIO prints every single time, and latest version don't support stopping it yet, and that
1127 : // isn't thread-safe-able anyway we do our own checks. This is the same code in ImageStreamIO_openIm...
1128 : int SM_fd;
1129 : char SM_fname[1024];
1130 : ImageStreamIO_filename( SM_fname, sizeof( SM_fname ), m_shmimName.c_str() );
1131 : SM_fd = open( SM_fname, O_RDWR );
1132 :
1133 : if( SM_fd == -1 )
1134 : {
1135 : if( !logged )
1136 : {
1137 : derivedT::template log<text_log>( "ImageStream " + m_shmimName + " not found (yet). Retrying . . .",
1138 : logPrio::LOG_NOTICE );
1139 : logged = true;
1140 : }
1141 :
1142 : return 1;
1143 : }
1144 :
1145 : // Found and opened, close it and then use ImageStreamIO
1146 : logged = false;
1147 : close( SM_fd );
1148 :
1149 : m_imageStream = reinterpret_cast<IMAGE *>( malloc( sizeof( IMAGE ) ) );
1150 :
1151 : if( ImageStreamIO_openIm( m_imageStream, m_shmimName.c_str() ) == 0 )
1152 : {
1153 : if( m_imageStream->md[0].sem < SEMAPHORE_MAXVAL )
1154 : {
1155 : ImageStreamIO_closeIm( m_imageStream );
1156 : free( m_imageStream );
1157 : m_imageStream = nullptr;
1158 :
1159 : return 1; // We just need to wait for the server process to finish startup.
1160 : }
1161 : else
1162 : {
1163 : char SM_fname[1024];
1164 : ImageStreamIO_filename( SM_fname, sizeof( SM_fname ), m_shmimName.c_str() );
1165 :
1166 : struct stat buffer;
1167 :
1168 : int rv = stat( SM_fname, &buffer );
1169 :
1170 : if( rv != 0 )
1171 : {
1172 : derivedT::template log<software_critical>(
1173 : { errno,
1174 : "Could not get inode for " + m_shmimName + ". Source process will need to be restarted." } );
1175 :
1176 : ImageStreamIO_closeIm( m_imageStream );
1177 :
1178 : free( m_imageStream );
1179 :
1180 : m_imageStream = nullptr;
1181 :
1182 : derived().m_shutdown = true;
1183 :
1184 : return -1;
1185 : }
1186 :
1187 : m_inode = buffer.st_ino;
1188 :
1189 : m_width = m_imageStream->md->size[0];
1190 :
1191 : if( m_imageStream->md->naxis == 2 )
1192 : {
1193 : m_height = m_imageStream->md->size[1];
1194 : m_circBuffLength = 1;
1195 : }
1196 : else if( m_imageStream->md->naxis == 3 )
1197 : {
1198 : m_height = m_imageStream->md->size[1];
1199 : m_circBuffLength = m_imageStream->md->size[2];
1200 : }
1201 : else
1202 : {
1203 : m_height = 1;
1204 : m_circBuffLength = 1;
1205 : }
1206 :
1207 : m_dataType = m_imageStream->md->datatype;
1208 : m_typeSize = ImageStreamIO_typesize( m_dataType );
1209 :
1210 : return 0;
1211 : }
1212 : }
1213 : else
1214 : {
1215 : free( m_imageStream );
1216 : m_imageStream = nullptr;
1217 :
1218 : return 1; // be patient
1219 : }
1220 : }
1221 :
1222 : template <class derivedT>
1223 7 : int frameGrabber<derivedT>::updateINDI()
1224 : {
1225 7 : if( !derived().m_indiDriver )
1226 7 : return 0;
1227 :
1228 0 : indi::updateIfChanged( m_indiP_shmimName, "name", m_shmimName, derived().m_indiDriver );
1229 0 : indi::updateIfChanged( m_indiP_frameSize, "width", m_width, derived().m_indiDriver );
1230 0 : indi::updateIfChanged( m_indiP_frameSize, "height", m_height, derived().m_indiDriver );
1231 0 : indi::updateIfChanged( m_indiP_frameSize, "depth", m_circBuffLength, derived().m_indiDriver );
1232 :
1233 0 : double fpsa = 0;
1234 0 : double fpsw = 0;
1235 0 : if( m_mna != 0 )
1236 0 : fpsa = 1.0 / m_mna;
1237 0 : if( m_mnw != 0 )
1238 0 : fpsw = 1.0 / m_mnw;
1239 :
1240 0 : indi::updateIfChanged<double>(
1241 0 : m_indiP_timing,
1242 : { "acq_fps",
1243 : "acq_min",
1244 : "acq_max",
1245 : "acq_jitter",
1246 : "write_fps",
1247 : "write_min",
1248 : "write_max",
1249 : "write_jitter",
1250 : "delta_aw",
1251 : "delta_aw_jitter" },
1252 0 : { fpsa, m_mina, m_maxa, sqrt( m_vara ), fpsw, m_minw, m_maxw, sqrt( m_varw ), m_mnwa, sqrt( m_varwa ) },
1253 0 : derived().m_indiDriver );
1254 :
1255 0 : return 0;
1256 : }
1257 :
1258 : template <class derivedT>
1259 4 : int frameGrabber<derivedT>::recordFGTimings( bool force )
1260 : {
1261 : static double last_mna = 0;
1262 : static double last_vara = 0;
1263 :
1264 : static double last_mnw = 0;
1265 : static double last_varw = 0;
1266 :
1267 : static double last_mnwa = 0;
1268 : static double last_varwa = 0;
1269 :
1270 4 : if( force || m_mna != last_mna || m_vara != last_vara || m_mnw != last_mnw || m_varw != last_varw ||
1271 0 : m_mnwa != last_mnwa || m_varwa != last_varwa )
1272 : {
1273 8 : derived().template telem<telem_fgtimings>(
1274 4 : { m_mna, sqrt( m_vara ), m_mnw, sqrt( m_varw ), m_mnwa, sqrt( m_varwa ) } );
1275 :
1276 4 : last_mna = m_mna;
1277 4 : last_vara = m_vara;
1278 4 : last_mnw = m_mnw;
1279 4 : last_varw = m_varw;
1280 4 : last_mnwa = m_mnwa;
1281 4 : last_varwa = m_varwa;
1282 : }
1283 :
1284 4 : return 0;
1285 : }
1286 :
1287 : /// Call frameGrabberT::setupConfig with error checking for frameGrabber
1288 : /**
1289 : * \param cfig the application configurator
1290 : */
1291 : #define FRAMEGRABBER_SETUP_CONFIG( cfig ) \
1292 : if( frameGrabberT::setupConfig( cfig ) < 0 ) \
1293 : { \
1294 : log<software_error>( { "Error from frameGrabberT::setupConfig" } ); \
1295 : m_shutdown = true; \
1296 : return; \
1297 : }
1298 :
1299 : /// Call frameGrabberT::loadConfig with error checking for frameGrabber
1300 : /** This must be inside a function that returns int, e.g. the standard loadConfigImpl.
1301 : * \param cfig the application configurator
1302 : */
1303 : #define FRAMEGRABBER_LOAD_CONFIG( cfig ) \
1304 : if( frameGrabberT::loadConfig( cfig ) < 0 ) \
1305 : { \
1306 : return log<software_error, -1>( { "Error from frameGrabberT::loadConfig" } ); \
1307 : }
1308 :
1309 : /// Call frameGrabberT::appStartup with error checking for frameGrabber
1310 : #define FRAMEGRABBER_APP_STARTUP \
1311 : if( frameGrabberT::appStartup() < 0 ) \
1312 : { \
1313 : return log<software_error, -1>( { "Error from frameGrabberT::appStartup" } ); \
1314 : }
1315 :
1316 : /// Call frameGrabberT::appLogic with error checking for frameGrabber
1317 : #define FRAMEGRABBER_APP_LOGIC \
1318 : if( frameGrabberT::appLogic() < 0 ) \
1319 : { \
1320 : return log<software_error, -1>( { "Error from frameGrabberT::appLogic" } ); \
1321 : }
1322 :
1323 : /// Call frameGrabberT::updateINDI with error checking for frameGrabber
1324 : #define FRAMEGRABBER_UPDATE_INDI \
1325 : if( frameGrabberT::updateINDI() < 0 ) \
1326 : { \
1327 : return log<software_error, -1>( { "Error from frameGrabberT::updateINDI" } ); \
1328 : }
1329 :
1330 : /// Call frameGrabberT::appShutdown with error checking for frameGrabber
1331 : #define FRAMEGRABBER_APP_SHUTDOWN \
1332 : if( frameGrabberT::appShutdown() < 0 ) \
1333 : { \
1334 : return log<software_error, -1>( { "Error from frameGrabberT::appShutdown" } ); \
1335 : }
1336 :
1337 : } // namespace dev
1338 : } // namespace app
1339 : } // namespace MagAOX
1340 : #endif
|