API
 
Loading...
Searching...
No Matches
mcp3208Ctrl_test.cpp
Go to the documentation of this file.
1/** \file mcp3208Ctrl_test.cpp
2 * \brief Catch2 tests for the mcp3208Ctrl app.
3 * \author Jared R. Males (jaredmales@gmail.com)
4 *
5 * \ingroup mcp3208Ctrl_files
6 */
7
8#include "../../../tests/testXWC.hpp"
9
10#include <vector>
11
12#define protected public
13#include "../mcp3208Ctrl.hpp"
14#undef protected
15
16namespace
17{
18
19/// Stub state for MCP3208 hardware access during unit tests.
20struct mcp3208StubState
21{
22 std::vector<unsigned short> m_channelValues; ///< Values returned for each channel read.
23 std::vector<int> m_readOrder; ///< Order in which channels were sampled.
24 int m_connectCalls{ 0 }; ///< Number of stubbed connect calls.
25};
26
27/// Access the shared MCP3208 stub state.
28mcp3208StubState &stubState()
29{
30 static mcp3208StubState state;
31 return state;
32}
33
34/// Reset the shared MCP3208 stub state.
35void resetStubState()
36{
37 stubState().m_channelValues.clear();
38 stubState().m_readOrder.clear();
39 stubState().m_connectCalls = 0;
40}
41
42/// Wrap a delay into `[0, period)` using the same modulo behavior as the app.
43double wrapDelay( const double rawDelay_ns /**< [in] unwrapped delay in nanoseconds */,
44 const double period_ns /**< [in] period in nanoseconds */ )
45{
46 if( period_ns <= 0.0 )
47 {
48 return 0.0;
49 }
50
51 const long long wrapCycles = static_cast<long long>( rawDelay_ns / period_ns );
52 double wrapped = rawDelay_ns - static_cast<double>( wrapCycles ) * period_ns;
53
54 if( wrapped < 0.0 )
55 {
56 wrapped += period_ns;
57 }
58 else if( wrapped >= period_ns )
59 {
60 wrapped -= period_ns;
61 }
62
63 return wrapped;
64}
65
66} // namespace
67
68namespace MCP3208Lib
69{
70
71MCP3208::MCP3208( const int dev, const int channel, const int baud, const int flags ) noexcept
72 : _handle( -1 ), _dev( dev ), _channel( channel ), _baud( baud ), _flags( flags )
73{
74}
75
77{
78}
79
81{
82 ++stubState().m_connectCalls;
83}
84
86{
87}
88
89unsigned short MCP3208::read( const std::uint8_t channel, const Mode ) const
90{
91 stubState().m_readOrder.push_back( static_cast<int>( channel ) );
92
93 if( static_cast<size_t>( channel ) < stubState().m_channelValues.size() )
94 {
95 return stubState().m_channelValues[static_cast<size_t>( channel )];
96 }
97
98 return 0;
99}
100
101} // namespace MCP3208Lib
102
103using namespace MagAOX::app;
104
105namespace libXWCTest
106{
107
108/** \defgroup mcp3208Ctrl_unit_test mcp3208Ctrl Unit Tests
109 * \brief Unit tests for the mcp3208Ctrl application.
110 *
111 * \ingroup application_unit_test
112 */
113
114/// Namespace for `mcp3208Ctrl` unit tests.
115/** \ingroup mcp3208Ctrl_unit_test
116 */
117namespace mcp3208CtrlTest
118{
119
120namespace
121{
122
123/// Test harness exposing small setup helpers for `mcp3208Ctrl`.
124class mcp3208Ctrl_test : public mcp3208Ctrl
125{
126 public:
127 /// Construct the test harness with a stable config name.
128 mcp3208Ctrl_test()
129 {
130 m_configName = "mcp3208Ctrl_test";
131 }
132
133 /// Initialize the local fps INDI property used by the callback tests.
134 void setupFpsProperty()
135 {
136 m_indiP_fps = pcf::IndiProperty( pcf::IndiProperty::Number );
137 m_indiP_fps.setName( "fps" );
138 m_indiP_fps.add( pcf::IndiElement( "current" ) );
139 m_indiP_fps["current"].setValue( m_fps );
140 m_indiP_fps.add( pcf::IndiElement( "target" ) );
141 m_indiP_fps["target"].setValue( m_fps );
142 }
143
144 /// Initialize the external fps INDI property used by the callback tests.
145 void setupFpsSourceProperty()
146 {
147 m_fpsDevice = "fpsdev";
148 m_fpsProperty = "fps";
149 m_fpsElement = "current";
150
151 m_indiP_fpsSource = pcf::IndiProperty( pcf::IndiProperty::Number );
152 m_indiP_fpsSource.setDevice( m_fpsDevice );
153 m_indiP_fpsSource.setName( m_fpsProperty );
154 }
155
156 /// Build an INDI property update for the local fps callback.
157 pcf::IndiProperty makeFpsUpdate( const double target /**< [in] the requested fps */ )
158 {
159 pcf::IndiProperty ip( pcf::IndiProperty::Number );
160 ip.setName( "fps" );
161 ip.add( pcf::IndiElement( "current" ) );
162 ip["current"].setValue( target );
163 ip.add( pcf::IndiElement( "target" ) );
164 ip["target"].setValue( target );
165 return ip;
166 }
167
168 /// Build an INDI property update for the external fps-source callback.
169 pcf::IndiProperty makeFpsSourceUpdate( const double current /**< [in] the reported fps */ )
170 {
171 pcf::IndiProperty ip( pcf::IndiProperty::Number );
172 ip.setDevice( m_fpsDevice );
173 ip.setName( m_fpsProperty );
174 ip.add( pcf::IndiElement( m_fpsElement ) );
175 ip[m_fpsElement].setValue( current );
176 return ip;
177 }
178
179 /// Initialize the alpha INDI property used by callback tests.
180 void setupAlphaProperty()
181 {
182 m_indiP_alpha = pcf::IndiProperty( pcf::IndiProperty::Number );
183 m_indiP_alpha.setName( "alpha" );
184 m_indiP_alpha.add( pcf::IndiElement( "current" ) );
185 m_indiP_alpha["current"].setValue( m_alpha );
186 m_indiP_alpha.add( pcf::IndiElement( "target" ) );
187 m_indiP_alpha["target"].setValue( m_alpha );
188 }
189
190 /// Build an INDI property update for the alpha callback.
191 pcf::IndiProperty makeAlphaUpdate( const double target /**< [in] requested global EMA alpha */ )
192 {
193 pcf::IndiProperty ip( pcf::IndiProperty::Number );
194 ip.setName( "alpha" );
195 ip.add( pcf::IndiElement( "current" ) );
196 ip["current"].setValue( target );
197 ip.add( pcf::IndiElement( "target" ) );
198 ip["target"].setValue( target );
199 return ip;
200 }
201
202 /// Initialize the synchronized-delay INDI property used by callback tests.
203 void setupSynchroDelayProperty()
204 {
205 m_indiP_synchroDelay = pcf::IndiProperty( pcf::IndiProperty::Number );
206 m_indiP_synchroDelay.setName( "synchroDelay" );
207 m_indiP_synchroDelay.add( pcf::IndiElement( "current" ) );
208 m_indiP_synchroDelay["current"].setValue( m_synchroPostDelay );
209 m_indiP_synchroDelay.add( pcf::IndiElement( "target" ) );
210 m_indiP_synchroDelay["target"].setValue( m_synchroPostDelay );
211 }
212
213 /// Build an INDI property update for the synchronized-delay callback.
214 pcf::IndiProperty makeSynchroDelayUpdate( const double target_us /**< [in] requested synchronized signed delay offset in microseconds */ )
215 {
216 pcf::IndiProperty ip( pcf::IndiProperty::Number );
217 ip.setName( "synchroDelay" );
218 ip.add( pcf::IndiElement( "current" ) );
219 ip["current"].setValue( target_us );
220 ip.add( pcf::IndiElement( "target" ) );
221 ip["target"].setValue( target_us );
222 return ip;
223 }
224
225 /// Initialize the timing-diagnostics INDI property used by diagnostics tests.
226 void setupTimingDiagnosticsProperty()
227 {
228 m_indiP_timingDiag = pcf::IndiProperty( pcf::IndiProperty::Number );
229 m_indiP_timingDiag.setName( "timingDiag" );
230 m_indiP_timingDiag.add( pcf::IndiElement( "avg_read_latency_us" ) );
231 m_indiP_timingDiag.add( pcf::IndiElement( "synchro_delay_us" ) );
232 m_indiP_timingDiag.add( pcf::IndiElement( "synchro_delay_target_us" ) );
233 m_indiP_timingDiag.add( pcf::IndiElement( "delay_applied_us" ) );
234 m_indiP_timingDiag.add( pcf::IndiElement( "delay_model_us" ) );
235 m_indiP_timingDiag.add( pcf::IndiElement( "delay_phase_error_us" ) );
236 m_indiP_timingDiag.add( pcf::IndiElement( "delay_lock" ) );
237 m_indiP_timingDiag.add( pcf::IndiElement( "delay_budget_us" ) );
238 m_indiP_timingDiag.add( pcf::IndiElement( "avg_non_delay_service_us" ) );
239 m_indiP_timingDiag.add( pcf::IndiElement( "delay_capped" ) );
240 m_indiP_timingDiag.add( pcf::IndiElement( "read_latency_error_us" ) );
241 m_indiP_timingDiag.add( pcf::IndiElement( "avg_semaphore_period_us" ) );
242 m_indiP_timingDiag.add( pcf::IndiElement( "channel_readout_us" ) );
243 m_indiP_timingDiag.add( pcf::IndiElement( "trigger_interval_us" ) );
244 m_indiP_timingDiag.add( pcf::IndiElement( "trigger_time_us" ) );
245 m_indiP_timingDiag.add( pcf::IndiElement( "local_frame_seq" ) );
246 m_indiP_timingDiag.add( pcf::IndiElement( "sync_frames_received" ) );
247 m_indiP_timingDiag.add( pcf::IndiElement( "sync_frames_written" ) );
248 m_indiP_timingDiag.add( pcf::IndiElement( "sync_frames_dropped" ) );
249 m_indiP_timingDiag.add( pcf::IndiElement( "sync_frame_id_gap_count" ) );
250 m_indiP_timingDiag.add( pcf::IndiElement( "sync_producer_frame_id" ) );
251 m_indiP_timingDiag.add( pcf::IndiElement( "sync_producer_frame_delta" ) );
252 m_indiP_timingDiag.add( pcf::IndiElement( "mode_code" ) );
253 }
254};
255
256} // namespace
257
258/// Preserve Doxygen links for the real `mcp3208Ctrl` APIs exercised by the tests.
259/**
260 * \ingroup mcp3208Ctrl_unit_test
261 */
262TEST_CASE( "mcp3208Ctrl Doxygen references are preserved", "[mcp3208Ctrl]" )
263{
264 mcp3208Ctrl_test app;
265
266 app.setupFpsProperty();
267 app.setupFpsSourceProperty();
268 app.setupAlphaProperty();
269 app.setupSynchroDelayProperty();
270
271 XWCTEST_DOXYGEN_REF( app.loadConfigImpl( app.config ) );
272 XWCTEST_DOXYGEN_REF( app.configureAcquisition() );
273 XWCTEST_DOXYGEN_REF( app.fps() );
274 XWCTEST_DOXYGEN_REF( app.startAcquisition() );
275 XWCTEST_DOXYGEN_REF( app.acquireAndCheckValid() );
276 XWCTEST_DOXYGEN_REF( app.loadImageIntoStream( nullptr ) );
277 XWCTEST_DOXYGEN_REF( app.reconfig() );
278 XWCTEST_DOXYGEN_REF( app.synchroStreamStale() );
279 XWCTEST_DOXYGEN_REF( app.checkRecordTimes() );
280 XWCTEST_DOXYGEN_REF( app.recordTelem( nullptr ) );
281 XWCTEST_DOXYGEN_REF( app.newCallBack_m_indiP_fps( app.makeFpsUpdate( 1000.0 ) ) );
282 XWCTEST_DOXYGEN_REF( app.setCallBack_m_indiP_fpsSource( app.makeFpsSourceUpdate( 1000.0 ) ) );
283 XWCTEST_DOXYGEN_REF( app.newCallBack_m_indiP_alpha( app.makeAlphaUpdate( 0.01 ) ) );
284 XWCTEST_DOXYGEN_REF( app.newCallBack_m_indiP_synchroDelay( app.makeSynchroDelayUpdate( 1.0 ) ) );
285 XWCTEST_DOXYGEN_REF( app.updateTriggerTiming( timespec{} ) );
286 XWCTEST_DOXYGEN_REF( app.updateTimingDiagnosticsIndi() );
287 XWCTEST_DOXYGEN_REF( app.delayBeforeRead() );
288 XWCTEST_DOXYGEN_REF( app.updateSynchroDelayController( 0.0 ) );
289 XWCTEST_DOXYGEN_REF( app.timespecToNs( timespec{} ) );
290 XWCTEST_DOXYGEN_REF( app.nsToTimespec( 0.0 ) );
291
292 SUCCEED();
293}
294
295/// Verify synchronized-acquisition defaults load from configuration.
296/**
297 * \ingroup mcp3208Ctrl_unit_test
298 */
299TEST_CASE( "mcp3208Ctrl configuration defaults load synchronized settings", "[mcp3208Ctrl]" )
300{
301 mcp3208Ctrl_test app;
302
303 app.setupConfig();
304
305 mx::app::writeConfigFile( "/tmp/mcp3208Ctrl_test.conf", { "none" }, { "nada" }, { "0" } );
306 app.config.readConfig( "/tmp/mcp3208Ctrl_test.conf" );
307
308 REQUIRE( app.loadConfigImpl( app.config ) == 0 );
309 REQUIRE( app.m_synchroShmimName.empty() );
310 REQUIRE( app.m_synchroPostDelay == 0 );
311 REQUIRE( app.m_synchroDtTransfer_ns == Approx( 3000.0 ) );
312 REQUIRE( app.m_synchroWfsProcess_ns == Approx( 51500.0 ) );
313 REQUIRE( app.m_synchroDtF_ns == Approx( 10000.0 ) );
314 REQUIRE( app.m_synchroWfsRead_ns == Approx( 276100.0 ) );
315 REQUIRE( app.m_delayLockAbsThreshold_ns == Approx( 50000.0 ) );
316 REQUIRE( app.m_delayLockFracThreshold == Approx( 0.1 ) );
317 REQUIRE( app.m_cadenceGuard_ns == Approx( 20000.0 ) );
318 REQUIRE( app.m_alpha == Approx( 0.01f ) );
319 REQUIRE( app.m_synchroDelayTarget == Approx( 0.0f ) );
320 REQUIRE( app.m_synchroDelay == Approx( 0.0f ) );
321 REQUIRE( app.m_wfs_fps == Approx( static_cast<double>( app.m_fps ) ) );
322}
323
324/// Verify synchronized-acquisition overrides load from configuration.
325/**
326 * \ingroup mcp3208Ctrl_unit_test
327 */
328TEST_CASE( "mcp3208Ctrl configuration overrides load synchronized settings", "[mcp3208Ctrl]" )
329{
330 mcp3208Ctrl_test app;
331
332 app.setupConfig();
333
334 mx::app::writeConfigFile( "/tmp/mcp3208Ctrl_test_override.conf",
335 { "synchro",
336 "synchro",
337 "synchro",
338 "synchro",
339 "synchro",
340 "synchro",
341 "synchro",
342 "synchro",
343 "synchro",
344 "synchro",
345 "accel" },
346 { "shmimName",
347 "postDelay",
348 "dtTransfer_ns",
349 "wfsProcess_ns",
350 "dtF_ns",
351 "wfsRead_ns",
352 "delayLockAbsThreshold_ns",
353 "delayLockFracThreshold",
354 "cadenceGuard_ns",
355 "alpha",
356 "numChannels" },
357 { "camwfs_sync", "17", "4000", "62000", "11000", "290000", "75000", "0.2", "15000", "0.25", "3" } );
358 app.config.readConfig( "/tmp/mcp3208Ctrl_test_override.conf" );
359
360 REQUIRE( app.loadConfigImpl( app.config ) == 0 );
361 REQUIRE( app.m_synchroShmimName == "camwfs_sync" );
362 REQUIRE( app.m_synchroPostDelay == 17 );
363 REQUIRE( app.m_synchroDtTransfer_ns == Approx( 4000.0 ) );
364 REQUIRE( app.m_synchroWfsProcess_ns == Approx( 62000.0 ) );
365 REQUIRE( app.m_synchroDtF_ns == Approx( 11000.0 ) );
366 REQUIRE( app.m_synchroWfsRead_ns == Approx( 290000.0 ) );
367 REQUIRE( app.m_delayLockAbsThreshold_ns == Approx( 75000.0 ) );
368 REQUIRE( app.m_delayLockFracThreshold == Approx( 0.2 ) );
369 REQUIRE( app.m_cadenceGuard_ns == Approx( 15000.0 ) );
370 REQUIRE( app.m_alpha == Approx( 0.25f ) );
371 REQUIRE( app.m_numChannels == 3 );
372 REQUIRE( app.m_synchroDelayTarget == Approx( 17000.0f ) );
373 REQUIRE( app.m_synchroDelay == Approx( 17000.0f ) );
374 REQUIRE( app.m_wfs_fps == Approx( static_cast<double>( app.m_fps ) ) );
375}
376
377/// Verify the user fps callback still updates cadence metadata.
378/**
379 * \ingroup mcp3208Ctrl_unit_test
380 */
381TEST_CASE( "mcp3208Ctrl fps callback updates trigger metadata", "[mcp3208Ctrl]" )
382{
383 mcp3208Ctrl_test app;
384
385 app.setupFpsProperty();
386
387 REQUIRE( app.newCallBack_m_indiP_fps( app.makeFpsUpdate( 500.0 ) ) == 0 );
388 REQUIRE( app.m_fps == Approx( 500.0f ) );
389 REQUIRE( app.m_wfs_fps == Approx( 500.0 ) );
390 REQUIRE( app.m_trigger == Approx( 1e9f / 500.0f ) );
391 REQUIRE( app.nano_sec_target == Approx( 1e9f / 500.0f ) );
392}
393
394/// Verify the external fps source callback still updates cadence metadata.
395/**
396 * \ingroup mcp3208Ctrl_unit_test
397 */
398TEST_CASE( "mcp3208Ctrl fps source callback updates trigger metadata", "[mcp3208Ctrl]" )
399{
400 mcp3208Ctrl_test app;
401
402 app.setupFpsSourceProperty();
403
404 REQUIRE( app.setCallBack_m_indiP_fpsSource( app.makeFpsSourceUpdate( 250.0 ) ) == 0 );
405 REQUIRE( app.m_fps == Approx( 250.0f ) );
406 REQUIRE( app.m_wfs_fps == Approx( 250.0 ) );
407 REQUIRE( app.m_trigger == Approx( 1e9f / 250.0f ) );
408 REQUIRE( app.nano_sec_target == Approx( 1e9f / 250.0f ) );
409}
410
411/// Verify synchronized alpha configuration is clamped to the valid range.
412/**
413 * \ingroup mcp3208Ctrl_unit_test
414 */
415TEST_CASE( "mcp3208Ctrl configuration clamps synchronized alpha", "[mcp3208Ctrl]" )
416{
417 mcp3208Ctrl_test app;
418
419 app.setupConfig();
420
421 mx::app::writeConfigFile( "/tmp/mcp3208Ctrl_test_alpha_low.conf", { "synchro" }, { "alpha" }, { "-0.5" } );
422 app.config.readConfig( "/tmp/mcp3208Ctrl_test_alpha_low.conf" );
423 REQUIRE( app.loadConfigImpl( app.config ) == 0 );
424 REQUIRE( app.m_alpha == Approx( 0.0f ) );
425
426 mx::app::writeConfigFile( "/tmp/mcp3208Ctrl_test_alpha_high.conf", { "synchro" }, { "alpha" }, { "1.5" } );
427 app.config.readConfig( "/tmp/mcp3208Ctrl_test_alpha_high.conf" );
428 REQUIRE( app.loadConfigImpl( app.config ) == 0 );
429 REQUIRE( app.m_alpha == Approx( 1.0f ) );
430}
431
432/// Verify alpha callback updates and clamps the global EMA coefficient.
433/**
434 * \ingroup mcp3208Ctrl_unit_test
435 */
436TEST_CASE( "mcp3208Ctrl alpha callback updates and clamps", "[mcp3208Ctrl]" )
437{
438 mcp3208Ctrl_test app;
439
440 app.setupAlphaProperty();
441
442 REQUIRE( app.newCallBack_m_indiP_alpha( app.makeAlphaUpdate( 0.4 ) ) == 0 );
443 REQUIRE( app.m_alpha == Approx( 0.4f ) );
444
445 REQUIRE( app.newCallBack_m_indiP_alpha( app.makeAlphaUpdate( -1.0 ) ) == 0 );
446 REQUIRE( app.m_alpha == Approx( 0.0f ) );
447
448 REQUIRE( app.newCallBack_m_indiP_alpha( app.makeAlphaUpdate( 2.0 ) ) == 0 );
449 REQUIRE( app.m_alpha == Approx( 1.0f ) );
450}
451
452/// Verify synchronized-delay callback updates delay state with signed offsets.
453/**
454 * \ingroup mcp3208Ctrl_unit_test
455 */
456TEST_CASE( "mcp3208Ctrl synchroDelay callback updates signed offsets", "[mcp3208Ctrl]" )
457{
458 mcp3208Ctrl_test app;
459
460 app.setupSynchroDelayProperty();
461 app.m_wfsPeriodMeasured_ns = 1000000.0;
462 app.m_delayModel_ns = 120000.0;
463
464 REQUIRE( app.newCallBack_m_indiP_synchroDelay( app.makeSynchroDelayUpdate( 25.0 ) ) == 0 );
465 REQUIRE( app.m_synchroPostDelay == 25 );
466 REQUIRE( app.m_synchroDelayTarget == Approx( 145000.0f ) );
467 REQUIRE( app.m_synchroDelay == Approx( 145000.0f ) );
468 REQUIRE( app.m_delayApplied_ns == Approx( 145000.0 ) );
469 REQUIRE( app.m_delayModel_ns == Approx( 120000.0 ) );
470
471 REQUIRE( app.newCallBack_m_indiP_synchroDelay( app.makeSynchroDelayUpdate( -5.0 ) ) == 0 );
472 REQUIRE( app.m_synchroPostDelay == -5 );
473 REQUIRE( app.m_synchroDelayTarget == Approx( 115000.0f ) );
474 REQUIRE( app.m_synchroDelay == Approx( 115000.0f ) );
475 REQUIRE( app.m_delayApplied_ns == Approx( 115000.0 ) );
476 REQUIRE( app.m_delayModel_ns == Approx( 120000.0 ) );
477}
478
479/// Verify synchronized-mode timing diagnostics publish loop state and derived error.
480/**
481 * \ingroup mcp3208Ctrl_unit_test
482 */
483TEST_CASE( "mcp3208Ctrl timing diagnostics publish synchronized loop metrics", "[mcp3208Ctrl]" )
484{
485 mcp3208Ctrl_test app;
486
487 app.setupTimingDiagnosticsProperty();
488 app.m_synchroShmimName = "camwfs_sync";
489 app.m_avgReadLatency_ns = 125000.0;
490 app.m_synchroDelay = 31000.0f;
491 app.m_synchroDelayTarget = 17000.0f;
492 app.m_delayApplied_ns = 24000.0;
493 app.m_delayModel_ns = 17000.0;
494 app.m_avgSemaphorePeriod_ns = 500000.0;
495 app.m_wfsPeriodMeasured_ns = 500000.0;
496 app.m_producerPeriodInst_ns = 510000.0;
497 app.m_avgProducerPeriod_ns = 500000.0;
498 app.m_channelReadoutTime_ns = 34000.0;
499 app.m_triggerInterval_ns = 600000.0;
500 app.m_atime = timespec{ 12, 3000000L };
501 app.m_triggerTime = timespec{ 12, 3456789L };
502 app.m_localFrameSeq = 44;
503 app.m_syncFramesReceived = 41;
504 app.m_syncFramesWritten = 40;
505 app.m_syncFramesDropped = 2;
506 app.m_syncFrameIdGapCount = 2;
507 app.m_syncProducerFrameId = 123456;
508 app.m_syncProducerFrameDelta = 3;
509 app.m_syncProducerFrameValid = true;
510
511 app.updateTimingDiagnosticsIndi();
512
513 REQUIRE( app.m_indiP_timingDiag["avg_read_latency_us"].get<double>() == Approx( 125.0 ) );
514 REQUIRE( app.m_indiP_timingDiag["synchro_delay_us"].get<double>() == Approx( 31.0 ) );
515 REQUIRE( app.m_indiP_timingDiag["synchro_delay_target_us"].get<double>() == Approx( 17.0 ) );
516 REQUIRE( app.m_indiP_timingDiag["delay_applied_us"].get<double>() == Approx( 24.0 ) );
517 REQUIRE( app.m_indiP_timingDiag["delay_model_us"].get<double>() == Approx( 17.0 ) );
518 REQUIRE( app.m_indiP_timingDiag["delay_phase_error_us"].get<double>() == Approx( 7.0 ) );
519 REQUIRE( app.m_indiP_timingDiag["delay_lock"].get<double>() == Approx( 1.0 ) );
520 REQUIRE( app.m_indiP_timingDiag["read_latency_error_us"].get<double>() == Approx( 108.0 ) );
521 REQUIRE( app.m_indiP_timingDiag["avg_semaphore_period_us"].get<double>() == Approx( 500.0 ) );
522 REQUIRE( app.m_indiP_timingDiag["channel_readout_us"].get<double>() == Approx( 34.0 ) );
523 REQUIRE( app.m_indiP_timingDiag["trigger_interval_us"].get<double>() == Approx( 600.0 ) );
524 REQUIRE( app.m_indiP_timingDiag["trigger_time_us"].get<double>() ==
525 Approx( 1e-3 * ( mcp3208Ctrl::timespecToNs( timespec{ 12, 3456789L } ) -
526 mcp3208Ctrl::timespecToNs( timespec{ 12, 3000000L } ) ) ) );
527 REQUIRE( app.m_indiP_timingDiag["local_frame_seq"].get<double>() == Approx( 44.0 ) );
528 REQUIRE( app.m_indiP_timingDiag["sync_frames_received"].get<double>() == Approx( 41.0 ) );
529 REQUIRE( app.m_indiP_timingDiag["sync_frames_written"].get<double>() == Approx( 40.0 ) );
530 REQUIRE( app.m_indiP_timingDiag["sync_frames_dropped"].get<double>() == Approx( 2.0 ) );
531 REQUIRE( app.m_indiP_timingDiag["sync_frame_id_gap_count"].get<double>() == Approx( 2.0 ) );
532 REQUIRE( app.m_indiP_timingDiag["sync_producer_frame_id"].get<double>() == Approx( 123456.0 ) );
533 REQUIRE( app.m_indiP_timingDiag["sync_producer_frame_delta"].get<double>() == Approx( 3.0 ) );
534 REQUIRE( app.m_indiP_timingDiag["mode_code"].get<double>() == Approx( 1.0 ) );
535}
536
537/// Verify timing diagnostics report timer mode and update mode code across transitions.
538/**
539 * \ingroup mcp3208Ctrl_unit_test
540 */
541TEST_CASE( "mcp3208Ctrl timing diagnostics track mode transitions", "[mcp3208Ctrl]" )
542{
543 mcp3208Ctrl_test app;
544
545 app.setupTimingDiagnosticsProperty();
546 app.m_synchroShmimName = "camwfs_sync";
547 app.m_atime = timespec{ 1, 2 };
548 app.m_triggerInterval_ns = 123456.0;
549 app.updateTimingDiagnosticsIndi();
550
551 REQUIRE( app.m_indiP_timingDiag["mode_code"].get<double>() == Approx( 1.0 ) );
552 REQUIRE( app.m_indiP_timingDiag["trigger_interval_us"].get<double>() == Approx( 123.456 ) );
553 REQUIRE( app.m_indiP_timingDiag["trigger_time_us"].get<double>() == Approx( 0.0 ) );
554 REQUIRE( app.m_indiP_timingDiag["delay_phase_error_us"].get<double>() == Approx( 0.0 ) );
555 REQUIRE( app.m_indiP_timingDiag["delay_lock"].get<double>() == Approx( 0.0 ) );
556 REQUIRE( app.m_indiP_timingDiag["sync_frames_received"].get<double>() == Approx( 0.0 ) );
557 REQUIRE( app.m_indiP_timingDiag["sync_frames_written"].get<double>() == Approx( 0.0 ) );
558 REQUIRE( app.m_indiP_timingDiag["sync_frames_dropped"].get<double>() == Approx( 0.0 ) );
559 REQUIRE( app.m_indiP_timingDiag["sync_frame_id_gap_count"].get<double>() == Approx( 0.0 ) );
560 REQUIRE( app.m_indiP_timingDiag["sync_producer_frame_id"].get<double>() == Approx( 0.0 ) );
561 REQUIRE( app.m_indiP_timingDiag["sync_producer_frame_delta"].get<double>() == Approx( 0.0 ) );
562
563 app.m_synchroShmimName.clear();
564 app.m_triggerInterval_ns = 456789.0;
565 app.updateTimingDiagnosticsIndi();
566
567 REQUIRE( app.m_indiP_timingDiag["mode_code"].get<double>() == Approx( 0.0 ) );
568 REQUIRE( app.m_indiP_timingDiag["trigger_interval_us"].get<double>() == Approx( 456.789 ) );
569 REQUIRE( app.m_indiP_timingDiag["trigger_time_us"].get<double>() == Approx( 0.0 ) );
570 REQUIRE( app.m_indiP_timingDiag["delay_phase_error_us"].get<double>() == Approx( 0.0 ) );
571 REQUIRE( app.m_indiP_timingDiag["delay_lock"].get<double>() == Approx( 0.0 ) );
572 REQUIRE( app.m_indiP_timingDiag["sync_frames_received"].get<double>() == Approx( 0.0 ) );
573 REQUIRE( app.m_indiP_timingDiag["sync_frames_written"].get<double>() == Approx( 0.0 ) );
574 REQUIRE( app.m_indiP_timingDiag["sync_frames_dropped"].get<double>() == Approx( 0.0 ) );
575 REQUIRE( app.m_indiP_timingDiag["sync_frame_id_gap_count"].get<double>() == Approx( 0.0 ) );
576 REQUIRE( app.m_indiP_timingDiag["sync_producer_frame_id"].get<double>() == Approx( 0.0 ) );
577 REQUIRE( app.m_indiP_timingDiag["sync_producer_frame_delta"].get<double>() == Approx( 0.0 ) );
578}
579
580/// Verify timing diagnostics wrap phase error and require both lock thresholds.
581/**
582 * \ingroup mcp3208Ctrl_unit_test
583 */
584TEST_CASE( "mcp3208Ctrl timing diagnostics compute wrapped phase error and lock thresholds", "[mcp3208Ctrl]" )
585{
586 mcp3208Ctrl_test app;
587
588 app.setupTimingDiagnosticsProperty();
589 app.m_synchroShmimName = "camwfs_sync";
590 app.m_delayApplied_ns = 50.0;
591 app.m_delayModel_ns = 900.0;
592 app.m_wfsPeriodMeasured_ns = 1000.0;
593 app.m_delayLockAbsThreshold_ns = 200.0;
594 app.m_delayLockFracThreshold = 0.1;
595
596 app.updateTimingDiagnosticsIndi();
597
598 REQUIRE( app.m_indiP_timingDiag["delay_phase_error_us"].get<double>() == Approx( 0.15 ) );
599 REQUIRE( app.m_indiP_timingDiag["delay_lock"].get<double>() == Approx( 0.0 ) );
600
601 app.m_delayLockFracThreshold = 0.2;
602 app.updateTimingDiagnosticsIndi();
603
604 REQUIRE( app.m_indiP_timingDiag["delay_phase_error_us"].get<double>() == Approx( 0.15 ) );
605 REQUIRE( app.m_indiP_timingDiag["delay_lock"].get<double>() == Approx( 1.0 ) );
606}
607
608/// Verify nanosecond and timespec conversions preserve normalized values.
609/**
610 * \ingroup mcp3208Ctrl_unit_test
611 */
612TEST_CASE( "mcp3208Ctrl timing helpers convert between nanoseconds and timespec", "[mcp3208Ctrl]" )
613{
614 const double ns = -250000000.0;
615 const timespec ts = mcp3208Ctrl::nsToTimespec( ns );
616
617 REQUIRE( ts.tv_sec == -1 );
618 REQUIRE( ts.tv_nsec == 750000000L );
619 REQUIRE( mcp3208Ctrl::timespecToNs( ts ) == Approx( ns ) );
620}
621
622/// Verify synchronized timing uses only the measured semaphore period for delay modeling.
623/**
624 * \ingroup mcp3208Ctrl_unit_test
625 */
626TEST_CASE( "mcp3208Ctrl updateTriggerTiming uses measured semaphore period for delay model", "[mcp3208Ctrl]" )
627{
628 mcp3208Ctrl_test app;
629
630 app.m_alpha = 0.2f;
631 app.m_firstSemaphore = false;
632 app.m_lastAtime = timespec{ 10, 100000000L };
633 app.m_avgSemaphorePeriod_ns = 500000.0;
634 app.m_wfs_fps = 1000.0;
635 app.m_synchroPostDelay = 0;
636
637 const timespec secondArrival{ 10, 101000000L };
638
639 app.updateTriggerTiming( secondArrival );
640
641 const double alpha = static_cast<double>( app.m_alpha );
642 const double expectedAvg_ns = alpha * 1000000.0 + ( 1.0 - alpha ) * 500000.0;
643 const double expectedMeasuredDeltaT_ns = expectedAvg_ns;
644 const double expectedBlendedDeltaT_ns = ( 1.0 - alpha ) * ( 1e9 / 1000.0 ) + alpha * expectedAvg_ns;
645 const double rawDelayMeasured_ns =
646 0.5 * expectedMeasuredDeltaT_ns - ( 3000.0 + 51500.0 + 10000.0 + 276100.0 );
647 const double rawDelayBlended_ns = 0.5 * expectedBlendedDeltaT_ns - ( 3000.0 + 51500.0 + 10000.0 + 276100.0 );
648 const double expectedDelayMeasured_ns = wrapDelay( rawDelayMeasured_ns, expectedMeasuredDeltaT_ns );
649 const double expectedDelayBlended_ns = wrapDelay( rawDelayBlended_ns, expectedBlendedDeltaT_ns );
650 const double measuredTrigger_ns = mcp3208Ctrl::timespecToNs( app.m_triggerTime );
651 const double measuredDelay_ns = measuredTrigger_ns - mcp3208Ctrl::timespecToNs( secondArrival );
652
653 REQUIRE( app.m_avgSemaphorePeriod_ns == Approx( expectedAvg_ns ) );
654 REQUIRE( app.m_wfsPeriodMeasured_ns == Approx( expectedMeasuredDeltaT_ns ) );
655 REQUIRE( app.m_delayModel_ns == Approx( expectedDelayMeasured_ns ) );
656 REQUIRE( app.m_synchroDelayTarget == Approx( static_cast<float>( expectedDelayMeasured_ns ) ) );
657 REQUIRE( app.m_triggerInterval_ns == Approx( 0.0 ) );
658 REQUIRE( measuredDelay_ns == Approx( expectedDelayMeasured_ns ) );
659 REQUIRE( expectedDelayMeasured_ns != Approx( expectedDelayBlended_ns ) );
660 REQUIRE( measuredDelay_ns >= 0.0 );
661 REQUIRE( measuredDelay_ns < expectedMeasuredDeltaT_ns );
662}
663
664/// Verify synchronized timing applies a positive user offset to the modeled delay target.
665/**
666 * \ingroup mcp3208Ctrl_unit_test
667 */
668TEST_CASE( "mcp3208Ctrl updateTriggerTiming applies positive synchroDelay offset", "[mcp3208Ctrl]" )
669{
670 mcp3208Ctrl_test app;
671
672 app.m_alpha = 1.0f;
673 app.m_firstSemaphore = false;
674 app.m_lastAtime = timespec{ 0, 0 };
675 app.m_avgSemaphorePeriod_ns = 250000.0;
676 app.m_synchroPostDelay = 100;
677
678 const timespec secondArrival{ 0, 1000000L };
679 app.updateTriggerTiming( secondArrival );
680
681 const double expectedPeriod_ns = 1000000.0;
682 const double rawModelDelay_ns = 0.5 * expectedPeriod_ns - ( 3000.0 + 51500.0 + 10000.0 + 276100.0 );
683 const double expectedModel_ns = wrapDelay( rawModelDelay_ns, expectedPeriod_ns );
684 const double expectedTarget_ns = wrapDelay( expectedModel_ns + 100000.0, expectedPeriod_ns );
685 const double measuredTrigger_ns = mcp3208Ctrl::timespecToNs( app.m_triggerTime );
686 const double measuredDelay_ns = measuredTrigger_ns - mcp3208Ctrl::timespecToNs( secondArrival );
687
688 REQUIRE( app.m_wfsPeriodMeasured_ns == Approx( expectedPeriod_ns ) );
689 REQUIRE( app.m_delayModel_ns == Approx( expectedModel_ns ) );
690 REQUIRE( app.m_synchroDelayTarget == Approx( static_cast<float>( expectedTarget_ns ) ) );
691 REQUIRE( measuredDelay_ns == Approx( expectedTarget_ns ) );
692}
693
694/// Verify synchronized timing applies a negative user offset with modulo wrap into the current period.
695/**
696 * \ingroup mcp3208Ctrl_unit_test
697 */
698TEST_CASE( "mcp3208Ctrl updateTriggerTiming applies negative synchroDelay offset with wrap", "[mcp3208Ctrl]" )
699{
700 mcp3208Ctrl_test app;
701
702 app.m_alpha = 1.0f;
703 app.m_firstSemaphore = false;
704 app.m_lastAtime = timespec{ 0, 0 };
705 app.m_avgSemaphorePeriod_ns = 250000.0;
706 app.m_synchroPostDelay = -300;
707
708 const timespec secondArrival{ 0, 1000000L };
709 app.updateTriggerTiming( secondArrival );
710
711 const double expectedPeriod_ns = 1000000.0;
712 const double rawModelDelay_ns = 0.5 * expectedPeriod_ns - ( 3000.0 + 51500.0 + 10000.0 + 276100.0 );
713 const double expectedModel_ns = wrapDelay( rawModelDelay_ns, expectedPeriod_ns );
714 const double expectedTarget_ns = wrapDelay( expectedModel_ns - 300000.0, expectedPeriod_ns );
715 const double measuredTrigger_ns = mcp3208Ctrl::timespecToNs( app.m_triggerTime );
716 const double measuredDelay_ns = measuredTrigger_ns - mcp3208Ctrl::timespecToNs( secondArrival );
717
718 REQUIRE( app.m_wfsPeriodMeasured_ns == Approx( expectedPeriod_ns ) );
719 REQUIRE( app.m_delayModel_ns == Approx( expectedModel_ns ) );
720 REQUIRE( app.m_synchroDelayTarget == Approx( static_cast<float>( expectedTarget_ns ) ) );
721 REQUIRE( measuredDelay_ns == Approx( expectedTarget_ns ) );
722}
723
724/// Verify synchronized timing falls back to EMA period when WFS fps is unavailable.
725/**
726 * \ingroup mcp3208Ctrl_unit_test
727 */
728TEST_CASE( "mcp3208Ctrl updateTriggerTiming falls back to EMA period when fps is invalid", "[mcp3208Ctrl]" )
729{
730 mcp3208Ctrl_test app;
731
732 app.m_alpha = 0.2f;
733 app.m_firstSemaphore = false;
734 app.m_lastAtime = timespec{ 0, 0 };
735 app.m_avgSemaphorePeriod_ns = 1000000.0;
736 app.m_wfs_fps = 0.0;
737
738 const timespec nextArrival{ 0, 2000000L };
739 app.updateTriggerTiming( nextArrival );
740
741 const double alpha = static_cast<double>( app.m_alpha );
742 const double expectedAvg_ns = alpha * 2000000.0 + ( 1.0 - alpha ) * 1000000.0;
743 const double rawDelay_ns = 0.5 * expectedAvg_ns - ( 3000.0 + 51500.0 + 10000.0 + 276100.0 );
744 const double expectedDelay_ns = wrapDelay( rawDelay_ns, expectedAvg_ns );
745 const double expectedTrigger_ns = mcp3208Ctrl::timespecToNs( nextArrival ) + expectedDelay_ns;
746 const double measuredTrigger_ns = mcp3208Ctrl::timespecToNs( app.m_triggerTime );
747 const double measuredDelay_ns = measuredTrigger_ns - mcp3208Ctrl::timespecToNs( nextArrival );
748
749 REQUIRE( app.m_avgSemaphorePeriod_ns == Approx( expectedAvg_ns ) );
750 REQUIRE( measuredTrigger_ns == Approx( expectedTrigger_ns ) );
751 REQUIRE( app.m_synchroDelayTarget == Approx( static_cast<float>( expectedDelay_ns ) ) );
752 REQUIRE( measuredDelay_ns >= 0.0 );
753 REQUIRE( measuredDelay_ns < expectedAvg_ns );
754}
755
756/// Verify synchronized timing wraps negative raw delays into the current WFS period.
757/**
758 * \ingroup mcp3208Ctrl_unit_test
759 */
760TEST_CASE( "mcp3208Ctrl updateTriggerTiming wraps delay with modulo period", "[mcp3208Ctrl]" )
761{
762 mcp3208Ctrl_test app;
763
764 app.m_firstSemaphore = false;
765 app.m_lastAtime = timespec{ 5, 0 };
766 app.m_avgSemaphorePeriod_ns = 100000.0;
767 app.m_wfs_fps = 20000.0;
768
769 const timespec nextArrival{ 5, 100000L };
770 app.updateTriggerTiming( nextArrival );
771
772 const double expectedAvg_ns = 100000.0;
773 const double expectedDeltaT_ns = expectedAvg_ns;
774 const double rawDelay_ns = 0.5 * expectedDeltaT_ns - ( 3000.0 + 51500.0 + 10000.0 + 276100.0 );
775 const double expectedDelay_ns = wrapDelay( rawDelay_ns, expectedDeltaT_ns );
776 const double measuredDelay_ns = mcp3208Ctrl::timespecToNs( app.m_triggerTime ) - mcp3208Ctrl::timespecToNs( nextArrival );
777
778 REQUIRE( app.m_avgSemaphorePeriod_ns == Approx( expectedAvg_ns ) );
779 REQUIRE( rawDelay_ns < 0.0 );
780 REQUIRE( app.m_synchroDelayTarget == Approx( static_cast<float>( expectedDelay_ns ) ) );
781 REQUIRE( measuredDelay_ns == Approx( expectedDelay_ns ) );
782 REQUIRE( measuredDelay_ns >= 0.0 );
783 REQUIRE( measuredDelay_ns < expectedDeltaT_ns );
784}
785
786/// Verify synchronized timing leaves positive raw delays unchanged by modulo wrapping.
787/**
788 * \ingroup mcp3208Ctrl_unit_test
789 */
790TEST_CASE( "mcp3208Ctrl updateTriggerTiming preserves positive raw delay", "[mcp3208Ctrl]" )
791{
792 mcp3208Ctrl_test app;
793
794 app.m_firstSemaphore = false;
795 app.m_lastAtime = timespec{ 4, 0 };
796 app.m_avgSemaphorePeriod_ns = 2000000.0;
797 app.m_wfs_fps = 0.0;
798
799 const timespec nextArrival{ 4, 2000000L };
800 app.updateTriggerTiming( nextArrival );
801
802 const double expectedAvg_ns = 2000000.0;
803 const double expectedDeltaT_ns = expectedAvg_ns;
804 const double rawDelay_ns = 0.5 * expectedDeltaT_ns - ( 3000.0 + 51500.0 + 10000.0 + 276100.0 );
805 const double measuredDelay_ns = mcp3208Ctrl::timespecToNs( app.m_triggerTime ) - mcp3208Ctrl::timespecToNs( nextArrival );
806
807 REQUIRE( rawDelay_ns > 0.0 );
808 REQUIRE( app.m_avgSemaphorePeriod_ns == Approx( expectedAvg_ns ) );
809 REQUIRE( app.m_delayModel_ns == Approx( rawDelay_ns ) );
810 REQUIRE( app.m_synchroDelayTarget == Approx( static_cast<float>( rawDelay_ns ) ) );
811 REQUIRE( measuredDelay_ns == Approx( rawDelay_ns ) );
812}
813
814/// Verify synchronized timing constants directly control the modeled delay target.
815/**
816 * \ingroup mcp3208Ctrl_unit_test
817 */
818TEST_CASE( "mcp3208Ctrl updateTriggerTiming uses configurable timing constants", "[mcp3208Ctrl]" )
819{
820 mcp3208Ctrl_test app;
821
822 app.m_firstSemaphore = false;
823 app.m_lastAtime = timespec{ 9, 0 };
824 app.m_avgSemaphorePeriod_ns = 1200000.0;
825 app.m_wfs_fps = 1000.0;
826 app.m_synchroDtTransfer_ns = 4000.0;
827 app.m_synchroWfsProcess_ns = 62000.0;
828 app.m_synchroDtF_ns = 11000.0;
829 app.m_synchroWfsRead_ns = 290000.0;
830
831 const timespec nextArrival{ 9, 1200000L };
832 app.updateTriggerTiming( nextArrival );
833
834 const double expectedAvg_ns = 1200000.0;
835 const double rawDelayCustom_ns = 0.5 * expectedAvg_ns - ( 4000.0 + 62000.0 + 11000.0 + 290000.0 );
836 const double rawDelayDefault_ns = 0.5 * expectedAvg_ns - ( 3000.0 + 51500.0 + 10000.0 + 276100.0 );
837 const double expectedDelayCustom_ns = wrapDelay( rawDelayCustom_ns, expectedAvg_ns );
838 const double measuredDelay_ns =
839 mcp3208Ctrl::timespecToNs( app.m_triggerTime ) - mcp3208Ctrl::timespecToNs( nextArrival );
840
841 REQUIRE( app.m_avgSemaphorePeriod_ns == Approx( expectedAvg_ns ) );
842 REQUIRE( app.m_delayModel_ns == Approx( expectedDelayCustom_ns ) );
843 REQUIRE( app.m_synchroDelayTarget == Approx( static_cast<float>( expectedDelayCustom_ns ) ) );
844 REQUIRE( measuredDelay_ns == Approx( expectedDelayCustom_ns ) );
845 REQUIRE( expectedDelayCustom_ns != Approx( wrapDelay( rawDelayDefault_ns, expectedAvg_ns ) ) );
846}
847
848/// Verify synchronized timing leaves trigger time unchanged when period estimate is non-positive.
849/**
850 * \ingroup mcp3208Ctrl_unit_test
851 */
852TEST_CASE( "mcp3208Ctrl updateTriggerTiming guards non-positive period", "[mcp3208Ctrl]" )
853{
854 mcp3208Ctrl_test app;
855
856 app.m_triggerTime = timespec{ 7, 12345L };
857 app.m_triggerInterval_ns = 42.0;
858 app.m_synchroDelayTarget = 12345.0f;
859 app.m_delayModel_ns = 12345.0;
860 app.m_wfsPeriodMeasured_ns = 67890.0;
861 app.m_firstSemaphore = true;
862 app.m_wfs_fps = 0.0;
863
864 const timespec nextArrival{ 7, 54321L };
865 app.updateTriggerTiming( nextArrival );
866
867 REQUIRE( app.m_firstSemaphore == false );
868 REQUIRE( app.m_avgSemaphorePeriod_ns == Approx( 0.0 ) );
869 REQUIRE( app.m_lastAtime.tv_sec == nextArrival.tv_sec );
870 REQUIRE( app.m_lastAtime.tv_nsec == nextArrival.tv_nsec );
871 REQUIRE( app.m_triggerTime.tv_sec == 7 );
872 REQUIRE( app.m_triggerTime.tv_nsec == 12345L );
873 REQUIRE( app.m_triggerInterval_ns == Approx( 0.0 ) );
874 REQUIRE( app.m_synchroDelayTarget == Approx( 12345.0f ) );
875 REQUIRE( app.m_delayModel_ns == Approx( 12345.0 ) );
876 REQUIRE( app.m_wfsPeriodMeasured_ns == Approx( 0.0 ) );
877}
878
879/// Verify timer-driven acquisition configures the published frame geometry.
880/**
881 * \ingroup mcp3208Ctrl_unit_test
882 */
883TEST_CASE( "mcp3208Ctrl timer mode configureAcquisition sizes the output frame", "[mcp3208Ctrl]" )
884{
885 mcp3208Ctrl_test app;
886
887 app.m_numChannels = 3;
888
889 REQUIRE( app.configureAcquisition() == 0 );
890 REQUIRE( app.m_values.size() == 3 );
891 REQUIRE( app.m_width == 3 );
892 REQUIRE( app.m_height == 1 );
893 REQUIRE( app.m_dataType == _DATATYPE_UINT16 );
894}
895
896/// Verify timer-driven acquisition still reads one frame of ADC values.
897/**
898 * \ingroup mcp3208Ctrl_unit_test
899 */
900TEST_CASE( "mcp3208Ctrl timer mode reads configured channels", "[mcp3208Ctrl]" )
901{
902 mcp3208Ctrl_test app;
903
904 resetStubState();
905 stubState().m_channelValues = { 11, 22, 33 };
906
907 app.m_numChannels = 3;
908 app.m_values.assign( 3, 0 );
909 app.m_gain = 0;
910 app.m_trigger = 0;
911 app.m_time_start = std::chrono::high_resolution_clock::now();
912
913 REQUIRE( app.acquireAndCheckValid() == 0 );
914 REQUIRE( app.m_values == std::vector<uint16_t>( { 11, 22, 33 } ) );
915 REQUIRE( stubState().m_readOrder == std::vector<int>( { 0, 1, 2 } ) );
916}
917
918/// Verify timer-mode trigger interval diagnostics initialize then report measured intervals.
919/**
920 * \ingroup mcp3208Ctrl_unit_test
921 */
922TEST_CASE( "mcp3208Ctrl timer mode trigger interval initializes then measures", "[mcp3208Ctrl]" )
923{
924 mcp3208Ctrl_test app;
925
926 resetStubState();
927 stubState().m_channelValues = { 17 };
928
929 app.m_numChannels = 1;
930 app.m_values.assign( 1, 0 );
931 app.m_gain = 0.0f;
932 app.m_trigger = 1000.0f;
933
934 app.m_time_start = std::chrono::high_resolution_clock::now() - std::chrono::milliseconds( 2 );
935 REQUIRE( app.acquireAndCheckValid() == 0 );
936 REQUIRE( app.m_triggerInterval_ns == Approx( 0.0 ) );
937
938 app.m_time_start = std::chrono::high_resolution_clock::now() - std::chrono::milliseconds( 4 );
939 REQUIRE( app.acquireAndCheckValid() == 0 );
940 REQUIRE( app.m_triggerInterval_ns > 1000000.0 );
941}
942
943/// Verify the current MCP3208 values are copied into the output image buffer.
944/**
945 * \ingroup mcp3208Ctrl_unit_test
946 */
947TEST_CASE( "mcp3208Ctrl loadImageIntoStream copies the current values", "[mcp3208Ctrl]" )
948{
949 mcp3208Ctrl_test app;
950 std::vector<uint16_t> dest( 3, 0 );
951
952 app.m_values = { 5, 6, 7 };
953
954 REQUIRE( app.loadImageIntoStream( dest.data() ) == 0 );
955 REQUIRE( dest == std::vector<uint16_t>( { 5, 6, 7 } ) );
956}
957
958/// Verify published-frame counters track local and synchronized stream writes.
959/**
960 * \ingroup mcp3208Ctrl_unit_test
961 */
962TEST_CASE( "mcp3208Ctrl loadImageIntoStream updates frame mapping counters", "[mcp3208Ctrl]" )
963{
964 mcp3208Ctrl_test app;
965 std::vector<uint16_t> dest( 2, 0 );
966
967 app.m_values = { 9, 8 };
968
969 REQUIRE( app.loadImageIntoStream( dest.data() ) == 0 );
970 REQUIRE( app.m_localFrameSeq == 1 );
971 REQUIRE( app.m_syncFramesWritten == 0 );
972
973 app.m_synchroShmimName = "camwfs_sync";
974 REQUIRE( app.loadImageIntoStream( dest.data() ) == 0 );
975 REQUIRE( app.m_localFrameSeq == 2 );
976 REQUIRE( app.m_syncFramesWritten == 1 );
977}
978
979/// Verify synchronized acquisition performs one ADC sweep per semaphore wake.
980/**
981 * \ingroup mcp3208Ctrl_unit_test
982 */
983TEST_CASE( "mcp3208Ctrl synchronized mode reads on semaphore wake", "[mcp3208Ctrl]" )
984{
985 mcp3208Ctrl_test app;
986 sem_t semaphore;
987
988 resetStubState();
989 stubState().m_channelValues = { 101, 202, 303 };
990
991 REQUIRE( sem_init( &semaphore, 0, 0 ) == 0 );
992 REQUIRE( sem_post( &semaphore ) == 0 );
993
994 app.m_synchroShmimName = "camwfs_sync";
995 app.m_numChannels = 3;
996 app.m_values.assign( 3, 0 );
997 app.m_synchroSemaphore = &semaphore;
998 app.m_synchroDelay = 0;
999 app.m_synchroDelayTarget = 0;
1000 app.m_gain = 0;
1001 app.m_wfs_fps = 2000.0;
1002
1003 REQUIRE( app.acquireAndCheckValid() == 0 );
1004 REQUIRE( app.m_values == std::vector<uint16_t>( { 101, 202, 303 } ) );
1005 REQUIRE( stubState().m_readOrder == std::vector<int>( { 0, 1, 2 } ) );
1006 REQUIRE( app.m_firstSemaphore == false );
1007 REQUIRE( app.m_lastAtime.tv_sec > 0 );
1008 REQUIRE( app.m_triggerTime.tv_sec > 0 );
1009 REQUIRE( app.m_currImageTimestamp.tv_sec > 0 );
1010 REQUIRE( app.m_firstReadLatency == false );
1011 REQUIRE( app.m_avgReadLatency_ns ==
1012 Approx( mcp3208Ctrl::timespecToNs( app.m_currImageTimestamp ) - mcp3208Ctrl::timespecToNs( app.m_atime ) ) );
1013
1014 REQUIRE( sem_destroy( &semaphore ) == 0 );
1015}
1016
1017/// Verify synchronized mode derives producer cadence from stream metadata counters and timestamps.
1018/**
1019 * \ingroup mcp3208Ctrl_unit_test
1020 */
1021TEST_CASE( "mcp3208Ctrl synchronized mode tracks producer cadence from metadata", "[mcp3208Ctrl]" )
1022{
1023 mcp3208Ctrl_test app;
1024 sem_t semaphore;
1025 IMAGE_METADATA metadata{};
1026
1027 resetStubState();
1028 stubState().m_channelValues = { 44 };
1029
1030 REQUIRE( sem_init( &semaphore, 0, 0 ) == 0 );
1031
1032 app.m_synchroShmimName = "camwfs_sync";
1033 app.m_numChannels = 1;
1034 app.m_values.assign( 1, 0 );
1035 app.m_synchroSemaphore = &semaphore;
1036 app.m_synchroStream.md = &metadata;
1037 app.m_synchroDelay = 0.0f;
1038 app.m_synchroDelayTarget = 0.0f;
1039 app.m_gain = 0.0f;
1040 app.m_alpha = 0.5f;
1041 app.m_wfs_fps = 2000.0;
1042
1043 metadata.cnt0 = 100;
1044 metadata.atime = timespec{ 10, 0 };
1045 REQUIRE( sem_post( &semaphore ) == 0 );
1046 REQUIRE( app.acquireAndCheckValid() == 0 );
1047
1048 REQUIRE( app.m_firstProducerSample == false );
1049 REQUIRE( app.m_producerPeriodInst_ns == Approx( 0.0 ) );
1050 REQUIRE( app.m_avgProducerPeriod_ns == Approx( 0.0 ) );
1051 REQUIRE( app.m_syncFramesReceived == 1 );
1052 REQUIRE( app.m_syncFramesDropped == 0 );
1053 REQUIRE( app.m_syncFrameIdGapCount == 0 );
1054 REQUIRE( app.m_syncProducerFrameId == 100 );
1055 REQUIRE( app.m_syncProducerFrameDelta == 0 );
1056 REQUIRE( app.m_syncProducerFrameValid == true );
1057
1058 metadata.cnt0 = 102;
1059 metadata.atime = timespec{ 10, 1000000L };
1060 REQUIRE( sem_post( &semaphore ) == 0 );
1061 REQUIRE( app.acquireAndCheckValid() == 0 );
1062
1063 REQUIRE( app.m_producerPeriodInst_ns == Approx( 500000.0 ) );
1064 REQUIRE( app.m_avgProducerPeriod_ns == Approx( 500000.0 ) );
1065 REQUIRE( app.m_lastProducerCnt0 == 102 );
1066 REQUIRE( app.m_syncFramesReceived == 2 );
1067 REQUIRE( app.m_syncFramesDropped == 1 );
1068 REQUIRE( app.m_syncFrameIdGapCount == 1 );
1069 REQUIRE( app.m_syncProducerFrameId == 102 );
1070 REQUIRE( app.m_syncProducerFrameDelta == 2 );
1071
1072 metadata.cnt0 = 104;
1073 metadata.atime = timespec{ 10, 2100000L };
1074 REQUIRE( sem_post( &semaphore ) == 0 );
1075 REQUIRE( app.acquireAndCheckValid() == 0 );
1076
1077 const double expectedPeriod2_ns = 550000.0;
1078 const double expectedAvg2_ns =
1079 static_cast<double>( app.m_alpha ) * expectedPeriod2_ns + ( 1.0 - static_cast<double>( app.m_alpha ) ) * 500000.0;
1080 REQUIRE( app.m_producerPeriodInst_ns == Approx( expectedPeriod2_ns ) );
1081 REQUIRE( app.m_avgProducerPeriod_ns == Approx( expectedAvg2_ns ) );
1082 REQUIRE( app.m_syncFramesReceived == 3 );
1083 REQUIRE( app.m_syncFramesDropped == 2 );
1084 REQUIRE( app.m_syncFrameIdGapCount == 2 );
1085 REQUIRE( app.m_syncProducerFrameId == 104 );
1086 REQUIRE( app.m_syncProducerFrameDelta == 2 );
1087
1088 const double periodBeforeNoAdvance_ns = app.m_producerPeriodInst_ns;
1089 const double avgBeforeNoAdvance_ns = app.m_avgProducerPeriod_ns;
1090
1091 metadata.cnt0 = 104;
1092 metadata.atime = timespec{ 10, 2200000L };
1093 REQUIRE( sem_post( &semaphore ) == 0 );
1094 REQUIRE( app.acquireAndCheckValid() == 0 );
1095
1096 REQUIRE( app.m_producerPeriodInst_ns == Approx( periodBeforeNoAdvance_ns ) );
1097 REQUIRE( app.m_avgProducerPeriod_ns == Approx( avgBeforeNoAdvance_ns ) );
1098 REQUIRE( app.m_syncFramesReceived == 4 );
1099 REQUIRE( app.m_syncFramesDropped == 2 );
1100 REQUIRE( app.m_syncFrameIdGapCount == 2 );
1101 REQUIRE( app.m_syncProducerFrameId == 104 );
1102 REQUIRE( app.m_syncProducerFrameDelta == 0 );
1103
1104 REQUIRE( sem_destroy( &semaphore ) == 0 );
1105}
1106
1107/// Verify synchronized read-latency EMA initializes from the first sample and smooths subsequent samples.
1108/**
1109 * \ingroup mcp3208Ctrl_unit_test
1110 */
1111TEST_CASE( "mcp3208Ctrl synchronized read latency EMA initializes and smooths", "[mcp3208Ctrl]" )
1112{
1113 mcp3208Ctrl_test app;
1114 sem_t semaphore;
1115
1116 resetStubState();
1117 stubState().m_channelValues = { 77 };
1118
1119 REQUIRE( sem_init( &semaphore, 0, 0 ) == 0 );
1120
1121 app.m_synchroShmimName = "camwfs_sync";
1122 app.m_numChannels = 1;
1123 app.m_values.assign( 1, 0 );
1124 app.m_synchroSemaphore = &semaphore;
1125 app.m_synchroDelayTarget = 0.0f;
1126 app.m_synchroDelay = 0.0f;
1127 app.m_gain = 0.0f;
1128 app.m_alpha = 0.2f;
1129 app.m_firstReadLatency = true;
1130 app.m_avgReadLatency_ns = 0.0;
1131
1132 REQUIRE( sem_post( &semaphore ) == 0 );
1133 REQUIRE( app.acquireAndCheckValid() == 0 );
1134
1135 const double readLatency0_ns =
1136 mcp3208Ctrl::timespecToNs( app.m_currImageTimestamp ) - mcp3208Ctrl::timespecToNs( app.m_atime );
1137
1138 REQUIRE( app.m_firstReadLatency == false );
1139 REQUIRE( app.m_avgReadLatency_ns == Approx( readLatency0_ns ) );
1140
1141 REQUIRE( sem_post( &semaphore ) == 0 );
1142 REQUIRE( app.acquireAndCheckValid() == 0 );
1143
1144 const double readLatency1_ns =
1145 mcp3208Ctrl::timespecToNs( app.m_currImageTimestamp ) - mcp3208Ctrl::timespecToNs( app.m_atime );
1146 const double alpha = static_cast<double>( app.m_alpha );
1147 const double expectedAvgLatency_ns = alpha * readLatency1_ns + ( 1.0 - alpha ) * readLatency0_ns;
1148
1149 REQUIRE( app.m_avgReadLatency_ns == Approx( expectedAvgLatency_ns ) );
1150
1151 REQUIRE( sem_destroy( &semaphore ) == 0 );
1152}
1153
1154/// Verify synchronized non-delay service EMA uses the configurable global alpha.
1155/**
1156 * \ingroup mcp3208Ctrl_unit_test
1157 */
1158TEST_CASE( "mcp3208Ctrl synchronized non-delay service EMA uses global alpha", "[mcp3208Ctrl]" )
1159{
1160 mcp3208Ctrl_test app;
1161 sem_t semaphore;
1162
1163 resetStubState();
1164 stubState().m_channelValues = { 55 };
1165
1166 REQUIRE( sem_init( &semaphore, 0, 0 ) == 0 );
1167
1168 app.m_synchroShmimName = "camwfs_sync";
1169 app.m_numChannels = 1;
1170 app.m_values.assign( 1, 0 );
1171 app.m_synchroSemaphore = &semaphore;
1172 app.m_synchroDelayTarget = 0.0f;
1173 app.m_synchroDelay = 0.0f;
1174 app.m_gain = 0.0f;
1175 app.m_alpha = 0.2f;
1176 app.m_firstNonDelayService = true;
1177 app.m_avgNonDelayService_ns = 0.0;
1178
1179 REQUIRE( sem_post( &semaphore ) == 0 );
1180 REQUIRE( app.acquireAndCheckValid() == 0 );
1181
1182 const double nonDelay0_ns = app.m_nonDelayService_ns;
1183 REQUIRE( app.m_firstNonDelayService == false );
1184 REQUIRE( app.m_avgNonDelayService_ns == Approx( nonDelay0_ns ) );
1185
1186 REQUIRE( sem_post( &semaphore ) == 0 );
1187 REQUIRE( app.acquireAndCheckValid() == 0 );
1188
1189 const double nonDelay1_ns = app.m_nonDelayService_ns;
1190 const double alpha = static_cast<double>( app.m_alpha );
1191 const double expectedAvgNonDelay_ns = alpha * nonDelay1_ns + ( 1.0 - alpha ) * nonDelay0_ns;
1192
1193 REQUIRE( app.m_avgNonDelayService_ns == Approx( expectedAvgNonDelay_ns ) );
1194
1195 REQUIRE( sem_destroy( &semaphore ) == 0 );
1196}
1197
1198/// Verify synchronized delay control uses read-latency EMA for the integrator correction.
1199/**
1200 * \ingroup mcp3208Ctrl_unit_test
1201 */
1202TEST_CASE( "mcp3208Ctrl synchronized delay controller uses read latency EMA", "[mcp3208Ctrl]" )
1203{
1204 mcp3208Ctrl_test app;
1205 sem_t semaphore;
1206
1207 resetStubState();
1208 stubState().m_channelValues = { 99 };
1209
1210 REQUIRE( sem_init( &semaphore, 0, 0 ) == 0 );
1211 REQUIRE( sem_post( &semaphore ) == 0 );
1212
1213 app.m_synchroShmimName = "camwfs_sync";
1214 app.m_numChannels = 1;
1215 app.m_values.assign( 1, 0 );
1216 app.m_synchroSemaphore = &semaphore;
1217 app.m_synchroDelayTarget = 0.0f;
1218 app.m_synchroDelay = 2000000.0f;
1219 app.m_gain = 1.0f;
1220 app.m_alpha = 0.2f;
1221 app.m_firstReadLatency = false;
1222 app.m_avgReadLatency_ns = 800000.0;
1223
1224 REQUIRE( app.acquireAndCheckValid() == 0 );
1225
1226 const double readLatency_ns =
1227 mcp3208Ctrl::timespecToNs( app.m_currImageTimestamp ) - mcp3208Ctrl::timespecToNs( app.m_atime );
1228 const double alpha = static_cast<double>( app.m_alpha );
1229 const double expectedAvgLatency_ns = alpha * readLatency_ns + ( 1.0 - alpha ) * 800000.0;
1230 const double expectedDelay_ns =
1231 ( 2000000.0 - expectedAvgLatency_ns ) > 0.0 ? ( 2000000.0 - expectedAvgLatency_ns ) : 0.0;
1232
1233 REQUIRE( app.m_avgReadLatency_ns == Approx( expectedAvgLatency_ns ) );
1234 REQUIRE( app.m_synchroDelay == Approx( expectedDelay_ns ) );
1235 REQUIRE( app.m_values[0] == 99 );
1236
1237 REQUIRE( sem_destroy( &semaphore ) == 0 );
1238}
1239
1240/// Verify synchronized delay control clamps at zero when the control step overshoots.
1241/**
1242 * \ingroup mcp3208Ctrl_unit_test
1243 */
1244TEST_CASE( "mcp3208Ctrl synchronized delay controller clamps to zero", "[mcp3208Ctrl]" )
1245{
1246 mcp3208Ctrl_test app;
1247 sem_t semaphore;
1248
1249 resetStubState();
1250 stubState().m_channelValues = { 11 };
1251
1252 REQUIRE( sem_init( &semaphore, 0, 0 ) == 0 );
1253 REQUIRE( sem_post( &semaphore ) == 0 );
1254
1255 app.m_synchroShmimName = "camwfs_sync";
1256 app.m_numChannels = 1;
1257 app.m_values.assign( 1, 0 );
1258 app.m_synchroSemaphore = &semaphore;
1259 app.m_synchroDelayTarget = 0.0f;
1260 app.m_synchroDelay = 1000.0f;
1261 app.m_gain = 1.0f;
1262
1263 REQUIRE( app.acquireAndCheckValid() == 0 );
1264 REQUIRE( app.m_synchroDelay == Approx( 0.0f ) );
1265 REQUIRE( app.m_values[0] == 11 );
1266
1267 REQUIRE( sem_destroy( &semaphore ) == 0 );
1268}
1269
1270/// Verify synchronized delay control blocks integrator windup while cadence capping is active.
1271/**
1272 * \ingroup mcp3208Ctrl_unit_test
1273 */
1274TEST_CASE( "mcp3208Ctrl synchronized delay controller applies anti-windup at cap", "[mcp3208Ctrl]" )
1275{
1276 mcp3208Ctrl_test app;
1277
1278 app.m_gain = 1.0f;
1279 app.m_synchroDelayTarget = 300000.0f;
1280 app.m_wfsPeriodMeasured_ns = 500000.0;
1281 app.m_delayBudget_ns = 100000.0;
1282 app.m_delayApplied_ns = 100000.0;
1283
1284 // While capped high, negative error would normally increase command; anti-windup should hold at the cap.
1285 app.m_avgReadLatency_ns = 100000.0;
1286 app.updateSynchroDelayController( 500000.0 );
1287 REQUIRE( app.m_synchroDelay == Approx( 100000.0f ) );
1288
1289 // If the controller correction reduces delay, allow command to move down below the cap.
1290 app.m_avgReadLatency_ns = 350000.0;
1291 app.updateSynchroDelayController( 110000.0 );
1292 REQUIRE( app.m_synchroDelay == Approx( 60000.0f ) );
1293}
1294
1295/// Verify synchronized timeout requests reconfiguration when the trigger stream is stale.
1296/**
1297 * \ingroup mcp3208Ctrl_unit_test
1298 */
1299TEST_CASE( "mcp3208Ctrl synchronized timeout requests reconfig for a stale stream", "[mcp3208Ctrl]" )
1300{
1301 mcp3208Ctrl_test app;
1302 sem_t semaphore;
1303
1304 resetStubState();
1305
1306 REQUIRE( sem_init( &semaphore, 0, 0 ) == 0 );
1307
1308 app.m_synchroShmimName = "camwfs_sync";
1309 app.m_values = { 7, 8 };
1310 app.m_synchroSemaphore = &semaphore;
1311 app.m_synchroStreamOpen = false;
1312
1313 REQUIRE( app.acquireAndCheckValid() == 1 );
1314 REQUIRE( app.m_reconfig == true );
1315 REQUIRE( app.m_values == std::vector<uint16_t>( { 7, 8 } ) );
1316 REQUIRE( stubState().m_readOrder.empty() );
1317
1318 REQUIRE( sem_destroy( &semaphore ) == 0 );
1319}
1320
1321/// Verify stale-stream detection notices a missing synchronization stream backing file.
1322/**
1323 * \ingroup mcp3208Ctrl_unit_test
1324 */
1325TEST_CASE( "mcp3208Ctrl synchronized stale helper detects missing streams", "[mcp3208Ctrl]" )
1326{
1327 mcp3208Ctrl_test app;
1328 IMAGE_METADATA metadata{};
1329
1330 app.m_synchroShmimName = "mcp3208Ctrl_unit_test_missing_stream";
1331 app.m_synchroStreamOpen = true;
1332 metadata.sem = SEMAPHORE_MAXVAL;
1333 app.m_synchroStream.md = &metadata;
1334
1335 REQUIRE( app.synchroStreamStale() == true );
1336}
1337
1338/// Verify `reconfig()` clears cached synchronization state.
1339/**
1340 * \ingroup mcp3208Ctrl_unit_test
1341 */
1342TEST_CASE( "mcp3208Ctrl reconfig clears cached synchronization state", "[mcp3208Ctrl]" )
1343{
1344 mcp3208Ctrl_test app;
1345
1346 app.m_synchroSemaphore = reinterpret_cast<sem_t *>( 0x1 );
1347 app.m_synchroSemaphoreNumber = 7;
1348 app.m_synchroStreamInode = 1234;
1349 app.m_synchroStreamOpen = false;
1350 app.m_atime = timespec{ 1, 1 };
1351 app.m_lastAtime = timespec{ 2, 2 };
1352 app.m_avgSemaphorePeriod_ns = 42.0;
1353 app.m_wfsPeriodMeasured_ns = 21.0;
1354 app.m_lastProducerAtime = timespec{ 3, 4 };
1355 app.m_lastProducerCnt0 = 123;
1356 app.m_producerPeriodInst_ns = 500000.0;
1357 app.m_avgProducerPeriod_ns = 510000.0;
1358 app.m_firstProducerSample = false;
1359 app.m_localFrameSeq = 22;
1360 app.m_syncFramesReceived = 21;
1361 app.m_syncFramesWritten = 20;
1362 app.m_syncFramesDropped = 4;
1363 app.m_syncFrameIdGapCount = 3;
1364 app.m_syncProducerFrameId = 1234567;
1365 app.m_syncProducerFrameDelta = 5;
1366 app.m_lastSyncProducerFrameId = 1234562;
1367 app.m_syncProducerFrameValid = true;
1368 app.m_firstSemaphore = false;
1369 app.m_avgReadLatency_ns = 84.0;
1370 app.m_firstReadLatency = false;
1371 app.m_delayModel_ns = 900.0;
1372 app.m_delayApplied_ns = 875.0;
1373 app.m_delayBudget_ns = 450000.0;
1374 app.m_nonDelayService_ns = 170000.0;
1375 app.m_avgNonDelayService_ns = 160000.0;
1376 app.m_firstNonDelayService = false;
1377 app.m_delayPhaseError_ns = -25.0;
1378 app.m_delayLock = 1.0;
1379 app.m_delayCapped = 1.0;
1380 app.m_triggerTime = timespec{ 3, 3 };
1381 app.m_triggerInterval_ns = 21.0;
1382 app.m_lastTriggerTime = timespec{ 4, 4 };
1383 app.m_firstTriggerTime = false;
1384 app.m_firstTimerTrigger = false;
1385
1386 REQUIRE( app.reconfig() == 0 );
1387 REQUIRE( app.m_synchroSemaphore == nullptr );
1388 REQUIRE( app.m_synchroSemaphoreNumber == 5 );
1389 REQUIRE( app.m_synchroStreamInode == 0 );
1390 REQUIRE( app.m_synchroStreamOpen == false );
1391 REQUIRE( app.m_atime.tv_sec == 0 );
1392 REQUIRE( app.m_atime.tv_nsec == 0 );
1393 REQUIRE( app.m_lastAtime.tv_sec == 0 );
1394 REQUIRE( app.m_lastAtime.tv_nsec == 0 );
1395 REQUIRE( app.m_avgSemaphorePeriod_ns == Approx( 0.0 ) );
1396 REQUIRE( app.m_wfsPeriodMeasured_ns == Approx( 0.0 ) );
1397 REQUIRE( app.m_lastProducerAtime.tv_sec == 0 );
1398 REQUIRE( app.m_lastProducerAtime.tv_nsec == 0 );
1399 REQUIRE( app.m_lastProducerCnt0 == 0 );
1400 REQUIRE( app.m_producerPeriodInst_ns == Approx( 0.0 ) );
1401 REQUIRE( app.m_avgProducerPeriod_ns == Approx( 0.0 ) );
1402 REQUIRE( app.m_firstProducerSample == true );
1403 REQUIRE( app.m_localFrameSeq == 0 );
1404 REQUIRE( app.m_syncFramesReceived == 0 );
1405 REQUIRE( app.m_syncFramesWritten == 0 );
1406 REQUIRE( app.m_syncFramesDropped == 0 );
1407 REQUIRE( app.m_syncFrameIdGapCount == 0 );
1408 REQUIRE( app.m_syncProducerFrameId == 0 );
1409 REQUIRE( app.m_syncProducerFrameDelta == 0 );
1410 REQUIRE( app.m_lastSyncProducerFrameId == 0 );
1411 REQUIRE( app.m_syncProducerFrameValid == false );
1412 REQUIRE( app.m_firstSemaphore == true );
1413 REQUIRE( app.m_avgReadLatency_ns == Approx( 0.0 ) );
1414 REQUIRE( app.m_firstReadLatency == true );
1415 REQUIRE( app.m_delayModel_ns == Approx( 0.0 ) );
1416 REQUIRE( app.m_delayApplied_ns == Approx( 0.0 ) );
1417 REQUIRE( app.m_delayBudget_ns == Approx( 0.0 ) );
1418 REQUIRE( app.m_nonDelayService_ns == Approx( 0.0 ) );
1419 REQUIRE( app.m_avgNonDelayService_ns == Approx( 0.0 ) );
1420 REQUIRE( app.m_firstNonDelayService == true );
1421 REQUIRE( app.m_delayPhaseError_ns == Approx( 0.0 ) );
1422 REQUIRE( app.m_delayLock == Approx( 0.0 ) );
1423 REQUIRE( app.m_delayCapped == Approx( 0.0 ) );
1424 REQUIRE( app.m_triggerTime.tv_sec == 0 );
1425 REQUIRE( app.m_triggerTime.tv_nsec == 0 );
1426 REQUIRE( app.m_triggerInterval_ns == Approx( 0.0 ) );
1427 REQUIRE( app.m_lastTriggerTime.tv_sec == 0 );
1428 REQUIRE( app.m_lastTriggerTime.tv_nsec == 0 );
1429 REQUIRE( app.m_firstTriggerTime == true );
1430 REQUIRE( app.m_firstTimerTrigger == true );
1431}
1432
1433} // namespace mcp3208CtrlTest
1434} // namespace libXWCTest
MCP3208(const int dev=DEFAULT_SPI_DEV, const int channel=DEFAULT_SPI_CHANNEL, const int baud=DEFAULT_SPI_BAUD, const int flags=DEFAULT_SPI_FLAGS) noexcept
Definition MCP3208.cpp:8
unsigned short read(const std::uint8_t channel, const Mode m=Mode::SINGLE) const
Definition MCP3208.cpp:62
virtual ~MCP3208()
Definition MCP3208.cpp:20
static double timespecToNs(const timespec &t)
Convert a timespec timestamp to nanoseconds.
static timespec nsToTimespec(double ns)
Convert nanoseconds to a normalized timespec value.
TEST_CASE("mcp3208Ctrl Doxygen references are preserved", "[mcp3208Ctrl]")
Preserve Doxygen links for the real mcp3208Ctrl APIs exercised by the tests.
#define XWCTEST_DOXYGEN_REF(fxn)
This inserts an unused call to a function signature to make doxygen make the link.
Definition testXWC.hpp:18
Namespace for all libXWC tests.