API
 
Loading...
Searching...
No Matches
dmPokeWFS.hpp
Go to the documentation of this file.
1/** \file dmPokeWFS.hpp
2 * \brief The MagAO-X DM Poke Centering header file
3 *
4 * \ingroup dmPokeWFS_files
5 */
6
7#ifndef dmPokeWFS_hpp
8#define dmPokeWFS_hpp
9
10#include <mx/improc/eigenImage.hpp>
11#include <mx/improc/milkImage.hpp>
12#include <mx/improc/eigenCube.hpp>
13using namespace mx::improc;
14
15#include "../../ImageStreamIO/pixaccess.hpp"
16
17/** \defgroup dmPokeWFS
18 * \brief The MagAO-X device to coordinate poking a deformable mirror's actuators and synchronize reads of a camera image.
19 *
20 * <a href="../handbook/operating/software/apps/dmPokeWFS.html">Application Documentation</a>
21 *
22 * \ingroup apps
23 *
24 */
25
26
27/** \defgroup dmPokeWFS_files
28 * \ingroup dmPokeWFS
29 */
30
31namespace MagAOX
32{
33namespace app
34{
35namespace dev
36{
37
38
39
40/// A base class to coordinate poking a deformable mirror's actuators and synchronizedreads of a camera image.
41/** CRTP class `derivedT` has the following requirements:
42 *
43 * - Must be derived from MagAOXApp<true>
44 *
45 * - Must be derived from `dev::shmimMonitor<DERIVEDNAME, dev::dmPokeWFS<DERIVEDNAME>::wfsShmimT>` (replace DERIVEDNAME with derivedT class name)
46 *
47 * - Must be derived from `dev::shmimMonitor<DERIVEDNAME, dev::dmPokeWFS<DERIVEDNAME>::darkShmimT>` (replace DERIVEDNAME with derivedT class name)
48 *
49 * - Must contain the following friend declarations (replace DERIVEDNAME with derivedT class name):
50 * \code
51 * friend class dev::shmimMonitor<DERIVEDNAME, dev::dmPokeWFS<DERIVEDNAME>::wfsShmimT>;
52 * friend class dev::shmimMonitor<DERIVEDNAME, dev::dmPokeWFS<DERIVEDNAME>::darkShmimT>;
53 * friend class dev::dmPokeWFS<DERIVEDNAME>
54 * \endcode
55 *
56 * - Must contain the following typedefs (replace DERIVEDNAME with derivedT class name):
57 * \code
58 * typedef dev::shmimMonitor<DERIVEDNAME, dev::dmPokeWFS<DERIVEDNAME>::wfsShmimT> shmimMonitorT;
59 * typedef dev::shmimMonitor<DERIVEDNAME, dev::dmPokeWFS<DERIVEDNAME>::darkShmimT> darkShmimMonitorT;
60 * typedef dev::dmPokeWFS<DERIVEDNAME> dmPokeWFST;
61 *
62 * \endcode
63 * - Must provide the following interfaces:
64 * \code
65 * shmimMonitorT & shmimMonitor()
66 * {
67 * return *static_cast<shmimMonitorT *>(this);
68 * }
69 *
70 * darkShmimMonitorT & darkShmimMonitor()
71 * {
72 * return *static_cast<darkShmimMonitorT *>(this);
73 * }
74 * \endcode
75 *
76 * - If derivedT has additional shmimMonitor parents, you will need to include these lines in the class
77 * declaration:
78 * \code
79 * using dmPokeWFST::allocate;
80 * using dmPokeWFST::processImage;
81 * \endcode
82 *
83 * - Must provide the following interface:
84 * \code
85 * // Run the sensor steps
86 * // Coordinates the actions of poking and collecting images.
87 * // Upon completion this calls runSensor. If \p firstRun == true, one time
88 * // actions such as taking a dark can be executed.
89 * //
90 * // returns 0 on success
91 * // returns < 0 on an error
92 * int runSensor(bool firstRun ///< [in] flag indicating this is the first call. triggers taking a dark if true.
93 * );
94 * \endcode
95 *
96 * - Must provide the following interface:
97 * \code
98 * // Analyze the poke image
99 * // This analyzes the resulting poke images.
100 * //
101 * // returns 0 on success
102 * // returns < 0 on an error
103 * int analyzeSensor();
104 * \endcode
105 * At the conclusion of analyzeSensor the measured signal (e.g. deltaX and deltaY) should be updated and set in m_indiP_measurement.
106 * The function \ref updateMeasurement() can be used for this. However, the updating of the loop counter and the subsequent INDI
107 * property update is handled automatically after that.
108 *
109 * - Must be a telemeter with the following interface:
110 *
111 * - Must be derived from `dev::telemeter<DERIVEDNAME>` (replace DERIVEDNAME with derivedT class name) and meet the requirements
112 * of `dev::telemeter`
113 *
114 * - In the function `derivedT::checkRecordTimes()` required by `dev::telemeter`, the `telem_pokeloop` type must be checked.
115 * The minimum `derivedT::checkRecordTimes()` is:
116 * \code
117 * int checkRecordTimes()
118 * {
119 * return telemeterT::checkRecordTimes(telem_pokeloop());
120 * }
121 * \endcode
122 *
123 * - Must call this base class's setupConfig(), loadConfig(), appStartup(), appStartup(), and appShutdown() in the
124 * appropriate functions. For convenience the following macros are defined to provide error checking:
125 * \code
126 * DMPOKEWFS_SETUP_CONFIG( cfig )
127 * DMPOKEWFS_LOAD_CONFIG( cfig )
128 * DMPOKEWFS_APP_STARTUP
129 * DMPOKEWFS_APP_LOGIC
130 * DMPOKEWFS_APP_SHUTDOWN
131 * \endcode
132 *
133 * \ingroup appdev
134 */
135template<class derivedT>
137{
138
139public:
140
142 {
143 static std::string configSection()
144 {
145 return "wfscam";
146 };
147
148 static std::string indiPrefix()
149 {
150 return "wfscam";
151 };
152 };
153
155 {
156 static std::string configSection()
157 {
158 return "wfsdark";
159 };
160
161 static std::string indiPrefix()
162 {
163 return "wfsdark";
164 };
165 };
166
167protected:
168
169 /** \name Configurable Parameters
170 *@{
171 */
172
173 std::string m_wfsCamDevName; ///<INDI device name of the WFS camera. Default is wfscam.shmimName.
174
175 double m_wfsSemWait {1.5}; ///< The time in sec to wait on the WFS semaphore. Default 0.5 sec.
176
177 double m_imageSemWait {0.5}; ///< The time in sec to wait on the image semaphore. Default 0.5 sec.
178
179 unsigned m_nPokeImages {5}; ///< The number of images to average for the poke images. Default is 5.
180
181 unsigned m_nPokeAverage {10}; ///< The number of poke sequences to average. Default is 10.
182
183 std::string m_dmChan;
184
185 std::vector<int> m_poke_x;
186 std::vector<int> m_poke_y;
187
188 float m_poke_amp {0.0};
189
190 float m_dmSleep {10000}; ///<The time to sleep for the DM command to be applied, in microseconds. Default is 10000.
191
192 ///@}
193
194 std::mutex m_wfsImageMutex;
195
196 mx::improc::milkImage<float> m_rawImage;
197
198 mx::improc::milkImage<float> m_pokeImage;
199 mx::improc::eigenImage<float> m_pokeLocal;
200
201 float (*wfsPixget)(void *, size_t) {nullptr}; ///< Pointer to a function to extract the image data as float
202
203 float m_wfsFps {-1}; ///< The WFS camera FPS
204
205 mx::improc::eigenImage<float> m_darkImage; ///< The dark image
206
207 bool m_darkValid {false}; ///< Flag indicating if dark is valid based on its size.
208
209 float (*darkPixget)(void *, size_t) {nullptr}; ///< Pointer to a function to extract the dark image data as float
210
211 mx::improc::milkImage<float> m_dmStream;
212
213 mx::improc::eigenImage<float> m_dmImage;
214
215 float m_deltaX {0};
216 float m_deltaY {0};
217 uint64_t m_counter {0};
218
219public:
220
221 /**\name MagAOXApp Interface
222 *
223 * @{
224 */
225
226 /// Setup the configuration system
227 /**
228 * This should be called in `derivedT::setupConfig` as
229 * \code
230 dmPokeWFS<derivedT,realT>::setupConfig(config);
231 \endcode
232 * with appropriate error checking.
233 */
234 int setupConfig( mx::app::appConfigurator & config /**< [in] an application configuration to load values to*/);
235
236 /// load the configuration system results
237 /**
238 * This should be called in `derivedT::loadConfig` as
239 * \code
240 dmPokeWFS<derivedT,realT>::loadConfig(config);
241 \endcode
242 * with appropriate error checking.
243 */
244 int loadConfig( mx::app::appConfigurator & config /**< [in] an application configuration from which to load values */);
245
246 /// Startup function
247 /**
248 * This should be called in `derivedT::appStartup` as
249 * \code
250 dmPokeWFS<derivedT,realT>::appStartup();
251 \endcode
252 * with appropriate error checking.
253 *
254 * \returns 0 on success
255 * \returns -1 on error, which is logged.
256 */
258
259 /// dmPokeWFS application logic
260 /** This should be called in `derivedT::appLogic` as
261 * \code
262 dmPokeWFS<derivedT,realT>::appLogic();
263 \endcode
264 * with appropriate error checking.
265 *
266 * \returns 0 on success
267 * \returns -1 on error, which is logged.
268 */
269 int appLogic();
270
271 /// dmPokeWFS shutdown
272 /** This should be called in `derivedT::appShutdown` as
273 * \code
274 dmPokeWFS<derivedT,realT>::appShutdown();
275 \endcode
276 * with appropriate error checking.
277 *
278 * \returns 0 on success
279 * \returns -1 on error, which is logged.
280 */
282
283 ///@}
284
285 /** \name shmimMonitor Interface
286 * @{
287 */
288
289 int allocate( const wfsShmimT & /**< [in] tag to differentiate shmimMonitor parents.*/);
290
291 int processImage( void * curr_src, ///< [in] pointer to the start of the current frame
292 const wfsShmimT & ///< [in] tag to differentiate shmimMonitor parents.
293 );
294 ///@}
295
296
297 /** \name darkShmimMonitor Interface
298 * @{
299 */
300
301 int allocate( const darkShmimT & /**< [in] tag to differentiate shmimMonitor parents.*/);
302
303 int processImage( void * curr_src, ///< [in] pointer to the start of the current frame
304 const darkShmimT & ///< [in] tag to differentiate shmimMonitor parents.
305 );
306 ///@}
307
308 /** \name WFS Thread
309 * This thread coordinates the WFS process
310 *
311 * @{
312 */
313protected:
314
315 int m_wfsThreadPrio {1}; ///< Priority of the WFS thread, should normally be > 00.
316
317 std::string m_wfsCpuset; ///< The cpuset for the framegrabber thread. Ignored if empty (the default).
318
319 std::thread m_wfsThread; ///< A separate thread for the actual WFSing
320
321 bool m_wfsThreadInit {true}; ///< Synchronizer to ensure wfs thread initializes before doing dangerous things.
322
323 pid_t m_wfsThreadID {0}; ///< WFS thread PID.
324
325 pcf::IndiProperty m_wfsThreadProp; ///< The property to hold the WFS thread details.
326
327 ///Thread starter, called by wfsThreadStart on thread construction. Calls wfsThreadExec.
328 static void wfsThreadStart( dmPokeWFS * s /**< [in] a pointer to an streamWriter instance (normally this) */);
329
330 /// Execute the frame grabber main loop.
332
333 sem_t m_wfsSemaphore; ///< Semaphore used to signal the WFS thread to start WFSing.
334
335 unsigned m_wfsSemWait_sec {1}; ///< The timeout for the WFS semaphore, seconds component.
336
337 unsigned m_wfsSemWait_nsec {0}; ///< The timeoutfor the WFS semaphore, nanoseconds component.
338
339 int m_measuring {0}; ///< Status of measuring: 0 no, 1 single in progress, 2 continuous in progress.
340
341 bool m_single {false}; ///< True a single measurement is in progress.
342
343 bool m_continuous {false}; ///< True if continuous measurements are in progress.
344
345 bool m_stopMeasurement {false}; ///< Used to request that the measurement in progress stop.
346
347 ///@}
348
349 sem_t m_imageSemaphore; ///< Semaphore used to signal that an image is ready
350
351 unsigned m_imageSemWait_sec {1}; ///< The timeout for the image semaphore, seconds component.
352
353 unsigned m_imageSemWait_nsec {0}; ///< The timeout for the image semaphore, nanoseconds component.
354
355
356 /// Apply a single DM poke pattern and record the results
357 /** This accumulates m_nPokeImages*m_nPokeAverage images in m_pokeLocal, so m_pokeLocal
358 * should be zeroed before the first call to this (e.g. for a +1 poke),
359 * but not zeroed before the second call (e.g. for the -1 poke). You also need
360 * to 0 the DM after finishing a poke pair.
361 * See basicRunSensor() for how to use.
362 *
363 * \returns +1 if exit is due to shutdown or stop request
364 * \returns 0 if no error
365 * \returns -1 if an error occurs
366 */
367 int basicTimedPoke(float pokeSign /**< [in] the sign, and possibly a scaling, to apply to m_pokeAmplitude*/);
368
369 /// Run the basic +/- poke sensor steps
370 /** Coordinates the actions of poking and collecting images.
371 *
372 * This can be called from the derived class runSensor.
373 *
374 * \returns +1 if exit is due to shutdown or stop request
375 * \returns 0 if no error
376 * \returns -1 if an error occurs
377 */
379
380 int updateMeasurement( float deltaX,
381 float deltaY
382 );
383
384 /** \name INDI Interface
385 * @{
386 */
387protected:
388
389 pcf::IndiProperty m_indiP_poke_amp;
391
392 pcf::IndiProperty m_indiP_nPokeImages;
394
395 pcf::IndiProperty m_indiP_nPokeAverage;
397
398 pcf::IndiProperty m_indiP_wfsFps; ///< Property to get the FPS from the WFS camera
400
401 pcf::IndiProperty m_indiP_single; ///< Switch to start a single measurement
403
404 pcf::IndiProperty m_indiP_continuous; ///< Switch to start continuous measurement
406
407 pcf::IndiProperty m_indiP_stop; ///< Switch to request that measurement stop
409
410 pcf::IndiProperty m_indiP_measurement; ///< Property to report the delta measurement, including the loop counter.
411
412
413 ///@}
414
415 /** \name Telemeter Interface
416 * @{
417 */
418
419 int recordTelem(const telem_pokeloop *);
420
421 int recordPokeLoop(bool force = false);
422
423 ///@}
424
425private:
426 derivedT & derived()
427 {
428 return *static_cast<derivedT *>(this);
429 }
430
431};
432
433template<class derivedT>
434int dmPokeWFS<derivedT>::setupConfig(mx::app::appConfigurator & config)
435{
436 if(derived().shmimMonitor().setupConfig(config) < 0)
437 {
438 derivedT::template log<software_error>({__FILE__, __LINE__, "shmimMonitorT::setupConfig"});
439 return -1;
440 }
441
442 config.add("wfscam.camDevName", "", "wfscam.camDevName", argType::Required, "wfscam", "camDevName", false, "string", "INDI device name of the WFS camera. Default is wfscam.shmimName.");
443 config.add("wfscam.loopSemWait", "", "wfscam.loopSemWait", argType::Required, "wfscam", "loopSemWait", false, "float", "The semaphore wait time for the wfs loop start signal");
444 config.add("wfscam.imageSemWait", "", "wfscam.imageSemWait", argType::Required, "wfscam", "imageSemWait", false, "float", "The semaphore wait time for the image availability signal");
445
446 if(derived().darkShmimMonitor().setupConfig(config) < 0)
447 {
448 derivedT::template log<software_error>({__FILE__, __LINE__, "darkShmimMonitorT::setupConfig"});
449 return -1;
450 }
451
452 config.add("pokecen.dmChannel", "", "pokecen.dmChannel", argType::Required, "pokecen", "dmChannel", false, "string", "The dm channel to use for pokes, e.g. dm01disp06.");
453 config.add("pokecen.pokeX", "", "pokecen.pokeX", argType::Required, "pokecen", "pokeX", false, "vector<int>", "The x-coordinates of the actuators to poke. ");
454 config.add("pokecen.pokeY", "", "pokecen.pokeY", argType::Required, "pokecen", "pokeY", false, "vector<int>", "The y-coordinates of the actuators to poke. ");
455 config.add("pokecen.pokeAmp", "", "pokecen.pokeAmp", argType::Required, "pokecen", "pokeAmp", false, "float", "The poke amplitude, in DM command units. Default is 0.");
456 config.add("pokecen.dmSleep", "", "pokecen.dmSleep", argType::Required, "pokecen", "dmSleep", false, "float", "The time to sleep for the DM command to be applied, in microseconds. Default is 10000.");
457 config.add("pokecen.nPokeImages", "", "pokecen.nPokeImages", argType::Required, "pokecen", "nPokeImages", false, "int", "The number of poke images to average. Default 5.");
458 config.add("pokecen.nPokeAverage", "", "pokecen.nPokeAverage", argType::Required, "pokecen", "nPokeAverage", false, "int", "The number of poke sequences to average. Default 10.");
459
460
461 return 0;
462}
463
464
465template<class derivedT>
466int dmPokeWFS<derivedT>::loadConfig( mx::app::appConfigurator & config)
467{
468 if(derived().shmimMonitor().loadConfig(config) < 0)
469 {
470 return derivedT::template log<software_error, -1>({__FILE__, __LINE__, "shmimMonitorT::loadConfig"});
471 }
472
473 m_wfsCamDevName = derived().shmimMonitor().shmimName();
474 config(m_wfsCamDevName, "wfscam.camDevName");
475
476 //configure the semaphore waits
477 config(m_wfsSemWait, "wfscam.loopSemWait");
478
479 m_wfsSemWait_sec = floor(m_wfsSemWait);
480 m_wfsSemWait_nsec = (m_wfsSemWait - m_wfsSemWait_sec) * 1e9;
481
482 config(m_imageSemWait, "wfscam.imageSemWait");
483
484 m_imageSemWait_sec = floor(m_imageSemWait);
485 m_imageSemWait_nsec = (m_imageSemWait - m_imageSemWait_sec) * 1e9;
486
487 if(derived().darkShmimMonitor().loadConfig(config) < 0)
488 {
489 return derivedT::template log<software_error, -1>({__FILE__, __LINE__, "darkShmimMonitorT::loadConfig"});
490 }
491
492 config(m_dmChan, "pokecen.dmChannel");
493
494 config(m_poke_x, "pokecen.pokeX");
495
496 config(m_poke_y, "pokecen.pokeY");
497
498 if(m_poke_x.size() == 0 || (m_poke_x.size() != m_poke_y.size()))
499 {
500 return derivedT::template log<software_error,-1>({__FILE__, __LINE__, "invalid poke specification"});
501 }
502
503 config(m_poke_amp, "pokecen.pokeAmp");
504
505 config(m_dmSleep, "pokecen.dmSleep");
506
507 config(m_nPokeImages, "pokecen.nPokeImages");
508
509 config(m_nPokeAverage, "pokecen.nPokeAverage");
510
511 return 0;
512}
513
514template<class derivedT>
516{
517 if( derived().shmimMonitor().appStartup() < 0)
518 {
519 return derivedT::template log<software_error, -1>({__FILE__,__LINE__});
520 }
521
522 if( derived().darkShmimMonitor().appStartup() < 0)
523 {
524 return derivedT::template log<software_error, -1>({__FILE__,__LINE__});
525 }
526
527 CREATE_REG_INDI_NEW_NUMBERF_DERIVED(m_indiP_poke_amp, "poke_amp", -1, 1, 1e-1, "%0.01f", "", "");
528 m_indiP_poke_amp["current"].setValue(m_poke_amp);
529 m_indiP_poke_amp["target"].setValue(m_poke_amp);
530
531 CREATE_REG_INDI_NEW_NUMBERI_DERIVED(m_indiP_nPokeImages, "nPokeImages", 1, 1000, 1, "%d", "", "");
532 m_indiP_nPokeImages["current"].setValue(m_nPokeImages);
533 m_indiP_nPokeImages["target"].setValue(m_nPokeImages);
534
535 CREATE_REG_INDI_NEW_NUMBERI_DERIVED(m_indiP_nPokeAverage, "nPokeAverage", 1, 1000, 1, "%d", "", "");
536 m_indiP_nPokeAverage["current"].setValue(m_nPokeAverage);
537 m_indiP_nPokeAverage["target"].setValue(m_nPokeAverage);
538
539 REG_INDI_SETPROP_DERIVED(m_indiP_wfsFps, m_wfsCamDevName, std::string("fps"));
540
541 CREATE_REG_INDI_NEW_TOGGLESWITCH_DERIVED( m_indiP_single, "single");
542
543 CREATE_REG_INDI_NEW_TOGGLESWITCH_DERIVED( m_indiP_continuous, "continuous");
544
545 CREATE_REG_INDI_NEW_REQUESTSWITCH_DERIVED( m_indiP_stop, "stop");
546
547 derived().template registerIndiPropertyReadOnly( m_indiP_measurement, "measurement", pcf::IndiProperty::Number, pcf::IndiProperty::ReadOnly, pcf::IndiProperty::Idle);
548 m_indiP_measurement.add({"delta_x", 0.0});
549 m_indiP_measurement.add({"delta_y", 0.0});
550 m_indiP_measurement.add({"counter", 0});
551
552 if(sem_init(&m_wfsSemaphore, 0,0) < 0)
553 {
554 return derivedT::template log<software_critical, -1>({__FILE__, __LINE__, errno,0, "Initializing wfs semaphore"});
555 }
556
557 if(sem_init(&m_imageSemaphore, 0,0) < 0)
558 {
559 return derivedT::template log<software_critical, -1>({__FILE__, __LINE__, errno,0, "Initializing image semaphore"});
560 }
561
562 if(derived().template threadStart( m_wfsThread, m_wfsThreadInit, m_wfsThreadID, m_wfsThreadProp, m_wfsThreadPrio, m_wfsCpuset, "wfs", this, wfsThreadStart) < 0)
563 {
564 return derivedT::template log<software_critical,-1>({__FILE__, __LINE__});
565 }
566
567 return 0;
568}
569
570template<class derivedT>
572{
573
574 if( derived().shmimMonitor().appLogic() < 0)
575 {
576 return derivedT::template log<software_error, -1>({__FILE__,__LINE__});
577 }
578
579 if( derived().darkShmimMonitor().appLogic() < 0)
580 {
581 return derivedT::template log<software_error, -1>({__FILE__,__LINE__});
582 }
583
584 //first do a join check to see if other threads have exited.
585 //these will throw if the threads are really gone
586 try
587 {
588 if(pthread_tryjoin_np(m_wfsThread.native_handle(),0) == 0)
589 {
590 derivedT::template log<software_error>({__FILE__, __LINE__, "WFS thread has exited"});
591 return -1;
592 }
593 }
594 catch(...)
595 {
596 derivedT::template log<software_error>({__FILE__, __LINE__, "WFS thread has exited"});
597 return -1;
598 }
599
600 if(m_measuring > 0)
601 {
602 if(m_continuous)
603 {
604 derived().template updateSwitchIfChanged(m_indiP_continuous, "toggle", pcf::IndiElement::SwitchStateType::On, INDI_OK);
605 }
606 else
607 {
608 derived().template updateSwitchIfChanged(m_indiP_continuous, "toggle", pcf::IndiElement::SwitchStateType::Off, INDI_IDLE);
609 }
610
611 if(m_single)
612 {
613 derived().template updateSwitchIfChanged(m_indiP_single, "toggle", pcf::IndiElement::SwitchStateType::On, INDI_OK);
614 }
615 else
616 {
617 derived().template updateSwitchIfChanged(m_indiP_single, "toggle", pcf::IndiElement::SwitchStateType::Off, INDI_IDLE);
618 }
619 }
620 else
621 {
622 derived().template updateSwitchIfChanged(m_indiP_continuous, "toggle", pcf::IndiElement::SwitchStateType::Off, INDI_IDLE);
623 derived().template updateSwitchIfChanged(m_indiP_single, "toggle", pcf::IndiElement::SwitchStateType::Off, INDI_IDLE);
624 }
625
626 derived().template updateIfChanged( m_indiP_nPokeImages, "current", m_nPokeImages);
627 derived().template updateIfChanged( m_indiP_nPokeAverage, "current", m_nPokeAverage);
628 derived().template updateIfChanged( m_indiP_poke_amp, "current", m_poke_amp);
629
630 return 0;
631}
632
633template<class derivedT>
635{
636 if(derived().shmimMonitor().appShutdown() < 0)
637 {
638 derivedT::template log<software_error>({__FILE__, __LINE__, "error from shmimMonitorT::appShutdown"});
639 }
640
641 if(derived().darkShmimMonitor().appShutdown() < 0)
642 {
643 derivedT::template log<software_error>({__FILE__, __LINE__, "error from darkShmimMonitorT::appShutdown"});
644 }
645
646 if (m_wfsThread.joinable())
647 {
648 pthread_kill(m_wfsThread.native_handle(), SIGUSR1);
649 try
650 {
651 m_wfsThread.join(); // this will throw if it was already joined
652 }
653 catch (...)
654 {
655 }
656 }
657
658 return 0;
659}
660
661template<class derivedT>
663{
664 static_cast<void>(dummy); //be unused
665
666 std::unique_lock<std::mutex> lock(m_wfsImageMutex);
667
668 m_rawImage.create( derived().m_configName + "_raw", derived().shmimMonitor().width(), derived().shmimMonitor().height());
669
670 wfsPixget = getPixPointer<float>(derived().shmimMonitor().dataType());
671
672 try
673 {
674 m_dmStream.open(m_dmChan);
675 }
676 catch(const std::exception& e)
677 {
678 return derivedT::template log<software_error,-1>({__FILE__, __LINE__, std::string("exception opening DM: ") + e.what()});
679 }
680
681 m_dmImage.resize(m_dmStream.rows(), m_dmStream.cols());
682
683 if(derived().darkShmimMonitor().width() == derived().shmimMonitor().width() &&
684 derived().darkShmimMonitor().height() == derived().shmimMonitor().height() )
685 {
686 m_darkValid = true;
687 }
688 else
689 {
690 m_darkValid = false;
691 }
692
693 if(m_pokeImage.rows() != derived().shmimMonitor().width() || m_pokeImage.cols() != derived().shmimMonitor().height())
694 {
695 m_pokeImage.create(derived().m_configName + "_poke", derived().shmimMonitor().width(), derived().shmimMonitor().height());
696 }
697
698 m_pokeLocal.resize(derived().shmimMonitor().width(), derived().shmimMonitor().height());
699
700 return 0;
701}
702
703template<class derivedT>
705 const wfsShmimT & dummy
706 )
707{
708 static_cast<void>(dummy); //be unused
709
710 std::unique_lock<std::mutex> lock(m_wfsImageMutex);
711
712 float * data = m_rawImage().data();
713 float * darkData = m_darkImage.data();
714
715 //Copy the data out as float no matter what type it is
716 uint64_t Npix = derived().shmimMonitor().width()*derived().shmimMonitor().height();
717
718 if(m_darkValid)
719 {
720 for(unsigned nn=0; nn < Npix; ++nn)
721 {
722 data[nn] = wfsPixget(curr_src, nn) - darkData[nn];
723 }
724 }
725 else
726 {
727 for(unsigned nn=0; nn < Npix; ++nn)
728 {
729 data[nn] = wfsPixget(curr_src, nn);
730 }
731 }
732
733 if(sem_post(&m_imageSemaphore) < 0)
734 {
735 return derivedT::template log<software_critical, -1>({__FILE__, __LINE__, errno, 0, "Error posting to semaphore"});
736 }
737
738 return 0;
739}
740
741//--dark shmim
742
743template<class derivedT>
745{
746 static_cast<void>(dummy); //be unused
747
748 std::unique_lock<std::mutex> lock(m_wfsImageMutex);
749
750 m_darkImage.resize(derived().darkShmimMonitor().width(), derived().darkShmimMonitor().height());
751
752 darkPixget = getPixPointer<float>(derived().darkShmimMonitor().dataType());
753
754 if(derived().darkShmimMonitor().width() == derived().shmimMonitor().width() &&
755 derived().darkShmimMonitor().height() == derived().shmimMonitor().height() )
756 {
757 std::cerr << "dark is valid " << derived().darkShmimMonitor().width() << " " << derived().shmimMonitor().width() << " ";
758 std::cerr << derived().darkShmimMonitor().height() << " " << derived().shmimMonitor().height() << "\n";
759 m_darkValid = true;
760 }
761 else
762 {
763 m_darkValid = false;
764 }
765
766 return 0;
767}
768
769template<class derivedT>
771 const darkShmimT & dummy
772 )
773{
774 static_cast<void>(dummy); //be unused
775
776 std::unique_lock<std::mutex> lock(m_wfsImageMutex);
777
778 float * darkData = m_darkImage.data();
779
780 //Copy the data out as float no matter what type it is
781 uint64_t nPix = derived().darkShmimMonitor().width()*derived().darkShmimMonitor().height();
782 for(unsigned nn=0; nn < nPix; ++nn)
783 {
784 darkData[nn] = darkPixget(curr_src, nn);
785 }
786
787 return 0;
788}
789
790template<class derivedT>
795
796template<class derivedT>
798{
799 m_wfsThreadID = syscall(SYS_gettid);
800
801 //Wait fpr the thread starter to finish initializing this thread.
802 while(m_wfsThreadInit == true && derived().m_shutdown == 0)
803 {
804 sleep(1);
805 }
806
807 while(derived().m_shutdown == 0)
808 {
809 timespec ts;
810 XWC_SEM_WAIT_TS_RETVOID_DERIVED(ts, m_wfsSemWait_sec, m_wfsSemWait_nsec);
811
812 XWC_SEM_TIMEDWAIT_LOOP_DERIVED( m_wfsSemaphore, ts )
813
814 //Lock a mutex here
815 if(m_single)
816 {
817 m_measuring = 1;
818 }
819 else if(m_continuous)
820 {
821 m_measuring = 2;
822 }
823 else
824 {
825 m_measuring = 0;
826 return;
827 }
828
829 derived().template state(stateCodes::OPERATING);
830
831 while(!m_pokeImage.valid())
832 {
833 mx::sys::milliSleep(10);
834 }
835
836 m_stopMeasurement = false;
837
838 bool firstRun = true;
839
840 while(!m_stopMeasurement && !derived().m_shutdown)
841 {
842 if( derived().runSensor(firstRun) < 0)
843 {
844 derivedT::template log<software_error>({__FILE__, __LINE__, "runSensor returned error"});
845 break;
846 }
847
848 if(m_stopMeasurement || derived().m_shutdown)
849 {
850 break;
851 }
852
853 if( derived().analyzeSensor() < 0)
854 {
855 derivedT::template log<software_error>({__FILE__, __LINE__, "runSensor returned error"});
856 break;
857 }
858
859 ++m_counter;
860 derived().updateIfChanged(m_indiP_measurement, "counter", m_counter);
861 derived().recordPokeLoop();
862
863 firstRun = false;
864
865 if(m_measuring == 1)
866 {
867 break;
868 }
869 }
870
871 m_measuring = 0;
872 m_single = 0;
873 m_continuous = 0;
874
875 derived().template state(stateCodes::READY);
876
877
878 } //outer loop, will exit if derived().m_shutdown==true
879
880 return;
881
882}
883
884template<class derivedT>
886{
887 timespec ts;
888
889 int sign = 1;
890 if(pokeSign < 0) sign = -1;
891
892 //Prepare the DM image with the pokes
893 m_dmImage.setZero();
894
895 for(size_t nn = 0; nn < m_poke_x.size(); ++nn)
896 {
897 m_dmImage( m_poke_x[nn], m_poke_y[nn]) = pokeSign*m_poke_amp;
898 }
899
900 //This is where the pokes are applied to the DM
901 m_dmStream = m_dmImage;
902
903 mx::sys::microSleep(m_dmSleep);
904
905 //flush semaphore so we take the _next_ good image
906 XWC_SEM_FLUSH_DERIVED(m_imageSemaphore);
907
908 //** And wait one image to be sure we are on a whole poke**//
909 XWC_SEM_WAIT_TS_DERIVED(ts, m_imageSemWait_sec, m_imageSemWait_nsec);
910 bool ready = false;
911 while(!ready && !(m_stopMeasurement || derived().m_shutdown))
912 {
913 XWC_SEM_TIMEDWAIT_LOOP_DERIVED( m_imageSemaphore, ts )
914 else
915 {
916 ready = true;
917 }
918 }
919
920 uint32_t n = 0;
921 while(n < m_nPokeImages && !(m_stopMeasurement || derived().m_shutdown))
922 {
923 //** Now we record the poke image **//
924 XWC_SEM_WAIT_TS_DERIVED(ts, m_imageSemWait_sec, m_imageSemWait_nsec);
925 XWC_SEM_TIMEDWAIT_LOOP_DERIVED( m_imageSemaphore, ts )
926
927 //If here, we got an image. m_rawImage will have been updated
928 m_pokeLocal += sign*m_rawImage();
929
930 ++n;
931 }
932
933 if(m_stopMeasurement || derived().m_shutdown)
934 {
935 m_dmImage.setZero();
936 m_dmStream = m_dmImage;
937 return 1;
938 }
939
940 return 0;
941}
942
943template<class derivedT>
945{
946 int rv;
947
948 if(!m_pokeImage.valid())
949 {
950 return derivedT::template log<software_error,-1>({__FILE__, __LINE__, "poke image is not allocated"});
951 }
952
953 m_pokeLocal.setZero();
954
955 for(unsigned nseq = 0; nseq < m_nPokeAverage; ++nseq)
956 {
957
958 //************** positive POKE **********************/
959
960 rv = basicTimedPoke(+1);
961
962 if(rv < 0)
963 {
964 derivedT::template log<software_error>({__FILE__, __LINE__});
965 return rv;
966 }
967 else if (rv > 0) // shutdown
968 {
969 return rv;
970 }
971
972 if(m_stopMeasurement || derived().m_shutdown)
973 {
974 break;
975 }
976
977 //************** NEGATIVE POKE **********************/
978
979 rv = basicTimedPoke(-1);
980
981 if(rv < 0)
982 {
983 derivedT::template log<software_error>({__FILE__, __LINE__});
984 return rv;
985 }
986 else if (rv > 0) // shutdown
987 {
988 return rv;
989 }
990
991 if(m_stopMeasurement || derived().m_shutdown)
992 {
993 break;
994 }
995 }
996
997 try
998 {
999 m_pokeImage = m_pokeLocal/(2.0*m_nPokeImages*m_nPokeAverage);
1000 }
1001 catch(const std::exception& e)
1002 {
1003 return derivedT::template log<software_error,-1>({__FILE__, __LINE__, e.what()});
1004 }
1005
1006
1007
1008 m_dmImage.setZero();
1009 m_dmStream = m_dmImage;
1010
1011 return 0;
1012}
1013
1014template<class derivedT>
1016 float deltaY
1017 )
1018{
1019 m_deltaX = deltaX;
1020 m_deltaY = deltaY;
1021 m_indiP_measurement["delta_x"] = deltaX;
1022 m_indiP_measurement["delta_y"] = deltaY;
1023
1024 return 0;
1025}
1026
1027template<class derivedT>
1028INDI_NEWCALLBACK_DEFN( dmPokeWFS<derivedT>, m_indiP_nPokeImages )(const pcf::IndiProperty &ipRecv)
1029{
1030 INDI_VALIDATE_CALLBACK_PROPS_DERIVED(m_indiP_nPokeImages, ipRecv)
1031
1032 float target;
1033
1034 if( derived().template indiTargetUpdate(m_indiP_nPokeImages, target, ipRecv, false) < 0)
1035 {
1036 return derivedT::template log<software_error,-1>({__FILE__, __LINE__});
1037 }
1038
1039 m_nPokeImages = target;
1040
1041 return 0;
1042}
1043
1044template<class derivedT>
1045INDI_NEWCALLBACK_DEFN( dmPokeWFS<derivedT>, m_indiP_nPokeAverage )(const pcf::IndiProperty &ipRecv)
1046{
1047 INDI_VALIDATE_CALLBACK_PROPS_DERIVED(m_indiP_nPokeAverage, ipRecv)
1048
1049 float target;
1050
1051 if( derived().template indiTargetUpdate(m_indiP_nPokeAverage, target, ipRecv, false) < 0)
1052 {
1053 return derivedT::template log<software_error,-1>({__FILE__, __LINE__});
1054 }
1055
1056 m_nPokeAverage = target;
1057
1058 return 0;
1059}
1060
1061template<class derivedT>
1062INDI_NEWCALLBACK_DEFN( dmPokeWFS<derivedT>, m_indiP_poke_amp )(const pcf::IndiProperty &ipRecv)
1063{
1065
1066 float target;
1067
1068 if( derived().template indiTargetUpdate(m_indiP_poke_amp, target, ipRecv, false) < 0)
1069 {
1070 return derivedT::template log<software_error,-1>({__FILE__, __LINE__});
1071 }
1072
1073 m_poke_amp = target;
1074
1075 return 0;
1076}
1077
1078template<class derivedT>
1079INDI_SETCALLBACK_DEFN( dmPokeWFS<derivedT>, m_indiP_wfsFps )(const pcf::IndiProperty &ipRecv)
1080{
1082
1083 if( ipRecv.find("current") != true ) //this isn't valid
1084 {
1085 return 0;
1086 }
1087
1088 m_wfsFps = ipRecv["current"].get<float>();
1089
1090 return 0;
1091}
1092
1093template<class derivedT>
1094INDI_NEWCALLBACK_DEFN( dmPokeWFS<derivedT>, m_indiP_single )(const pcf::IndiProperty &ipRecv)
1095{
1097
1098 if( ipRecv.find("toggle") != true ) //this isn't valid
1099 {
1100 return -1;
1101 }
1102
1103 if( ipRecv["toggle"].getSwitchState() == pcf::IndiElement::On )
1104 {
1105 if(m_measuring == 0)
1106 {
1107 m_continuous = 0;
1108 m_single = 1;
1109 if(sem_post(&m_wfsSemaphore) < 0)
1110 {
1111 return derivedT::template log<software_critical, -1>({__FILE__, __LINE__, errno, 0, "Error posting to semaphore"});
1112 }
1113 }
1114 }
1115
1116 return 0;
1117}
1118
1119template<class derivedT>
1120INDI_NEWCALLBACK_DEFN( dmPokeWFS<derivedT>, m_indiP_continuous )(const pcf::IndiProperty &ipRecv)
1121{
1122 INDI_VALIDATE_CALLBACK_PROPS_DERIVED(m_indiP_continuous, ipRecv)
1123
1124 if( ipRecv.find("toggle") != true ) //this isn't valid
1125 {
1126 return -1;
1127 }
1128
1129 if( ipRecv["toggle"].getSwitchState() == pcf::IndiElement::On )
1130 {
1131 if(m_measuring == 0)
1132 {
1133 m_continuous = 1;
1134 m_single = 0;
1135 if(sem_post(&m_wfsSemaphore) < 0)
1136 {
1137 return derivedT::template log<software_critical, -1>({__FILE__, __LINE__, errno, 0, "Error posting to semaphore"});
1138 }
1139 }
1140 }
1141 else if( ipRecv["toggle"].getSwitchState() == pcf::IndiElement::Off )
1142 {
1143 if(m_measuring != 0)
1144 {
1145 m_stopMeasurement = true;
1146 }
1147 }
1148
1149 return 0;
1150}
1151
1152template<class derivedT>
1153INDI_NEWCALLBACK_DEFN( dmPokeWFS<derivedT>, m_indiP_stop )(const pcf::IndiProperty &ipRecv)
1154{
1156
1157 if( ipRecv.find("request") != true ) //this isn't valid
1158 {
1159 return -1;
1160 }
1161
1162 if( ipRecv["request"].getSwitchState() == pcf::IndiElement::On )
1163 {
1164 if(m_measuring != 0)
1165 {
1166 m_stopMeasurement = true;
1167 }
1168 }
1169
1170 return 0;
1171}
1172
1173template<class derivedT>
1174int dmPokeWFS<derivedT>::recordTelem(const telem_pokeloop *)
1175{
1176 return recordPokeLoop(true);
1177}
1178
1179template<class derivedT>
1181{
1182 static int measuring = -1;
1183 static float deltaX = std::numeric_limits<float>::max();
1184 static float deltaY = std::numeric_limits<float>::max();
1185 static uint64_t counter = std::numeric_limits<uint64_t>::max();
1186
1187 if(force || (m_counter != counter) || (m_deltaX != deltaX) || (m_deltaY != deltaY) || (m_measuring != measuring))
1188 {
1189 uint8_t meas = m_measuring;
1190 derived().template telem<telem_pokeloop>({meas, m_deltaX, m_deltaY, m_counter});
1191
1192 measuring = m_measuring;
1193 deltaX = m_deltaX;
1194 deltaY = m_deltaY;
1195 counter = m_counter;
1196 }
1197
1198 return 0;
1199}
1200
1201/// Call dmPokeWFS::setupConfig with error checking
1202/**
1203 * \param cfig the application configurator
1204 */
1205#define DMPOKEWFS_SETUP_CONFIG( cfig ) \
1206 if(dmPokeWFST::setupConfig(cfig) < 0) \
1207 { \
1208 log<software_error>({__FILE__, __LINE__, "Error from dmPokeWFST::setupConfig"}); \
1209 m_shutdown = true; \
1210 return; \
1211 }
1212
1213/// Call dmPokeWFS::loadConfig with error checking
1214/** This must be inside a function that returns int, e.g. the standard loadConfigImpl.
1215 * \param cfig the application configurator
1216 */
1217#define DMPOKEWFS_LOAD_CONFIG( cfig ) \
1218 if(dmPokeWFST::loadConfig(cfig) < 0) \
1219 { \
1220 return log<software_error,-1>({__FILE__, __LINE__, "Error from dmPokeWFST::loadConfig"}); \
1221 }
1222
1223/// Call dmPokeWFS::appStartup with error checking
1224#define DMPOKEWFS_APP_STARTUP \
1225 if( dmPokeWFST::appStartup() < 0) \
1226 { \
1227 return log<software_error, -1>({__FILE__,__LINE__}); \
1228 }
1229
1230/// Call dmPokeWFS::appLogic with error checking
1231#define DMPOKEWFS_APP_LOGIC \
1232 if( dmPokeWFST::appLogic() < 0) \
1233 { \
1234 return log<software_error, -1>({__FILE__,__LINE__}); \
1235 }
1236
1237/// Call dmPokeWFS::appShutdown with error checking
1238#define DMPOKEWFS_APP_SHUTDOWN \
1239 if(dmPokeWFST::appShutdown() < 0) \
1240 { \
1241 log<software_error>({__FILE__, __LINE__, "error from dmPokeWFST::appShutdown"}); \
1242 }
1243
1244} //namespace dev
1245} //namespace app
1246} //namespace MagAOX
1247
1248#endif //dmPokeWFS_hpp
A base class to coordinate poking a deformable mirror's actuators and synchronizedreads of a camera i...
pcf::IndiProperty m_indiP_poke_amp
mx::improc::eigenImage< float > m_pokeLocal
mx::improc::milkImage< float > m_rawImage
bool m_continuous
True if continuous measurements are in progress.
INDI_NEWCALLBACK_DECL(derivedT, m_indiP_nPokeAverage)
INDI_NEWCALLBACK_DECL(derivedT, m_indiP_continuous)
std::string m_wfsCamDevName
INDI device name of the WFS camera. Default is wfscam.shmimName.
double m_imageSemWait
The time in sec to wait on the image semaphore. Default 0.5 sec.
void wfsThreadExec()
Execute the frame grabber main loop.
unsigned m_wfsSemWait_nsec
The timeoutfor the WFS semaphore, nanoseconds component.
int updateMeasurement(float deltaX, float deltaY)
float(* wfsPixget)(void *, size_t)
sem_t m_imageSemaphore
Semaphore used to signal that an image is ready.
INDI_NEWCALLBACK_DECL(derivedT, m_indiP_single)
static void wfsThreadStart(dmPokeWFS *s)
Thread starter, called by wfsThreadStart on thread construction. Calls wfsThreadExec.
sem_t m_wfsSemaphore
Semaphore used to signal the WFS thread to start WFSing.
float m_wfsFps
Pointer to a function to extract the image data as float.
float m_dmSleep
The time to sleep for the DM command to be applied, in microseconds. Default is 10000.
pcf::IndiProperty m_indiP_nPokeAverage
std::vector< int > m_poke_y
int processImage(void *curr_src, const darkShmimT &)
float(* darkPixget)(void *, size_t)
mx::improc::eigenImage< float > m_dmImage
int m_wfsThreadPrio
Priority of the WFS thread, should normally be > 00.
unsigned m_nPokeImages
The number of images to average for the poke images. Default is 5.
int appShutdown()
dmPokeWFS shutdown
pcf::IndiProperty m_wfsThreadProp
The property to hold the WFS thread details.
bool m_darkValid
Flag indicating if dark is valid based on its size.
mx::improc::milkImage< float > m_pokeImage
unsigned m_wfsSemWait_sec
The timeout for the WFS semaphore, seconds component.
pcf::IndiProperty m_indiP_single
Switch to start a single measurement.
pcf::IndiProperty m_indiP_measurement
Property to report the delta measurement, including the loop counter.
int basicTimedPoke(float pokeSign)
Apply a single DM poke pattern and record the results.
int allocate(const wfsShmimT &)
unsigned m_imageSemWait_sec
The timeout for the image semaphore, seconds component.
unsigned m_imageSemWait_nsec
The timeout for the image semaphore, nanoseconds component.
mx::improc::milkImage< float > m_dmStream
Pointer to a function to extract the dark image data as float.
double m_wfsSemWait
The time in sec to wait on the WFS semaphore. Default 0.5 sec.
INDI_NEWCALLBACK_DECL(derivedT, m_indiP_nPokeImages)
unsigned m_nPokeAverage
The number of poke sequences to average. Default is 10.
int loadConfig(mx::app::appConfigurator &config)
load the configuration system results
pcf::IndiProperty m_indiP_wfsFps
Property to get the FPS from the WFS camera.
INDI_NEWCALLBACK_DECL(derivedT, m_indiP_poke_amp)
pcf::IndiProperty m_indiP_stop
Switch to request that measurement stop.
bool m_wfsThreadInit
Synchronizer to ensure wfs thread initializes before doing dangerous things.
pcf::IndiProperty m_indiP_nPokeImages
INDI_NEWCALLBACK_DECL(derivedT, m_indiP_stop)
pid_t m_wfsThreadID
WFS thread PID.
int basicRunSensor()
Run the basic +/- poke sensor steps.
int allocate(const darkShmimT &)
int recordPokeLoop(bool force=false)
std::thread m_wfsThread
A separate thread for the actual WFSing.
int appStartup()
Startup function.
int m_measuring
Status of measuring: 0 no, 1 single in progress, 2 continuous in progress.
bool m_stopMeasurement
Used to request that the measurement in progress stop.
INDI_SETCALLBACK_DECL(derivedT, m_indiP_wfsFps)
int setupConfig(mx::app::appConfigurator &config)
Setup the configuration system.
std::string m_wfsCpuset
The cpuset for the framegrabber thread. Ignored if empty (the default).
int appLogic()
dmPokeWFS application logic
bool m_single
True a single measurement is in progress.
int recordTelem(const telem_pokeloop *)
mx::improc::eigenImage< float > m_darkImage
The dark image.
int processImage(void *curr_src, const wfsShmimT &)
pcf::IndiProperty m_indiP_continuous
Switch to start continuous measurement.
std::vector< int > m_poke_x
#define INDI_NEWCALLBACK_DEFN(class, prop)
Define the callback for a new property request.
#define CREATE_REG_INDI_NEW_NUMBERI_DERIVED(prop, name, min, max, step, format, label, group)
Create and register a NEW INDI property as a standard number as int, using the standard callback name...
#define CREATE_REG_INDI_NEW_REQUESTSWITCH_DERIVED(prop, name)
Create and register a NEW INDI property as a standard request switch, using the standard callback nam...
#define INDI_SETCALLBACK_DEFN(class, prop)
Define the callback for a set property request.
#define CREATE_REG_INDI_NEW_TOGGLESWITCH_DERIVED(prop, name)
Create and register a NEW INDI property as a standard toggle switch, using the standard callback name...
#define CREATE_REG_INDI_NEW_NUMBERF_DERIVED(prop, name, min, max, step, format, label, group)
Create and register a NEW INDI property as a standard number as float, using the standard callback na...
@ OPERATING
The device is operating, other than homing.
@ READY
The device is ready for operation, but is not operating.
#define INDI_VALIDATE_CALLBACK_PROPS_DERIVED(prop1, prop2)
Standard check for matching INDI properties in a callback in a CRTP base class.
#define REG_INDI_SETPROP_DERIVED(prop, devName, propName)
#define INDI_IDLE
Definition indiUtils.hpp:28
#define INDI_OK
Definition indiUtils.hpp:29
const pcf::IndiProperty & ipRecv
updateIfChanged(m_indiP_angle, "target", m_angle)
std::unique_lock< std::mutex > lock(m_indiMutex)
Definition dm.hpp:24
#define XWC_SEM_FLUSH_DERIVED(sem)
#define XWC_SEM_WAIT_TS_RETVOID_DERIVED(ts, sec, nsec)
Add the wait time to a timespec for a sem_timedwait call, with no value returned on error,...
#define XWC_SEM_TIMEDWAIT_LOOP_DERIVED(sem, ts)
Perform a sem_timedwait in the context of a standard loop in MagAO-X code using the derived class.
#define XWC_SEM_WAIT_TS_DERIVED(ts, sec, nsec)
Add the wait time to a timespec for a sem_timedwait call, with -1 returned on error.