API
 
Loading...
Searching...
No Matches
ocam2KCtrl_test.cpp
Go to the documentation of this file.
1/** \file ocam2KCtrl_test.cpp
2 * \brief Catch2 tests for the ocam2KCtrl app.
3 * \author OpenAI Codex
4 *
5 * \ingroup ocam2KCtrl_files
6 */
7
8/** \defgroup ocam2KCtrl_unit_test ocam2KCtrl Unit Tests
9 * \brief Unit tests for the ocam2KCtrl application.
10 *
11 * \ingroup application_unit_test
12 */
13
14#include "../../../tests/testXWC.hpp"
15
16#include <chrono>
17#include <array>
18#include <cstdio>
19#include <cstdlib>
20#include <deque>
21#include <semaphore.h>
22#include <stdexcept>
23#include <string>
24#include <thread>
25#include <unistd.h>
26#include <vector>
27
28#define protected public
29#include "../ocam2KCtrl.hpp"
30#undef protected
31
32using namespace MagAOX::app;
33
34namespace
35{
36
37/// Scripted serial response returned by the EDT serial-command stubs.
38struct serialResponse
39{
40 std::string response; ///< Response returned by the next serial read.
41 int commandResult{ 0 }; ///< Return code from `pdv_serial_command`.
42 int initialWaitResult{ 1 }; ///< Return code from the first `pdv_serial_wait`.
43};
44
45/// Shared EDT stub state used to verify acquisition-related calls from `ocam2KCtrl`.
46struct edtStubState
47{
48 /// Reset the stub to a known default state before each test section.
49 void reset()
50 {
51 startImagesCalls = 0;
52 lastStartNumBuffs = -1;
53 startImageCalls = 0;
54 waitTimeSec = 0;
55 waitTimeNsec = 0;
56 waitImage.fill( 0 );
57 readcfgReturn = 0;
58 serialResponses.clear();
59 serialCommands.clear();
60 activeSerialResponse.clear();
61 activeSerialReadPending = false;
62 activeSerialTransaction = false;
63 activeSerialWaitResult = 0;
64 activeSerialWaitServed = false;
65 }
66
67 int startImagesCalls{ 0 }; ///< Number of `pdv_start_images` calls observed.
68 int lastStartNumBuffs{ -1 }; ///< Most recent requested EDT buffer count.
69 int startImageCalls{ 0 }; ///< Number of `pdv_start_image` calls observed.
70 uint waitTimeSec{ 0 }; ///< Seconds returned by `pdv_wait_last_image_timed`.
71 uint waitTimeNsec{ 0 }; ///< Nanoseconds returned by `pdv_wait_last_image_timed`.
72 std::array<u_char, 32> waitImage{}; ///< Raw EDT frame buffer returned to the app.
73 int readcfgReturn{ 0 }; ///< Return code forced from `pdv_readcfg`.
74 std::deque<serialResponse> serialResponses; ///< Scripted serial responses queued for future commands.
75 std::vector<std::string> serialCommands; ///< Serial commands issued by the app under test.
76 std::string activeSerialResponse; ///< Active serial response returned by the current command.
77 bool activeSerialReadPending{ false }; ///< Indicates that the next `pdv_serial_read` should return data.
78 bool activeSerialTransaction{ false }; ///< Indicates that a serial command is mid-transaction.
79 int activeSerialWaitResult{ 0 }; ///< Return code for the first wait after a command.
80 bool activeSerialWaitServed{ false }; ///< Tracks whether the first serial wait has been consumed.
81};
82
83/// Shared OCAM SDK stub state used to drive descramble output in tests.
84struct ocam2StubState
85{
86 /// Reset the stub to its default state before each test section.
87 void reset()
88 {
89 lastId = 0;
90 lastRaw = nullptr;
91 lastOutput = nullptr;
92 outputImage.clear();
93 imageNumber = 0;
94 lastInitMode = OCAM2_NORMAL;
95 lastDescrambleFile.clear();
96 nextInitId = 1;
97 initReturn = OCAM2_OK;
98 exitCalls = 0;
99 lastExitId = 0;
100 reportedMode = OCAM2_NORMAL;
101 }
102
103 ocam2_id lastId{ 0 }; ///< Most recent OCAM handle passed into `ocam2_descramble`.
104 const short *lastRaw{ nullptr }; ///< Most recent raw source frame passed to `ocam2_descramble`.
105 short *lastOutput{ nullptr }; ///< Most recent output frame passed to `ocam2_descramble`.
106 std::vector<int16_t> outputImage; ///< Pixel values the descramble stub should copy into the output buffer.
107 unsigned int imageNumber{ 0 }; ///< Frame number returned by the descramble stub.
108 ocam2_mode lastInitMode{ OCAM2_NORMAL }; ///< Most recent OCAM SDK mode requested by `ocam2_init`.
109 std::string lastDescrambleFile; ///< Most recent descramble file passed to `ocam2_init`.
110 ocam2_id nextInitId{ 1 }; ///< OCAM id returned by the next successful `ocam2_init`.
111 ocam2_rc initReturn{ OCAM2_OK }; ///< Return code from `ocam2_init`.
112 int exitCalls{ 0 }; ///< Number of `ocam2_exit` invocations.
113 ocam2_id lastExitId{ 0 }; ///< Most recent OCAM id passed to `ocam2_exit`.
114 ocam2_mode reportedMode{ OCAM2_NORMAL }; ///< Mode returned by `ocam2_getMode`.
115};
116
117/// Global EDT stub state for the `extern "C"` wrappers below.
118edtStubState g_edtStubState;
119
120/// Global OCAM SDK stub state for the `extern "C"` wrappers below.
121ocam2StubState g_ocam2StubState;
122
123/// Reset all external-library stub state before a test.
124void resetStubState()
125{
126 g_edtStubState.reset();
127 g_ocam2StubState.reset();
128}
129
130/// Store a frame number into the raw EDT image buffer at the OCAM metadata offset.
131[[maybe_unused]] void setStubFrameNumber( unsigned int frameNumber )
132{
133 reinterpret_cast<int *>( g_edtStubState.waitImage.data() )[OCAM2_IMAGE_NB_OFFSET / 4] =
134 static_cast<int>( frameNumber );
135}
136
137/// Queue one scripted serial response for the next `pdvSerialWriteRead` invocation.
138[[maybe_unused]] void
139queueSerialResponse( const std::string &response, int commandResult = 0, int initialWaitResult = 1 )
140{
141 g_edtStubState.serialResponses.push_back( { response, commandResult, initialWaitResult } );
142}
143
144} // namespace
145
146extern "C"
147{
148
150 {
151 return reinterpret_cast<Dependent *>( malloc( sizeof( Dependent ) ) );
152 }
153
154 int pdv_readcfg( const char *configFile, Dependent *dd_p, Edtinfo *edtinfo )
155 {
156 static_cast<void>( configFile );
157 static_cast<void>( dd_p );
158 static_cast<void>( edtinfo );
159 return g_edtStubState.readcfgReturn;
160 }
161
162 EdtDev *edt_open_channel( const char *deviceName, int unit, int channel )
163 {
164 static EdtDev device;
165 static_cast<void>( deviceName );
166 static_cast<void>( unit );
167 static_cast<void>( channel );
168 return &device;
169 }
170
171 void edt_perror( char *errstr )
172 {
173 if( errstr != nullptr )
174 {
175 errstr[0] = '\0';
176 }
177 }
178
179 int pdv_initcam( EdtDev *edt_p,
180 Dependent *dd_p,
181 int unit,
182 Edtinfo *edtinfo,
183 const char *configFile,
184 char *bitdir,
185 int pdv_debug )
186 {
187 static_cast<void>( edt_p );
188 static_cast<void>( dd_p );
189 static_cast<void>( unit );
190 static_cast<void>( edtinfo );
191 static_cast<void>( configFile );
192 static_cast<void>( bitdir );
193 static_cast<void>( pdv_debug );
194 return 0;
195 }
196
197 void edt_close( EdtDev *edt_p )
198 {
199 static_cast<void>( edt_p );
200 }
201
202 PdvDev *pdv_open_channel( const char *deviceName, int unit, int channel )
203 {
204 static PdvDev device;
205 static_cast<void>( deviceName );
206 static_cast<void>( unit );
207 static_cast<void>( channel );
208 return &device;
209 }
210
211 void pdv_close( PdvDev *pdv_p )
212 {
213 static_cast<void>( pdv_p );
214 }
215
216 void pdv_flush_fifo( PdvDev *pdv_p )
217 {
218 static_cast<void>( pdv_p );
219 }
220
222 {
223 static_cast<void>( pdv_p );
224 }
225
226 int pdv_get_width( PdvDev *pdv_p )
227 {
228 static_cast<void>( pdv_p );
229 return 240;
230 }
231
232 int pdv_get_height( PdvDev *pdv_p )
233 {
234 static_cast<void>( pdv_p );
235 return 121;
236 }
237
238 int pdv_get_depth( PdvDev *pdv_p )
239 {
240 static_cast<void>( pdv_p );
241 return 16;
242 }
243
245 {
246 static char cameraType[] = "stub_pdv";
247 static_cast<void>( pdv_p );
248 return cameraType;
249 }
250
251 void pdv_multibuf( PdvDev *pdv_p, int numBuffs )
252 {
253 static_cast<void>( pdv_p );
254 static_cast<void>( numBuffs );
255 }
256
257 void pdv_start_images( PdvDev *pdv_p, int numBuffs )
258 {
259 static_cast<void>( pdv_p );
260 ++g_edtStubState.startImagesCalls;
261 g_edtStubState.lastStartNumBuffs = numBuffs;
262 }
263
264 u_char *pdv_wait_last_image_timed( PdvDev *pdv_p, uint dmaTimeStamp[2] )
265 {
266 static_cast<void>( pdv_p );
267 dmaTimeStamp[0] = g_edtStubState.waitTimeSec;
268 dmaTimeStamp[1] = g_edtStubState.waitTimeNsec;
269 return g_edtStubState.waitImage.data();
270 }
271
272 void pdv_start_image( PdvDev *pdv_p )
273 {
274 static_cast<void>( pdv_p );
275 ++g_edtStubState.startImageCalls;
276 }
277
278 int pdv_serial_read( PdvDev *pdv_p, char *buf, int size )
279 {
280 static_cast<void>( pdv_p );
281 if( buf != nullptr && size > 0 )
282 {
283 buf[0] = '\0';
284 }
285
286 if( !g_edtStubState.activeSerialReadPending || buf == nullptr || size <= 0 )
287 {
288 return 0;
289 }
290
291 size_t copyCount = g_edtStubState.activeSerialResponse.size();
292 if( copyCount > static_cast<size_t>( size - 1 ) )
293 {
294 copyCount = static_cast<size_t>( size - 1 );
295 }
296
297 std::copy_n( g_edtStubState.activeSerialResponse.data(), copyCount, buf );
298 buf[copyCount] = '\0';
299
300 g_edtStubState.activeSerialReadPending = false;
301
302 return static_cast<int>( copyCount );
303 }
304
305 int pdv_serial_command( PdvDev *pdv_p, const char *command )
306 {
307 static_cast<void>( pdv_p );
308 g_edtStubState.serialCommands.emplace_back( command == nullptr ? "" : command );
309
310 g_edtStubState.activeSerialResponse.clear();
311 g_edtStubState.activeSerialReadPending = false;
312 g_edtStubState.activeSerialTransaction = false;
313 g_edtStubState.activeSerialWaitResult = 0;
314 g_edtStubState.activeSerialWaitServed = false;
315
316 if( g_edtStubState.serialResponses.empty() )
317 {
318 return 0;
319 }
320
321 serialResponse response = g_edtStubState.serialResponses.front();
322 g_edtStubState.serialResponses.pop_front();
323
324 if( response.commandResult < 0 )
325 {
326 return response.commandResult;
327 }
328
329 g_edtStubState.activeSerialResponse = response.response;
330 g_edtStubState.activeSerialReadPending = true;
331 g_edtStubState.activeSerialTransaction = true;
332 g_edtStubState.activeSerialWaitResult = response.initialWaitResult;
333
334 return response.commandResult;
335 }
336
337 int pdv_serial_wait( PdvDev *pdv_p, int timeout, int count )
338 {
339 static_cast<void>( pdv_p );
340 static_cast<void>( timeout );
341 static_cast<void>( count );
342
343 if( !g_edtStubState.activeSerialTransaction )
344 {
345 return 0;
346 }
347
348 if( !g_edtStubState.activeSerialWaitServed )
349 {
350 g_edtStubState.activeSerialWaitServed = true;
351 return g_edtStubState.activeSerialWaitResult;
352 }
353
354 return 0;
355 }
356
357 int pdv_get_waitchar( PdvDev *pdv_p, u_char *waitc )
358 {
359 static_cast<void>( pdv_p );
360 if( waitc != nullptr )
361 {
362 *waitc = '\n';
363 }
364 return 1;
365 }
366
367 const char *ocam2_sdkVersion()
368 {
369 return "stub";
370 }
371
372 const char *ocam2_sdkBuild()
373 {
374 return "stub";
375 }
376
377 ocam2_rc ocam2_init( ocam2_mode mode, const char *descrbFile, ocam2_id *id )
378 {
379 g_ocam2StubState.lastInitMode = mode;
380 g_ocam2StubState.lastDescrambleFile = descrbFile == nullptr ? "" : descrbFile;
381 if( id != nullptr )
382 {
383 *id = g_ocam2StubState.nextInitId;
384 }
385 g_ocam2StubState.reportedMode = mode;
386 return g_ocam2StubState.initReturn;
387 }
388
389 void ocam2_descramble( ocam2_id id, unsigned int *number, short *image, const short *imageRaw )
390 {
391 g_ocam2StubState.lastId = id;
392 g_ocam2StubState.lastRaw = imageRaw;
393 g_ocam2StubState.lastOutput = image;
394 if( number != nullptr )
395 {
396 *number = g_ocam2StubState.imageNumber;
397 }
398 if( image != nullptr )
399 {
400 for( size_t nn = 0; nn < g_ocam2StubState.outputImage.size(); ++nn )
401 {
402 image[nn] = g_ocam2StubState.outputImage[nn];
403 }
404 }
405 }
406
408 {
409 ++g_ocam2StubState.exitCalls;
410 g_ocam2StubState.lastExitId = id;
411 return OCAM2_OK;
412 }
413
415 {
416 static_cast<void>( id );
417 return g_ocam2StubState.reportedMode;
418 }
419
420 const char *ocam2_modeStr( ocam2_mode mode )
421 {
422 static_cast<void>( mode );
423 return "stub";
424 }
425
426} // extern "C"
427
428namespace libXWCTest
429{
430
431/// Namespace for `ocam2KCtrl` unit tests.
432/** \ingroup ocam2KCtrl_unit_test
433 */
434namespace ocam2KCtrlTest
435{
436
437namespace
438{
439
440/// Build a unique shmim name for one temporary test stream.
441std::string uniqueShmimName( const std::string &suffix )
442{
443 static unsigned counter = 0;
444
445 ++counter;
446
447 return "ocam2KCtrl_test_" + suffix + "_" + std::to_string( ::getpid() ) + "_" + std::to_string( counter );
448}
449
450/// Build a unique temporary config-file path for one test.
451[[maybe_unused]] std::string uniqueConfigPath( const std::string &suffix )
452{
453 return "/tmp/ocam2KCtrl_test_" + suffix + "_" + std::to_string( ::getpid() ) + ".conf";
454}
455
456/// RAII wrapper for a temporary 1x1 uint8 ImageStreamIO stream used by tests.
457class tempStream
458{
459 public:
460 /// Create the temporary stream or throw if ImageStreamIO setup fails.
461 explicit tempStream( const std::string &name,
462 uint32_t width = 1,
463 uint32_t height = 1,
464 uint32_t depth = 1,
465 uint8_t dataType = _DATATYPE_UINT8 )
466 : m_name( name )
467 {
468 uint32_t imsize[3] = { width, height, depth };
469
470 if( ImageStreamIO_createIm_gpu( &m_image,
471 m_name.c_str(),
472 3,
473 imsize,
474 dataType,
475 -1,
476 1,
477 IMAGE_NB_SEMAPHORE,
478 0,
479 CIRCULAR_BUFFER | ZAXIS_TEMPORAL,
480 0 ) != IMAGESTREAMIO_SUCCESS )
481 {
482 throw std::runtime_error( "failed to create temporary ImageStreamIO stream" );
483 }
484
485 m_image.md[0].cnt1 = 0;
486 }
487
488 /// Destroy the temporary stream.
489 ~tempStream()
490 {
491 if( m_owner )
492 {
493 ImageStreamIO_destroyIm( &m_image );
494 }
495 }
496
497 /// Return the underlying temporary stream.
498 IMAGE *image()
499 {
500 return &m_image;
501 }
502
503 /// Release destruction ownership after the stream is handed off elsewhere.
504 void dismiss()
505 {
506 m_owner = false;
507 }
508
509 private:
510 std::string m_name; ///< Unique shmim name for the temporary stream.
511 IMAGE m_image{}; ///< ImageStreamIO handle for the temporary stream.
512 bool m_owner{ true }; ///< Whether this wrapper should destroy the underlying shmim on teardown.
513};
514
515/// Test harness exposing protected `ocam2KCtrl` helpers.
516class ocam2KCtrl_test : public MagAOX::app::ocam2KCtrl
517{
518 public:
519 /// Construct a testable controller instance with unique shmim names.
520 ocam2KCtrl_test()
521 {
522 m_configName = "ocam2KCtrl_test";
523 m_shmimName = uniqueShmimName( "main" );
524 m_syncShmimName = uniqueShmimName( "sync" );
525 }
526
527 /// Tear down any sync stream left behind by the test.
528 ~ocam2KCtrl_test() noexcept
529 {
530 destroySyncStream();
531 }
532};
533
534/// Put the app into the nominal powered-on state used by the serial helpers.
535[[maybe_unused]] void setPoweredOn( ocam2KCtrl_test &app )
536{
537 static_cast<MagAOXAppT &>( app ).m_powerState = 1;
538 app.m_powerTargetState = 1;
539}
540
541/// Seed one minimal but internally consistent camera-mode setup for startup and acquisition tests.
542[[maybe_unused]] void configureStartupMode( ocam2KCtrl_test &app,
543 const std::string &modeName = "science",
544 const std::string &serialCommand = "mode science",
545 const std::string &configFileName = "stub.cfg" )
546{
547 dev::cameraConfig config;
548
549 app.m_startupMode = modeName;
550 app.m_modeName = modeName;
551 app.m_configDir = "/tmp";
552 static_cast<dev::dssShutter<ocam2KCtrl> &>( app ).m_powerDevice = "pwr";
553 static_cast<dev::dssShutter<ocam2KCtrl> &>( app ).m_powerChannel = "cam";
554 static_cast<dev::dssShutter<ocam2KCtrl> &>( app ).m_dioDevice = "dio";
555 static_cast<dev::dssShutter<ocam2KCtrl> &>( app ).m_sensorChannel = "sensor";
556 static_cast<dev::dssShutter<ocam2KCtrl> &>( app ).m_triggerChannel = "trigger";
557 static_cast<dev::dssShutter<ocam2KCtrl> &>( app ).m_shutterWait = 0;
558 static_cast<dev::dssShutter<ocam2KCtrl> &>( app ).m_shutterTimeout = 0.1;
559 app.m_tel.logPath( "/tmp" );
560 app.m_tel.logName( app.m_configName );
561 app.m_tel.logExt( "bintel" );
562 config.m_serialCommand = serialCommand;
563 config.m_configFile = configFileName;
564 config.m_binningX = 1;
565 config.m_binningY = 1;
566 config.m_digitalBinX = 1;
567 config.m_digitalBinY = 1;
568 app.m_cameraModes[modeName] = config;
569}
570
571/// Start a short-lived framegrabber thread so `frameGrabber::appLogic()` sees a running worker.
572void startFgThread( ocam2KCtrl_test &app, int sleepMs = 200 )
573{
574 app.m_fgThread =
575 std::thread( [sleepMs]() { std::this_thread::sleep_for( std::chrono::milliseconds( sleepMs ) ); } );
576}
577
578/// Join the temporary framegrabber thread if a test started one.
579void joinFgThread( ocam2KCtrl_test &app )
580{
581 if( app.m_fgThread.joinable() )
582 {
583 app.m_fgThread.join();
584 }
585}
586
587/// Keep a temporary framegrabber thread joined even when a test fails mid-scope.
588struct fgThreadScope
589{
590 ocam2KCtrl_test &m_app; ///< App whose temporary framegrabber thread is managed by this scope.
591
592 /// Start a temporary framegrabber thread for the lifetime of this scope.
593 explicit fgThreadScope( ocam2KCtrl_test &app, int sleepMs = 200 ) : m_app( app )
594 {
595 startFgThread( m_app, sleepMs );
596 }
597
598 /// Join the temporary framegrabber thread on scope exit.
599 ~fgThreadScope()
600 {
601 joinFgThread( m_app );
602 }
603};
604
605/// Ensure startup-driven telemetry resources are shut down when a test exits early.
606struct startupScope
607{
608 ocam2KCtrl_test &m_app; ///< App whose startup resources are managed by this scope.
609 bool m_started; ///< Tracks whether `appStartup()` completed successfully in the test.
610
611 /// Construct a startup guard for one test app instance.
612 explicit startupScope( ocam2KCtrl_test &app ) : m_app( app ), m_started( false )
613 {
614 }
615
616 /// Record whether startup completed so shutdown only runs when needed.
617 void markStarted( bool started )
618 {
619 m_started = started;
620 }
621
622 /// Shut down telemetry logging and app resources on scope exit after a successful startup.
623 ~startupScope()
624 {
625 if( m_started )
626 {
627 m_app.m_shutdown = 1;
628 m_app.m_tel.logShutdown( true );
629 static_cast<void>( m_app.appShutdown() );
630 }
631 }
632};
633
634/// Report whether a telemetry timestamp has been written at least once.
635[[maybe_unused]] bool hasRecordedTime( const timespec &ts )
636{
637 return ts.tv_sec != 0 || ts.tv_nsec != 0;
638}
639
640} // namespace
641
642#ifndef OCAM2KCTRL_TEST_SUPPORT_ONLY
643
644/// Verify the sync stream is created as a 1x1 uint8 ImageStreamIO buffer.
645/**
646 * \ingroup ocam2KCtrl_unit_test
647 */
648TEST_CASE( "ocam2KCtrl sync stream creation uses a 1x1 uint8 layout", "[ocam2KCtrl]" )
649{
650 ocam2KCtrl_test app;
651 IMAGE openedSyncStream;
652
653 resetStubState();
654
655 // clang-format off
656 #ifdef OCAM2KCTRL_TEST_DOXYGEN_REF
658 #endif
659 // clang-format on
660
661 REQUIRE( app.ensureSyncStream() == 0 );
662 REQUIRE( app.m_syncImageStream != nullptr );
663 REQUIRE( app.m_syncImageStream->md[0].datatype == _DATATYPE_UINT8 );
664 REQUIRE( app.m_syncImageStream->md[0].size[0] == 1 );
665 REQUIRE( app.m_syncImageStream->md[0].size[1] == 1 );
666 REQUIRE( app.m_syncImageStream->md[0].size[2] == 1 );
667 REQUIRE( app.m_syncImageStream->md[0].cnt1 == 0 );
668
669 REQUIRE( ImageStreamIO_openIm( &openedSyncStream, app.m_syncShmimName.c_str() ) == IMAGESTREAMIO_SUCCESS );
670 REQUIRE( openedSyncStream.md[0].datatype == _DATATYPE_UINT8 );
671 REQUIRE( openedSyncStream.md[0].size[0] == 1 );
672 REQUIRE( openedSyncStream.md[0].size[1] == 1 );
673 REQUIRE( openedSyncStream.md[0].size[2] == 1 );
674 REQUIRE( ImageStreamIO_closeIm( &openedSyncStream ) == IMAGESTREAMIO_SUCCESS );
675}
676
677/// Verify the sync stream mirrors main-stream metadata and posts a semaphore.
678/**
679 * \ingroup ocam2KCtrl_unit_test
680 */
681TEST_CASE( "ocam2KCtrl sync stream publication mirrors metadata and posts semaphores", "[ocam2KCtrl]" )
682{
683 ocam2KCtrl_test app;
684 tempStream sourceStream( uniqueShmimName( "source" ) );
685 int semIndex = -1;
686 int semValue = -1;
687
688 resetStubState();
689
690 // clang-format off
691 #ifdef OCAM2KCTRL_TEST_DOXYGEN_REF
693 #endif
694 // clang-format on
695
696 SECTION( "null source streams are ignored" )
697 {
698 REQUIRE( app.frameGrabberPostPublish( nullptr ) == 0 );
699 }
700
701 SECTION( "publication fails cleanly if the sync stream has not been prepared" )
702 {
703 REQUIRE( app.frameGrabberPostPublish( sourceStream.image() ) == -1 );
704 }
705
706 SECTION( "publication mirrors metadata and posts one semaphore on the sync stream" )
707 {
708 REQUIRE( app.ensureSyncStream() == 0 );
709
710 sourceStream.image()->md[0].writetime.tv_sec = 1234;
711 sourceStream.image()->md[0].writetime.tv_nsec = 5678;
712 sourceStream.image()->md[0].atime.tv_sec = 1234;
713 sourceStream.image()->md[0].atime.tv_nsec = 4321;
714 sourceStream.image()->md[0].cnt0 = 77;
715 sourceStream.image()->md[0].cnt1 = 0;
716 sourceStream.image()->writetimearray[0] = sourceStream.image()->md[0].writetime;
717 sourceStream.image()->atimearray[0] = sourceStream.image()->md[0].atime;
718 sourceStream.image()->cntarray[0] = sourceStream.image()->md[0].cnt0;
719
720 semIndex = ImageStreamIO_getsemwaitindex( app.m_syncImageStream, 0 );
721 REQUIRE( semIndex >= 0 );
722 ImageStreamIO_semflush( app.m_syncImageStream, semIndex );
723 REQUIRE( sem_getvalue( app.m_syncImageStream->semptr[semIndex], &semValue ) == 0 );
724 REQUIRE( semValue == 0 );
725
726 app.m_syncImageStream->array.UI8[0] = 99;
727
728 REQUIRE( app.frameGrabberPostPublish( sourceStream.image() ) == 0 );
729 REQUIRE( sem_getvalue( app.m_syncImageStream->semptr[semIndex], &semValue ) == 0 );
730 REQUIRE( semValue == 1 );
731 REQUIRE( app.m_syncImageStream->array.UI8[0] == 0 );
732 REQUIRE( app.m_syncImageStream->md[0].writetime.tv_sec == sourceStream.image()->md[0].writetime.tv_sec );
733 REQUIRE( app.m_syncImageStream->md[0].writetime.tv_nsec == sourceStream.image()->md[0].writetime.tv_nsec );
734 REQUIRE( app.m_syncImageStream->md[0].atime.tv_sec == sourceStream.image()->md[0].atime.tv_sec );
735 REQUIRE( app.m_syncImageStream->md[0].atime.tv_nsec == sourceStream.image()->md[0].atime.tv_nsec );
736 REQUIRE( app.m_syncImageStream->md[0].cnt0 == sourceStream.image()->md[0].cnt0 );
737 REQUIRE( app.m_syncImageStream->md[0].cnt1 == 0 );
738 REQUIRE( app.m_syncImageStream->writetimearray[0].tv_sec == sourceStream.image()->md[0].writetime.tv_sec );
739 REQUIRE( app.m_syncImageStream->atimearray[0].tv_nsec == sourceStream.image()->md[0].atime.tv_nsec );
740 REQUIRE( app.m_syncImageStream->cntarray[0] == sourceStream.image()->md[0].cnt0 );
741 }
742}
743
744/// Verify sync-stream preparation reuses valid streams and recovers from stale or mismatched ones.
745/**
746 * \ingroup ocam2KCtrl_unit_test
747 */
748TEST_CASE( "ocam2KCtrl ensureSyncStream reuses and replaces existing stream state", "[ocam2KCtrl]" )
749{
750 ocam2KCtrl_test app;
751
752 resetStubState();
753
754 // clang-format off
755 #ifdef OCAM2KCTRL_TEST_DOXYGEN_REF
757 #endif
758 // clang-format on
759
760 SECTION( "a valid existing sync stream is reused in place" )
761 {
762 REQUIRE( app.ensureSyncStream() == 0 );
763
764 IMAGE *existingStream = app.m_syncImageStream;
765
766 REQUIRE( app.ensureSyncStream() == 0 );
767 REQUIRE( app.m_syncImageStream == existingStream );
768 }
769
770 SECTION( "a mismatched in-memory sync stream is destroyed and recreated with the expected layout" )
771 {
772 app.m_syncImageStream = reinterpret_cast<IMAGE *>( malloc( sizeof( IMAGE ) ) );
773 REQUIRE( app.m_syncImageStream != nullptr );
774
775 {
776 uint32_t wrongSize[3] = { 1, 2, 1 };
777
778 REQUIRE( ImageStreamIO_createIm_gpu( app.m_syncImageStream,
779 app.m_syncShmimName.c_str(),
780 3,
781 wrongSize,
782 _DATATYPE_UINT8,
783 -1,
784 1,
785 IMAGE_NB_SEMAPHORE,
786 0,
787 CIRCULAR_BUFFER | ZAXIS_TEMPORAL,
788 0 ) == IMAGESTREAMIO_SUCCESS );
789 }
790
791 void *wrongPtr = app.m_syncImageStream;
792
793 REQUIRE( app.ensureSyncStream() == 0 );
794 REQUIRE( app.m_syncImageStream != nullptr );
795 REQUIRE( static_cast<void *>( app.m_syncImageStream ) == wrongPtr );
796 REQUIRE( app.m_syncImageStream->md[0].datatype == _DATATYPE_UINT8 );
797 REQUIRE( app.m_syncImageStream->md[0].size[0] == 1 );
798 REQUIRE( app.m_syncImageStream->md[0].size[1] == 1 );
799 REQUIRE( app.m_syncImageStream->md[0].size[2] == 1 );
800 REQUIRE( app.m_syncImageStream->md[0].size[1] != 2 );
801 }
802
803 SECTION( "a stale on-disk sync stream is reopened, destroyed, and replaced" )
804 {
805 tempStream staleStream( app.m_syncShmimName, 1, 1, 1, _DATATYPE_UINT8 );
806 staleStream.dismiss();
807
808 REQUIRE( app.m_syncImageStream == nullptr );
809 REQUIRE( app.ensureSyncStream() == 0 );
810 REQUIRE( app.m_syncImageStream != nullptr );
811 REQUIRE( app.m_syncImageStream->md[0].datatype == _DATATYPE_UINT8 );
812 REQUIRE( app.m_syncImageStream->md[0].size[0] == 1 );
813 REQUIRE( app.m_syncImageStream->md[0].size[1] == 1 );
814 REQUIRE( app.m_syncImageStream->md[0].size[2] == 1 );
815 }
816
817 SECTION( "an invalid pre-existing sync-stream file returns an error" )
818 {
819 char syncFileName[1024];
820 ImageStreamIO_filename( syncFileName, sizeof( syncFileName ), app.m_syncShmimName.c_str() );
821
822 std::FILE *syncFile = std::fopen( syncFileName, "w" );
823 REQUIRE( syncFile != nullptr );
824 REQUIRE( std::fputs( "not an ImageStreamIO stream\n", syncFile ) >= 0 );
825 REQUIRE( std::fclose( syncFile ) == 0 );
826
827 REQUIRE( app.ensureSyncStream() == -1 );
828 REQUIRE( app.m_syncImageStream == nullptr );
829 REQUIRE( ::unlink( syncFileName ) == 0 );
830 }
831}
832
833/// Verify the stdCamera state string is assembled from the OCAM state members.
834/**
835 * \ingroup ocam2KCtrl_unit_test
836 */
837TEST_CASE( "ocam2KCtrl stateString reports mode fps gain and setpoint", "[ocam2KCtrl]" )
838{
839 ocam2KCtrl_test app;
840
841 resetStubState();
842
843 // clang-format off
844 #ifdef OCAM2KCTRL_TEST_DOXYGEN_REF
846 #endif
847 // clang-format on
848
849 app.m_modeName = "science";
850 app.m_fps = 150.0f;
851 app.m_emGain = 42;
852 app.m_ccdTempSetpt = 19.5;
853
854 REQUIRE( app.stateString() == "science_150.000000_42.000000_19.500000" );
855}
856
857/// Verify the state string validity depends on operating state and temperature lock.
858/**
859 * \ingroup ocam2KCtrl_unit_test
860 */
861TEST_CASE( "ocam2KCtrl stateStringValid requires OPERATING and on-target temperature control", "[ocam2KCtrl]" )
862{
863 ocam2KCtrl_test app;
864
865 resetStubState();
866
867 // clang-format off
868 #ifdef OCAM2KCTRL_TEST_DOXYGEN_REF
870 #endif
871 // clang-format on
872
873 app.state( stateCodes::OPERATING );
874 app.m_tempControlOnTarget = true;
875 REQUIRE( app.stateStringValid() );
876
877 app.m_tempControlOnTarget = false;
878 REQUIRE( !app.stateStringValid() );
879
880 app.state( stateCodes::READY );
881 app.m_tempControlOnTarget = true;
882 REQUIRE( !app.stateStringValid() );
883}
884
885/// Verify OCAM-specific configuration values load defaults, overrides, and gain clamps.
886/**
887 * \ingroup ocam2KCtrl_unit_test
888 */
889TEST_CASE( "ocam2KCtrl configuration loading handles defaults and supported gain limits", "[ocam2KCtrl]" )
890{
891 resetStubState();
892
893 // clang-format off
894 #ifdef OCAM2KCTRL_TEST_DOXYGEN_REF
897 #endif
898 // clang-format on
899
900 SECTION( "loadConfig falls back to the main shmim name for the sync stream" )
901 {
902 ocam2KCtrl_test app;
903
904 app.setupConfig();
905
906 mx::app::writeConfigFile( uniqueConfigPath( "defaults" ), { "unused" }, { "value" }, { "0" } );
907 app.config.readConfig( uniqueConfigPath( "defaults" ) );
908
909 app.m_shmimName = "ocam_main_default";
910 app.m_syncShmimName = "";
911
912 app.loadConfig();
913
914 REQUIRE( app.m_ocamDescrambleFile == "" );
915 REQUIRE( app.m_maxEMGain == Approx( 600.0f ) );
916 REQUIRE( app.m_syncShmimName == app.m_shmimName + "_sync" );
917 }
918
919 SECTION( "loadConfig applies configured overrides for the descramble and stream names" )
920 {
921 ocam2KCtrl_test app;
922
923 app.setupConfig();
924
925 mx::app::writeConfigFile( uniqueConfigPath( "overrides" ),
926 { "camera", "camera", "framegrabber", "framegrabber" },
927 { "ocamDescrambleFile", "maxEMGain", "shmimName", "syncShmimName" },
928 { "descramble_stub.txt", "321", "ocam_main_override", "ocam_sync_override" } );
929 app.config.readConfig( uniqueConfigPath( "overrides" ) );
930
931 app.loadConfig();
932
933 REQUIRE( app.m_ocamDescrambleFile == "descramble_stub.txt" );
934 REQUIRE( app.m_maxEMGain == Approx( 321.0f ) );
935 REQUIRE( app.m_shmimName == "ocam_main_override" );
936 REQUIRE( app.m_syncShmimName == "ocam_sync_override" );
937 }
938
939 SECTION( "loadConfig clamps maxEMGain below the supported minimum" )
940 {
941 ocam2KCtrl_test app;
942
943 app.setupConfig();
944
945 mx::app::writeConfigFile( uniqueConfigPath( "gain_low" ), { "camera" }, { "maxEMGain" }, { "0" } );
946 app.config.readConfig( uniqueConfigPath( "gain_low" ) );
947
948 app.loadConfig();
949
950 REQUIRE( app.m_maxEMGain == Approx( 1.0f ) );
951 }
952
953 SECTION( "loadConfig clamps maxEMGain above the supported maximum" )
954 {
955 ocam2KCtrl_test app;
956
957 app.setupConfig();
958
959 mx::app::writeConfigFile( uniqueConfigPath( "gain_high" ), { "camera" }, { "maxEMGain" }, { "700" } );
960 app.config.readConfig( uniqueConfigPath( "gain_high" ) );
961
962 app.loadConfig();
963
964 REQUIRE( app.m_maxEMGain == Approx( 600.0f ) );
965 }
966}
967
968/// Verify temperature queries handle valid and malformed serial responses.
969/**
970 * \ingroup ocam2KCtrl_unit_test
971 */
972TEST_CASE( "ocam2KCtrl getTemps handles valid and malformed serial responses", "[ocam2KCtrl]" )
973{
974 ocam2KCtrl_test app;
975
976 resetStubState();
977
978 // clang-format off
979 #ifdef OCAM2KCTRL_TEST_DOXYGEN_REF
981 #endif
982 // clang-format on
983
984 setPoweredOn( app );
985
986 SECTION( "valid temperature response updates cached temperatures and status" )
987 {
988 queueSerialResponse( "Temperatures : CCD[20.4] CPU[41] POWER[34] BIAS[47] WATER[24.2] LEFT[33] RIGHT[38] "
989 "SET[205]\nCooling Power [102]mW.\n\n" );
990
991 REQUIRE( app.getTemps() == 0 );
992 REQUIRE( g_edtStubState.serialCommands.size() == 1 );
993 REQUIRE( g_edtStubState.serialCommands[0] == "temp" );
994 REQUIRE( app.m_temps.CCD == Approx( 20.4f ) );
995 REQUIRE( app.m_temps.CPU == Approx( 41.0f ) );
996 REQUIRE( app.m_ccdTemp == Approx( 20.4f ) );
997 REQUIRE( app.m_ccdTempSetpt == Approx( 20.5f ) );
998 REQUIRE( app.m_tempControlStatus == true );
999 REQUIRE( app.m_tempControlStatusStr == "ON TARGET" );
1000 REQUIRE( app.m_tempControlOnTarget == true );
1001 }
1002
1003 SECTION( "malformed temperature response marks cached temperatures invalid without forcing reconfig" )
1004 {
1005 queueSerialResponse( "Temperatures : CCD[20.4]\n" );
1006
1007 app.m_temps.CCD = 5;
1008 app.m_ccdTemp = 5;
1009 app.m_ccdTempSetpt = 6;
1010 app.m_tempControlStatus = true;
1011 app.m_tempControlOnTarget = false;
1012 app.m_tempControlStatusStr = "ON TARGET";
1013
1014 REQUIRE( app.getTemps() == 0 );
1015 REQUIRE( g_edtStubState.serialCommands.size() == 1 );
1016 REQUIRE( g_edtStubState.serialCommands[0] == "temp" );
1017 REQUIRE( app.m_temps.CCD == Approx( -999.0f ) );
1018 REQUIRE( app.m_temps.SET == Approx( -999.0f ) );
1019 REQUIRE( app.m_ccdTemp == Approx( -999.0f ) );
1020 REQUIRE( app.m_ccdTempSetpt == Approx( -999.0f ) );
1021 REQUIRE( app.m_tempControlStatus == false );
1022 REQUIRE( app.m_tempControlStatusStr == "UNKNOWN" );
1023 REQUIRE( app.m_tempControlOnTarget == false );
1024 }
1025
1026 SECTION( "low cooling power with a warm detector reports temperature control off" )
1027 {
1028 queueSerialResponse( "Temperatures : CCD[20.4] CPU[41] POWER[34] BIAS[47] WATER[24.2] LEFT[33] RIGHT[38] "
1029 "SET[165]\nCooling Power [2]mW.\n\n" );
1030
1031 REQUIRE( app.getTemps() == 0 );
1032 REQUIRE( app.m_temps.CCD == Approx( 20.4f ) );
1033 REQUIRE( app.m_temps.SET == Approx( 16.5f ) );
1034 REQUIRE( app.m_tempControlStatus == false );
1035 REQUIRE( app.m_tempControlStatusStr == "TEMP OFF" );
1036 REQUIRE( app.m_tempControlOnTarget == false );
1037 }
1038
1039 SECTION( "serial failures while powered off return -1 immediately" )
1040 {
1041 static_cast<MagAOXAppT &>( app ).m_powerState = 0;
1042 app.m_powerTargetState = 0;
1043
1044 queueSerialResponse( "", -1 );
1045
1046 REQUIRE( app.getTemps() == -1 );
1047 REQUIRE( g_edtStubState.serialCommands.size() == 1 );
1048 REQUIRE( g_edtStubState.serialCommands[0] == "temp" );
1049 }
1050
1051 SECTION( "serial failures while powered on return a software error" )
1052 {
1053 queueSerialResponse( "", -1 );
1054
1055 REQUIRE( app.getTemps() == -1 );
1056 REQUIRE( g_edtStubState.serialCommands.size() == 1 );
1057 REQUIRE( g_edtStubState.serialCommands[0] == "temp" );
1058 }
1059
1060 SECTION( "malformed temperature responses while powered off return -1" )
1061 {
1062 static_cast<MagAOXAppT &>( app ).m_powerState = 0;
1063 app.m_powerTargetState = 0;
1064
1065 queueSerialResponse( "Temperatures : CCD[20.4]\n" );
1066
1067 REQUIRE( app.getTemps() == -1 );
1068 REQUIRE( g_edtStubState.serialCommands.size() == 1 );
1069 REQUIRE( g_edtStubState.serialCommands[0] == "temp" );
1070 }
1071}
1072
1073/// Verify FPS queries handle valid and malformed serial responses, plus synchro mode.
1074/**
1075 * \ingroup ocam2KCtrl_unit_test
1076 */
1077TEST_CASE( "ocam2KCtrl getFPS handles valid and malformed serial responses", "[ocam2KCtrl]" )
1078{
1079 ocam2KCtrl_test app;
1080
1081 resetStubState();
1082
1083 // clang-format off
1084 #ifdef OCAM2KCTRL_TEST_DOXYGEN_REF
1086 #endif
1087 // clang-format on
1088
1089 SECTION( "valid fps response updates the cached frame rate" )
1090 {
1091 setPoweredOn( app );
1092 app.m_synchro = false;
1093 app.m_fps = 0;
1094
1095 queueSerialResponse( "fps [150.5] Hz\n" );
1096
1097 REQUIRE( app.getFPS() == 0 );
1098 REQUIRE( g_edtStubState.serialCommands.size() == 1 );
1099 REQUIRE( g_edtStubState.serialCommands[0] == "fps" );
1100 REQUIRE( app.m_fps == Approx( 150.5f ) );
1101 }
1102
1103 SECTION( "malformed fps response is non-fatal and leaves the cached value unchanged" )
1104 {
1105 setPoweredOn( app );
1106 app.m_synchro = false;
1107 app.m_fps = 75.0f;
1108
1109 queueSerialResponse( "fps 150.5\n" );
1110
1111 REQUIRE( app.getFPS() == 0 );
1112 REQUIRE( g_edtStubState.serialCommands.size() == 1 );
1113 REQUIRE( g_edtStubState.serialCommands[0] == "fps" );
1114 REQUIRE( app.m_fps == Approx( 75.0f ) );
1115 }
1116
1117 SECTION( "malformed fps responses while powered off return -1" )
1118 {
1119 static_cast<MagAOXAppT &>( app ).m_powerState = 0;
1120 app.m_powerTargetState = 0;
1121 app.m_synchro = false;
1122 app.m_fps = 75.0f;
1123
1124 queueSerialResponse( "fps 150.5\n" );
1125
1126 REQUIRE( app.getFPS() == -1 );
1127 REQUIRE( g_edtStubState.serialCommands.size() == 1 );
1128 REQUIRE( g_edtStubState.serialCommands[0] == "fps" );
1129 REQUIRE( app.m_fps == Approx( 75.0f ) );
1130 }
1131
1132 SECTION( "serial failures while powered on return a software error" )
1133 {
1134 setPoweredOn( app );
1135 app.m_synchro = false;
1136 app.m_fps = 75.0f;
1137
1138 queueSerialResponse( "", -1 );
1139
1140 REQUIRE( app.getFPS() == -1 );
1141 REQUIRE( g_edtStubState.serialCommands.size() == 1 );
1142 REQUIRE( g_edtStubState.serialCommands[0] == "fps" );
1143 REQUIRE( app.m_fps == Approx( 75.0f ) );
1144 }
1145
1146 SECTION( "synchro mode bypasses serial queries and mirrors the sync frequency" )
1147 {
1148 app.m_synchro = true;
1149 app.m_syncFreq = 205.25f;
1150 app.m_fps = 0;
1151
1152 REQUIRE( app.getFPS() == 0 );
1153 REQUIRE( g_edtStubState.serialCommands.empty() );
1154 REQUIRE( app.m_fps == Approx( 205.25f ) );
1155 }
1156}
1157
1158/// Verify the temperature-control helpers handle safe, unsafe, and valid setpoint requests.
1159/**
1160 * \ingroup ocam2KCtrl_unit_test
1161 */
1162TEST_CASE( "ocam2KCtrl temperature control helpers handle valid and invalid requests", "[ocam2KCtrl]" )
1163{
1164 ocam2KCtrl_test app;
1165
1166 resetStubState();
1167
1168 // clang-format off
1169 #ifdef OCAM2KCTRL_TEST_DOXYGEN_REF
1172 #endif
1173 // clang-format on
1174
1175 setPoweredOn( app );
1176
1177 SECTION( "setTempControl refuses to turn cooling off below the safe temperature" )
1178 {
1179 app.m_tempControlStatusSet = false;
1180 app.m_tempControlStatus = true;
1181 app.m_ccdTemp = 18.5f;
1182
1183 REQUIRE( app.setTempControl() == -1 );
1184 REQUIRE( g_edtStubState.serialCommands.empty() );
1185 REQUIRE( app.m_tempControlStatus == true );
1186 }
1187
1188 SECTION( "setTempControl turns cooling off when the detector is warm enough" )
1189 {
1190 app.m_tempControlStatusSet = false;
1191 app.m_tempControlStatus = true;
1192 app.m_ccdTemp = 20.0f;
1193
1194 queueSerialResponse( "temperature control off\n" );
1195
1196 REQUIRE( app.setTempControl() == 0 );
1197 REQUIRE( g_edtStubState.serialCommands.size() == 1 );
1198 REQUIRE( g_edtStubState.serialCommands[0] == "temp off" );
1199 REQUIRE( app.m_tempControlStatusSet == false );
1200 REQUIRE( app.m_tempControlStatus == false );
1201 }
1202
1203 SECTION( "setTempControl turns cooling on and reapplies the configured setpoint" )
1204 {
1205 app.m_tempControlStatusSet = true;
1206 app.m_tempControlStatus = false;
1207 app.m_ccdTempSetpt = 15.5f;
1208
1209 queueSerialResponse( "temperature control on\n" );
1210 queueSerialResponse( "setpoint updated\n" );
1211
1212 REQUIRE( app.setTempControl() == 0 );
1213 REQUIRE( g_edtStubState.serialCommands.size() == 2 );
1214 REQUIRE( g_edtStubState.serialCommands[0] == "temp on" );
1215 REQUIRE( g_edtStubState.serialCommands[1] == "temp 15.500000" );
1216 REQUIRE( app.m_tempControlStatusSet == true );
1217 REQUIRE( app.m_tempControlStatus == true );
1218 }
1219
1220 SECTION( "setTempControl returns -1 on serial failure once power is already off" )
1221 {
1222 static_cast<MagAOXAppT &>( app ).m_powerState = 0;
1223 app.m_powerTargetState = 0;
1224 app.m_tempControlStatusSet = true;
1225 app.m_tempControlStatus = false;
1226 app.m_ccdTempSetpt = 15.5f;
1227
1228 queueSerialResponse( "", -1 );
1229
1230 REQUIRE( app.setTempControl() == -1 );
1231 REQUIRE( g_edtStubState.serialCommands.size() == 1 );
1232 REQUIRE( g_edtStubState.serialCommands[0] == "temp on" );
1233 }
1234
1235 SECTION( "setTempControl returns a software error on serial failure while still powered" )
1236 {
1237 app.m_tempControlStatusSet = true;
1238 app.m_tempControlStatus = false;
1239 app.m_ccdTempSetpt = 15.5f;
1240
1241 queueSerialResponse( "", -1 );
1242
1243 REQUIRE( app.setTempControl() == -1 );
1244 REQUIRE( g_edtStubState.serialCommands.size() == 1 );
1245 REQUIRE( g_edtStubState.serialCommands[0] == "temp on" );
1246 }
1247
1248 SECTION( "setTempSetPt rejects out-of-range high setpoints without serial traffic" )
1249 {
1250 app.m_ccdTempSetpt = 30.0f;
1251
1252 REQUIRE( app.setTempSetPt() == -1 );
1253 REQUIRE( g_edtStubState.serialCommands.empty() );
1254 }
1255
1256 SECTION( "setTempSetPt rejects out-of-range low setpoints without serial traffic" )
1257 {
1258 app.m_ccdTempSetpt = -50.1f;
1259
1260 REQUIRE( app.setTempSetPt() == -1 );
1261 REQUIRE( g_edtStubState.serialCommands.empty() );
1262 }
1263
1264 SECTION( "setTempSetPt sends valid setpoints to the camera" )
1265 {
1266 app.m_ccdTempSetpt = 12.25f;
1267
1268 queueSerialResponse( "setpoint updated\n" );
1269
1270 REQUIRE( app.setTempSetPt() == 0 );
1271 REQUIRE( g_edtStubState.serialCommands.size() == 1 );
1272 REQUIRE( g_edtStubState.serialCommands[0] == "temp 12.250000" );
1273 }
1274
1275 SECTION( "setTempSetPt returns -1 on serial failure once power is already off" )
1276 {
1277 static_cast<MagAOXAppT &>( app ).m_powerState = 0;
1278 app.m_powerTargetState = 0;
1279 app.m_ccdTempSetpt = 12.25f;
1280
1281 queueSerialResponse( "", -1 );
1282
1283 REQUIRE( app.setTempSetPt() == -1 );
1284 REQUIRE( g_edtStubState.serialCommands.size() == 1 );
1285 REQUIRE( g_edtStubState.serialCommands[0] == "temp 12.250000" );
1286 }
1287
1288 SECTION( "setTempSetPt returns a software error on serial failure while still powered" )
1289 {
1290 app.m_ccdTempSetpt = 12.25f;
1291
1292 queueSerialResponse( "", -1 );
1293
1294 REQUIRE( app.setTempSetPt() == -1 );
1295 REQUIRE( g_edtStubState.serialCommands.size() == 1 );
1296 REQUIRE( g_edtStubState.serialCommands[0] == "temp 12.250000" );
1297 }
1298}
1299
1300/// Verify the shutter adapter rejects invalid requests before touching the DSS threads.
1301/**
1302 * \ingroup ocam2KCtrl_unit_test
1303 */
1304TEST_CASE( "ocam2KCtrl setShutter rejects invalid shutter requests", "[ocam2KCtrl]" )
1305{
1306 ocam2KCtrl_test app;
1307
1308 resetStubState();
1309
1310 // clang-format off
1311 #ifdef OCAM2KCTRL_TEST_DOXYGEN_REF
1313 #endif
1314 // clang-format on
1315
1316 REQUIRE( app.setShutter( 2 ) == -1 );
1317}
1318
1319/// Verify simple helper methods update local app state without hardware dependencies.
1320/**
1321 * \ingroup ocam2KCtrl_unit_test
1322 */
1323TEST_CASE( "ocam2KCtrl helper interfaces reset local state and power-off lifecycle flags", "[ocam2KCtrl]" )
1324{
1325 ocam2KCtrl_test app;
1326
1327 resetStubState();
1328
1329 // clang-format off
1330 #ifdef OCAM2KCTRL_TEST_DOXYGEN_REF
1338 #endif
1339 // clang-format on
1340
1341 app.m_tempControlStatusSet = true;
1342 app.m_tempControlStatus = true;
1343 REQUIRE( app.powerOnDefaults() == 0 );
1344 REQUIRE( app.m_tempControlStatusSet == false );
1345 REQUIRE( app.m_tempControlStatus == false );
1346
1347 app.m_fps = 250.5f;
1348 REQUIRE( app.fps() == Approx( 250.5f ) );
1349
1350 REQUIRE( app.setExpTime() == 0 );
1351 REQUIRE( app.setNextROI() == 0 );
1352
1353 REQUIRE( app.ensureSyncStream() == 0 );
1354 REQUIRE( app.m_syncImageStream != nullptr );
1355
1356 app.m_powerOnCounter = -1;
1357 app.m_poweredOn = false;
1358 app.m_width = 17;
1359 app.m_height = 19;
1360 app.m_circBuffLength = 23;
1361 app.m_reconfig = false;
1362
1363 REQUIRE( app.onPowerOff() == 0 );
1364 REQUIRE( app.m_powerOnCounter == 0 );
1365 REQUIRE( app.m_poweredOn == true );
1366 REQUIRE( app.m_width == 0 );
1367 REQUIRE( app.m_height == 0 );
1368 REQUIRE( app.m_circBuffLength == 1 );
1369 REQUIRE( app.m_reconfig == true );
1370
1371 REQUIRE( app.whilePowerOff() == 0 );
1372
1373 REQUIRE( app.appShutdown() == 0 );
1374 REQUIRE( app.m_syncImageStream == nullptr );
1375}
1376
1377/// Verify acquisition startup requests EDT buffering and resets the image counter.
1378/**
1379 * \ingroup ocam2KCtrl_unit_test
1380 */
1381TEST_CASE( "ocam2KCtrl startAcquisition resets frame tracking and starts EDT buffers", "[ocam2KCtrl]" )
1382{
1383 ocam2KCtrl_test app;
1384
1385 resetStubState();
1386
1387 // clang-format off
1388 #ifdef OCAM2KCTRL_TEST_DOXYGEN_REF
1390 #endif
1391 // clang-format on
1392
1393 app.m_numBuffs = 6;
1394 app.m_lastImageNumber = 99;
1395
1396 REQUIRE( app.startAcquisition() == 0 );
1397 REQUIRE( app.m_lastImageNumber == -1 );
1398 REQUIRE( g_edtStubState.startImagesCalls == 1 );
1399 REQUIRE( g_edtStubState.lastStartNumBuffs == 6 );
1400}
1401
1402/// Verify frame acquisition timestamps and frame-number handling across valid and invalid sequences.
1403/**
1404 * \ingroup ocam2KCtrl_unit_test
1405 */
1406TEST_CASE( "ocam2KCtrl acquireAndCheckValid handles valid, skipped, and corrupt frame numbers", "[ocam2KCtrl]" )
1407{
1408 ocam2KCtrl_test app;
1409
1410 resetStubState();
1411
1412 // clang-format off
1413 #ifdef OCAM2KCTRL_TEST_DOXYGEN_REF
1415 #endif
1416 // clang-format on
1417
1418 static_cast<MagAOXAppT &>( app ).m_powerState = 1;
1419 app.m_powerTargetState = 1;
1420 app.m_modeName = "science";
1421
1422 SECTION( "first valid frame initializes the previous-frame tracker" )
1423 {
1424 resetStubState();
1425
1426 g_edtStubState.waitTimeSec = 12;
1427 g_edtStubState.waitTimeNsec = 345;
1428 setStubFrameNumber( 101 );
1429
1430 app.m_lastImageNumber = -1;
1431
1432 REQUIRE( app.acquireAndCheckValid() == 0 );
1433 REQUIRE( app.m_currImageTimestamp.tv_sec == 12 );
1434 REQUIRE( app.m_currImageTimestamp.tv_nsec == 345 );
1435 REQUIRE( app.m_currImageNumber == 101 );
1436 REQUIRE( app.m_lastImageNumber == 101 );
1437 REQUIRE( g_edtStubState.startImageCalls == 1 );
1438 }
1439
1440 SECTION( "small skipped-frame gaps request reconfiguration" )
1441 {
1442 resetStubState();
1443
1444 setStubFrameNumber( 15 );
1445
1446 app.m_lastImageNumber = 12;
1447 app.m_nextMode = "";
1448 app.m_reconfig = false;
1449
1450 REQUIRE( app.acquireAndCheckValid() == 1 );
1451 REQUIRE( app.m_lastImageNumber == -1 );
1452 REQUIRE( app.m_nextMode == "science" );
1453 REQUIRE( app.m_reconfig == true );
1454 }
1455
1456 SECTION( "large frame-number jumps on powered hardware are treated as corruption" )
1457 {
1458 resetStubState();
1459
1460 setStubFrameNumber( 500 );
1461
1462 app.m_lastImageNumber = 12;
1463 app.m_nextMode = "";
1464 app.m_reconfig = false;
1465
1466 REQUIRE( app.acquireAndCheckValid() == 1 );
1467 REQUIRE( app.m_lastImageNumber == -1 );
1468 REQUIRE( app.m_nextMode == "science" );
1469 REQUIRE( app.m_reconfig == true );
1470 }
1471
1472 SECTION( "large frame-number jumps during power loss return an error instead of reconfiguring" )
1473 {
1474 resetStubState();
1475
1476 setStubFrameNumber( 500 );
1477
1478 static_cast<MagAOXAppT &>( app ).m_powerState = 0;
1479 app.m_powerTargetState = 0;
1480 app.m_lastImageNumber = 12;
1481 app.m_nextMode = "";
1482 app.m_reconfig = false;
1483
1484 REQUIRE( app.acquireAndCheckValid() == -1 );
1485 REQUIRE( app.m_nextMode == "" );
1486 REQUIRE( app.m_reconfig == false );
1487 }
1488}
1489
1490/// Verify the raw image pointer is passed through to the OCAM descramble routine.
1491/**
1492 * \ingroup ocam2KCtrl_unit_test
1493 */
1494TEST_CASE( "ocam2KCtrl loadImageIntoStream uses the OCAM descramble output", "[ocam2KCtrl]" )
1495{
1496 ocam2KCtrl_test app;
1497 std::array<int16_t, 4> rawImage{ 1, 2, 3, 4 };
1498 std::array<int16_t, 4> destImage{ 0, 0, 0, 0 };
1499
1500 resetStubState();
1501
1502 // clang-format off
1503 #ifdef OCAM2KCTRL_TEST_DOXYGEN_REF
1505 #endif
1506 // clang-format on
1507
1508 SECTION( "the non-digital path writes the descrambled frame directly to the destination" )
1509 {
1510 app.m_ocam2_id = 7;
1511 app.m_digitalBin = false;
1512 app.m_image_p = reinterpret_cast<u_char *>( rawImage.data() );
1513
1514 g_ocam2StubState.imageNumber = 44;
1515 g_ocam2StubState.outputImage = { 10, 20, 30, 40 };
1516
1517 REQUIRE( app.loadImageIntoStream( destImage.data() ) == 0 );
1518 REQUIRE( g_ocam2StubState.lastId == 7 );
1519 REQUIRE( g_ocam2StubState.lastRaw == rawImage.data() );
1520 REQUIRE( g_ocam2StubState.lastOutput == destImage.data() );
1521 REQUIRE( destImage[0] == 10 );
1522 REQUIRE( destImage[1] == 20 );
1523 REQUIRE( destImage[2] == 30 );
1524 REQUIRE( destImage[3] == 40 );
1525 }
1526
1527 SECTION( "the digital-binning path descrambles into the work image before filling the output frame" )
1528 {
1529 app.m_ocam2_id = 8;
1530 app.m_digitalBin = true;
1531 app.m_digitalBinX = 2;
1532 app.m_digitalBinY = 1;
1533 app.m_width = 2;
1534 app.m_height = 2;
1535 app.m_image_p = reinterpret_cast<u_char *>( rawImage.data() );
1536 app.m_digitalBinWork.resize( 4, 2 );
1537
1538 destImage.fill( 0 );
1539 g_ocam2StubState.imageNumber = 55;
1540 g_ocam2StubState.outputImage = { 7, 7, 7, 7, 7, 7, 7, 7 };
1541
1542 REQUIRE( app.loadImageIntoStream( destImage.data() ) == 0 );
1543 REQUIRE( g_ocam2StubState.lastId == 8 );
1544 REQUIRE( g_ocam2StubState.lastRaw == rawImage.data() );
1545 REQUIRE( g_ocam2StubState.lastOutput == app.m_digitalBinWork.data() );
1546 REQUIRE( destImage[0] == 7 );
1547 REQUIRE( destImage[1] == 7 );
1548 REQUIRE( destImage[2] == 7 );
1549 REQUIRE( destImage[3] == 7 );
1550 }
1551}
1552
1553/// Verify the serial gain helpers accept valid responses and handle malformed or tripped ones.
1554/**
1555 * \ingroup ocam2KCtrl_unit_test
1556 */
1557TEST_CASE( "ocam2KCtrl serial gain helpers handle valid and invalid responses", "[ocam2KCtrl]" )
1558{
1559 ocam2KCtrl_test app;
1560
1561 resetStubState();
1562
1563 // clang-format off
1564 #ifdef OCAM2KCTRL_TEST_DOXYGEN_REF
1568 #endif
1569 // clang-format on
1570
1571 setPoweredOn( app );
1572
1573 SECTION( "valid EM gain response updates the cached value" )
1574 {
1575 app.m_emGain = 1;
1576
1577 queueSerialResponse( "Gain set to 42 \n\n" );
1578
1579 REQUIRE( app.getEMGain() == 0 );
1580 REQUIRE( g_edtStubState.serialCommands.size() == 1 );
1581 REQUIRE( g_edtStubState.serialCommands[0] == "gain" );
1582 REQUIRE( app.m_emGain == 42 );
1583 }
1584
1585 SECTION( "malformed EM gain response returns an error and keeps the previous value" )
1586 {
1587 app.m_emGain = 77;
1588
1589 queueSerialResponse( "Gain set to \n\n" );
1590
1591 REQUIRE( app.getEMGain() == -1 );
1592 REQUIRE( g_edtStubState.serialCommands.size() == 1 );
1593 REQUIRE( g_edtStubState.serialCommands[0] == "gain" );
1594 REQUIRE( app.m_emGain == 77 );
1595 }
1596
1597 SECTION( "malformed EM gain responses while powered off return -1 immediately" )
1598 {
1599 static_cast<MagAOXAppT &>( app ).m_powerState = 0;
1600 app.m_powerTargetState = 0;
1601 app.m_emGain = 77;
1602
1603 queueSerialResponse( "Gain set to \n\n" );
1604
1605 REQUIRE( app.getEMGain() == -1 );
1606 REQUIRE( g_edtStubState.serialCommands.size() == 1 );
1607 REQUIRE( g_edtStubState.serialCommands[0] == "gain" );
1608 REQUIRE( app.m_emGain == 77 );
1609 }
1610
1611 SECTION( "HV trip response forces the cached EM gain back to the safe minimum" )
1612 {
1613 app.m_emGain = 77;
1614
1615 queueSerialResponse( "HV trip\n" );
1616
1617 REQUIRE( app.getEMGain() == -1 );
1618 REQUIRE( g_edtStubState.serialCommands.size() == 1 );
1619 REQUIRE( g_edtStubState.serialCommands[0] == "gain" );
1620 REQUIRE( app.m_emGain == 1 );
1621 }
1622
1623 SECTION( "resetEMProtection marks the protection state as reset" )
1624 {
1625 app.m_protectionReset = false;
1626 app.m_protectionResetConfirmed = 2;
1627
1628 queueSerialResponse( "Protection reset\n" );
1629
1630 REQUIRE( app.resetEMProtection() == 0 );
1631 REQUIRE( g_edtStubState.serialCommands.size() == 1 );
1632 REQUIRE( g_edtStubState.serialCommands[0] == "protection reset" );
1633 REQUIRE( app.m_protectionReset == true );
1634 REQUIRE( app.m_protectionResetConfirmed == 0 );
1635 }
1636
1637 SECTION( "resetEMProtection returns a software error while still powered" )
1638 {
1639 app.m_indiP_emProt = pcf::IndiProperty( pcf::IndiProperty::Text );
1640 app.m_indiP_emProt.add( pcf::IndiElement( "status" ) );
1641 app.m_indiP_emProt["status"].set( "CONFIRM" );
1642
1643 queueSerialResponse( "", -1 );
1644
1645 REQUIRE( app.resetEMProtection() == -1 );
1646 REQUIRE( g_edtStubState.serialCommands.size() == 1 );
1647 REQUIRE( g_edtStubState.serialCommands[0] == "protection reset" );
1648 }
1649
1650 SECTION( "resetEMProtection returns -1 once power is already off" )
1651 {
1652 static_cast<MagAOXAppT &>( app ).m_powerState = 0;
1653 app.m_powerTargetState = 0;
1654
1655 queueSerialResponse( "", -1 );
1656
1657 REQUIRE( app.resetEMProtection() == -1 );
1658 REQUIRE( g_edtStubState.serialCommands.size() == 1 );
1659 REQUIRE( g_edtStubState.serialCommands[0] == "protection reset" );
1660 }
1661
1662 SECTION( "setEMGain refuses unsafe requests before protection reset" )
1663 {
1664 app.m_protectionReset = false;
1665 app.m_emGainSet = 10;
1666
1667 REQUIRE( app.setEMGain() == 0 );
1668 REQUIRE( g_edtStubState.serialCommands.empty() );
1669 }
1670
1671 SECTION( "setEMGain refuses out-of-range requests without serial traffic" )
1672 {
1673 app.m_protectionReset = true;
1674 app.m_maxEMGain = 100;
1675 app.m_emGainSet = 200;
1676
1677 REQUIRE( app.setEMGain() == 0 );
1678 REQUIRE( g_edtStubState.serialCommands.empty() );
1679 }
1680
1681 SECTION( "setEMGain sends the requested gain after protection reset" )
1682 {
1683 app.m_protectionReset = true;
1684 app.m_maxEMGain = 600;
1685 app.m_emGainSet = 25;
1686
1687 queueSerialResponse( "Gain set to 25 \n\n" );
1688
1689 REQUIRE( app.setEMGain() == 0 );
1690 REQUIRE( g_edtStubState.serialCommands.size() == 1 );
1691 REQUIRE( g_edtStubState.serialCommands[0] == "gain 25" );
1692 }
1693
1694 SECTION( "setEMGain returns -1 on serial failure once power is already off" )
1695 {
1696 static_cast<MagAOXAppT &>( app ).m_powerState = 0;
1697 app.m_powerTargetState = 0;
1698 app.m_protectionReset = true;
1699 app.m_maxEMGain = 600;
1700 app.m_emGainSet = 25;
1701
1702 queueSerialResponse( "", -1 );
1703
1704 REQUIRE( app.setEMGain() == -1 );
1705 REQUIRE( g_edtStubState.serialCommands.size() == 1 );
1706 REQUIRE( g_edtStubState.serialCommands[0] == "gain 25" );
1707 }
1708
1709 SECTION( "setEMGain returns a software error on serial failure while still powered" )
1710 {
1711 app.m_protectionReset = true;
1712 app.m_maxEMGain = 600;
1713 app.m_emGainSet = 25;
1714
1715 queueSerialResponse( "", -1 );
1716
1717 REQUIRE( app.setEMGain() == -1 );
1718 REQUIRE( g_edtStubState.serialCommands.size() == 1 );
1719 REQUIRE( g_edtStubState.serialCommands[0] == "gain 25" );
1720 }
1721}
1722
1723/// Verify the serial setter commands send the expected OCAM command sequence.
1724/**
1725 * \ingroup ocam2KCtrl_unit_test
1726 */
1727TEST_CASE( "ocam2KCtrl serial setter commands send the expected sequence", "[ocam2KCtrl]" )
1728{
1729 ocam2KCtrl_test app;
1730
1731 resetStubState();
1732
1733 // clang-format off
1734 #ifdef OCAM2KCTRL_TEST_DOXYGEN_REF
1737 #endif
1738 // clang-format on
1739
1740 setPoweredOn( app );
1741
1742 SECTION( "setFPS sends the requested rate and queues a reconfiguration" )
1743 {
1744 app.m_synchro = false;
1745 app.m_fpsSet = 250.5f;
1746 app.m_modeName = "science";
1747 app.m_nextMode = "";
1748 app.m_reconfig = false;
1749
1750 queueSerialResponse( "fps updated\n" );
1751
1752 REQUIRE( app.setFPS() == 0 );
1753 REQUIRE( g_edtStubState.serialCommands.size() == 1 );
1754 REQUIRE( g_edtStubState.serialCommands[0] == "fps 250.500000" );
1755 REQUIRE( app.m_nextMode == "science" );
1756 REQUIRE( app.m_reconfig == true );
1757 }
1758
1759 SECTION( "setFPS returns a software error on serial failure while still powered" )
1760 {
1761 app.m_synchro = false;
1762 app.m_fpsSet = 250.5f;
1763
1764 queueSerialResponse( "", -1 );
1765
1766 REQUIRE( app.setFPS() == -1 );
1767 REQUIRE( g_edtStubState.serialCommands.size() == 1 );
1768 REQUIRE( g_edtStubState.serialCommands[0] == "fps 250.500000" );
1769 }
1770
1771 SECTION( "setFPS uses the sync-device property path when synchro is enabled" )
1772 {
1773 app.m_synchro = true;
1774 app.m_fpsSet = 88.5f;
1775
1776 REQUIRE( app.setFPS() == 0 );
1777 REQUIRE( g_edtStubState.serialCommands.empty() );
1778 }
1779
1780 SECTION( "setFPS returns -1 on serial failure once power is already off" )
1781 {
1782 static_cast<MagAOXAppT &>( app ).m_powerState = 0;
1783 app.m_powerTargetState = 0;
1784 app.m_synchro = false;
1785 app.m_fpsSet = 250.5f;
1786
1787 queueSerialResponse( "", -1 );
1788
1789 REQUIRE( app.setFPS() == -1 );
1790 REQUIRE( g_edtStubState.serialCommands.size() == 1 );
1791 REQUIRE( g_edtStubState.serialCommands[0] == "fps 250.500000" );
1792 }
1793
1794 SECTION( "setSynchro off uses the OCAM serial path for both synchro and FPS" )
1795 {
1796 app.m_synchroSet = false;
1797 app.m_fpsSet = 175.0f;
1798 app.m_indiP_synchro = pcf::IndiProperty( pcf::IndiProperty::Switch );
1799 app.m_indiP_synchro.add( pcf::IndiElement( "toggle", pcf::IndiElement::On ) );
1800
1801 queueSerialResponse( "fps max\n" );
1802 queueSerialResponse( "synchro off\n" );
1803 queueSerialResponse( "fps restored\n" );
1804
1805 REQUIRE( app.setSynchro() == 0 );
1806 REQUIRE( g_edtStubState.serialCommands.size() == 3 );
1807 REQUIRE( g_edtStubState.serialCommands[0] == "fps 0" );
1808 REQUIRE( g_edtStubState.serialCommands[1] == "synchro off" );
1809 REQUIRE( g_edtStubState.serialCommands[2] == "fps 175.000000" );
1810 REQUIRE( app.m_synchro == false );
1811 }
1812
1813 SECTION( "setSynchro on updates the local synchro state and leaves FPS to the sync device" )
1814 {
1815 app.m_synchroSet = true;
1816 app.m_fpsSet = 175.0f;
1817 app.m_indiP_synchro = pcf::IndiProperty( pcf::IndiProperty::Switch );
1818 app.m_indiP_synchro.add( pcf::IndiElement( "toggle", pcf::IndiElement::Off ) );
1819
1820 queueSerialResponse( "fps max\n" );
1821 queueSerialResponse( "synchro on\n" );
1822
1823 REQUIRE( app.setSynchro() == 0 );
1824 REQUIRE( g_edtStubState.serialCommands.size() == 2 );
1825 REQUIRE( g_edtStubState.serialCommands[0] == "fps 0" );
1826 REQUIRE( g_edtStubState.serialCommands[1] == "synchro on" );
1827 REQUIRE( app.m_synchro == true );
1828 }
1829
1830 SECTION( "setSynchro returns a software error when the initial fps-max command fails on powered hardware" )
1831 {
1832 app.m_synchroSet = false;
1833 app.m_fpsSet = 175.0f;
1834
1835 queueSerialResponse( "", -1 );
1836
1837 REQUIRE( app.setSynchro() == -1 );
1838 REQUIRE( g_edtStubState.serialCommands.size() == 1 );
1839 REQUIRE( g_edtStubState.serialCommands[0] == "fps 0" );
1840 }
1841
1842 SECTION( "setSynchro returns a software error when the synchro command fails on powered hardware" )
1843 {
1844 app.m_synchroSet = true;
1845 app.m_fpsSet = 175.0f;
1846
1847 queueSerialResponse( "fps max\n" );
1848 queueSerialResponse( "", -1 );
1849
1850 REQUIRE( app.setSynchro() == -1 );
1851 REQUIRE( g_edtStubState.serialCommands.size() == 2 );
1852 REQUIRE( g_edtStubState.serialCommands[0] == "fps 0" );
1853 REQUIRE( g_edtStubState.serialCommands[1] == "synchro on" );
1854 }
1855
1856 SECTION( "setSynchro returns -1 when the initial fps-max command fails while powered off" )
1857 {
1858 static_cast<MagAOXAppT &>( app ).m_powerState = 0;
1859 app.m_powerTargetState = 0;
1860 app.m_synchroSet = false;
1861 app.m_fpsSet = 175.0f;
1862
1863 queueSerialResponse( "", -1 );
1864
1865 REQUIRE( app.setSynchro() == -1 );
1866 REQUIRE( g_edtStubState.serialCommands.size() == 1 );
1867 REQUIRE( g_edtStubState.serialCommands[0] == "fps 0" );
1868 }
1869
1870 SECTION( "setSynchro returns -1 when the synchro command fails while powered off" )
1871 {
1872 static_cast<MagAOXAppT &>( app ).m_powerState = 0;
1873 app.m_powerTargetState = 0;
1874 app.m_synchroSet = true;
1875 app.m_fpsSet = 175.0f;
1876
1877 queueSerialResponse( "fps max\n" );
1878 queueSerialResponse( "", -1 );
1879
1880 REQUIRE( app.setSynchro() == -1 );
1881 REQUIRE( g_edtStubState.serialCommands.size() == 2 );
1882 REQUIRE( g_edtStubState.serialCommands[0] == "fps 0" );
1883 REQUIRE( g_edtStubState.serialCommands[1] == "synchro on" );
1884 }
1885}
1886
1887/// Verify acquisition configuration programs the OCAM mode and handles invalid frame shapes.
1888/**
1889 * \ingroup ocam2KCtrl_unit_test
1890 */
1891TEST_CASE( "ocam2KCtrl configureAcquisition handles valid and invalid OCAM modes", "[ocam2KCtrl]" )
1892{
1893 ocam2KCtrl_test app;
1894 dev::cameraConfig config;
1895
1896 resetStubState();
1897
1898 // clang-format off
1899 #ifdef OCAM2KCTRL_TEST_DOXYGEN_REF
1901 #endif
1902 // clang-format on
1903
1904 setPoweredOn( app );
1905
1906 app.m_configDir = "/tmp";
1907 app.m_ocamDescrambleFile = "ocam_descramble_stub.txt";
1908 app.m_modeName = "science";
1909 config.m_serialCommand = "mode science";
1910 config.m_binningX = 1;
1911 config.m_binningY = 1;
1912 config.m_digitalBinX = 2;
1913 config.m_digitalBinY = 3;
1914 app.m_cameraModes["science"] = config;
1915
1916 SECTION( "configureAcquisition sets up the SDK, digital binning, and sync stream" )
1917 {
1918 app.m_raw_height = 121;
1919 app.m_fpsSet = 50.0f;
1920 app.m_synchroSet = false;
1921 app.m_ocam2_id = 9;
1922
1923 queueSerialResponse( "mode set\n" );
1924 queueSerialResponse( "fps set\n" );
1925 queueSerialResponse( "fps max\n" );
1926 queueSerialResponse( "synchro off\n" );
1927 queueSerialResponse( "fps restored\n" );
1928
1929 REQUIRE( app.configureAcquisition() == 0 );
1930 REQUIRE( g_edtStubState.serialCommands.size() == 5 );
1931 REQUIRE( g_edtStubState.serialCommands[0] == "mode science" );
1932 REQUIRE( g_edtStubState.serialCommands[1] == "fps 50.000000" );
1933 REQUIRE( g_edtStubState.serialCommands[2] == "fps 0" );
1934 REQUIRE( g_edtStubState.serialCommands[3] == "synchro off" );
1935 REQUIRE( g_edtStubState.serialCommands[4] == "fps 50.000000" );
1936 REQUIRE( g_ocam2StubState.exitCalls == 1 );
1937 REQUIRE( g_ocam2StubState.lastExitId == 9 );
1938 REQUIRE( g_ocam2StubState.lastInitMode == OCAM2_NORMAL );
1939 REQUIRE( g_ocam2StubState.lastDescrambleFile == "/tmp/ocam_descramble_stub.txt" );
1940 REQUIRE( app.m_ocam2_id == 1 );
1941 REQUIRE( app.m_currentROI.x == Approx( 119.5 ) );
1942 REQUIRE( app.m_currentROI.y == Approx( 119.5 ) );
1943 REQUIRE( app.m_currentROI.w == Approx( 240.0 ) );
1944 REQUIRE( app.m_currentROI.h == Approx( 240.0 ) );
1945 REQUIRE( app.m_currentROI.bin_x == 1 );
1946 REQUIRE( app.m_currentROI.bin_y == 1 );
1947 REQUIRE( app.m_digitalBin == true );
1948 REQUIRE( app.m_width == 120 );
1949 REQUIRE( app.m_height == 80 );
1950 REQUIRE( app.m_dataType == _DATATYPE_UINT16 );
1951 REQUIRE( app.m_syncImageStream != nullptr );
1952 REQUIRE( app.state() == stateCodes::OPERATING );
1953 }
1954
1955 SECTION( "configureAcquisition keeps the full OCAM frame size when digital binning is disabled" )
1956 {
1957 app.m_raw_height = 121;
1958 app.m_fpsSet = 0.0f;
1959 app.m_synchroSet = false;
1960 app.m_cameraModes["science"].m_digitalBinX = 1;
1961 app.m_cameraModes["science"].m_digitalBinY = 1;
1962
1963 queueSerialResponse( "mode set\n" );
1964 queueSerialResponse( "fps max\n" );
1965 queueSerialResponse( "synchro off\n" );
1966 queueSerialResponse( "fps restored\n" );
1967
1968 REQUIRE( app.configureAcquisition() == 0 );
1969 REQUIRE( g_ocam2StubState.lastInitMode == OCAM2_NORMAL );
1970 REQUIRE( app.m_digitalBin == false );
1971 REQUIRE( app.m_width == 240 );
1972 REQUIRE( app.m_height == 240 );
1973 }
1974
1975 SECTION( "configureAcquisition selects OCAM binning mode for 62-row raw frames" )
1976 {
1977 app.m_raw_height = 62;
1978 app.m_fpsSet = 0.0f;
1979 app.m_synchroSet = false;
1980 app.m_cameraModes["science"].m_digitalBinX = 1;
1981 app.m_cameraModes["science"].m_digitalBinY = 1;
1982
1983 queueSerialResponse( "mode set\n" );
1984 queueSerialResponse( "fps max\n" );
1985 queueSerialResponse( "synchro off\n" );
1986 queueSerialResponse( "fps restored\n" );
1987
1988 REQUIRE( app.configureAcquisition() == 0 );
1989 REQUIRE( g_ocam2StubState.lastInitMode == OCAM2_BINNING );
1990 REQUIRE( app.m_width == 120 );
1991 REQUIRE( app.m_height == 120 );
1992 }
1993
1994 SECTION( "configureAcquisition selects OCAM 1x3 binning mode for 41-row raw frames" )
1995 {
1996 app.m_raw_height = 41;
1997 app.m_fpsSet = 0.0f;
1998 app.m_synchroSet = false;
1999 app.m_cameraModes["science"].m_digitalBinX = 1;
2000 app.m_cameraModes["science"].m_digitalBinY = 1;
2001
2002 queueSerialResponse( "mode set\n" );
2003 queueSerialResponse( "fps max\n" );
2004 queueSerialResponse( "synchro off\n" );
2005 queueSerialResponse( "fps restored\n" );
2006
2007 REQUIRE( app.configureAcquisition() == 0 );
2008 REQUIRE( g_ocam2StubState.lastInitMode == OCAM2_BINNING1x3 );
2009 REQUIRE( app.m_width == 240 );
2010 REQUIRE( app.m_height == 80 );
2011 }
2012
2013 SECTION( "configureAcquisition selects OCAM 1x4 binning mode for 31-row raw frames" )
2014 {
2015 app.m_raw_height = 31;
2016 app.m_fpsSet = 0.0f;
2017 app.m_synchroSet = false;
2018 app.m_cameraModes["science"].m_digitalBinX = 1;
2019 app.m_cameraModes["science"].m_digitalBinY = 1;
2020
2021 queueSerialResponse( "mode set\n" );
2022 queueSerialResponse( "fps max\n" );
2023 queueSerialResponse( "synchro off\n" );
2024 queueSerialResponse( "fps restored\n" );
2025
2026 REQUIRE( app.configureAcquisition() == 0 );
2027 REQUIRE( g_ocam2StubState.lastInitMode == OCAM2_BINNING1x4 );
2028 REQUIRE( app.m_width == 240 );
2029 REQUIRE( app.m_height == 60 );
2030 }
2031
2032 SECTION( "configureAcquisition reports a set-mode serial failure on powered hardware" )
2033 {
2034 app.m_raw_height = 121;
2035 app.m_fpsSet = 0.0f;
2036 app.m_synchroSet = false;
2037
2038 queueSerialResponse( "", -1 );
2039
2040 REQUIRE( app.configureAcquisition() == -1 );
2041 REQUIRE( g_edtStubState.serialCommands.size() == 1 );
2042 REQUIRE( g_edtStubState.serialCommands[0] == "mode science" );
2043 }
2044
2045 SECTION( "configureAcquisition returns -1 quietly once power is already off during the set-mode command" )
2046 {
2047 static_cast<MagAOXAppT &>( app ).m_powerState = 0;
2048 app.m_powerTargetState = 0;
2049 app.m_raw_height = 121;
2050 app.m_fpsSet = 0.0f;
2051 app.m_synchroSet = false;
2052
2053 queueSerialResponse( "", -1 );
2054
2055 REQUIRE( app.configureAcquisition() == -1 );
2056 REQUIRE( g_edtStubState.serialCommands.size() == 1 );
2057 REQUIRE( g_edtStubState.serialCommands[0] == "mode science" );
2058 }
2059
2060 SECTION( "configureAcquisition logs but continues when setSynchro fails" )
2061 {
2062 app.m_raw_height = 121;
2063 app.m_fpsSet = 0.0f;
2064 app.m_synchroSet = false;
2065 app.m_cameraModes["science"].m_digitalBinX = 1;
2066 app.m_cameraModes["science"].m_digitalBinY = 1;
2067
2068 queueSerialResponse( "mode set\n" );
2069 queueSerialResponse( "fps max\n" );
2070 queueSerialResponse( "", -1 );
2071
2072 REQUIRE( app.configureAcquisition() == 0 );
2073 REQUIRE( g_edtStubState.serialCommands.size() == 3 );
2074 REQUIRE( g_edtStubState.serialCommands[0] == "mode science" );
2075 REQUIRE( g_edtStubState.serialCommands[1] == "fps 0" );
2076 REQUIRE( g_edtStubState.serialCommands[2] == "synchro off" );
2077 REQUIRE( g_ocam2StubState.lastInitMode == OCAM2_NORMAL );
2078 }
2079
2080 SECTION( "configureAcquisition returns -1 if OCAM initialization fails after power is lost" )
2081 {
2082 static_cast<MagAOXAppT &>( app ).m_powerState = 0;
2083 app.m_powerTargetState = 0;
2084 app.m_raw_height = 121;
2085 app.m_fpsSet = 0.0f;
2086 app.m_synchroSet = false;
2087 app.m_cameraModes["science"].m_digitalBinX = 1;
2088 app.m_cameraModes["science"].m_digitalBinY = 1;
2089 g_ocam2StubState.initReturn = OCAM2_ERROR;
2090
2091 queueSerialResponse( "mode set\n" );
2092 queueSerialResponse( "fps max\n" );
2093 queueSerialResponse( "synchro off\n" );
2094 queueSerialResponse( "fps restored\n" );
2095
2096 REQUIRE( app.configureAcquisition() == -1 );
2097 REQUIRE( app.m_syncImageStream == nullptr );
2098 }
2099
2100 SECTION( "configureAcquisition returns -1 if OCAM initialization fails on powered hardware" )
2101 {
2102 app.m_raw_height = 121;
2103 app.m_fpsSet = 0.0f;
2104 app.m_synchroSet = false;
2105 app.m_cameraModes["science"].m_digitalBinX = 1;
2106 app.m_cameraModes["science"].m_digitalBinY = 1;
2107 g_ocam2StubState.initReturn = OCAM2_ERROR;
2108
2109 queueSerialResponse( "mode set\n" );
2110 queueSerialResponse( "fps max\n" );
2111 queueSerialResponse( "synchro off\n" );
2112 queueSerialResponse( "fps restored\n" );
2113
2114 REQUIRE( app.configureAcquisition() == -1 );
2115 REQUIRE( app.m_syncImageStream == nullptr );
2116 }
2117
2118 SECTION( "configureAcquisition rejects unsupported raw frame heights" )
2119 {
2120 app.m_raw_height = 100;
2121 app.m_fpsSet = 0.0f;
2122 app.m_synchroSet = false;
2123
2124 queueSerialResponse( "mode set\n" );
2125 queueSerialResponse( "fps max\n" );
2126 queueSerialResponse( "synchro off\n" );
2127 queueSerialResponse( "fps restored\n" );
2128
2129 REQUIRE( app.configureAcquisition() == -1 );
2130 REQUIRE( g_edtStubState.serialCommands.size() == 4 );
2131 REQUIRE( g_edtStubState.serialCommands[0] == "mode science" );
2132 REQUIRE( g_edtStubState.serialCommands[1] == "fps 0" );
2133 REQUIRE( g_edtStubState.serialCommands[2] == "synchro off" );
2134 REQUIRE( g_edtStubState.serialCommands[3] == "fps 0.000000" );
2135 REQUIRE( g_ocam2StubState.exitCalls == 0 );
2136 REQUIRE( g_ocam2StubState.lastDescrambleFile.empty() );
2137 REQUIRE( app.m_syncImageStream == nullptr );
2138 }
2139}
2140
2141/// Verify the INDI callbacks update confirmation state and sync-frequency-driven reconfiguration.
2142/**
2143 * \ingroup ocam2KCtrl_unit_test
2144 */
2145TEST_CASE( "ocam2KCtrl INDI callbacks update local state", "[ocam2KCtrl]" )
2146{
2147 ocam2KCtrl_test app;
2148
2149 resetStubState();
2150
2151 // clang-format off
2152 #ifdef OCAM2KCTRL_TEST_DOXYGEN_REF
2153 XWCTEST_DOXYGEN_REF( ocam2KCtrl::newCallBack_m_indiP_emProtReset( pcf::IndiProperty() ) );
2154 XWCTEST_DOXYGEN_REF( ocam2KCtrl::setCallBack_m_indiP_syncFreq( pcf::IndiProperty() ) );
2155 #endif
2156 // clang-format on
2157
2158 setPoweredOn( app );
2159
2160 SECTION( "EM protection reset callback requires a confirmation before issuing the serial reset" )
2161 {
2162 pcf::IndiProperty ipRecv( pcf::IndiProperty::Switch );
2163
2164 app.m_indiP_emProtReset.setDevice( "ocam2KCtrl" );
2165 app.m_indiP_emProtReset.setName( "emProtReset" );
2166
2167 ipRecv.setDevice( "ocam2KCtrl" );
2168 ipRecv.setName( "emProtReset" );
2169 ipRecv.add( pcf::IndiElement( "request", pcf::IndiElement::On ) );
2170
2171 REQUIRE( app.newCallBack_m_indiP_emProtReset( ipRecv ) == 0 );
2172 REQUIRE( app.m_protectionResetConfirmed == 1 );
2173 REQUIRE( g_edtStubState.serialCommands.empty() );
2174
2175 queueSerialResponse( "Protection reset\n" );
2176
2177 REQUIRE( app.newCallBack_m_indiP_emProtReset( ipRecv ) == 0 );
2178 REQUIRE( g_edtStubState.serialCommands.size() == 1 );
2179 REQUIRE( g_edtStubState.serialCommands[0] == "protection reset" );
2180 REQUIRE( app.m_protectionReset == true );
2181 REQUIRE( app.m_protectionResetConfirmed == 0 );
2182 }
2183
2184 SECTION( "EM protection reset callback ignores requests while power is off" )
2185 {
2186 pcf::IndiProperty ipRecv( pcf::IndiProperty::Switch );
2187
2188 static_cast<MagAOXAppT &>( app ).m_powerState = 0;
2189 app.m_powerTargetState = 0;
2190 app.m_indiP_emProtReset.setDevice( "ocam2KCtrl" );
2191 app.m_indiP_emProtReset.setName( "emProtReset" );
2192 ipRecv.setDevice( "ocam2KCtrl" );
2193 ipRecv.setName( "emProtReset" );
2194 ipRecv.add( pcf::IndiElement( "request", pcf::IndiElement::On ) );
2195
2196 REQUIRE( app.newCallBack_m_indiP_emProtReset( ipRecv ) == 0 );
2197 REQUIRE( app.m_protectionResetConfirmed == 0 );
2198 REQUIRE( g_edtStubState.serialCommands.empty() );
2199 }
2200
2201 SECTION( "EM protection reset callback rejects the wrong property name" )
2202 {
2203 pcf::IndiProperty ipRecv( pcf::IndiProperty::Switch );
2204
2205 app.m_indiP_emProtReset.setDevice( "ocam2KCtrl" );
2206 app.m_indiP_emProtReset.setName( "emProtReset" );
2207 ipRecv.setDevice( "ocam2KCtrl" );
2208 ipRecv.setName( "wrongName" );
2209 ipRecv.add( pcf::IndiElement( "request", pcf::IndiElement::On ) );
2210
2211 REQUIRE( app.newCallBack_m_indiP_emProtReset( ipRecv ) == -1 );
2212 }
2213
2214 SECTION( "EM protection reset callback ignores requests without the request element or with it off" )
2215 {
2216 pcf::IndiProperty missingReq( pcf::IndiProperty::Switch );
2217 pcf::IndiProperty offReq( pcf::IndiProperty::Switch );
2218
2219 app.m_indiP_emProtReset.setDevice( "ocam2KCtrl" );
2220 app.m_indiP_emProtReset.setName( "emProtReset" );
2221
2222 missingReq.setDevice( "ocam2KCtrl" );
2223 missingReq.setName( "emProtReset" );
2224 offReq.setDevice( "ocam2KCtrl" );
2225 offReq.setName( "emProtReset" );
2226 offReq.add( pcf::IndiElement( "request", pcf::IndiElement::Off ) );
2227
2228 REQUIRE( app.newCallBack_m_indiP_emProtReset( missingReq ) == 0 );
2229 REQUIRE( app.newCallBack_m_indiP_emProtReset( offReq ) == 0 );
2230 REQUIRE( app.m_protectionResetConfirmed == 0 );
2231 REQUIRE( g_edtStubState.serialCommands.empty() );
2232 }
2233
2234 SECTION( "sync frequency callback queues a reconfiguration when synchro mode changes the effective FPS" )
2235 {
2236 pcf::IndiProperty ipRecv( pcf::IndiProperty::Number );
2237
2238 app.m_indiP_syncFreq.setDevice( "syncDevice" );
2239 app.m_indiP_syncFreq.setName( "C1freq" );
2240
2241 ipRecv.setDevice( "syncDevice" );
2242 ipRecv.setName( "C1freq" );
2243 ipRecv.add( pcf::IndiElement( "current" ) );
2244 ipRecv["current"] = 123.4;
2245
2246 app.m_synchro = true;
2247 app.m_fps = 100.0f;
2248 app.m_modeName = "science";
2249 app.m_nextMode = "";
2250 app.m_reconfig = false;
2251
2252 REQUIRE( app.setCallBack_m_indiP_syncFreq( ipRecv ) == 0 );
2253 REQUIRE( app.m_syncFreq == Approx( 123.4f ) );
2254 REQUIRE( app.m_fps == Approx( 123.4f ) );
2255 REQUIRE( app.m_nextMode == "science" );
2256 REQUIRE( app.m_reconfig == true );
2257 }
2258
2259 SECTION( "sync frequency callback rejects updates that do not include the current element" )
2260 {
2261 pcf::IndiProperty ipRecv( pcf::IndiProperty::Number );
2262
2263 app.m_indiP_syncFreq.setDevice( "syncDevice" );
2264 app.m_indiP_syncFreq.setName( "C1freq" );
2265 ipRecv.setDevice( "syncDevice" );
2266 ipRecv.setName( "C1freq" );
2267
2268 REQUIRE( app.setCallBack_m_indiP_syncFreq( ipRecv ) == -1 );
2269 }
2270}
2271
2272/// Verify the generated static INDI wrappers forward into the instance callbacks.
2273/**
2274 * \ingroup ocam2KCtrl_unit_test
2275 */
2276TEST_CASE( "ocam2KCtrl static INDI callback wrappers forward requests to the instance", "[ocam2KCtrl]" )
2277{
2278 ocam2KCtrl_test app;
2279
2280 resetStubState();
2281
2282 // clang-format off
2283 #ifdef OCAM2KCTRL_TEST_DOXYGEN_REF
2284 XWCTEST_DOXYGEN_REF( ocam2KCtrl::st_newCallBack_m_indiP_emProtReset( nullptr, pcf::IndiProperty() ) );
2285 XWCTEST_DOXYGEN_REF( ocam2KCtrl::st_setCallBack_m_indiP_syncFreq( nullptr, pcf::IndiProperty() ) );
2286 #endif
2287 // clang-format on
2288
2289 setPoweredOn( app );
2290
2291 SECTION( "the new-property wrapper forwards EM protection reset requests" )
2292 {
2293 pcf::IndiProperty ipRecv( pcf::IndiProperty::Switch );
2294
2295 app.m_indiP_emProtReset.setDevice( "ocam2KCtrl" );
2296 app.m_indiP_emProtReset.setName( "emProtReset" );
2297
2298 ipRecv.setDevice( "ocam2KCtrl" );
2299 ipRecv.setName( "emProtReset" );
2300 ipRecv.add( pcf::IndiElement( "request", pcf::IndiElement::On ) );
2301
2302 REQUIRE( ocam2KCtrl::st_newCallBack_m_indiP_emProtReset( &app, ipRecv ) == 0 );
2303 REQUIRE( app.m_protectionResetConfirmed == 1 );
2304 }
2305
2306 SECTION( "the set-property wrapper forwards sync frequency updates" )
2307 {
2308 pcf::IndiProperty ipRecv( pcf::IndiProperty::Number );
2309
2310 app.m_indiP_syncFreq.setDevice( "syncDevice" );
2311 app.m_indiP_syncFreq.setName( "C1freq" );
2312
2313 ipRecv.setDevice( "syncDevice" );
2314 ipRecv.setName( "C1freq" );
2315 ipRecv.add( pcf::IndiElement( "current" ) );
2316 ipRecv["current"] = 222.5;
2317
2318 app.m_synchro = true;
2319 app.m_fps = 100.0f;
2320 app.m_modeName = "science";
2321
2322 REQUIRE( ocam2KCtrl::st_setCallBack_m_indiP_syncFreq( &app, ipRecv ) == 0 );
2323 REQUIRE( app.m_syncFreq == Approx( 222.5f ) );
2324 REQUIRE( app.m_fps == Approx( 222.5f ) );
2325 REQUIRE( app.m_nextMode == "science" );
2326 REQUIRE( app.m_reconfig == true );
2327 }
2328}
2329
2330/// Verify telemetry wrapper helpers emit their records and interval checks trigger stale telemetry.
2331/**
2332 * \ingroup ocam2KCtrl_unit_test
2333 */
2334TEST_CASE( "ocam2KCtrl telemetry wrappers record snapshots and stale intervals", "[ocam2KCtrl]" )
2335{
2336 ocam2KCtrl_test app;
2337
2338 resetStubState();
2339
2340 // clang-format off
2341 #ifdef OCAM2KCTRL_TEST_DOXYGEN_REF
2343 XWCTEST_DOXYGEN_REF( ocam2KCtrl::recordTelem( static_cast<const MagAOX::logger::ocam_temps *>( nullptr ) ) );
2344 XWCTEST_DOXYGEN_REF( ocam2KCtrl::recordTelem( static_cast<const MagAOX::logger::telem_stdcam *>( nullptr ) ) );
2346 #endif
2347 // clang-format on
2348
2349 app.m_temps.CCD = 20.1f;
2350 app.m_temps.CPU = 41.0f;
2351 app.m_temps.POWER = 34.0f;
2352 app.m_temps.BIAS = 47.0f;
2353 app.m_temps.WATER = 24.2f;
2354 app.m_temps.LEFT = 33.0f;
2355 app.m_temps.RIGHT = 38.0f;
2356 app.m_temps.COOLING_POWER = 102.0f;
2357 app.m_modeName = "science";
2358 app.m_currentROI.x = 119.5;
2359 app.m_currentROI.y = 119.5;
2360 app.m_currentROI.w = 240.0;
2361 app.m_currentROI.h = 240.0;
2362 app.m_currentROI.bin_x = 1;
2363 app.m_currentROI.bin_y = 1;
2364 app.m_fps = 150.0f;
2365 app.m_emGain = 25.0f;
2366 app.m_ccdTemp = 20.1f;
2367 app.m_ccdTempSetpt = 20.5f;
2368 app.m_tempControlStatus = true;
2369 app.m_tempControlOnTarget = true;
2370 app.m_tempControlStatusStr = "ON TARGET";
2371 app.m_shutterStatus = "open";
2372 app.m_shutterState = 1;
2373 app.m_synchro = false;
2374 app.m_mna = 1.0;
2375 app.m_vara = 0.01;
2376 app.m_mnw = 2.0;
2377 app.m_varw = 0.04;
2378 app.m_mnwa = 3.0;
2379 app.m_varwa = 0.09;
2380
2381 SECTION( "recordTelem overloads forward directly to the per-type telemetry recorders" )
2382 {
2386
2387 REQUIRE( app.recordTelem( static_cast<const MagAOX::logger::ocam_temps *>( nullptr ) ) == 0 );
2388 REQUIRE( hasRecordedTime( MagAOX::logger::ocam_temps::lastRecord ) );
2389
2390 REQUIRE( app.recordTelem( static_cast<const MagAOX::logger::telem_stdcam *>( nullptr ) ) == 0 );
2391 REQUIRE( hasRecordedTime( MagAOX::logger::telem_stdcam::lastRecord ) );
2392
2393 REQUIRE( app.recordTelem( static_cast<const MagAOX::logger::telem_fgtimings *>( nullptr ) ) == 0 );
2394 REQUIRE( hasRecordedTime( MagAOX::logger::telem_fgtimings::lastRecord ) );
2395 }
2396
2397 SECTION( "checkRecordTimes emits all telemetry records when their intervals have elapsed" )
2398 {
2402
2403 app.m_maxInterval = 10.0;
2404 static_cast<MagAOXAppT &>( app ).m_loopPause = 0;
2405
2406 REQUIRE( app.checkRecordTimes() == 0 );
2407 REQUIRE( hasRecordedTime( MagAOX::logger::ocam_temps::lastRecord ) );
2408 REQUIRE( hasRecordedTime( MagAOX::logger::telem_stdcam::lastRecord ) );
2409 REQUIRE( hasRecordedTime( MagAOX::logger::telem_fgtimings::lastRecord ) );
2410 }
2411}
2412
2413/// Verify appLogic covers connection transitions, error handling, and READY-state housekeeping.
2414/**
2415 * \ingroup ocam2KCtrl_unit_test
2416 */
2417TEST_CASE( "ocam2KCtrl appLogic handles connection and housekeeping flow", "[ocam2KCtrl]" )
2418{
2419 resetStubState();
2420
2421 // clang-format off
2422 #ifdef OCAM2KCTRL_TEST_DOXYGEN_REF
2424 #endif
2425 // clang-format on
2426
2427 SECTION( "NOTCONNECTED returns early during power loss" )
2428 {
2429 ocam2KCtrl_test app;
2430
2431 app.state( stateCodes::NOTCONNECTED );
2432 app.m_temps.CCD = 12.0f;
2433
2434 static_cast<MagAOXAppT &>( app ).m_powerState = 0;
2435 app.m_powerTargetState = 0;
2436
2437 fgThreadScope fgThread( app );
2438
2439 REQUIRE( app.appLogic() == 0 );
2440 REQUIRE( app.state() == stateCodes::NOTCONNECTED );
2441 REQUIRE( app.m_temps.CCD == Approx( -999.0f ) );
2442 REQUIRE( g_edtStubState.serialCommands.empty() );
2443 }
2444
2445 SECTION( "POWEROFF falls through to the final return without camera traffic" )
2446 {
2447 ocam2KCtrl_test app;
2448
2449 static_cast<MagAOXAppT &>( app ).m_powerState = 0;
2450 app.m_powerTargetState = 0;
2451 app.state( stateCodes::POWEROFF );
2452 fgThreadScope fgThread( app );
2453
2454 REQUIRE( app.appLogic() == 0 );
2455 REQUIRE( app.state() == stateCodes::POWEROFF );
2456 REQUIRE( g_edtStubState.serialCommands.empty() );
2457 }
2458
2459 SECTION( "NOTCONNECTED success falls through the READY housekeeping path in the same loop" )
2460 {
2461 ocam2KCtrl_test app;
2462
2463 setPoweredOn( app );
2464
2465 app.state( stateCodes::NOTCONNECTED );
2466 app.m_fpsSet = 0.0f;
2467 app.m_poweredOn = true;
2468 app.m_ccdTempSetpt = 18.5f;
2469 app.m_tempControlOnTarget = false;
2470 app.m_indiP_emProt = pcf::IndiProperty( pcf::IndiProperty::Text );
2471 app.m_indiP_emProt.setDevice( "ocam2KCtrl" );
2472 app.m_indiP_emProt.setName( "emProtReset" );
2473 app.m_indiP_emProt.add( pcf::IndiElement( "status" ) );
2474 app.m_indiP_emProt["status"].set( "CONFIRMED" );
2475 app.m_protectionResetConfirmed = 1;
2476 app.m_protectionResetReqTime = mx::sys::get_curr_time() - 11.0;
2477
2478 queueSerialResponse( "camera online\n" );
2479 queueSerialResponse( "fps [150.5] Hz\n" );
2480 queueSerialResponse( "setpoint updated\n" );
2481 queueSerialResponse( "fps max\n" );
2482 queueSerialResponse( "synchro off\n" );
2483 queueSerialResponse( "fps restored\n" );
2484 queueSerialResponse( "Temperatures : CCD[20.4] CPU[41] POWER[34] BIAS[47] WATER[24.2] LEFT[33] RIGHT[38] "
2485 "SET[185]\nCooling Power [102]mW.\n\n" );
2486 queueSerialResponse( "fps [150.5] Hz\n" );
2487 queueSerialResponse( "Gain set to 42 \n\n" );
2488
2489 fgThreadScope fgThread( app );
2490
2491 REQUIRE( app.appLogic() == 0 );
2492 REQUIRE( app.state() == stateCodes::FAILURE );
2493 REQUIRE( app.m_fps == Approx( 150.5f ) );
2494 REQUIRE( app.m_poweredOn == false );
2495 REQUIRE( app.m_synchroSet == false );
2496 REQUIRE( app.m_protectionResetConfirmed == 0 );
2497 REQUIRE( app.m_shutdown == 1 );
2498 REQUIRE( g_edtStubState.serialCommands ==
2499 std::vector<std::string>{
2500 "fps", "fps", "temp 18.500000", "fps 0", "synchro off", "fps 0.000000", "temp", "fps", "gain" } );
2501 }
2502
2503 SECTION( "NOTCONNECTED returns after a failed connectivity probe while power remains on" )
2504 {
2505 ocam2KCtrl_test app;
2506
2507 setPoweredOn( app );
2508 app.state( stateCodes::NOTCONNECTED );
2509
2510 queueSerialResponse( "", -1 );
2511
2512 fgThreadScope fgThread( app );
2513
2514 REQUIRE( app.appLogic() == 0 );
2515 REQUIRE( app.state() == stateCodes::NOTCONNECTED );
2516 REQUIRE( g_edtStubState.serialCommands == std::vector<std::string>{ "fps" } );
2517 }
2518
2519 SECTION( "CONNECTED with a requested FPS transitions through OPERATING before housekeeping" )
2520 {
2521 ocam2KCtrl_test app;
2522
2523 setPoweredOn( app );
2524
2525 app.state( stateCodes::CONNECTED );
2526 app.m_fpsSet = 10.0f;
2527 app.m_poweredOn = false;
2528 app.m_protectionResetConfirmed = 0;
2529
2530 queueSerialResponse( "fps [150.5] Hz\n" );
2531 queueSerialResponse( "fps max\n" );
2532 queueSerialResponse( "synchro off\n" );
2533 queueSerialResponse( "fps 10.000000 restored\n" );
2534 queueSerialResponse( "Temperatures : CCD[20.4] CPU[41] POWER[34] BIAS[47] WATER[24.2] LEFT[33] RIGHT[38] "
2535 "SET[205]\nCooling Power [102]mW.\n\n" );
2536 queueSerialResponse( "fps [150.5] Hz\n" );
2537 queueSerialResponse( "Gain set to 42 \n\n" );
2538
2539 fgThreadScope fgThread( app );
2540
2541 REQUIRE( app.appLogic() == 0 );
2542 REQUIRE( app.state() == stateCodes::FAILURE );
2543 REQUIRE( app.m_shutdown == 1 );
2544 REQUIRE( g_edtStubState.serialCommands ==
2545 std::vector<std::string>{ "fps", "fps 0", "synchro off", "fps 10.000000", "temp", "fps", "gain" } );
2546 }
2547
2548 SECTION( "CONNECTED enters ERROR when getFPS fails on powered hardware" )
2549 {
2550 ocam2KCtrl_test app;
2551
2552 setPoweredOn( app );
2553 app.state( stateCodes::CONNECTED );
2554
2555 queueSerialResponse( "", -1 );
2556
2557 fgThreadScope fgThread( app );
2558
2559 REQUIRE( app.appLogic() == 0 );
2560 REQUIRE( app.state() == stateCodes::ERROR );
2561 REQUIRE( g_edtStubState.serialCommands == std::vector<std::string>{ "fps" } );
2562 }
2563
2564 SECTION( "CONNECTED returns quietly if getFPS fails after power is lost" )
2565 {
2566 ocam2KCtrl_test app;
2567
2568 static_cast<MagAOXAppT &>( app ).m_powerState = 0;
2569 app.m_powerTargetState = 0;
2570 app.state( stateCodes::CONNECTED );
2571
2572 queueSerialResponse( "", -1 );
2573
2574 fgThreadScope fgThread( app );
2575
2576 REQUIRE( app.appLogic() == 0 );
2577 REQUIRE( app.state() == stateCodes::CONNECTED );
2578 REQUIRE( g_edtStubState.serialCommands == std::vector<std::string>{ "fps" } );
2579 }
2580
2581 SECTION( "CONNECTED returns quietly when setTempSetPt fails after power is lost" )
2582 {
2583 ocam2KCtrl_test app;
2584
2585 static_cast<MagAOXAppT &>( app ).m_powerState = 0;
2586 app.m_powerTargetState = 0;
2587 app.state( stateCodes::CONNECTED );
2588 app.m_fpsSet = 0.0f;
2589 app.m_poweredOn = true;
2590 app.m_ccdTempSetpt = 18.5f;
2591
2592 queueSerialResponse( "fps [150.5] Hz\n" );
2593 queueSerialResponse( "", -1 );
2594
2595 fgThreadScope fgThread( app );
2596
2597 REQUIRE( app.appLogic() == 0 );
2598 REQUIRE( app.state() == stateCodes::READY );
2599 REQUIRE( app.m_poweredOn == false );
2600 REQUIRE( g_edtStubState.serialCommands == std::vector<std::string>{ "fps", "temp 18.500000" } );
2601 }
2602
2603 SECTION( "CONNECTED returns a software error when setTempSetPt fails on powered hardware" )
2604 {
2605 ocam2KCtrl_test app;
2606
2607 setPoweredOn( app );
2608 app.state( stateCodes::CONNECTED );
2609 app.m_fpsSet = 0.0f;
2610 app.m_poweredOn = true;
2611 app.m_ccdTempSetpt = 18.5f;
2612
2613 queueSerialResponse( "fps [150.5] Hz\n" );
2614 queueSerialResponse( "", -1 );
2615
2616 fgThreadScope fgThread( app );
2617
2618 REQUIRE( app.appLogic() == 0 );
2619 REQUIRE( app.state() == stateCodes::READY );
2620 REQUIRE( app.m_poweredOn == false );
2621 REQUIRE( g_edtStubState.serialCommands == std::vector<std::string>{ "fps", "temp 18.500000" } );
2622 }
2623
2624 SECTION( "CONNECTED logs but continues when setSynchro fails during the initial connect" )
2625 {
2626 ocam2KCtrl_test app;
2627
2628 setPoweredOn( app );
2629 app.state( stateCodes::CONNECTED );
2630 app.m_fpsSet = 0.0f;
2631 app.m_poweredOn = false;
2632
2633 queueSerialResponse( "fps [150.5] Hz\n" );
2634 queueSerialResponse( "fps max\n" );
2635 queueSerialResponse( "", -1 );
2636 queueSerialResponse( "Temperatures : CCD[20.4] CPU[41] POWER[34] BIAS[47] WATER[24.2] LEFT[33] RIGHT[38] "
2637 "SET[205]\nCooling Power [102]mW.\n\n" );
2638 queueSerialResponse( "fps [150.5] Hz\n" );
2639 queueSerialResponse( "Gain set to 42 \n\n" );
2640
2641 fgThreadScope fgThread( app );
2642
2643 REQUIRE( app.appLogic() == 0 );
2644 REQUIRE( app.state() == stateCodes::FAILURE );
2645 REQUIRE( app.m_shutdown == 1 );
2646 REQUIRE( g_edtStubState.serialCommands ==
2647 std::vector<std::string>{ "fps", "fps 0", "synchro off", "temp", "fps", "gain" } );
2648 }
2649
2650 SECTION( "READY expires stale protection resets and completes housekeeping before telemetry shutdown" )
2651 {
2652 ocam2KCtrl_test app;
2653
2654 setPoweredOn( app );
2655
2656 app.state( stateCodes::READY );
2657 app.m_protectionResetConfirmed = 1;
2658 app.m_protectionResetReqTime = mx::sys::get_curr_time() - 11.0;
2659 app.m_indiP_emProt = pcf::IndiProperty( pcf::IndiProperty::Text );
2660 app.m_indiP_emProt.add( pcf::IndiElement( "status" ) );
2661 app.m_indiP_emProt["status"].set( "CONFIRM" );
2662
2663 queueSerialResponse( "Temperatures : CCD[20.4] CPU[41] POWER[34] BIAS[47] WATER[24.2] LEFT[33] RIGHT[38] "
2664 "SET[205]\nCooling Power [102]mW.\n\n" );
2665 queueSerialResponse( "fps [150.5] Hz\n" );
2666 queueSerialResponse( "Gain set to 42 \n\n" );
2667
2668 fgThreadScope fgThread( app );
2669
2670 REQUIRE( app.appLogic() == 0 );
2671 REQUIRE( app.m_protectionResetConfirmed == 0 );
2672 REQUIRE( app.m_shutdown == 1 );
2673 REQUIRE( app.state() == stateCodes::FAILURE );
2674 REQUIRE( app.m_temps.CCD == Approx( 20.4f ) );
2675 REQUIRE( app.m_fps == Approx( 150.5f ) );
2676 REQUIRE( app.m_emGain == 42 );
2677 }
2678
2679 SECTION( "READY returns immediately if the INDI mutex is already locked elsewhere" )
2680 {
2681 ocam2KCtrl_test app;
2682
2683 setPoweredOn( app );
2684 app.state( stateCodes::READY );
2685
2686 std::unique_lock<std::mutex> hold( app.m_indiMutex );
2687 fgThreadScope fgThread( app );
2688
2689 REQUIRE( app.appLogic() == 0 );
2690 REQUIRE( g_edtStubState.serialCommands.empty() );
2691 }
2692
2693 SECTION( "READY moves to ERROR when temperature polling fails on powered hardware" )
2694 {
2695 ocam2KCtrl_test app;
2696
2697 setPoweredOn( app );
2698 app.state( stateCodes::READY );
2699
2700 queueSerialResponse( "", -1 );
2701
2702 fgThreadScope fgThread( app );
2703
2704 REQUIRE( app.appLogic() == 0 );
2705 REQUIRE( app.state() == stateCodes::ERROR );
2706 REQUIRE( app.m_temps.CCD == Approx( -999.0f ) );
2707 REQUIRE( g_edtStubState.serialCommands == std::vector<std::string>{ "temp" } );
2708 }
2709
2710 SECTION( "READY returns quietly when temperature polling fails after power is lost" )
2711 {
2712 ocam2KCtrl_test app;
2713
2714 static_cast<MagAOXAppT &>( app ).m_powerState = 0;
2715 app.m_powerTargetState = 0;
2716 app.state( stateCodes::READY );
2717
2718 queueSerialResponse( "", -1 );
2719
2720 fgThreadScope fgThread( app );
2721
2722 REQUIRE( app.appLogic() == 0 );
2723 REQUIRE( app.state() == stateCodes::READY );
2724 REQUIRE( g_edtStubState.serialCommands == std::vector<std::string>{ "temp" } );
2725 }
2726
2727 SECTION( "READY moves to ERROR when FPS polling fails after temperatures succeed" )
2728 {
2729 ocam2KCtrl_test app;
2730
2731 setPoweredOn( app );
2732 app.state( stateCodes::READY );
2733
2734 queueSerialResponse( "Temperatures : CCD[20.4] CPU[41] POWER[34] BIAS[47] WATER[24.2] LEFT[33] RIGHT[38] "
2735 "SET[205]\nCooling Power [102]mW.\n\n" );
2736 queueSerialResponse( "", -1 );
2737
2738 fgThreadScope fgThread( app );
2739
2740 REQUIRE( app.appLogic() == 0 );
2741 REQUIRE( app.state() == stateCodes::ERROR );
2742 REQUIRE( g_edtStubState.serialCommands == std::vector<std::string>{ "temp", "fps" } );
2743 }
2744
2745 SECTION( "READY returns quietly when FPS polling fails after power is lost" )
2746 {
2747 ocam2KCtrl_test app;
2748
2749 static_cast<MagAOXAppT &>( app ).m_powerState = 0;
2750 app.m_powerTargetState = 0;
2751 app.state( stateCodes::READY );
2752
2753 queueSerialResponse( "Temperatures : CCD[20.4] CPU[41] POWER[34] BIAS[47] WATER[24.2] LEFT[33] RIGHT[38] "
2754 "SET[205]\nCooling Power [102]mW.\n\n" );
2755 queueSerialResponse( "", -1 );
2756
2757 fgThreadScope fgThread( app );
2758
2759 REQUIRE( app.appLogic() == 0 );
2760 REQUIRE( app.state() == stateCodes::READY );
2761 REQUIRE( g_edtStubState.serialCommands == std::vector<std::string>{ "temp", "fps" } );
2762 }
2763
2764 SECTION( "READY returns quietly when EM gain polling fails after power is lost" )
2765 {
2766 ocam2KCtrl_test app;
2767
2768 static_cast<MagAOXAppT &>( app ).m_powerState = 0;
2769 app.m_powerTargetState = 0;
2770 app.state( stateCodes::READY );
2771
2772 queueSerialResponse( "Temperatures : CCD[20.4] CPU[41] POWER[34] BIAS[47] WATER[24.2] LEFT[33] RIGHT[38] "
2773 "SET[205]\nCooling Power [102]mW.\n\n" );
2774 queueSerialResponse( "fps [150.5] Hz\n" );
2775 queueSerialResponse( "", -1 );
2776
2777 fgThreadScope fgThread( app );
2778
2779 REQUIRE( app.appLogic() == 0 );
2780 REQUIRE( app.state() == stateCodes::READY );
2781 REQUIRE( g_edtStubState.serialCommands == std::vector<std::string>{ "temp", "fps", "gain" } );
2782 }
2783
2784 SECTION( "READY moves to ERROR when EM gain polling fails on powered hardware" )
2785 {
2786 ocam2KCtrl_test app;
2787
2788 setPoweredOn( app );
2789 app.state( stateCodes::READY );
2790
2791 queueSerialResponse( "Temperatures : CCD[20.4] CPU[41] POWER[34] BIAS[47] WATER[24.2] LEFT[33] RIGHT[38] "
2792 "SET[205]\nCooling Power [102]mW.\n\n" );
2793 queueSerialResponse( "fps [150.5] Hz\n" );
2794 queueSerialResponse( "", -1 );
2795
2796 fgThreadScope fgThread( app );
2797
2798 REQUIRE( app.appLogic() == 0 );
2799 REQUIRE( app.state() == stateCodes::ERROR );
2800 REQUIRE( g_edtStubState.serialCommands == std::vector<std::string>{ "temp", "fps", "gain" } );
2801 }
2802}
2803
2804/// Verify reconfiguration reloads the requested mode and leaves the app in READY.
2805/**
2806 * \ingroup ocam2KCtrl_unit_test
2807 */
2808TEST_CASE( "ocam2KCtrl reconfig reloads the next mode through edtCamera", "[ocam2KCtrl]" )
2809{
2810 ocam2KCtrl_test app;
2811 dev::cameraConfig config;
2812
2813 resetStubState();
2814
2815 // clang-format off
2816 #ifdef OCAM2KCTRL_TEST_DOXYGEN_REF
2818 #endif
2819 // clang-format on
2820
2821 config.m_configFile = "stub.cfg";
2822 app.m_cameraModes["science"] = config;
2823 app.m_nextMode = "science";
2824 app.state( stateCodes::OPERATING );
2825
2826 REQUIRE( app.reconfig() == 0 );
2827 REQUIRE( app.state() == stateCodes::READY );
2828 REQUIRE( app.m_nextMode == "" );
2829 REQUIRE( app.m_modeName == "science" );
2830 REQUIRE( app.m_raw_width == 240 );
2831 REQUIRE( app.m_raw_height == 121 );
2832 REQUIRE( app.m_raw_depth == 16 );
2833 REQUIRE( app.m_cameraType == "stub_pdv" );
2834 REQUIRE( app.fps() == Approx( app.m_fps ) );
2835}
2836
2837#endif // OCAM2KCTRL_TEST_SUPPORT_ONLY
2838
2839} // namespace ocam2KCtrlTest
2840} // namespace libXWCTest
MagAO-X Uniblitz DSS Shutter interface.
int resetEMProtection()
Reset the EM Protection.
int setShutter(int sh)
Sets the shutter state, via call to dssShutter::setShutterState(int) [stdCamera interface].
std::string stateString()
Return the current stdCamera state string.
virtual int appLogic()
Implementation of the FSM for the OCAM 2K.
int recordTelem(const ocam_temps *)
int frameGrabberPostPublish(IMAGE *imageStream)
Publish the sync-only stream immediately after the main framegrabber publication.
int setNextROI()
Required by stdCamera, but this does not do anything for this camera [stdCamera interface].
virtual void setupConfig()
Setup the configuration system (called by MagAOXApp::setup())
int ensureSyncStream()
Ensure the sync-only ImageStreamIO stream exists with the expected 1x1 uint8 layout.
virtual int onPowerOff()
Implementation of the on-power-off FSM logic.
bool stateStringValid()
Report whether the current stdCamera state string is valid for persistence and telemetry.
int powerOnDefaults()
Set defaults for a power on state.
int setSynchro()
Set the synchro state. [stdCamera interface].
virtual int appShutdown()
Do any needed shutdown tasks.
int getEMGain()
Get the current EM Gain.
virtual void loadConfig()
load the configuration system results (called by MagAOXApp::setup())
int setExpTime()
Required by stdCamera, but this does not do anything for this camera [stdCamera interface].
float fps()
Implementation of the frameGrabber fps interface.
int loadImageIntoStream(void *dest)
Implementation of the framegrabber loadImageIntoStream interface.
int setTempControl()
Turn temperature control on or off.
int reconfig()
Implementation of the framegrabber reconfig interface.
int setTempSetPt()
Set the CCD temperature setpoint [stdCamera interface].
int startAcquisition()
Implementation of the framegrabber startAcquisition interface.
int setEMGain()
Set the EM gain.
int setFPS()
Set the frame rate. [stdCamera interface].
int getFPS()
Get the current frame rate.
int configureAcquisition()
Implementation of the framegrabber configureAcquisition interface.
int acquireAndCheckValid()
Implementation of the framegrabber acquireAndCheckValid interface.
virtual int whilePowerOff()
Implementation of the while-powered-off FSM.
int getTemps()
Get the current device temperatures.
unsigned int uint
Definition edtinc.h:9
unsigned char u_char
Definition edtinc.h:10
Stub EDT dependent-configuration type used by unit tests.
Definition edtinc.h:26
Stub EDT device handle type used by unit tests.
Definition edtinc.h:14
Stub EDT configuration info structure used by unit tests.
Definition edtinc.h:32
Stub EDT camera handle type used by unit tests.
Definition edtinc.h:20
@ OPERATING
The device is operating, other than homing.
@ POWEROFF
The device power is off.
@ FAILURE
The application has failed, should be used when m_shutdown is set for an error.
@ ERROR
The application has encountered an error, from which it is recovering (with or without intervention)
@ READY
The device is ready for operation, but is not operating.
@ CONNECTED
The application has connected to the device or service.
@ NOTCONNECTED
The application is not connected to the device or service.
TEST_CASE("ocam2KCtrl lifecycle entrypoints handle startup failures and POWERON logic", "[ocam2KCtrl]")
Verify lifecycle entrypoints cover startup failure handling and the POWERON fast-return path.
#define XWCTEST_DOXYGEN_REF(fxn)
This inserts an unused call to a function signature to make doxygen make the link.
Definition testXWC.hpp:18
std::string m_serialCommand
The command to send to the camera to place it in this mode.
Definition stdCamera.hpp:36
std::string m_configFile
The file to use for this mode, e.g. an EDT configuration file.
Definition stdCamera.hpp:35
A camera configuration.
Definition stdCamera.hpp:34
const pcf::IndiProperty & ipRecv
Namespace for all libXWC tests.
void pdv_flush_fifo(PdvDev *pdv_p)
Flush the stub PDV FIFO.
int pdv_readcfg(const char *configFile, Dependent *dd_p, Edtinfo *edtinfo)
Read a stub EDT configuration file.
void pdv_start_images(PdvDev *pdv_p, int numBuffs)
Start stub PDV acquisition for multiple images.
void edt_perror(char *errstr)
Report the last stub EDT error message.
const char * ocam2_sdkBuild()
Return sdk build.
ocam2_rc ocam2_init(ocam2_mode mode, const char *descrbFile, ocam2_id *id)
Create a camera instance with the provided mode.
void pdv_start_image(PdvDev *pdv_p)
Start acquisition of the next stub PDV image.
EdtDev * edt_open_channel(const char *deviceName, int unit, int channel)
Open a stub EDT device channel.
int pdv_get_waitchar(PdvDev *pdv_p, u_char *waitc)
Return the configured stub PDV serial terminator.
void pdv_serial_read_enable(PdvDev *pdv_p)
Enable stub PDV serial reads.
int pdv_serial_command(PdvDev *pdv_p, const char *command)
Send a command over the stub PDV serial channel.
void pdv_close(PdvDev *pdv_p)
Close a stub PDV device handle.
void ocam2_descramble(ocam2_id id, unsigned int *number, short *image, const short *imageRaw)
Create a camera instance with the provided mode.
void edt_close(EdtDev *edt_p)
Close a stub EDT device channel.
const char * ocam2_sdkVersion()
Return sdk version.
int pdv_initcam(EdtDev *edt_p, Dependent *dd_p, int unit, Edtinfo *edtinfo, const char *configFile, char *bitdir, int pdv_debug)
Initialize a stub EDT camera instance.
int pdv_get_width(PdvDev *pdv_p)
Return the stub PDV frame width.
const char * ocam2_modeStr(ocam2_mode mode)
Return a description text for camera mode.
int pdv_get_height(PdvDev *pdv_p)
Return the stub PDV frame height.
ocam2_rc ocam2_exit(ocam2_id id)
Clear a camera instance.
PdvDev * pdv_open_channel(const char *deviceName, int unit, int channel)
Open a stub PDV device handle.
int pdv_get_depth(PdvDev *pdv_p)
Return the stub PDV frame bit depth.
int pdv_serial_wait(PdvDev *pdv_p, int timeout, int count)
Wait for serial data on the stub PDV channel.
char * pdv_get_cameratype(PdvDev *pdv_p)
Return the stub PDV camera type string.
u_char * pdv_wait_last_image_timed(PdvDev *pdv_p, uint dmaTimeStamp[2])
Wait for the last stub PDV image and fill the DMA timestamp.
Dependent * pdv_alloc_dependent()
Allocate a stub EDT dependent configuration object.
void pdv_multibuf(PdvDev *pdv_p, int numBuffs)
Configure the stub PDV ring-buffer depth.
ocam2_mode ocam2_getMode(ocam2_id id)
Return the camera mode.
int pdv_serial_read(PdvDev *pdv_p, char *buf, int size)
Read bytes from the stub PDV serial channel.
int ocam2_id
Library camera identifier.
Definition ocam2_sdk.h:268
ocam2_rc
Enum of ocam2 library return code.
Definition ocam2_sdk.h:258
@ OCAM2_OK
Definition ocam2_sdk.h:260
@ OCAM2_ERROR
Definition ocam2_sdk.h:259
@ OCAM2_BINNING
Definition ocam2_sdk.h:236
@ OCAM2_BINNING1x3
Definition ocam2_sdk.h:244
@ OCAM2_BINNING1x4
Definition ocam2_sdk.h:246
@ OCAM2_NORMAL
Definition ocam2_sdk.h:226
enum workMode ocam2_mode
typedef of ocam2 camera mode
#define OCAM2_IMAGE_NB_OFFSET
Definition ocam2_sdk.h:208
Log entry recording the build-time git state.
static timespec lastRecord
The time of the last time this log was recorded. Used by the telemetry system.
Log entry recording framegrabber timings.
static timespec lastRecord
The timestamp of the last time this log was recorded. Used by the telemetry system.
Log entry recording stdcam stage specific status.
static timespec lastRecord
The timestamp of the last time this log was recorded. Used by the telemetry system.