API
 
Loading...
Searching...
No Matches
mcp3208Ctrl.hpp
Go to the documentation of this file.
1/** \file mcp3208Ctrl.hpp
2 * \brief The MagAO-X mcp3208 Controller header file
3 *
4 * \ingroup mcp3208Ctrl_files
5 */
6
7#ifndef mcp3208Ctrl_hpp
8#define mcp3208Ctrl_hpp
9
10#include "../../libMagAOX/libMagAOX.hpp" //Note this is included on command line to trigger pch
11#include "../../magaox_git_version.h"
12#include "dependencies/MCP3208.h" // Included for the MCP3208 device interface
13
14/** \defgroup mcp3208Ctrl
15 * \brief The MagAO-X application to readout a mcp3208 A/D on a raspberry Pi.
16 *
17 * <a href="../handbook/operating/software/apps/XXXXXX.html">Application Documentation</a>
18 *
19 * \ingroup apps
20 *
21 */
22
23/** \defgroup mcp3208Ctrl_files
24 * \ingroup mcp3208Ctrl
25 */
26
27namespace MagAOX
28{
29namespace app
30{
31
32/** MagAO-X application to read MCP3208 channels on a Raspberry Pi.
33 *
34 * The controller can either acquire on its internal timer loop or synchronize reads to an
35 * ImageStreamIO semaphore.
36 *
37 * \ingroup mcp3208Ctrl
38 */
39class mcp3208Ctrl : public MagAOXApp<true>, public dev::frameGrabber<mcp3208Ctrl>, public dev::telemeter<mcp3208Ctrl>
40{
41
42 // Give the test harness access.
43 friend class mcp3208Ctrl_test;
44 friend class dev::frameGrabber<mcp3208Ctrl>;
45 friend class dev::telemeter<mcp3208Ctrl>;
46
49
50 /// The active MCP3208 device interface used for hardware access.
52
53 static constexpr bool c_frameGrabber_flippable = false; /**< app:dev config indicating these images can not be
54 flipped */
55
56 protected:
57 /** \name Configurable Parameters - Data
58 * @{
59 */
60
61 int m_numChannels{ 2 }; ///< The number of MCP3208 channels read into each output frame.
62
63 std::string m_fpsDevice; ///< Device name providing external fps metadata for framegrabber sizing.
64 std::string m_fpsProperty{ "fps" }; ///< Property name providing external fps metadata.
65 std::string m_fpsElement{ "current" }; ///< Property element containing the fps value.
66
67 float m_fpsTol{ 0 }; ///< The tolerance used when monitoring fps metadata changes.
68
69 std::string m_numChannelsDevice; ///< Device name for getting numChannels to set circular buffer length.
70 std::string m_numChannelsProperty{ "numChannels" }; ///< Property name for getting numChannels to set circular buffer length.
71 std::string m_numChannelsElement{ "current" }; ///< Element name for getting numChannels to set circular buffer length.
72
73 float m_numChannelsTol{ 0 }; ///< The tolerance for detecting a change in numChannels.
74
75 std::string m_synchroShmimName; ///< The synchronization ImageStreamIO stream name; empty selects timer mode.
76 int m_synchroPostDelay{ 0 }; ///< Signed microsecond phase offset added to synchronized delay-model predictions.
77
78 double m_synchroDtTransfer_ns{ 3e3 }; ///< Transfer-latency term in the synchronized delay model.
79
80 double m_synchroWfsProcess_ns{ 51.5e3 }; ///< WFS processing-latency term in the synchronized delay model.
81
82 double m_synchroDtF_ns{ 10e3 }; ///< Filter/transport-latency term in the synchronized delay model.
83
84 double m_synchroWfsRead_ns{ 276.1e3 }; ///< WFS read-latency term in the synchronized delay model.
85
86 double m_delayLockAbsThreshold_ns{ 50e3 }; ///< Absolute phase-error threshold for declaring delay lock.
87
88 double m_delayLockFracThreshold{ 0.1 }; ///< Fractional period phase-error threshold for declaring delay lock.
89
90 double m_cadenceGuard_ns{ 20e3 }; ///< Reserved per-frame margin protecting synchronized cadence from overruns.
91
92 float m_alpha{ 0.01f }; ///< Global exponential moving-average coefficient applied to timing smoothers.
93
94 ///@}
95
96 /** \name Runtime State - Data
97 * @{
98 */
99
100 /// Report the configured channel count metadata to the framegrabber.
101 int numChannels();
102
103 // Creating INDI property for number of channels to read out
104 pcf::IndiProperty m_indiP_numChannels;
106
107 pcf::IndiProperty m_indiP_numChannelsSource;
109
110 /// INDI property exposing the local fps target.
111 pcf::IndiProperty m_indiP_fps;
112
113 /// Handle updates to the local fps target property.
115
116 float m_fps{ 2000 }; ///< The target acquisition rate in frames per second.
117
118 /// INDI property subscription used to follow an external fps source.
119 pcf::IndiProperty m_indiP_fpsSource;
120
121 /// Handle updates from the configured external fps source.
123
124 /// INDI property exposing the global EMA alpha used by timing smoothers.
125 pcf::IndiProperty m_indiP_alpha;
126
127 /// Handle updates to the global EMA alpha property.
129
130 /// INDI property exposing the synchronized-mode signed phase offset in microseconds.
131 pcf::IndiProperty m_indiP_synchroDelay;
132
133 /// Handle updates to the synchronized-mode signed phase offset property.
135
136 /// INDI property exposing runtime timing diagnostics for acquisition health checks.
137 pcf::IndiProperty m_indiP_timingDiag;
138
139 float m_trigger{ 1e9f / m_fps }; ///< The timer-mode read interval in nanoseconds.
140 float m_gain{ .1 }; ///< The simple integrator gain used for timer and synchro delay control.
141 float nano_sec_target{ 1e9f / m_fps }; ///< The timer-mode target interval in nanoseconds.
142 float m_synchroDelay{ 0 }; ///< The commanded pre-read delay in synchronized mode, in nanoseconds.
143 float m_synchroDelayTarget{ 0 }; ///< The synchronized-mode effective delay target in nanoseconds after applying signed offset and wrap.
144
145 /// Secondary MCP3208 handle retained with the legacy class state.
147
148 /// The timer-mode reference point for the next internal acquisition cycle.
149 std::chrono::time_point<std::chrono::high_resolution_clock> m_time_start;
150
151 /// The most recently read MCP3208 channel values published to the output stream.
152 std::vector<uint16_t> m_values;
153
154 IMAGE m_synchroStream{}; ///< The opened synchronization stream used for semaphore-triggered reads.
155 bool m_synchroStreamOpen{ false }; ///< Tracks whether the synchronization stream is currently open.
156 ino_t m_synchroStreamInode{ 0 }; ///< Cached inode used to detect synchronization stream recreation.
157 int m_synchroSemaphoreNumber{ 5 }; ///< The claimed semaphore slot for synchronization waits.
158 sem_t *m_synchroSemaphore{ nullptr }; ///< Cached pointer to the claimed synchronization semaphore.
159
160 timespec m_atime{}; ///< The most recent semaphore-arrival timestamp on the local realtime clock.
161
162 timespec m_lastAtime{}; ///< The previous semaphore-arrival timestamp used for period estimation.
163
164 double m_avgSemaphorePeriod_ns{ 0.0 }; ///< Exponential moving-average estimate of semaphore period in nanoseconds.
165
166 double m_wfsPeriodMeasured_ns{ 0.0 }; ///< Measured semaphore period used as the synchronized delay-model WFS period.
167
168 timespec m_lastProducerAtime{}; ///< Previous producer atime from the synchronization stream metadata.
169
170 uint64_t m_lastProducerCnt0{ 0 }; ///< Previous producer frame counter from the synchronization stream metadata.
171
172 double m_producerPeriodInst_ns{ 0.0 }; ///< Instantaneous producer period estimate from metadata timestamps.
173
174 double m_avgProducerPeriod_ns{ 0.0 }; ///< Exponential moving-average producer period estimate from metadata.
175
176 bool m_firstProducerSample{ true }; ///< Tracks first-sample initialization for producer-period estimation.
177
178 uint64_t m_localFrameSeq{ 0 }; ///< Monotonic local acquisition sequence incremented on each published frame.
179
180 uint64_t m_syncFramesReceived{ 0 }; ///< Count of synchronized frames received from semaphore wakes.
181
182 uint64_t m_syncFramesWritten{ 0 }; ///< Count of synchronized frames published to the output stream.
183
184 uint64_t m_syncFramesDropped{ 0 }; ///< Count of missing producer frame IDs inferred from positive counter gaps.
185
186 uint64_t m_syncFrameIdGapCount{ 0 }; ///< Number of producer-frame gap events where ID delta exceeded one.
187
188 uint64_t m_syncProducerFrameId{ 0 }; ///< Latest producer frame ID (`cnt0`) observed on the synchronization stream.
189
190 uint64_t m_syncProducerFrameDelta{ 0 }; ///< Latest producer-frame ID delta between consecutive synchronized wakes.
191
192 uint64_t m_lastSyncProducerFrameId{ 0 }; ///< Previous producer frame ID used to detect synchronized frame-ID gaps.
193
194 bool m_syncProducerFrameValid{ false }; ///< Tracks whether producer frame-ID gap tracking has a valid prior sample.
195
196 bool m_firstSemaphore{ true }; ///< Tracks first-arrival initialization for semaphore period estimation.
197
198 double m_avgReadLatency_ns{ 0.0 }; ///< Exponential moving-average estimate of semaphore-to-read latency in nanoseconds.
199
200 bool m_firstReadLatency{ true }; ///< Tracks first-arrival initialization for semaphore-to-read latency estimation.
201
202 double m_wfs_fps{ 0.0 }; ///< WFS frame rate estimate used for timing prediction; initialized from configured fps before callbacks.
203
204 double m_delayModel_ns{ 0.0 }; ///< Current modulo-wrapped delay predicted by the synchronized phase model.
205
206 double m_delayApplied_ns{ 0.0 }; ///< Delay applied to the most recent synchronized ADC read.
207
208 double m_delayBudget_ns{ 0.0 }; ///< Maximum delay allowed this cycle after cadence budgeting.
209
210 double m_nonDelayService_ns{ 0.0 }; ///< Measured wake-to-return service time minus applied delay for the latest cycle.
211
212 double m_avgNonDelayService_ns{ 0.0 }; ///< Exponential moving-average of non-delay synchronized service time.
213
214 bool m_firstNonDelayService{ true }; ///< Tracks first-sample initialization for non-delay service-time averaging.
215
216 double m_delayPhaseError_ns{ 0.0 }; ///< Wrapped phase error between applied and modeled synchronized delay.
217
218 double m_delayLock{ 0.0 }; ///< Delay-lock state exported to diagnostics as 1.0 (locked) or 0.0 (unlocked).
219
220 double m_delayCapped{ 0.0 }; ///< Delay-cap state exported as 1.0 when cadence budgeting limits the applied delay.
221
222 timespec m_triggerTime{}; ///< Computed trigger timestamp aligned to the estimated WFS integration midpoint.
223
224 double m_triggerInterval_ns{ 0.0 }; ///< Measured interval between consecutive trigger events in nanoseconds.
225
226 double m_channelReadoutTime_ns{ 0.0 }; ///< Measured duration of reading all configured channels in nanoseconds.
227
228 timespec m_lastTriggerTime{}; ///< Previous trigger timestamp used to compute the synchronized trigger interval.
229
230 bool m_firstTriggerTime{ true }; ///< Tracks first-trigger initialization for synchronized trigger-interval measurement.
231
232 bool m_firstTimerTrigger{ true }; ///< Tracks first-trigger initialization for timer-mode trigger-interval measurement.
233
234 ///@}
235
236 /** \name Synchronization Helpers
237 * @{
238 */
239
240 /// Open the synchronization stream when semaphore-driven acquisition is enabled.
241 /**
242 * \returns 0 on success.
243 * \returns 1 when the synchronization stream is not yet ready.
244 */
245 int openSynchroStream();
246
247 /// Claim a semaphore slot from the synchronization stream.
248 /**
249 * \returns 0 on success.
250 * \returns -1 on error.
251 */
253
254 /// Check whether the synchronization stream has disappeared or been recreated.
255 /**
256 * \returns true when the current synchronization stream handle is stale.
257 * \returns false when the synchronization stream still matches the cached inode.
258 */
259 bool synchroStreamStale();
260
261 /// Release the synchronization semaphore claim and close the synchronization stream.
262 void closeSynchroStream();
263
264 /// Acquire one frame using the internal timer loop.
265 /**
266 * \returns 0 when a new sample is ready.
267 * \returns 1 when the loop should continue without publishing.
268 */
270
271 /// Acquire one frame using the synchronization semaphore.
272 /**
273 * \returns 0 when a new sample is ready.
274 * \returns 1 when the loop should continue without publishing.
275 * \returns -1 on a critical timing error.
276 */
278
279 /// Read the current realtime clock value.
280 /**
281 * \returns 0 on success.
282 * \returns -1 on error.
283 */
284 int getRealtime( timespec &ts /**< [out] the current realtime clock value */ );
285
286 /// Wait on the claimed synchronization semaphore until the supplied timeout.
287 /**
288 * \returns 0 on success.
289 * \returns -1 on error.
290 */
291 int waitOnSemaphore( sem_t *sem /**< [in] the semaphore to wait on */,
292 timespec &ts /**< [in] the absolute timeout for the wait */ );
293
294 /// Convert a timespec timestamp to nanoseconds.
295 static inline double timespecToNs( const timespec &t /**< [in] the timespec value to convert */ );
296
297 /// Convert nanoseconds to a normalized timespec value.
298 static inline timespec nsToTimespec( double ns /**< [in] the nanosecond value to convert */ );
299
300 /// Read one MCP3208 channel value.
301 /**
302 * \returns 0 on success.
303 * \returns -1 on error.
304 */
305 int readChannelValue( int channel /**< [in] the MCP3208 channel index to read */,
306 uint16_t &value /**< [out] the sampled channel value */ );
307
308 /// Apply the current controlled delay between semaphore wake and ADC read.
309 void delayBeforeRead();
310
311 /// Update the synchronized delay command with anti-windup and physical bounds.
312 void updateSynchroDelayController( double desiredDelay_ns /**< [in] the unconstrained synchronized-delay command */ );
313
314 /// Update synchronized trigger timing from the current semaphore arrival.
315 void updateTriggerTiming( const timespec &atime /**< [in] the semaphore-arrival timestamp */ );
316
317 /// Publish acquisition timing diagnostics to the INDI read-only property.
318 /** The exported `trigger_interval_us` value reports measured current-to-previous trigger interval.
319 *
320 * The exported `trigger_time_us` value is relative to the latest semaphore arrival (`m_atime`).
321 */
323
324 ///@}
325
326 public:
327 /** \name Application Lifecycle
328 * @{
329 */
330
331 /// Construct the application with the current repository version metadata.
332 mcp3208Ctrl();
333
334 /// Destroy the application.
336 {
337 }
338
339 /// Register configuration entries for the application and helper devices.
340 virtual void setupConfig();
341
342 /// Load configuration values into the application state.
343 /** This helper is separated from `loadConfig()` to support unit testing.
344 */
345 int loadConfigImpl( mx::app::appConfigurator &_config /**< [in] the populated application configurator */ );
346
347 /// Load the configured application state.
348 virtual void loadConfig();
349
350 /// Start the application and initialize its INDI and hardware state.
351 /**
352 * \returns 0 on success.
353 * \returns -1 on error.
354 */
355 virtual int appStartup();
356
357 /// Execute one iteration of the application FSM.
358 /**
359 * \returns 0 on no critical error
360 * \returns -1 on an error requiring shutdown
361 */
362 virtual int appLogic();
363
364 /// Shutdown the application and release synchronization resources.
365 /**
366 * \returns 0 on success.
367 * \returns -1 on error.
368 */
369 virtual int appShutdown();
370
371 ///@}
372
373 /** \name Framegrabber Interface
374 * @{
375 */
376
377 /// Configure the output image geometry and acquisition mode.
378 /**
379 * \returns 0 on success
380 * \returns 1 when configuration should be retried
381 */
383
384 /// Report the current acquisition rate metadata to the framegrabber.
385 /**
386 * \returns the current fps value.
387 */
388 float fps();
389
390 /// Prepare the selected acquisition mode to begin producing samples.
391 /**
392 * \returns 0 on success
393 * \returns -1 on error
394 */
395 int startAcquisition();
396
397 /// Acquire one sample and indicate whether it should be published.
398 /**
399 * \returns 0 when a new sample is ready.
400 * \returns 1 when no new sample should be published.
401 * \returns -1 on a critical error.
402 */
404
405 /// Copy the current MCP3208 values into the output image buffer.
406 /**
407 * \returns 0 on success
408 * \returns -1 on error
409 */
410 int loadImageIntoStream( void *dest /**< [out] the destination image buffer */ );
411
412 /// Reset synchronization resources before the next acquisition configuration.
413 /**
414 * \returns 0 on success
415 * \returns -1 on error
416 */
417 int reconfig();
418
419 ///@}
420
421 /** \name Telemeter Interface
422 *
423 * @{
424 */
425 /// Check whether framegrabber timing telemetry should be recorded this cycle.
426 /**
427 * \returns 0 on success.
428 * \returns -1 on error.
429 */
430 int checkRecordTimes();
431
432 /// Record framegrabber timing telemetry.
433 /**
434 * \returns 0 on success.
435 * \returns -1 on error.
436 */
437 int recordTelem( const telem_fgtimings *telem /**< [in] the telemeter tag requested by the interface */ );
438
439 ///@}
440};
441
442mcp3208Ctrl::mcp3208Ctrl() : MagAOXApp( MAGAOX_CURRENT_SHA1, MAGAOX_REPO_MODIFIED )
443{
444 return;
445}
446
447inline double mcp3208Ctrl::timespecToNs( const timespec &t )
448{
449 return static_cast<double>( t.tv_sec ) * 1e9 + static_cast<double>( t.tv_nsec );
450}
451
452inline timespec mcp3208Ctrl::nsToTimespec( double ns )
453{
454 timespec t;
455
456 t.tv_sec = static_cast<time_t>( ns / 1e9 );
457 t.tv_nsec = static_cast<long>( ns - static_cast<double>( t.tv_sec ) * 1e9 );
458
459 if( t.tv_nsec >= 1000000000L )
460 {
461 t.tv_nsec -= 1000000000L;
462 ++t.tv_sec;
463 }
464
465 if( t.tv_nsec < 0 )
466 {
467 t.tv_nsec += 1000000000L;
468 --t.tv_sec;
469 }
470
471 return t;
472}
473
474void mcp3208Ctrl::updateTriggerTiming( const timespec &atime )
475{
476 double dt_ns = 0.0;
477
478 if( !m_firstSemaphore )
479 {
481
482 const double alpha = static_cast<double>( m_alpha );
484 }
485 else
486 {
488 m_firstSemaphore = false;
489 }
490
491 m_lastAtime = atime;
492
495
496 if( deltaT_wfs_ns <= 0.0 )
497 {
499 return;
500 }
501
502 const double raw_delay_ns =
504
505 double t_delay_model_ns = std::fmod( raw_delay_ns, deltaT_wfs_ns );
506
507 if( t_delay_model_ns < 0.0 )
508 {
510 }
511
512 const double delayOffset_ns = 1e3 * static_cast<double>( m_synchroPostDelay );
514 if( t_delay_ns < 0.0 )
515 {
517 }
518
520 m_synchroDelayTarget = static_cast<float>( t_delay_ns );
521
522 const double t_trigger_ns = timespecToNs( atime ) + t_delay_ns;
524
526 {
528 m_firstTriggerTime = false;
529 }
530 else
531 {
533 if( m_triggerInterval_ns < 0.0 )
534 {
536 }
537 }
538
540}
541
543{
545 TELEMETER_SETUP_CONFIG( config );
546
547 config.add( "fps.device",
548 "",
549 "fps.device",
550 argType::Required,
551 "fps",
552 "device",
553 false,
554 "string",
555 "Device name for getting fps to set circular buffer length." );
556
557 config.add( "fps.property",
558 "",
559 "fps.property",
560 argType::Required,
561 "fps",
562 "property",
563 false,
564 "string",
565 "Property name for getting fps to set circular buffer length. Default is 'fps'." );
566
567 config.add( "fps.element",
568 "",
569 "fps.element",
570 argType::Required,
571 "fps",
572 "element",
573 false,
574 "string",
575 "Property name for getting fps to set circular buffer length. Default is 'current'." );
576
577 config.add( "fps.tol",
578 "",
579 "fps.tol",
580 argType::Required,
581 "fps",
582 "tol",
583 false,
584 "float",
585 "Tolerance for detecting a change in FPS. Default is 0." );
586
587 config.add( "synchro.shmimName",
588 "",
589 "synchro.shmimName",
590 argType::Required,
591 "synchro",
592 "shmimName",
593 false,
594 "string",
595 "The ImageStreamIO stream used to synchronize acquisition. Default is timer-driven operation." );
596
597 config.add( "synchro.postDelay",
598 "",
599 "synchro.postDelay",
600 argType::Required,
601 "synchro",
602 "postDelay",
603 false,
604 "int",
605 "Signed phase offset in microseconds added to synchronized delay-model predictions. Default is 0." );
606
607 config.add( "synchro.dtTransfer_ns",
608 "",
609 "synchro.dtTransfer_ns",
610 argType::Required,
611 "synchro",
612 "dtTransfer_ns",
613 false,
614 "double",
615 "Transfer-latency term in nanoseconds for synchronized delay modeling. Default is 3000." );
616
617 config.add( "synchro.wfsProcess_ns",
618 "",
619 "synchro.wfsProcess_ns",
620 argType::Required,
621 "synchro",
622 "wfsProcess_ns",
623 false,
624 "double",
625 "WFS processing-latency term in nanoseconds for synchronized delay modeling. Default is 51500." );
626
627 config.add( "synchro.dtF_ns",
628 "",
629 "synchro.dtF_ns",
630 argType::Required,
631 "synchro",
632 "dtF_ns",
633 false,
634 "double",
635 "Filter/transport-latency term in nanoseconds for synchronized delay modeling. Default is 10000." );
636
637 config.add( "synchro.wfsRead_ns",
638 "",
639 "synchro.wfsRead_ns",
640 argType::Required,
641 "synchro",
642 "wfsRead_ns",
643 false,
644 "double",
645 "WFS read-latency term in nanoseconds for synchronized delay modeling. Default is 276100." );
646
647 config.add( "synchro.delayLockAbsThreshold_ns",
648 "",
649 "synchro.delayLockAbsThreshold_ns",
650 argType::Required,
651 "synchro",
652 "delayLockAbsThreshold_ns",
653 false,
654 "double",
655 "Absolute synchronized phase-error threshold in nanoseconds for delay-lock diagnostics. Default is 50000." );
656
657 config.add( "synchro.delayLockFracThreshold",
658 "",
659 "synchro.delayLockFracThreshold",
660 argType::Required,
661 "synchro",
662 "delayLockFracThreshold",
663 false,
664 "double",
665 "Fractional synchronized phase-error threshold for delay-lock diagnostics. Default is 0.1." );
666
667 config.add( "synchro.cadenceGuard_ns",
668 "",
669 "synchro.cadenceGuard_ns",
670 argType::Required,
671 "synchro",
672 "cadenceGuard_ns",
673 false,
674 "double",
675 "Reserved nanoseconds in each synchronized cycle for non-delay work. Default is 20000." );
676
677 config.add( "synchro.alpha",
678 "",
679 "synchro.alpha",
680 argType::Required,
681 "synchro",
682 "alpha",
683 false,
684 "float",
685 "Global EMA coefficient for synchronized timing smoothers. Default is 0.01." );
686
687 config.add( "numChannels.device",
688 "",
689 "numChannels.device",
690 argType::Required,
691 "numChannels",
692 "device",
693 false,
694 "int",
695 "Setting the number of channels needed to readout accelerometers" );
696
697 config.add( "numChannels.property",
698 "",
699 "numChannels.property",
700 argType::Required,
701 "numChannels",
702 "property",
703 false,
704 "string",
705 "Property name for getting numChannels to set circular buffer length. Default is 'numChannels'." );
706
707 config.add( "numChannels.element",
708 "",
709 "numChannels.element",
710 argType::Required,
711 "numChannels",
712 "element",
713 false,
714 "string",
715 "Property name for getting numChannels to set circular buffer length. Default is 'current'." );
716
717 config.add( "numChannels.tol",
718 "",
719 "numChannels.tol",
720 argType::Required,
721 "numChannels",
722 "tol",
723 false,
724 "float",
725 "Tolerance for detecting a change in numChannels. Default is 0." );
726
727 config.add( "framegrabber.cpuset",
728 "",
729 "framegrabber.cpuset",
730 argType::Required,
731 "framegrabber",
732 "cpuset",
733 false,
734 "string",
735 "The cpuset to assign the framegrabber thread to." );
736}
737
738int mcp3208Ctrl::loadConfigImpl( mx::app::appConfigurator &_config )
739{
740
743
744 _config( m_fpsDevice, "fps.device" );
745 _config( m_fpsProperty, "fps.property" );
746 _config( m_fpsElement, "fps.element" );
747 _config( m_fpsTol, "fps.tol" );
748 _config( m_synchroShmimName, "synchro.shmimName" );
749 _config( m_synchroPostDelay, "synchro.postDelay" );
750 _config( m_synchroDtTransfer_ns, "synchro.dtTransfer_ns" );
751 _config( m_synchroWfsProcess_ns, "synchro.wfsProcess_ns" );
752 _config( m_synchroDtF_ns, "synchro.dtF_ns" );
753 _config( m_synchroWfsRead_ns, "synchro.wfsRead_ns" );
754 _config( m_delayLockAbsThreshold_ns, "synchro.delayLockAbsThreshold_ns" );
755 _config( m_delayLockFracThreshold, "synchro.delayLockFracThreshold" );
756 _config( m_cadenceGuard_ns, "synchro.cadenceGuard_ns" );
757 _config( m_alpha, "synchro.alpha" );
758
759 _config( m_numChannels, "accel.numChannels" ); // making number of mcp3208 channels we read out configurable
760 _config( m_numChannelsDevice, "numChannels.device" );
761 _config( m_numChannelsProperty, "numChannels.property" );
762 _config( m_numChannelsElement, "numChannels.element" );
763 _config( m_numChannelsTol, "numChannels.tol" );
764
765 _config(m_fgCpuset, "framegrabber.cpuset");
766
767 if( m_synchroDtTransfer_ns < 0.0 )
768 {
770 }
771
772 if( m_synchroWfsProcess_ns < 0.0 )
773 {
775 }
776
777 if( m_synchroDtF_ns < 0.0 )
778 {
779 m_synchroDtF_ns = 0.0;
780 }
781
782 if( m_synchroWfsRead_ns < 0.0 )
783 {
785 }
786
788 {
790 }
791
792 if( m_delayLockFracThreshold < 0.0 )
793 {
795 }
796
797 if( m_cadenceGuard_ns < 0.0 )
798 {
799 m_cadenceGuard_ns = 0.0;
800 }
801
802 if( m_alpha < 0.0f )
803 {
804 m_alpha = 0.0f;
805 }
806 else if( m_alpha > 1.0f )
807 {
808 m_alpha = 1.0f;
809 }
810
813 m_delayModel_ns = 0.0;
814 m_delayApplied_ns = static_cast<double>( m_synchroDelayTarget );
815 m_delayBudget_ns = 0.0;
820 m_delayLock = 0.0;
821 m_delayCapped = 0.0;
823 m_lastProducerAtime = timespec{};
828 m_localFrameSeq = 0;
839
840 return 0;
841}
842
844{
845 loadConfigImpl( config );
846}
847
849{
852
853 // INDI prop for user to set fps
854 CREATE_REG_INDI_NEW_NUMBERF( m_indiP_fps, "fps", 0, 10000, 1, "%d", "", "" );
855 m_indiP_fps["current"].setValue( m_fps );
856 m_indiP_fps["target"].setValue( m_fps );
857
858 // INDI prop for user to set global timing EMA alpha
859 CREATE_REG_INDI_NEW_NUMBERF( m_indiP_alpha, "alpha", 0.0, 1.0, 0.00001, "%.5f", "", "" );
860 m_indiP_alpha["current"].setValue( m_alpha );
861 m_indiP_alpha["target"].setValue( m_alpha );
862
863 // INDI prop for user to set number of channels A/D reads out
864 CREATE_REG_INDI_NEW_NUMBERF( m_indiP_numChannels, "numChannels", 0, 8, 1, "%d", "", "" );
865 m_indiP_numChannels["current"].setValue( m_numChannels );
866 m_indiP_numChannels["target"].setValue( m_numChannels );
867
868 // INDI prop for user to set synchronized-mode signed phase offset in microseconds
869 CREATE_REG_INDI_NEW_NUMBERF( m_indiP_synchroDelay, "synchroDelay", -1000000, 1000000, 1, "%d", "us", "" );
870 m_indiP_synchroDelay["current"].setValue( m_synchroPostDelay );
871 m_indiP_synchroDelay["target"].setValue( m_synchroPostDelay );
872
873 CREATE_REG_INDI_RO_NUMBER( m_indiP_timingDiag, "timingDiag", "Timing Diagnostics", "Diagnostics" );
874 m_indiP_timingDiag.add( pcf::IndiElement( "avg_read_latency_us" ) );
875 m_indiP_timingDiag.add( pcf::IndiElement( "synchro_delay_us" ) );
876 m_indiP_timingDiag.add( pcf::IndiElement( "synchro_delay_target_us" ) );
877 m_indiP_timingDiag.add( pcf::IndiElement( "delay_applied_us" ) );
878 m_indiP_timingDiag.add( pcf::IndiElement( "delay_model_us" ) );
879 m_indiP_timingDiag.add( pcf::IndiElement( "delay_phase_error_us" ) );
880 m_indiP_timingDiag.add( pcf::IndiElement( "delay_lock" ) );
881 m_indiP_timingDiag.add( pcf::IndiElement( "delay_budget_us" ) );
882 m_indiP_timingDiag.add( pcf::IndiElement( "avg_non_delay_service_us" ) );
883 m_indiP_timingDiag.add( pcf::IndiElement( "delay_capped" ) );
884 m_indiP_timingDiag.add( pcf::IndiElement( "read_latency_error_us" ) );
885 m_indiP_timingDiag.add( pcf::IndiElement( "avg_semaphore_period_us" ) );
886 m_indiP_timingDiag.add( pcf::IndiElement( "channel_readout_us" ) );
887 m_indiP_timingDiag.add( pcf::IndiElement( "trigger_interval_us" ) );
888 m_indiP_timingDiag.add( pcf::IndiElement( "trigger_time_us" ) );
889 m_indiP_timingDiag.add( pcf::IndiElement( "local_frame_seq" ) );
890 m_indiP_timingDiag.add( pcf::IndiElement( "sync_frames_received" ) );
891 m_indiP_timingDiag.add( pcf::IndiElement( "sync_frames_written" ) );
892 m_indiP_timingDiag.add( pcf::IndiElement( "sync_frames_dropped" ) );
893 m_indiP_timingDiag.add( pcf::IndiElement( "sync_frame_id_gap_count" ) );
894 m_indiP_timingDiag.add( pcf::IndiElement( "sync_producer_frame_id" ) );
895 m_indiP_timingDiag.add( pcf::IndiElement( "sync_producer_frame_delta" ) );
896 m_indiP_timingDiag.add( pcf::IndiElement( "mode_code" ) );
897
898 if( m_fpsDevice != "" )
899 {
901 }
902
903 if( m_numChannelsDevice != "" )
904 {
906 }
907
908 {
909 // Get the maximum privileges available
910 elevatedPrivileges elPriv( this );
911
912 m_adc.connect();
913 }
914
916
918 return 0;
919}
920
922{
923 constexpr double c_timerModeCode = 0.0;
924 constexpr double c_synchroModeCode = 1.0;
925 constexpr double c_nsToUs = 1e-3;
926
927 const bool synchroMode = !m_synchroShmimName.empty();
928 const double readLatencyError_ns = m_avgReadLatency_ns - static_cast<double>( m_synchroDelayTarget );
931 const double delayModelDiag_ns = synchroMode ? m_delayModel_ns : 0.0;
932 const double delayBudgetDiag_ns = synchroMode ? m_delayBudget_ns : 0.0;
934 const double delayCappedDiag = synchroMode ? m_delayCapped : 0.0;
936
937 double triggerTime_ns = 0.0;
938 if( ( m_atime.tv_sec != 0 || m_atime.tv_nsec != 0 ) &&
939 ( m_triggerTime.tv_sec != 0 || m_triggerTime.tv_nsec != 0 ) )
940 {
942 if( triggerTime_ns < 0.0 )
943 {
944 triggerTime_ns = 0.0;
945 }
946 }
947
948 double delayPhaseError_ns = 0.0;
949 double delayLock = 0.0;
950
951 if( synchroMode && wfsPeriodMeasured_ns > 0.0 )
952 {
953 const double wfsPeriod_ns = wfsPeriodMeasured_ns;
954
956 if( delayAppliedWrapped_ns < 0.0 )
957 {
959 }
960
962 if( delayModelWrapped_ns < 0.0 )
963 {
965 }
966
968 if( delayPhaseError_ns <= -0.5 * wfsPeriod_ns )
969 {
971 }
972 else if( delayPhaseError_ns > 0.5 * wfsPeriod_ns )
973 {
975 }
976
977 const double absDelayPhaseError_ns = std::fabs( delayPhaseError_ns );
980 {
981 delayLock = 1.0;
982 }
983 }
984
987
989 const double synchroDelay_us = ( synchroMode ? static_cast<double>( m_synchroDelay ) : 0.0 ) * c_nsToUs;
990 const double synchroDelayTarget_us = static_cast<double>( m_synchroDelayTarget ) * c_nsToUs;
1000 const double triggerTime_us = triggerTime_ns * c_nsToUs;
1001 const double localFrameSeqDiag = static_cast<double>( m_localFrameSeq );
1002 const double syncFramesReceivedDiag = synchroMode ? static_cast<double>( m_syncFramesReceived ) : 0.0;
1003 const double syncFramesWrittenDiag = synchroMode ? static_cast<double>( m_syncFramesWritten ) : 0.0;
1004 const double syncFramesDroppedDiag = synchroMode ? static_cast<double>( m_syncFramesDropped ) : 0.0;
1005 const double syncFrameIdGapCountDiag = synchroMode ? static_cast<double>( m_syncFrameIdGapCount ) : 0.0;
1006 const double syncProducerFrameIdDiag =
1007 ( synchroMode && m_syncProducerFrameValid ) ? static_cast<double>( m_syncProducerFrameId ) : 0.0;
1008 const double syncProducerFrameDeltaDiag =
1009 ( synchroMode && m_syncProducerFrameValid ) ? static_cast<double>( m_syncProducerFrameDelta ) : 0.0;
1010
1012 { "avg_read_latency_us",
1013 "synchro_delay_us",
1014 "synchro_delay_target_us",
1015 "delay_applied_us",
1016 "delay_model_us",
1017 "delay_phase_error_us",
1018 "delay_lock",
1019 "delay_budget_us",
1020 "avg_non_delay_service_us",
1021 "delay_capped",
1022 "read_latency_error_us",
1023 "avg_semaphore_period_us",
1024 "channel_readout_us",
1025 "trigger_interval_us",
1026 "trigger_time_us",
1027 "local_frame_seq",
1028 "sync_frames_received",
1029 "sync_frames_written",
1030 "sync_frames_dropped",
1031 "sync_frame_id_gap_count",
1032 "sync_producer_frame_id",
1033 "sync_producer_frame_delta",
1034 "mode_code" },
1057 modeCode } );
1058}
1059
1061{
1064
1066
1067 updatesIfChanged<float>( m_indiP_fps, { "current", "target" }, { m_fps, m_fps } );
1068
1069 updatesIfChanged<float>( m_indiP_alpha, { "current", "target" }, { m_alpha, m_alpha } );
1070
1071 updatesIfChanged<int>( m_indiP_numChannels, { "current", "target" }, { m_numChannels, m_numChannels } );
1072
1074 m_indiP_synchroDelay, { "current", "target" }, { m_synchroPostDelay, m_synchroPostDelay } );
1075
1077
1078 return 0;
1079}
1080
1082{
1085
1087
1088 return 0;
1089}
1090
1092{
1093 m_values.resize( m_numChannels );
1094
1096 m_height = 1;
1098
1099 if( !m_synchroShmimName.empty() )
1100 {
1101 log<text_log>( "Configuring semaphore-synchronized acquisition from " + m_synchroShmimName +
1102 " with phase offset " + std::to_string( m_synchroPostDelay ) + " us.",
1104
1105 if( openSynchroStream() != 0 )
1106 {
1107 return 1;
1108 }
1109
1110 if( claimSynchroSemaphore() != 0 )
1111 {
1113 return 1;
1114 }
1115 }
1116 else
1117 {
1118 log<text_log>( "Configuring timer-driven acquisition.", logPrio::LOG_INFO );
1119 }
1120
1121 return 0;
1122}
1123
1125{
1126 return m_numChannels;
1127}
1128
1130{
1131 return m_fps;
1132}
1133
1135{
1136 if( !m_synchroShmimName.empty() )
1137 {
1138 if( !m_synchroStreamOpen )
1139 {
1140 return -1;
1141 }
1142
1143 if( m_synchroSemaphore == nullptr && claimSynchroSemaphore() != 0 )
1144 {
1145 return -1;
1146 }
1147
1149
1151 m_firstSemaphore = true;
1153 m_firstReadLatency = true;
1154 m_avgReadLatency_ns = 0.0;
1155 m_lastAtime = timespec{};
1156 m_atime = timespec{};
1157 m_triggerTime = timespec{};
1159 m_lastProducerAtime = timespec{};
1163 m_firstProducerSample = true;
1172 m_delayModel_ns = 0.0;
1173 m_delayApplied_ns = 0.0;
1174 m_delayBudget_ns = 0.0;
1179 m_delayLock = 0.0;
1180 m_delayCapped = 0.0;
1181 }
1182
1185 m_lastTriggerTime = timespec{};
1186 m_firstTriggerTime = true;
1187 m_firstTimerTrigger = true;
1188 m_localFrameSeq = 0;
1189 m_delayApplied_ns = 0.0;
1190 m_delayBudget_ns = 0.0;
1193 m_delayLock = 0.0;
1194 m_delayCapped = 0.0;
1195
1196 m_time_start = std::chrono::high_resolution_clock::now();
1197
1198 return 0;
1199}
1200
1202{
1203 if( !m_synchroShmimName.empty() )
1204 {
1206 }
1207
1209}
1210
1212{
1213 memcpy( dest, m_values.data(), m_values.size() * sizeof( uint16_t ) );
1215 if( !m_synchroShmimName.empty() )
1216 {
1218 }
1219 return 0;
1220}
1221
1223{
1225 return 0;
1226}
1227
1229{
1230 char shmimFilename[1024];
1231 struct stat buffer;
1232
1234 {
1235 return 0;
1236 }
1237
1239 {
1240 return 1;
1241 }
1242
1243 if( m_synchroStream.md[0].sem < SEMAPHORE_MAXVAL )
1244 {
1246 memset( &m_synchroStream, 0, sizeof( m_synchroStream ) );
1247 return 1;
1248 }
1249
1251 if( stat( shmimFilename, &buffer ) != 0 )
1252 {
1254 memset( &m_synchroStream, 0, sizeof( m_synchroStream ) );
1255 return log<software_error, 1>( { __FILE__, __LINE__, errno, "stat" } );
1256 }
1257
1258 m_synchroStreamInode = buffer.st_ino;
1259 m_synchroStreamOpen = true;
1260 return 0;
1261}
1262
1264{
1265 if( !m_synchroStreamOpen )
1266 {
1267 return -1;
1268 }
1269
1270 if( m_synchroSemaphore != nullptr )
1271 {
1272 return 0;
1273 }
1274
1276 if( m_synchroSemaphoreNumber < 0 )
1277 {
1278 return log<software_error, -1>( { __FILE__, __LINE__, "No valid semaphore found for " + m_synchroShmimName } );
1279 }
1280
1282
1283 if( m_synchroSemaphore == nullptr )
1284 {
1285 return log<software_error, -1>(
1286 { __FILE__, __LINE__, "No valid semaphore pointer found for " + m_synchroShmimName } );
1287 }
1288
1289 return 0;
1290}
1291
1293{
1294 int shmimFd;
1295 char shmimFilename[1024];
1296 struct stat buffer;
1297
1298 if( !m_synchroStreamOpen || m_synchroStream.md[0].sem <= 0 )
1299 {
1300 return true;
1301 }
1302
1304
1305 shmimFd = open( shmimFilename, O_RDWR );
1306 if( shmimFd == -1 )
1307 {
1308 return true;
1309 }
1310
1311 close( shmimFd );
1312
1313 if( stat( shmimFilename, &buffer ) != 0 )
1314 {
1315 return true;
1316 }
1317
1318 return buffer.st_ino != m_synchroStreamInode;
1319}
1320
1322{
1324 {
1325 if( m_synchroSemaphore != nullptr && m_synchroSemaphoreNumber >= 0 )
1326 {
1328 }
1329
1331 }
1332
1333 memset( &m_synchroStream, 0, sizeof( m_synchroStream ) );
1334 m_synchroSemaphore = nullptr;
1337 m_synchroStreamOpen = false;
1338 m_atime = timespec{};
1339 m_lastAtime = timespec{};
1342 m_lastProducerAtime = timespec{};
1346 m_firstProducerSample = true;
1355 m_firstSemaphore = true;
1356 m_avgReadLatency_ns = 0.0;
1357 m_firstReadLatency = true;
1358 m_delayModel_ns = 0.0;
1359 m_delayApplied_ns = 0.0;
1360 m_delayBudget_ns = 0.0;
1365 m_delayLock = 0.0;
1366 m_delayCapped = 0.0;
1367 m_triggerTime = timespec{};
1370 m_localFrameSeq = 0;
1371 m_lastTriggerTime = timespec{};
1372 m_firstTriggerTime = true;
1373 m_firstTimerTrigger = true;
1374}
1375
1377{
1378 while( !m_shutdown && !m_reconfig )
1379 {
1380 // Get current time
1381 auto now = std::chrono::high_resolution_clock::now();
1382 auto elapsed = std::chrono::duration_cast<std::chrono::nanoseconds>( now - m_time_start );
1383
1384 // Read every 500 microseconds
1385 if( elapsed.count() >= m_trigger )
1386 {
1388 {
1390 m_firstTimerTrigger = false;
1391 }
1392 else
1393 {
1394 m_triggerInterval_ns = static_cast<double>( elapsed.count() );
1395 }
1396
1397 m_time_start = now; // Reset start time
1398
1399 const auto readStart = std::chrono::high_resolution_clock::now();
1400 for( int i = 0; i < m_numChannels; ++i )
1401 {
1402 if( readChannelValue( i, m_values[i] ) < 0 )
1403 {
1404 return 1;
1405 }
1406 }
1407 const auto readEnd = std::chrono::high_resolution_clock::now();
1408 m_channelReadoutTime_ns = static_cast<double>(
1409 std::chrono::duration_cast<std::chrono::nanoseconds>( readEnd - readStart ).count() );
1410
1411 m_trigger = m_trigger - m_gain * ( elapsed.count() - nano_sec_target );
1412
1413 return 0;
1414 }
1415 else
1416 {
1417 mx::sys::nanoSleep( 1000 ); // Sleep for 1 microsecond to prevent busy waiting
1418 }
1419 }
1420
1421 return 0;
1422}
1423
1425{
1426 timespec ts;
1427
1428 if( m_synchroSemaphore == nullptr )
1429 {
1430 m_reconfig = true;
1431 return 1;
1432 }
1433
1434 errno = 0;
1435 if( getRealtime( ts ) < 0 )
1436 {
1437 return log<software_critical, -1>( { __FILE__, __LINE__, errno, 0, "clock_gettime" } );
1438 }
1439
1440 ts.tv_sec += 1;
1441
1442 errno = 0;
1444 {
1445 if( errno == EINTR )
1446 {
1447 return 1;
1448 }
1449
1450 if( errno == ETIMEDOUT )
1451 {
1452 if( synchroStreamStale() )
1453 {
1454 log<text_log>( "Synchronized trigger stream changed, reconfiguring.", logPrio::LOG_NOTICE );
1455 m_reconfig = true;
1456 }
1457
1458 return 1;
1459 }
1460
1461 log<software_error>( { __FILE__, __LINE__, errno, "sem_timedwait" } );
1462 m_reconfig = true;
1463 return 1;
1464 }
1465
1467
1468 if( m_synchroStream.md != nullptr )
1469 {
1470 const uint64_t producerCnt0 = m_synchroStream.md[0].cnt0;
1471 const timespec producerAtime = m_synchroStream.md[0].atime;
1472 const bool producerAtimeValid = ( producerAtime.tv_sec != 0 || producerAtime.tv_nsec != 0 );
1473
1476 {
1479 }
1481 {
1484
1485 if( producerFrameDelta > 1 )
1486 {
1489 }
1490 }
1491 else
1492 {
1494 }
1496
1497 if( producerAtimeValid )
1498 {
1500 {
1505 m_firstProducerSample = false;
1506 }
1507 else if( producerCnt0 > m_lastProducerCnt0 )
1508 {
1511
1512 if( producerDt_ns > 0.0 )
1513 {
1514 const double producerPeriod_ns = producerDt_ns / static_cast<double>( producerFrameDelta );
1516
1517 const double alphaProducer = static_cast<double>( m_alpha );
1518 if( m_avgProducerPeriod_ns > 0.0 )
1519 {
1522 }
1523 else
1524 {
1526 }
1527 }
1528
1531 }
1532 else if( producerCnt0 < m_lastProducerCnt0 )
1533 {
1538 }
1539 }
1540 }
1541
1542 if( getRealtime( m_atime ) < 0 )
1543 {
1544 return log<software_critical, -1>( { __FILE__, __LINE__, errno, 0, "clock_gettime" } );
1545 }
1546
1548
1550
1551 double desiredDelay_ns = static_cast<double>( m_synchroDelay );
1552 if( desiredDelay_ns < 0.0 )
1553 {
1554 desiredDelay_ns = 0.0;
1555 }
1556
1557 if( m_wfsPeriodMeasured_ns > 0.0 )
1558 {
1560 if( m_delayBudget_ns < 0.0 )
1561 {
1562 m_delayBudget_ns = 0.0;
1563 }
1564 }
1565 else
1566 {
1567 m_delayBudget_ns = 0.0;
1568 }
1569
1571 {
1573 m_delayCapped = 1.0;
1574 }
1575 else
1576 {
1578 m_delayCapped = 0.0;
1579 }
1580
1582
1584 {
1585 return log<software_critical, -1>( { __FILE__, __LINE__, errno, 0, "clock_gettime" } );
1586 }
1587
1589 const double alpha = static_cast<double>( m_alpha );
1590
1591 if( !m_firstReadLatency )
1592 {
1594 }
1595 else
1596 {
1598 m_firstReadLatency = false;
1599 }
1600
1602
1603 const auto readStart = std::chrono::high_resolution_clock::now();
1604 for( int i = 0; i < m_numChannels; ++i )
1605 {
1606 if( readChannelValue( i, m_values[i] ) < 0 )
1607 {
1608 m_reconfig = true;
1609 return 1;
1610 }
1611 }
1612 const auto readEnd = std::chrono::high_resolution_clock::now();
1613 m_channelReadoutTime_ns = static_cast<double>(
1614 std::chrono::duration_cast<std::chrono::nanoseconds>( readEnd - readStart ).count() );
1615
1616 timespec cycleEnd;
1617 if( getRealtime( cycleEnd ) < 0 )
1618 {
1619 return log<software_critical, -1>( { __FILE__, __LINE__, errno, 0, "clock_gettime" } );
1620 }
1621
1623 if( nonDelayService_ns < 0.0 )
1624 {
1625 nonDelayService_ns = 0.0;
1626 }
1627
1629 const double alphaService = static_cast<double>( m_alpha );
1631 {
1634 }
1635 else
1636 {
1638 m_firstNonDelayService = false;
1639 }
1640
1641 return 0;
1642}
1643
1644int mcp3208Ctrl::getRealtime( timespec &ts )
1645{
1646 return clock_gettime( CLOCK_REALTIME, &ts );
1647}
1648
1649int mcp3208Ctrl::waitOnSemaphore( sem_t *sem, timespec &ts )
1650{
1651 return sem_timedwait( sem, &ts );
1652}
1653
1654int mcp3208Ctrl::readChannelValue( int channel, uint16_t &value )
1655{
1656 value = m_adc.read( channel );
1657 return 0;
1658}
1659
1661{
1662 if( m_delayApplied_ns > 0.0 )
1663 {
1664 mx::sys::nanoSleep( static_cast<unsigned>( m_delayApplied_ns ) );
1665 return;
1666 }
1667}
1668
1669void mcp3208Ctrl::updateSynchroDelayController( double desiredDelay_ns )
1670{
1671 constexpr double c_satEpsilon_ns = 1e-6;
1672
1673 const double controlStep_ns =
1674 static_cast<double>( m_gain ) * ( m_avgReadLatency_ns - static_cast<double>( m_synchroDelayTarget ) );
1676
1677 if( updatedDelay_ns < 0.0 )
1678 {
1679 updatedDelay_ns = 0.0;
1680 }
1681
1682 const bool upperSaturated =
1685
1687 {
1689 }
1690
1692 {
1694 }
1695
1696 m_synchroDelay = static_cast<float>( updatedDelay_ns );
1697}
1698
1703
1705{
1706 return recordFGTimings( true );
1707}
1708
1709// INDI callback handling for fps configuration.
1710INDI_NEWCALLBACK_DEFN( mcp3208Ctrl, m_indiP_fps )( const pcf::IndiProperty &ipRecv )
1711{
1712 if( ipRecv.getName() != m_indiP_fps.getName() )
1713 {
1714 log<software_error>( { __FILE__, __LINE__, "wrong INDI property received." } );
1715 return -1;
1716 }
1717
1718 float target;
1719
1720 if( indiTargetUpdate( m_indiP_fps, target, ipRecv, true ) < 0 )
1721 {
1722 log<software_error>( { __FILE__, __LINE__ } );
1723 return -1;
1724 }
1725
1726 m_fps = target;
1727 m_wfs_fps = m_fps;
1728 m_trigger = 1e9f / m_fps; // Update trigger value based off new fps
1729 nano_sec_target = 1e9f / m_fps;
1730
1731 log<text_log>( "set fps = " + std::to_string( m_fps ) );
1732 return 0;
1733}
1734
1735// INDI callback handling for global EMA alpha configuration.
1736INDI_NEWCALLBACK_DEFN( mcp3208Ctrl, m_indiP_alpha )( const pcf::IndiProperty &ipRecv )
1737{
1738 if( ipRecv.getName() != m_indiP_alpha.getName() )
1739 {
1740 log<software_error>( { __FILE__, __LINE__, "wrong INDI property received." } );
1741 return -1;
1742 }
1743
1744 float target;
1745 if( indiTargetUpdate( m_indiP_alpha, target, ipRecv, true ) < 0 )
1746 {
1747 log<software_error>( { __FILE__, __LINE__ } );
1748 return -1;
1749 }
1750
1751 if( target < 0.0f )
1752 {
1753 target = 0.0f;
1754 }
1755 else if( target > 1.0f )
1756 {
1757 target = 1.0f;
1758 }
1759
1760 m_alpha = target;
1761
1762 log<text_log>( "set alpha = " + std::to_string( m_alpha ) );
1763 return 0;
1764}
1765
1766// INDI callback handling for synchronized-mode signed phase offset configuration.
1767INDI_NEWCALLBACK_DEFN( mcp3208Ctrl, m_indiP_synchroDelay )( const pcf::IndiProperty &ipRecv )
1768{
1769 if( ipRecv.getName() != m_indiP_synchroDelay.getName() )
1770 {
1771 log<software_error>( { __FILE__, __LINE__, "wrong INDI property received." } );
1772 return -1;
1773 }
1774
1775 int target;
1776 if( indiTargetUpdate( m_indiP_synchroDelay, target, ipRecv, true ) < 0 )
1777 {
1778 log<software_error>( { __FILE__, __LINE__ } );
1779 return -1;
1780 }
1781
1782 m_synchroPostDelay = target;
1783 if( m_wfsPeriodMeasured_ns > 0.0 )
1784 {
1785 double t_delay_ns = std::fmod( m_delayModel_ns + 1e3 * static_cast<double>( m_synchroPostDelay ), m_wfsPeriodMeasured_ns );
1786 if( t_delay_ns < 0.0 )
1787 {
1788 t_delay_ns += m_wfsPeriodMeasured_ns;
1789 }
1790
1791 m_synchroDelayTarget = static_cast<float>( t_delay_ns );
1792 }
1793 else
1794 {
1795 m_synchroDelayTarget = 1e3f * static_cast<float>( m_synchroPostDelay );
1796 }
1797 m_synchroDelay = m_synchroDelayTarget;
1798 m_delayApplied_ns = static_cast<double>( m_synchroDelay );
1799
1800 log<text_log>( "set synchroDelay offset = " + std::to_string( m_synchroPostDelay ) + " us" );
1801 return 0;
1802}
1803
1804INDI_SETCALLBACK_DEFN( mcp3208Ctrl, m_indiP_fpsSource )( const pcf::IndiProperty &ipRecv )
1805{
1806 INDI_VALIDATE_CALLBACK_PROPS( m_indiP_fpsSource, ipRecv );
1807
1808 if( ipRecv.find( m_fpsElement ) != true ) // this isn't valid
1809 {
1810 log<software_error>( { __FILE__, __LINE__, "No current property in fps source." } );
1811 return 0;
1812 }
1813
1814 float target = ipRecv[m_fpsElement].get<float>();
1815
1816 m_fps = target;
1817 m_wfs_fps = m_fps;
1818 m_trigger = 1e9f / m_fps; // Update trigger value based off new fps
1819 nano_sec_target = 1e9f / m_fps;
1820
1821 log<text_log>( "set fps from " + m_fpsDevice + " = " + std::to_string( m_fps ) );
1822 return 0;
1823
1824} // INDI_SETCALLBACK_DEFN(mcp3208Ctrl, m_indiP_fpsSource)
1825
1826INDI_NEWCALLBACK_DEFN( mcp3208Ctrl, m_indiP_numChannels )( const pcf::IndiProperty &ipRecv )
1827{
1828 if( ipRecv.getName() != m_indiP_numChannels.getName() )
1829 {
1830 log<software_error>( { __FILE__, __LINE__, "wrong INDI property received." } );
1831 return -1;
1832 }
1833
1834 float ch_target;
1835
1836 if( indiTargetUpdate( m_indiP_numChannels, ch_target, ipRecv, true ) < 0 )
1837 {
1838 log<software_error>( { __FILE__, __LINE__ } );
1839 return -1;
1840 }
1841
1842 m_numChannels = ch_target;
1843
1844 log<text_log>( "set numChannels = " + std::to_string( m_numChannels ));
1845 return 0;
1846}
1847
1848INDI_SETCALLBACK_DEFN( mcp3208Ctrl, m_indiP_numChannelsSource )( const pcf::IndiProperty &ipRecv )
1849{
1850 INDI_VALIDATE_CALLBACK_PROPS( m_indiP_numChannelsSource, ipRecv );
1851
1852 if( ipRecv.find( m_numChannelsElement ) != true ) // this isn't valid
1853 {
1854 log<software_error>( { __FILE__, __LINE__, "No current property in numChannels source." } );
1855 return 0;
1856 }
1857
1858 float ch_target = ipRecv[m_numChannelsElement].get<float>();
1859
1860 m_numChannels = ch_target;
1861
1862 log<text_log>( "set numChannels from " + m_numChannelsDevice + " = " + std::to_string( m_numChannels ));
1863 return 0;
1864}
1865
1866} // namespace app
1867} // namespace MagAOX
1868
1869#endif // mcp3208Ctrl_hpp
unsigned short read(const std::uint8_t channel, const Mode m=Mode::SINGLE) const
Definition MCP3208.cpp:62
The base-class for XWCTk applications.
stateCodes::stateCodeT state()
Get the current state code.
int m_shutdown
Flag to signal it's time to shutdown. When not 0, the main loop exits.
static int log(const typename logT::messageT &msg, logPrioT level=logPrio::LOG_DEFAULT)
Make a log entry.
timespec m_currImageTimestamp
The timestamp of the current image.
uint32_t m_width
The width of the image, once deinterlaced etc.
std::string m_fgCpuset
The cpuset to assign the framegrabber thread to. Not used if empty, the default.
uint8_t m_dataType
The ImageStreamIO type code.
bool m_reconfig
Flag to set if a camera reconfiguration requires a framegrabber reset.
uint32_t m_height
The height of the image, once deinterlaced etc.
bool m_firstTriggerTime
Tracks first-trigger initialization for synchronized trigger-interval measurement.
bool synchroStreamStale()
Check whether the synchronization stream has disappeared or been recreated.
bool m_firstTimerTrigger
Tracks first-trigger initialization for timer-mode trigger-interval measurement.
virtual int appStartup()
Start the application and initialize its INDI and hardware state.
int startAcquisition()
Prepare the selected acquisition mode to begin producing samples.
dev::frameGrabber< mcp3208Ctrl > frameGrabberT
float m_gain
The simple integrator gain used for timer and synchro delay control.
double m_nonDelayService_ns
Measured wake-to-return service time minus applied delay for the latest cycle.
uint64_t m_lastProducerCnt0
Previous producer frame counter from the synchronization stream metadata.
pcf::IndiProperty m_indiP_numChannels
float m_fpsTol
The tolerance used when monitoring fps metadata changes.
friend class mcp3208Ctrl_test
float nano_sec_target
The timer-mode target interval in nanoseconds.
double m_synchroDtF_ns
Filter/transport-latency term in the synchronized delay model.
void closeSynchroStream()
Release the synchronization semaphore claim and close the synchronization stream.
bool m_firstProducerSample
Tracks first-sample initialization for producer-period estimation.
uint64_t m_syncFramesDropped
Count of missing producer frame IDs inferred from positive counter gaps.
int loadImageIntoStream(void *dest)
Copy the current MCP3208 values into the output image buffer.
double m_delayLockAbsThreshold_ns
Absolute phase-error threshold for declaring delay lock.
float m_trigger
The timer-mode read interval in nanoseconds.
INDI_SETCALLBACK_DECL(mcp3208Ctrl, m_indiP_fpsSource)
Handle updates from the configured external fps source.
virtual int appShutdown()
Shutdown the application and release synchronization resources.
double m_delayPhaseError_ns
Wrapped phase error between applied and modeled synchronized delay.
void updateTriggerTiming(const timespec &atime)
Update synchronized trigger timing from the current semaphore arrival.
INDI_NEWCALLBACK_DECL(mcp3208Ctrl, m_indiP_synchroDelay)
Handle updates to the synchronized-mode signed phase offset property.
float m_synchroDelay
The commanded pre-read delay in synchronized mode, in nanoseconds.
double m_avgProducerPeriod_ns
Exponential moving-average producer period estimate from metadata.
int acquireTimerAndCheckValid()
Acquire one frame using the internal timer loop.
std::string m_synchroShmimName
The synchronization ImageStreamIO stream name; empty selects timer mode.
int loadConfigImpl(mx::app::appConfigurator &_config)
Load configuration values into the application state.
ino_t m_synchroStreamInode
Cached inode used to detect synchronization stream recreation.
pcf::IndiProperty m_indiP_fpsSource
INDI property subscription used to follow an external fps source.
virtual int appLogic()
Execute one iteration of the application FSM.
int checkRecordTimes()
Check whether framegrabber timing telemetry should be recorded this cycle.
bool m_firstNonDelayService
Tracks first-sample initialization for non-delay service-time averaging.
timespec m_lastProducerAtime
Previous producer atime from the synchronization stream metadata.
timespec m_lastTriggerTime
Previous trigger timestamp used to compute the synchronized trigger interval.
bool m_firstSemaphore
Tracks first-arrival initialization for semaphore period estimation.
void updateTimingDiagnosticsIndi()
Publish acquisition timing diagnostics to the INDI read-only property.
pcf::IndiProperty m_indiP_timingDiag
INDI property exposing runtime timing diagnostics for acquisition health checks.
uint64_t m_syncProducerFrameDelta
Latest producer-frame ID delta between consecutive synchronized wakes.
void delayBeforeRead()
Apply the current controlled delay between semaphore wake and ADC read.
double m_wfs_fps
WFS frame rate estimate used for timing prediction; initialized from configured fps before callbacks.
float fps()
Report the current acquisition rate metadata to the framegrabber.
virtual void loadConfig()
Load the configured application state.
int acquireSynchroAndCheckValid()
Acquire one frame using the synchronization semaphore.
int readChannelValue(int channel, uint16_t &value)
Read one MCP3208 channel value.
virtual void setupConfig()
Register configuration entries for the application and helper devices.
~mcp3208Ctrl() noexcept
Destroy the application.
int waitOnSemaphore(sem_t *sem, timespec &ts)
Wait on the claimed synchronization semaphore until the supplied timeout.
int m_numChannels
The number of MCP3208 channels read into each output frame.
double m_delayBudget_ns
Maximum delay allowed this cycle after cadence budgeting.
uint64_t m_localFrameSeq
Monotonic local acquisition sequence incremented on each published frame.
uint64_t m_syncFramesReceived
Count of synchronized frames received from semaphore wakes.
int numChannels()
Report the configured channel count metadata to the framegrabber.
float m_numChannelsTol
The tolerance for detecting a change in numChannels.
static constexpr bool c_frameGrabber_flippable
std::chrono::time_point< std::chrono::high_resolution_clock > m_time_start
The timer-mode reference point for the next internal acquisition cycle.
int getRealtime(timespec &ts)
Read the current realtime clock value.
double m_delayApplied_ns
Delay applied to the most recent synchronized ADC read.
int m_synchroSemaphoreNumber
The claimed semaphore slot for synchronization waits.
int recordTelem(const telem_fgtimings *telem)
Record framegrabber timing telemetry.
MCP3208Lib::MCP3208 m_adc
The active MCP3208 device interface used for hardware access.
std::string m_fpsElement
Property element containing the fps value.
dev::telemeter< mcp3208Ctrl > telemeterT
uint64_t m_syncFrameIdGapCount
Number of producer-frame gap events where ID delta exceeded one.
double m_synchroWfsRead_ns
WFS read-latency term in the synchronized delay model.
double m_synchroWfsProcess_ns
WFS processing-latency term in the synchronized delay model.
uint64_t m_lastSyncProducerFrameId
Previous producer frame ID used to detect synchronized frame-ID gaps.
pcf::IndiProperty m_indiP_fps
INDI property exposing the local fps target.
pcf::IndiProperty m_indiP_alpha
INDI property exposing the global EMA alpha used by timing smoothers.
double m_delayLockFracThreshold
Fractional period phase-error threshold for declaring delay lock.
pcf::IndiProperty m_indiP_numChannelsSource
uint64_t m_syncFramesWritten
Count of synchronized frames published to the output stream.
int configureAcquisition()
Configure the output image geometry and acquisition mode.
float m_alpha
Global exponential moving-average coefficient applied to timing smoothers.
int acquireAndCheckValid()
Acquire one sample and indicate whether it should be published.
double m_wfsPeriodMeasured_ns
Measured semaphore period used as the synchronized delay-model WFS period.
INDI_SETCALLBACK_DECL(mcp3208Ctrl, m_indiP_numChannelsSource)
INDI_NEWCALLBACK_DECL(mcp3208Ctrl, m_indiP_alpha)
Handle updates to the global EMA alpha property.
IMAGE m_synchroStream
The opened synchronization stream used for semaphore-triggered reads.
int reconfig()
Reset synchronization resources before the next acquisition configuration.
pcf::IndiProperty m_indiP_synchroDelay
INDI property exposing the synchronized-mode signed phase offset in microseconds.
static double timespecToNs(const timespec &t)
Convert a timespec timestamp to nanoseconds.
std::string m_numChannelsProperty
Property name for getting numChannels to set circular buffer length.
double m_producerPeriodInst_ns
Instantaneous producer period estimate from metadata timestamps.
bool m_firstReadLatency
Tracks first-arrival initialization for semaphore-to-read latency estimation.
double m_avgReadLatency_ns
Exponential moving-average estimate of semaphore-to-read latency in nanoseconds.
double m_avgSemaphorePeriod_ns
Exponential moving-average estimate of semaphore period in nanoseconds.
double m_cadenceGuard_ns
Reserved per-frame margin protecting synchronized cadence from overruns.
float m_synchroDelayTarget
The synchronized-mode effective delay target in nanoseconds after applying signed offset and wrap.
int m_synchroPostDelay
Signed microsecond phase offset added to synchronized delay-model predictions.
timespec m_triggerTime
Computed trigger timestamp aligned to the estimated WFS integration midpoint.
std::string m_fpsProperty
Property name providing external fps metadata.
INDI_NEWCALLBACK_DECL(mcp3208Ctrl, m_indiP_fps)
Handle updates to the local fps target property.
std::string m_numChannelsDevice
Device name for getting numChannels to set circular buffer length.
double m_delayModel_ns
Current modulo-wrapped delay predicted by the synchronized phase model.
float m_fps
The target acquisition rate in frames per second.
timespec m_atime
The most recent semaphore-arrival timestamp on the local realtime clock.
std::string m_numChannelsElement
Element name for getting numChannels to set circular buffer length.
int openSynchroStream()
Open the synchronization stream when semaphore-driven acquisition is enabled.
mcp3208Ctrl()
Construct the application with the current repository version metadata.
double m_avgNonDelayService_ns
Exponential moving-average of non-delay synchronized service time.
std::vector< uint16_t > m_values
The most recently read MCP3208 channel values published to the output stream.
timespec m_lastAtime
The previous semaphore-arrival timestamp used for period estimation.
sem_t * m_synchroSemaphore
Cached pointer to the claimed synchronization semaphore.
INDI_NEWCALLBACK_DECL(mcp3208Ctrl, m_indiP_numChannels)
double m_delayLock
Delay-lock state exported to diagnostics as 1.0 (locked) or 0.0 (unlocked).
uint64_t m_syncProducerFrameId
Latest producer frame ID (cnt0) observed on the synchronization stream.
double m_delayCapped
Delay-cap state exported as 1.0 when cadence budgeting limits the applied delay.
static timespec nsToTimespec(double ns)
Convert nanoseconds to a normalized timespec value.
int claimSynchroSemaphore()
Claim a semaphore slot from the synchronization stream.
void updateSynchroDelayController(double desiredDelay_ns)
Update the synchronized delay command with anti-windup and physical bounds.
MCP3208Lib::MCP3208 adc
Secondary MCP3208 handle retained with the legacy class state.
double m_channelReadoutTime_ns
Measured duration of reading all configured channels in nanoseconds.
std::string m_fpsDevice
Device name providing external fps metadata for framegrabber sizing.
double m_triggerInterval_ns
Measured interval between consecutive trigger events in nanoseconds.
bool m_synchroStreamOpen
Tracks whether the synchronization stream is currently open.
bool m_syncProducerFrameValid
Tracks whether producer frame-ID gap tracking has a valid prior sample.
double m_synchroDtTransfer_ns
Transfer-latency term in the synchronized delay model.
#define FRAMEGRABBER_SETUP_CONFIG(cfig)
Call frameGrabberT::setupConfig with error checking for frameGrabber.
#define FRAMEGRABBER_APP_LOGIC
Call frameGrabberT::appLogic with error checking for frameGrabber.
#define FRAMEGRABBER_APP_SHUTDOWN
Call frameGrabberT::appShutdown with error checking for frameGrabber.
#define FRAMEGRABBER_UPDATE_INDI
Call frameGrabberT::updateINDI with error checking for frameGrabber.
#define FRAMEGRABBER_LOAD_CONFIG(cfig)
Call frameGrabberT::loadConfig with error checking for frameGrabber.
#define FRAMEGRABBER_APP_STARTUP
Call frameGrabberT::appStartup with error checking for frameGrabber.
#define INDI_NEWCALLBACK_DEFN(class, prop)
Define the callback for a new property request.
#define CREATE_REG_INDI_NEW_NUMBERF(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...
#define INDI_SETCALLBACK_DEFN(class, prop)
Define the callback for a set property request.
#define REG_INDI_SETPROP(prop, devName, propName)
Register a SET INDI property with the class, using the standard callback name.
#define CREATE_REG_INDI_RO_NUMBER(prop, name, label, group)
Create and register a RO INDI property as a number, using the standard callback name.
@ OPERATING
The device is operating, other than homing.
#define INDI_VALIDATE_CALLBACK_PROPS(prop1, prop2)
Standard check for matching INDI properties in a callback.
const pcf::IndiProperty & ipRecv
Definition dm.hpp:19
static constexpr logPrioT LOG_NOTICE
A normal but significant condition.
static constexpr logPrioT LOG_INFO
Informational. The info log level is the lowest level recorded during normal operations.
A device base class which saves telemetry.
Definition telemeter.hpp:75
int telem(const typename telT::messageT &msg)
Make a telemetry recording.
Software CRITICAL log entry.
Software ERR log entry.
Log entry recording framegrabber timings.
#define TELEMETER_APP_LOGIC
Call telemeter::appLogic with error checking.
#define TELEMETER_LOAD_CONFIG(cfig)
Call telemeter::loadConfig with error checking.
#define TELEMETER_APP_STARTUP
Call telemeter::appStartup with error checking.
#define TELEMETER_SETUP_CONFIG(cfig)
Call telemeter::setupConfig with error checking.
#define TELEMETER_APP_SHUTDOWN
Call telemeter::appShutdown with error checking.