From bee96ade071ec28b7cc1d2b3500be79d25c512db Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tom=C3=A1=C5=A1=20Han=C3=A1k?= Date: Tue, 5 May 2026 19:03:50 +0200 Subject: [PATCH 1/4] PVCAM: Removed polling for acquisition state. Polling doesn't work reliably with recent fast cameras. Registering an EOF callback handler is the recommended way for many years already. Removed all logic related to polling, including a property for selecting between polling and callbacks. --- DeviceAdapters/PVCAM/AcqConfig.h | 4 - DeviceAdapters/PVCAM/AcqThread.cpp | 8 +- DeviceAdapters/PVCAM/Makefile.am | 2 - DeviceAdapters/PVCAM/PVCAM.vcxproj | 2 - DeviceAdapters/PVCAM/PVCAM.vcxproj.filters | 6 - DeviceAdapters/PVCAM/PVCAMAdapter.h | 21 +- DeviceAdapters/PVCAM/PVCAMUniversal.cpp | 387 ++------------------- DeviceAdapters/PVCAM/PollingThread.cpp | 42 --- DeviceAdapters/PVCAM/PollingThread.h | 26 -- DeviceAdapters/PVCAM/Version.h | 4 +- 10 files changed, 43 insertions(+), 459 deletions(-) delete mode 100644 DeviceAdapters/PVCAM/PollingThread.cpp delete mode 100644 DeviceAdapters/PVCAM/PollingThread.h diff --git a/DeviceAdapters/PVCAM/AcqConfig.h b/DeviceAdapters/PVCAM/AcqConfig.h index 00e4addf5..3f50e4f90 100644 --- a/DeviceAdapters/PVCAM/AcqConfig.h +++ b/DeviceAdapters/PVCAM/AcqConfig.h @@ -195,10 +195,6 @@ class AcqConfig */ bool CircBufSizeAuto{ true }; /** - * True if PVCAM callbacks are active, false to use polling - */ - bool CallbacksEnabled{ true }; - /** * Enables or disables custom streaming to disk. * Please note that this streaming is enabled for continuous acquisition only. * The streaming to disk is fully controlled by PVCAM adapter and should only diff --git a/DeviceAdapters/PVCAM/AcqThread.cpp b/DeviceAdapters/PVCAM/AcqThread.cpp index c4f1c2a06..d85b49584 100644 --- a/DeviceAdapters/PVCAM/AcqThread.cpp +++ b/DeviceAdapters/PVCAM/AcqThread.cpp @@ -44,7 +44,7 @@ int AcqThread::svc() { camera_->LogAdapterMessage("AcqThead loop started"); int nRet = DEVICE_OK; - while(!requestStop_) + while (!requestStop_) { resumeEvent_.Wait(); if (requestStop_) @@ -57,10 +57,8 @@ int AcqThread::svc() if (nRet != DEVICE_OK) continue; // Error logged, ignore and try again - // Frame successfully arrived and ready in the buffer. If we are - // not using callbacks we need to manually push it to the core. - if (!camera_->acqCfgCur_.CallbacksEnabled) - camera_->FrameAcquired(); + // Frame successfully arrived and ready in the buffer. + // Frame pushed to the MM core from PVCAM callback handler. } camera_->LogAdapterMessage("AcqThead loop exited"); return nRet; diff --git a/DeviceAdapters/PVCAM/Makefile.am b/DeviceAdapters/PVCAM/Makefile.am index 3a7fdd408..ba27928b8 100644 --- a/DeviceAdapters/PVCAM/Makefile.am +++ b/DeviceAdapters/PVCAM/Makefile.am @@ -15,8 +15,6 @@ libmmgr_dal_PVCAM_la_SOURCES = \ AcqThread.h \ Event.cpp \ Event.h \ - PollingThread.cpp \ - PollingThread.h \ PpParam.cpp \ PpParam.h \ PVCAMAdapter.cpp \ diff --git a/DeviceAdapters/PVCAM/PVCAM.vcxproj b/DeviceAdapters/PVCAM/PVCAM.vcxproj index 134f3544d..dce49dcad 100644 --- a/DeviceAdapters/PVCAM/PVCAM.vcxproj +++ b/DeviceAdapters/PVCAM/PVCAM.vcxproj @@ -96,7 +96,6 @@ - @@ -116,7 +115,6 @@ - diff --git a/DeviceAdapters/PVCAM/PVCAM.vcxproj.filters b/DeviceAdapters/PVCAM/PVCAM.vcxproj.filters index cc6aadaea..c4780a10a 100644 --- a/DeviceAdapters/PVCAM/PVCAM.vcxproj.filters +++ b/DeviceAdapters/PVCAM/PVCAM.vcxproj.filters @@ -33,9 +33,6 @@ Source Files - - Source Files - Source Files @@ -89,9 +86,6 @@ Header Files - - Header Files - Header Files diff --git a/DeviceAdapters/PVCAM/PVCAMAdapter.h b/DeviceAdapters/PVCAM/PVCAMAdapter.h index 8231a5d64..eab76e1c7 100644 --- a/DeviceAdapters/PVCAM/PVCAMAdapter.h +++ b/DeviceAdapters/PVCAM/PVCAMAdapter.h @@ -145,7 +145,6 @@ enum PvCameraModel //============================================================================= //======================================================== FORWARD DECLARATIONS -class PollingThread; class AcqThread; class StreamWriter; template class PvParam; @@ -501,11 +500,6 @@ class Universal : public CCameraBase */ int OnDiskStreamingCoreSkipRatio(MM::PropertyBase* pProp, MM::ActionType eAct); - /** - * Switches between Callbacks or Polling acquisition type. - */ - int OnAcquisitionMethod(MM::PropertyBase* pProp, MM::ActionType eAct); - /** * Post processing parameter handler. Post processing features and parameters are * read out from the camera dynamically. Based on the camera provided information @@ -621,10 +615,7 @@ class Universal : public CCameraBase protected: /** - * This method is called from the static PVCAM callback or polling thread. - * The method should finish as fast as possible to avoid blocking the PVCAM. - * If the execution of this method takes longer than frame readout + exposure, - * the FrameAcquired for the next frame may not be called. + * This method is called from the static PVCAM callback. */ int FrameAcquired(); /** @@ -632,9 +623,6 @@ class Universal : public CCameraBase */ int ProcessFrame(const void* pData, size_t dataSz, const PvFrameInfo& frameNfo); - int PollingThreadRun(void); - void PollingThreadExiting() throw(); - private: // Make object non-copyable Universal(const Universal&) = delete; @@ -691,9 +679,6 @@ class Universal : public CCameraBase * acquisition thread. */ int waitForFrameSeq(); - int waitForFrameSeqPolling(const MM::MMTime& timeout); - int waitForFrameSeqCallbacks(const MM::MMTime& timeout); - int waitForFrameConPolling(const MM::MMTime& timeout); int PrepareSeqAcq(); // Note: no longer a device interface function @@ -849,10 +834,8 @@ class Universal : public CCameraBase std::map> expTimeResLimits_{}; // [expTimeRes]={min,max} - friend class PollingThread; - std::unique_ptr pollingThd_{}; // Pointer to the sequencing thread friend class AcqThread; - std::unique_ptr acqThd_{}; // Non-CB live thread + std::unique_ptr acqThd_{}; // Non-circular buffer "live" acq. thread std::unique_ptr customDiskWriter_{}; // Writer for custom disk streaming feature bool customDiskWriterActive_{ false }; // Cached value updated after writer->Start diff --git a/DeviceAdapters/PVCAM/PVCAMUniversal.cpp b/DeviceAdapters/PVCAM/PVCAMUniversal.cpp index b0ae81d35..605ea67fd 100644 --- a/DeviceAdapters/PVCAM/PVCAMUniversal.cpp +++ b/DeviceAdapters/PVCAM/PVCAMUniversal.cpp @@ -41,7 +41,6 @@ // Local #include "AcqThread.h" -#include "PollingThread.h" #include "PVCAMParam.h" #include "StreamWriter.h" #include "Version.h" @@ -141,9 +140,6 @@ const char* g_Keyword_Replication = "Nearest Neighbor Replication"; const char* g_Keyword_Bilinear = "Bilinear"; const char* g_Keyword_SmoothHue = "Smooth Hue"; const char* g_Keyword_AdaptiveSmoothHue = "Adaptive Smooth Hue (edge detecting)"; -const char* g_Keyword_AcqMethod = "AcquisitionMethod"; // Callbacks/Polling -const char* g_Keyword_AcqMethod_Callbacks = "Callbacks"; -const char* g_Keyword_AcqMethod_Polling = "Polling"; const char* g_Keyword_CircBufFrameCnt = "CircularBufferFrameCount"; const char* g_Keyword_CircBufSizeAuto = "CircularBufferAutoSize"; // ON/OFF const char* g_Keyword_CircBufFrameRecovery = "CircularBufferFrameRecovery"; // ON/OFF @@ -230,7 +226,6 @@ Universal::Universal(short cameraId, const char* deviceName) deviceName_(deviceName), // Sizes larger than 3 caused image tearing in ICX-674. Reason unknown. circBufFrameCount_(CIRC_BUF_FRAME_CNT_DEF), - pollingThd_(std::make_unique(this)), acqThd_(std::make_unique(this)), customDiskWriter_(std::make_unique(this)) { @@ -263,11 +258,6 @@ Universal::~Universal() if (initialized_) Shutdown(); } - if (!pollingThd_->GetStop()) - { - pollingThd_->SetStop(true); - pollingThd_->wait(); - } if (metaFrameStruct_) pl_md_release_frame_struct(metaFrameStruct_); @@ -1123,28 +1113,15 @@ int Universal::Initialize() // CALLBACKS // Check if we can use PVCAM callbacks. This is recommended way to get notified when the frame - // readout is finished. Otherwise we will fall back to old polling method. - acqCfgNew_.CallbacksEnabled = false; - if ( pl_cam_register_callback_ex3( hPVCAM_, PL_CALLBACK_EOF, (void*)&Universal::PvcamCallbackEofEx3, this ) == PV_OK ) - { - pAct = new CPropertyAction(this, &Universal::OnAcquisitionMethod); - nRet = CreateProperty(g_Keyword_AcqMethod, g_Keyword_AcqMethod_Polling, MM::String, false, pAct ); - AddAllowedValue(g_Keyword_AcqMethod, g_Keyword_AcqMethod_Polling); - AddAllowedValue(g_Keyword_AcqMethod, g_Keyword_AcqMethod_Callbacks); - LogAdapterMessage( "Using callbacks for frame acquisition" ); - acqCfgNew_.CallbacksEnabled = true; - } - else - { - LogAdapterMessage( "pl_cam_register_callback_ex3 failed! Using polling for frame acquisition" ); - } + // readout is finished. + if (!pl_cam_register_callback_ex3( + hPVCAM_, PL_CALLBACK_EOF, &Universal::PvcamCallbackEofEx3, this)) + return LogPvcamError(__LINE__, "Failed to register EOF callback handler"); // FRAME_INFO SUPPORT // Initialize the FRAME_INFO structure, this will contain the frame metadata provided by PVCAM - if ( !pl_create_frame_info_struct( &pFrameInfo_ ) ) - { + if (!pl_create_frame_info_struct(&pFrameInfo_)) return LogPvcamError(__LINE__, "Failed to initialize the FRAME_INFO structure"); - } // TRIGGER TABLE // We will create a property for every TrigTab and LastMuxed combination so we will end up with something similar @@ -1445,16 +1422,12 @@ int Universal::Shutdown() { if (initialized_) { - rs_bool ret; + if (!pl_cam_deregister_callback( hPVCAM_, PL_CALLBACK_EOF )) + LogPvcamError(__LINE__, "pl_cam_deregister_callback EOF"); - if ( acqCfgCur_.CallbacksEnabled ) - { - pl_cam_deregister_callback( hPVCAM_, PL_CALLBACK_EOF ); - } - ret = pl_cam_close(hPVCAM_); - if (!ret) + if (!pl_cam_close(hPVCAM_)) LogPvcamError(__LINE__, "pl_cam_close"); - assert(ret); + refCount_--; if (PVCAM_initialized_ && refCount_ <= 0) { @@ -1463,11 +1436,13 @@ int Universal::Shutdown() LogPvcamError(__LINE__, "pl_pvcam_uninit"); PVCAM_initialized_ = false; } + if ( pFrameInfo_ ) { pl_release_frame_info_struct( pFrameInfo_ ); pFrameInfo_ = nullptr; } + initialized_ = false; } return DEVICE_OK; @@ -1944,10 +1919,6 @@ int Universal::StartSequenceAcquisition(long numImages, double interval_ms, bool // the callbacks will start coming pretty fast. Do not waste time here, what can be done // before start_cont() should be done there. - if ( !acqCfgCur_.CallbacksEnabled && acqCfgCur_.CircBufEnabled ) - { - pollingThd_->Start(); - } isAcquiring_ = true; std::ostringstream os; @@ -3281,24 +3252,6 @@ int Universal::OnDiskStreamingCoreSkipRatio(MM::PropertyBase* pProp, MM::ActionT return DEVICE_OK; } -int Universal::OnAcquisitionMethod(MM::PropertyBase* pProp, MM::ActionType eAct) -{ - START_ONPROPERTY("Universal::OnAcquisitionMethod", eAct); - if (eAct == MM::AfterSet) - { - std::string val; - pProp->Get(val); - - acqCfgNew_.CallbacksEnabled = (val.compare(g_Keyword_AcqMethod_Callbacks) == 0); - applyAcqConfig(); - } - else if (eAct == MM::BeforeGet) - { - pProp->Set(acqCfgCur_.CallbacksEnabled ? g_Keyword_AcqMethod_Callbacks : g_Keyword_AcqMethod_Polling); - } - return DEVICE_OK; -} - int Universal::OnPostProcProperties(MM::PropertyBase* pProp, MM::ActionType eAct, long index) { START_ONPROPERTY("Universal::OnPostProcProperties", eAct); @@ -3916,14 +3869,16 @@ int Universal::FrameAcquired() recFrNfo.SetTimestampMsec(recAppTimeStampEOF); recFrNfo.SetRecovered(true); - // Notify our CB wrapper that a new frame has "arrived", it will increase - // its internal counters and indexes. - circBuf_.ReportFrameArrived(recFrNfo, pRecFrameData); + imagesAcquired_++; + // Notify our CB wrapper that a new frame has "arrived", + // it will increase its internal counters and indexes. + circBuf_.ReportFrameArrived(recFrNfo, pRecFrameData); // Process the new frame the same way as the frame // would arrive correctly with a callback. ProcessFrame(pRecFrameData, circBuf_.FrameSize(), recFrNfo); - imagesAcquired_++; + + imagesInserted_++; imagesRecovered_++; } } @@ -3961,11 +3916,8 @@ int Universal::FrameAcquired() } ProcessFrame(pCurrFramePtr, currFrameSize, currFrameNfo); } - else - { - // Single snap: just increase the number of actually acquired frames. - imagesInserted_++; - } + + imagesInserted_++; eofEvent_.Set(); return DEVICE_OK; @@ -4178,10 +4130,8 @@ int Universal::ProcessFrame(const void* pData, size_t dataSz, const PvFrameInfo& } } - imagesInserted_++; - // If we already have all frames inserted tell the camera to stop - if (acqCfgCur_.CallbacksEnabled && imagesInserted_ >= imagesToAcquire_) + if (imagesInserted_ + 1 >= imagesToAcquire_) { // TODO: Can we replace this last occurrence with StopSequenceAcquisition()? abortAcquisitionInternal(); @@ -4190,92 +4140,6 @@ int Universal::ProcessFrame(const void* pData, size_t dataSz, const PvFrameInfo& return ret; } -/* -* Do actual capture -* Called from the acquisition thread function -*/ -int Universal::PollingThreadRun(void) -{ - START_METHOD(">>>Universal::PollingThreadRun"); - - int ret = DEVICE_ERR; - char dbgBuf[128]; // Debug log buffer - pollingThd_->SetStop(false); // make sure this thread's status is updated properly. - - try - { - const long long usec = triggerTimeout_ * 1000000LL - + getEstimatedMaxReadoutTimeMs() * 1000LL - + (long long)(4 * GetExposure() * 1000.0); - - const MM::MMTime timeout((long)(usec / 1000000L), (long)(usec % 1000000L)); - - do - { - ret = waitForFrameConPolling(timeout); - if (ret == DEVICE_OK) - { - ret = FrameAcquired(); - } - else - { - break; - } - } - while (DEVICE_OK == ret && !pollingThd_->GetStop() && imagesInserted_ < imagesToAcquire_); - - snprintf( dbgBuf, sizeof(dbgBuf), - "ACQ LOOP FINISHED: thdGetStop:%u, ret:%u, imagesInserted_: %lu, imagesToAcquire_: %lu", - pollingThd_->GetStop(), ret, imagesInserted_, imagesToAcquire_); - LogAdapterMessage( __LINE__, dbgBuf ); - - if (imagesInserted_ >= imagesToAcquire_) - imagesInserted_ = 0; - PollingThreadExiting(); - pollingThd_->SetStop(true); - - START_METHOD("<<AcqFinished(this, DEVICE_OK); - pollingThd_->SetStop(true); - return ret; - } -} - -void Universal::PollingThreadExiting() throw () -{ - try - { - { - std::lock_guard pvcamGuard(g_pvcamLock); - if (!pl_exp_stop_cont(hPVCAM_, CCS_HALT)) - LogPvcamError(__LINE__, "pl_exp_stop_cont"); - if (!pl_exp_finish_seq(hPVCAM_, circBuf_.Data(), 0)) - LogPvcamError(__LINE__, "pl_exp_finish_seq"); - } - - sequenceModeReady_ = false; - isAcquiring_ = false; - - LogAdapterMessage(g_Msg_SEQUENCE_ACQUISITION_THREAD_EXITING); - // TODO: Can it ever be null? - auto *core = GetCoreCallback(); - if (core) - core->AcqFinished(this, DEVICE_OK); - } - catch (...) - { - LogAdapterMessage(__LINE__, g_Msg_EXCEPTION_IN_ON_THREAD_EXITING); - } -} - //============================================================================= //===================================================================== PRIVATE @@ -5185,87 +5049,12 @@ int Universal::waitForFrameSeq() { START_METHOD("Universal::waitForFrameSeq"); - int nRet = DEVICE_OK; - - const long long usec = triggerTimeout_ * 1000000LL - + getEstimatedMaxReadoutTimeMs() * 1000LL - + (long long)(4 * GetExposure() * 1000.0); - - const MM::MMTime timeout((long)(usec / 1000000L), (long)(usec % 1000000L)); - - if (!acqCfgCur_.CallbacksEnabled) - { - nRet = waitForFrameSeqPolling(timeout); - } - else - { - nRet = waitForFrameSeqCallbacks(timeout); - } - - return nRet; -} - -int Universal::waitForFrameSeqPolling(const MM::MMTime& timeout) -{ - // This function can be called very often so avoid any frequent - // logging or other expensive calls. - rs_bool pvRet = FALSE; - int16 pvErr = 0; - int16 pvStatus = READOUT_NOT_ACTIVE; - uns32 pvBytesArrived = 0; - - MM::MMTime timeElapsed(0,0); - const MM::MMTime startTime = GetCurrentMMTime(); - - // Poll PVCAM for status changes. If we miss the EXPOSURE_IN_PROGRESS we - // silently skip to check READOUT_IN_PROGRESS, after that we assume that - // the frame is ready. - do - { - CDeviceUtils::SleepMs(1); - { - std::lock_guard pvcamGuard(g_pvcamLock); - pvRet = pl_exp_check_status(hPVCAM_, &pvStatus, &pvBytesArrived); - if (pvRet != PV_OK) - pvErr = pl_error_code(); - } - timeElapsed = GetCurrentMMTime() - startTime; - } - while (pvRet == TRUE && pvStatus == EXPOSURE_IN_PROGRESS && timeElapsed < timeout); - - while (pvRet == TRUE && pvStatus == READOUT_IN_PROGRESS && timeElapsed < timeout) - { - CDeviceUtils::SleepMs(1); - { - std::lock_guard pvcamGuard(g_pvcamLock); - pvRet = pl_exp_check_status(hPVCAM_, &pvStatus, &pvBytesArrived); - if (pvRet != PV_OK) - pvErr = pl_error_code(); - } - timeElapsed = GetCurrentMMTime() - startTime; - } - - if (pvRet == TRUE && pvStatus != READOUT_FAILED && timeElapsed < timeout) - return DEVICE_OK; + const unsigned int msec = + static_cast(triggerTimeout_) * 1000U + + getEstimatedMaxReadoutTimeMs() + + static_cast(4 * GetExposure()); - { - std::lock_guard pvcamGuard(g_pvcamLock); - // Abort the acquisition (ignore error if abort fails, just log it) - if (!pl_exp_abort(hPVCAM_, CCS_HALT)) - LogPvcamError(__LINE__, "waitForFrameSeqPolling(): pl_exp_abort() failed"); - } - if (pvRet == FALSE) - return LogPvcamError(__LINE__, "waitForFrameSeqPolling(): pl_exp_check_status() failed", pvErr); - if (pvStatus == READOUT_FAILED) - return LogAdapterError(ERR_FRAME_READOUT_FAILED, __LINE__, "waitForFrameSeqPolling(): pvStatus == READOUT_FAILED"); - if (timeElapsed > timeout) - return LogAdapterError(ERR_OPERATION_TIMED_OUT, __LINE__, "waitForFrameSeqPolling(): timeElapsed > timeout"); - return DEVICE_ERR; -} - -int Universal::waitForFrameSeqCallbacks(const MM::MMTime& timeout) -{ - const bool arrivedInTime = eofEvent_.Wait(static_cast(timeout.getMsec())); + const bool arrivedInTime = eofEvent_.Wait(msec); if (arrivedInTime) return DEVICE_OK; @@ -5273,78 +5062,9 @@ int Universal::waitForFrameSeqCallbacks(const MM::MMTime& timeout) std::lock_guard pvcamGuard(g_pvcamLock); // Abort the acquisition (ignore error if abort fails, just log it) if (!pl_exp_abort(hPVCAM_, CCS_HALT)) - LogPvcamError(__LINE__, "waitForFrameSeqCallbacks(): pl_exp_abort() failed"); - } - return LogAdapterError(ERR_OPERATION_TIMED_OUT, __LINE__, "waitForFrameSeqCallbacks(): Readout has timed out"); -} - -int Universal::waitForFrameConPolling(const MM::MMTime& timeout) -{ - rs_bool pvRet = FALSE; - int16 pvErr = 0; - int16 pvStatus = READOUT_NOT_ACTIVE; - uns32 pvBytesArrived = 0; - uns32 pvBufferCnt = 0; - - MM::MMTime timeElapsed(0,0); - const MM::MMTime startTime = GetCurrentMMTime(); - - bool bStop = false; - - do - { - CDeviceUtils::SleepMs(1); - { - std::lock_guard pvcamGuard(g_pvcamLock); - pvRet = pl_exp_check_cont_status(hPVCAM_, &pvStatus, &pvBytesArrived, &pvBufferCnt); - if (pvRet != PV_OK) - pvErr = pl_error_code(); - } - timeElapsed = GetCurrentMMTime() - startTime; - bStop = pollingThd_->GetStop(); - } - while (pvRet && (pvStatus == EXPOSURE_IN_PROGRESS || pvStatus == READOUT_NOT_ACTIVE) && timeElapsed < timeout && !bStop); - - while (pvRet && (pvStatus == READOUT_IN_PROGRESS) && timeElapsed < timeout && !bStop) - { - CDeviceUtils::SleepMs(1); - { - std::lock_guard pvcamGuard(g_pvcamLock); - pvRet = pl_exp_check_cont_status(hPVCAM_, &pvStatus, &pvBytesArrived, &pvBufferCnt); - if (pvRet != PV_OK) - pvErr = pl_error_code(); - } - timeElapsed = GetCurrentMMTime() - startTime; - bStop = pollingThd_->GetStop(); - } - - if (bStop) - { - LogAdapterMessage( "waitForFrameConPolling(): Stop called - breaking the loop", true); - return DEVICE_ERR; - } - if (pvRet == TRUE && timeElapsed < timeout && pvStatus != READOUT_FAILED) - { - // Because we could miss the FRAME_AVAILABLE and the camera could of gone back - // to EXPOSURE_IN_PROGRESS and so on depending on how long we could of been stalled - // in this thread we only check for READOUT_FAILED and assume that because we got here - // we have one or more frames ready. - return DEVICE_OK; - } - - { - std::lock_guard pvcamGuard(g_pvcamLock); - // Abort the acquisition (ignore error if abort fails, just log it) - if (!pl_exp_abort(hPVCAM_, CCS_HALT)) - LogPvcamError(__LINE__, "waitForFrameConPolling(): pl_exp_abort() failed"); + LogPvcamError(__LINE__, "waitForFrameSeq(): pl_exp_abort() failed"); } - if (pvRet == FALSE) - return LogPvcamError(__LINE__, "waitForFrameConPolling(): pl_exp_check_cont_status() failed", pvErr); - if (pvStatus == READOUT_FAILED) - return LogAdapterError(ERR_FRAME_READOUT_FAILED, __LINE__, "waitForFrameConPolling(): pvStatus == READOUT_FAILED"); - if (timeElapsed > timeout) - return LogAdapterError(ERR_OPERATION_TIMED_OUT, __LINE__, "waitForFrameConPolling(): timeElapsed > timeout"); - return DEVICE_ERR; + return LogAdapterError(ERR_OPERATION_TIMED_OUT, __LINE__, "waitForFrameSeq(): Readout has timed out"); } int Universal::postProcessSingleFrame(unsigned char** pOutBuf, unsigned char* pInBuf, size_t inBufSz) @@ -5483,32 +5203,22 @@ int Universal::abortAcquisitionInternal() START_METHOD("Universal::abortAcquisitionInternal"); int nRet = DEVICE_OK; - // removed redundant calls to pl_exp_stop_cont & - // pl_exp_finish_seq because they get called automatically when the thread exits. - if(isAcquiring_) + if (isAcquiring_) { if (acqCfgCur_.CircBufEnabled) { - if (acqCfgCur_.CallbacksEnabled) { + std::lock_guard pvcamGuard(g_pvcamLock); + if (!pl_exp_stop_cont( hPVCAM_, CCS_CLEAR )) { - std::lock_guard pvcamGuard(g_pvcamLock); - if (!pl_exp_stop_cont( hPVCAM_, CCS_CLEAR )) - { - nRet = DEVICE_ERR; - LogPvcamError( __LINE__, "pl_exp_stop_cont() failed" ); - } + nRet = DEVICE_ERR; + LogPvcamError( __LINE__, "pl_exp_stop_cont() failed" ); } - sequenceModeReady_ = false; - // Inform the core that the acquisition has finished - // (this also closes the shutter if used) - GetCoreCallback()->AcqFinished(this, nRet); - } - else - { - pollingThd_->SetStop(true); - pollingThd_->wait(); } + sequenceModeReady_ = false; + // Inform the core that the acquisition has finished + // (this also closes the shutter if used) + GetCoreCallback()->AcqFinished(this, nRet); } else { @@ -6629,31 +6339,6 @@ int Universal::applyAcqConfig(bool forceSetup) setupRequired = true; } - if (acqCfgNew_.CallbacksEnabled != acqCfgCur_.CallbacksEnabled) - { - configChanged = true; - setupRequired = true; - std::lock_guard pvcamGuard(g_pvcamLock); - if (acqCfgNew_.CallbacksEnabled) - { - if (pl_cam_register_callback_ex3(hPVCAM_, PL_CALLBACK_EOF, (void*)&Universal::PvcamCallbackEofEx3, this) != PV_OK) - { - acqCfgNew_ = acqCfgCur_; // New settings not accepted, reset it back to previous state - nRet = LogPvcamError(__LINE__, "pl_cam_register_callback_ex3() failed"); - return nRet; - } - } - else - { - if (pl_cam_deregister_callback(hPVCAM_, PL_CALLBACK_EOF) != PV_OK) - { - acqCfgNew_ = acqCfgCur_; // New settings not accepted, reset it back to previous state - nRet = LogPvcamError(__LINE__, "pl_cam_deregister_callback() failed"); - return nRet; - } - } - } - if (acqCfgNew_.DiskStreamingEnabled != acqCfgCur_.DiskStreamingEnabled) { configChanged = true; diff --git a/DeviceAdapters/PVCAM/PollingThread.cpp b/DeviceAdapters/PVCAM/PollingThread.cpp deleted file mode 100644 index f522e481a..000000000 --- a/DeviceAdapters/PVCAM/PollingThread.cpp +++ /dev/null @@ -1,42 +0,0 @@ -#include "PollingThread.h" - -#include "PVCAMAdapter.h" - -PollingThread::PollingThread(Universal* camera) - : camera_(camera) -{ -} - -PollingThread::~PollingThread() -{ -} - -void PollingThread::SetStop(bool stop) -{ - stop_ = stop; -} - -bool PollingThread::GetStop() const -{ - return stop_; -} - -void PollingThread::Start() -{ - stop_ = false; - activate(); -} - -int PollingThread::svc() -{ - int ret = DEVICE_ERR; - try - { - ret = camera_->PollingThreadRun(); - } - catch(...) - { - camera_->LogAdapterMessage(g_Msg_EXCEPTION_IN_THREAD, false); - } - return ret; -} diff --git a/DeviceAdapters/PVCAM/PollingThread.h b/DeviceAdapters/PVCAM/PollingThread.h deleted file mode 100644 index 86b6f9714..000000000 --- a/DeviceAdapters/PVCAM/PollingThread.h +++ /dev/null @@ -1,26 +0,0 @@ -#pragma once - -// MMDevice -#include "DeviceThreads.h" - -class Universal; - -/** -* Acquisition thread used for polling acquisition only. -*/ -class PollingThread : public MMDeviceThreadBase -{ -public: - explicit PollingThread(Universal* camera); - virtual ~PollingThread(); - - void SetStop(bool stop); - bool GetStop() const; - void Start(); - - virtual int svc() override; // From MMDeviceThreadBase - -private: - Universal* const camera_; - bool stop_{ true }; -}; diff --git a/DeviceAdapters/PVCAM/Version.h b/DeviceAdapters/PVCAM/Version.h index ce37a244e..cf7afa6ac 100644 --- a/DeviceAdapters/PVCAM/Version.h +++ b/DeviceAdapters/PVCAM/Version.h @@ -1,5 +1,5 @@ #pragma once #define PVCAM_ADAPTER_VERSION_MAJOR 1 -#define PVCAM_ADAPTER_VERSION_MINOR 3 -#define PVCAM_ADAPTER_VERSION_REVISION 85 +#define PVCAM_ADAPTER_VERSION_MINOR 4 +#define PVCAM_ADAPTER_VERSION_REVISION 0 From 495542c8962dfa4e914d5470f14bbf0603e0437d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tom=C3=A1=C5=A1=20Han=C3=A1k?= Date: Tue, 5 May 2026 19:25:05 +0200 Subject: [PATCH 2/4] PVCAM: Fixed auto-shutter not being always closed at the acquisition end. When running live acquisition with circular buffer mode disabled, the auto-shutter stayed open at the acquisition end. This has been also reported in PR 907, but the fix has not been merged (yet). --- DeviceAdapters/PVCAM/PVCAMUniversal.cpp | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/DeviceAdapters/PVCAM/PVCAMUniversal.cpp b/DeviceAdapters/PVCAM/PVCAMUniversal.cpp index 605ea67fd..d2e5e5aaa 100644 --- a/DeviceAdapters/PVCAM/PVCAMUniversal.cpp +++ b/DeviceAdapters/PVCAM/PVCAMUniversal.cpp @@ -5216,15 +5216,16 @@ int Universal::abortAcquisitionInternal() } } sequenceModeReady_ = false; - // Inform the core that the acquisition has finished - // (this also closes the shutter if used) - GetCoreCallback()->AcqFinished(this, nRet); } else { acqThd_->Pause(); } + // Inform the core that the acquisition has finished + // (this also closes the shutter if used) + GetCoreCallback()->AcqFinished(this, nRet); + customDiskWriter_->Stop(); customDiskWriterActive_ = false; From a6668d6d04950fc19c3438d959fa3389d9181712 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tom=C3=A1=C5=A1=20Han=C3=A1k?= Date: Wed, 6 May 2026 15:33:49 +0200 Subject: [PATCH 3/4] PVCAM: Fixed error handling in EOF callback handler. The FrameAcquired() function locks acqLock_. Any error in here or nested call would cause a deadlock by calling StopSequenceAcquisition(). Switched to abortAcquisitionInternal() that doesn't lock and done that on a few exit points only. --- DeviceAdapters/PVCAM/PVCAMAdapter.h | 2 +- DeviceAdapters/PVCAM/PVCAMUniversal.cpp | 231 +++++++++++----------- DeviceAdapters/PVCAM/PvCircularBuffer.cpp | 3 +- DeviceAdapters/PVCAM/PvCircularBuffer.h | 2 +- DeviceAdapters/PVCAM/Version.h | 2 +- 5 files changed, 116 insertions(+), 124 deletions(-) diff --git a/DeviceAdapters/PVCAM/PVCAMAdapter.h b/DeviceAdapters/PVCAM/PVCAMAdapter.h index eab76e1c7..e41a20106 100644 --- a/DeviceAdapters/PVCAM/PVCAMAdapter.h +++ b/DeviceAdapters/PVCAM/PVCAMAdapter.h @@ -621,7 +621,7 @@ class Universal : public CCameraBase /** * Called from FrameAcquired(), inserts the frame to the MMCore. */ - int ProcessFrame(const void* pData, size_t dataSz, const PvFrameInfo& frameNfo); + int ProcessFrame(const void* pData, const PvFrameInfo& frameNfo); private: // Make object non-copyable diff --git a/DeviceAdapters/PVCAM/PVCAMUniversal.cpp b/DeviceAdapters/PVCAM/PVCAMUniversal.cpp index d2e5e5aaa..ddc36a0ca 100644 --- a/DeviceAdapters/PVCAM/PVCAMUniversal.cpp +++ b/DeviceAdapters/PVCAM/PVCAMUniversal.cpp @@ -3779,125 +3779,121 @@ int Universal::FrameAcquired() if (!isAcquiring_) return DEVICE_OK; - rs_bool rsbRet = FALSE; - void* pCurrFramePtr = nullptr; + int ret = DEVICE_OK; + void* pCurrFramePtr = nullptr; PvFrameInfo currFrameNfo; currFrameNfo.SetTimestampMsec(GetCurrentMMTime().getMsec()); { std::lock_guard pvcamGuard(g_pvcamLock); - rsbRet = pl_exp_get_latest_frame_ex(hPVCAM_, &pCurrFramePtr, pFrameInfo_ ); - if (rsbRet != PV_OK) - LogPvcamError(__LINE__, "pl_exp_get_latest_frame_ex() failed"); + if (!pl_exp_get_latest_frame_ex(hPVCAM_, &pCurrFramePtr, pFrameInfo_)) + ret = LogPvcamError(__LINE__, "pl_exp_get_latest_frame_ex() failed"); } - if (rsbRet == PV_OK) + if (ret != DEVICE_OK) { - currFrameNfo.SetPvHCam(pFrameInfo_->hCam); - currFrameNfo.SetPvFrameNr(pFrameInfo_->FrameNr); - currFrameNfo.SetPvReadoutTime(pFrameInfo_->ReadoutTime); - currFrameNfo.SetPvTimeStamp(pFrameInfo_->TimeStamp); - currFrameNfo.SetPvTimeStampBOF(pFrameInfo_->TimeStampBOF); + abortAcquisitionInternal(); + return ret; + } - if (acqCfgCur_.CircBufEnabled) + currFrameNfo.SetPvHCam(pFrameInfo_->hCam); + currFrameNfo.SetPvFrameNr(pFrameInfo_->FrameNr); + currFrameNfo.SetPvReadoutTime(pFrameInfo_->ReadoutTime); + currFrameNfo.SetPvTimeStamp(pFrameInfo_->TimeStamp); + currFrameNfo.SetPvTimeStampBOF(pFrameInfo_->TimeStampBOF); + + if (acqCfgCur_.CircBufEnabled) + { + const int currFrameNr = currFrameNfo.PvFrameNr(); + const int prevFrameNr = lastPvFrameNr_; + if (currFrameNr == prevFrameNr) { - const int currFrameNr = currFrameNfo.PvFrameNr(); - const int prevFrameNr = lastPvFrameNr_; - if (currFrameNr == prevFrameNr) - { - // Received a duplicate callback? This seems like a bug in PVCAM, - // it occurs for optiMos at high frame rates. For now just silently ignore it, - // because the next one will correctly arrive right after that. - return DEVICE_OK; - } + // Received a duplicate callback? This seems like a bug in PVCAM, + // it occurs for optiMos at high frame rates. For now just silently ignore it, + // because the next one will correctly arrive right after that. + return DEVICE_OK; + } - // Check whether we haven't missed a callback - if (currFrameNr > prevFrameNr + 1) - { - const int missedCbCount = currFrameNr - prevFrameNr - 1; + // Check whether we haven't missed a callback + if (currFrameNr > prevFrameNr + 1) + { + const int missedCbCount = currFrameNr - prevFrameNr - 1; - if (circBufFrameRecoveryEnabled_) + if (circBufFrameRecoveryEnabled_) + { + // Get the last known frame index in the CB + const int lastFrIdx = circBuf_.LatestFrameIndex(); + if (lastFrIdx < 0) { - // Get the last known frame index in the CB - const int lastFrIdx = circBuf_.LatestFrameIndex(); - if (lastFrIdx < 0) - { - // We cannot perform frame recovery because we don't have a frame in the buffer yet - // so we cannot recover the metadata. This mostly happens with Polling acquisition - // because it can easily miss several frames when starting acquisition. - } - else + // We cannot perform frame recovery because we don't have a frame in the buffer yet + // so we cannot recover the metadata. This mostly happens with Polling acquisition + // because it can easily miss several frames when starting acquisition. + } + else + { + const PvFrameInfo& lastFrNfo = circBuf_.FrameInfo(lastFrIdx); + + // We need to re-create the FRAME_INFOs by averaging the known frame infos. + // This is not really nice way of fixing things but since the camera is running on + // constant rate the recovered data will be accurate enough. Plus, we mark the frame as recovered + // so the user will be aware of this. + const int recReadoutTm = static_cast((lastFrNfo.PvReadoutTime() + currFrameNfo.PvReadoutTime()) / 2); + const long long lastPvTimestampBOF = lastFrNfo.PvTimeStampBOF(); + const long long lastPvTimestampEOF = lastFrNfo.PvTimeStamp(); + const double lastApTimestampMsec = lastFrNfo.TimeStampMsec(); + const double div = missedCbCount + 1; + const double avgBofDiff = (currFrameNfo.PvTimeStampBOF() - lastPvTimestampBOF) / div; + const double avgEofDiff = (currFrameNfo.PvTimeStamp() - lastPvTimestampEOF) / div; + const double avgAppDiff = (currFrameNfo.TimeStampMsec() - lastApTimestampMsec) / div; + + for (int i = 0; i < missedCbCount; ++i) { - const PvFrameInfo& lastFrNfo = circBuf_.FrameInfo(lastFrIdx); - - // We need to re-create the FRAME_INFOs by averaging the known frame infos. - // This is not really nice way of fixing things but since the camera is running on - // constant rate the recovered data will be accurate enough. Plus, we mark the frame as recovered - // so the user will be aware of this. - const int recReadoutTm = static_cast((lastFrNfo.PvReadoutTime() + currFrameNfo.PvReadoutTime()) / 2); - const long long lastPvTimestampBOF = lastFrNfo.PvTimeStampBOF(); - const long long lastPvTimestampEOF = lastFrNfo.PvTimeStamp(); - const double lastApTimestampMsec = lastFrNfo.TimeStampMsec(); - const double div = missedCbCount + 1; - const double avgBofDiff = (currFrameNfo.PvTimeStampBOF() - lastPvTimestampBOF) / div; - const double avgEofDiff = (currFrameNfo.PvTimeStamp() - lastPvTimestampEOF) / div; - const double avgAppDiff = (currFrameNfo.TimeStampMsec() - lastApTimestampMsec) / div; - - for (int i = 0; i < missedCbCount; ++i) + // Get the index of the next frame in the CB. The data for this frame has been + // correctly delivered by the driver, however since we missed a callback we also + // missed the FRAME_INFO. Thus we need to recreate the FRAME_INFO ourselves. + // This can be removed once PVCAM implements better way of retrieving particular frames. + const unsigned int nextFrIdx = (lastFrIdx + i + 1) % circBuf_.Capacity(); + + // Retrieve the data pointer for the skipped callback + void* pRecFrameData = circBuf_.FrameData(nextFrIdx); + + // Re-create the FRAME_INFO + const short int recHCam = lastFrNfo.PvHCam(); + const int recFrameNr = prevFrameNr + i + 1; + const long long recTimeStampBOF = static_cast(lastPvTimestampBOF + ((i + 1)*avgBofDiff)); + const long long recTimeStampEOF = static_cast(lastPvTimestampEOF + ((i + 1)*avgEofDiff)); + const double recAppTimeStampEOF = lastApTimestampMsec + ((i + 1)*avgAppDiff); + + PvFrameInfo recFrNfo; + recFrNfo.SetPvHCam(recHCam); + recFrNfo.SetPvFrameNr(recFrameNr); + recFrNfo.SetPvReadoutTime(recReadoutTm); + recFrNfo.SetPvTimeStamp(recTimeStampEOF); + recFrNfo.SetPvTimeStampBOF(recTimeStampBOF); + recFrNfo.SetTimestampMsec(recAppTimeStampEOF); + recFrNfo.SetRecovered(true); + + imagesAcquired_++; + + // Process the new frame the same way as the frame + // would arrive correctly with a callback. + ret = ProcessFrame(pRecFrameData, recFrNfo); + if (ret != DEVICE_OK) { - // Get the index of the next frame in the CB. The data for this frame has been - // correctly delivered by the driver, however since we missed a callback we also - // missed the FRAME_INFO. Thus we need to recreate the FRAME_INFO ourselves. - // This can be removed once PVCAM implements better way of retrieving particular frames. - const unsigned int nextFrIdx = (lastFrIdx + i + 1) % circBuf_.Capacity(); - - // Retrieve the data pointer for the skipped callback - void* pRecFrameData = circBuf_.FrameData(nextFrIdx); - - // Re-create the FRAME_INFO - const short int recHCam = lastFrNfo.PvHCam(); - const int recFrameNr = prevFrameNr + i + 1; - const long long recTimeStampBOF = static_cast(lastPvTimestampBOF + ((i + 1)*avgBofDiff)); - const long long recTimeStampEOF = static_cast(lastPvTimestampEOF + ((i + 1)*avgEofDiff)); - const double recAppTimeStampEOF = lastApTimestampMsec + ((i + 1)*avgAppDiff); - - PvFrameInfo recFrNfo; - recFrNfo.SetPvHCam(recHCam); - recFrNfo.SetPvFrameNr(recFrameNr); - recFrNfo.SetPvReadoutTime(recReadoutTm); - recFrNfo.SetPvTimeStamp(recTimeStampEOF); - recFrNfo.SetPvTimeStampBOF(recTimeStampBOF); - recFrNfo.SetTimestampMsec(recAppTimeStampEOF); - recFrNfo.SetRecovered(true); - - imagesAcquired_++; - - // Notify our CB wrapper that a new frame has "arrived", - // it will increase its internal counters and indexes. - circBuf_.ReportFrameArrived(recFrNfo, pRecFrameData); - // Process the new frame the same way as the frame - // would arrive correctly with a callback. - ProcessFrame(pRecFrameData, circBuf_.FrameSize(), recFrNfo); - - imagesInserted_++; - imagesRecovered_++; + abortAcquisitionInternal(); + return ret; } + + imagesInserted_++; + imagesRecovered_++; } } - else - { // Frame recovery is disabled - // TODO: Again, should we report an error? - } } - lastPvFrameNr_ = currFrameNr; + else + { // Frame recovery is disabled + // TODO: Again, should we report an error? + } } - } - - if ( rsbRet != PV_OK ) - { - std::lock_guard pvcamGuard(g_pvcamLock); - if (pl_exp_abort( hPVCAM_, CCS_CLEAR ) != PV_OK) - LogPvcamError(__LINE__, "pl_exp_abort() failed"); - return DEVICE_ERR; + lastPvFrameNr_ = currFrameNr; } imagesAcquired_++; // A new frame has been successfully retrieved from the camera @@ -3906,15 +3902,14 @@ int Universal::FrameAcquired() // so we have to check. In case of SnapImage the singleFrameBufRaw_ already // contains the data (since it's passed to pl_start_seq() and no PushImage() // is done - the single image is retrieved with GetImageBuffer(). - if ( !snappingSingleFrame_ ) + if (!snappingSingleFrame_) { - size_t currFrameSize = singleFrameBufRawSz_; - if (acqCfgCur_.CircBufEnabled) + ret = ProcessFrame(pCurrFramePtr, currFrameNfo); + if (ret != DEVICE_OK) { - currFrameSize = circBuf_.FrameSize(); - circBuf_.ReportFrameArrived(currFrameNfo, pCurrFramePtr); + abortAcquisitionInternal(); + return ret; } - ProcessFrame(pCurrFramePtr, currFrameSize, currFrameNfo); } imagesInserted_++; @@ -3923,7 +3918,7 @@ int Universal::FrameAcquired() return DEVICE_OK; } -int Universal::ProcessFrame(const void* pData, size_t dataSz, const PvFrameInfo& frameNfo) +int Universal::ProcessFrame(const void* pData, const PvFrameInfo& frameNfo) { // Ignore inserts if we already have all images inserted. // This should never happen but stay on safe side. @@ -3934,15 +3929,18 @@ int Universal::ProcessFrame(const void* pData, size_t dataSz, const PvFrameInfo& if (!isAcquiring_) // Cannot guard it with acqLock_ return DEVICE_OK; + if (acqCfgCur_.CircBufEnabled) + { + // Notify our CB wrapper that a new frame has "arrived", + // it will increase its internal counters and indexes. + circBuf_.ReportFrameArrived(frameNfo, pData); + } + int ret = DEVICE_OK; ret = customDiskWriter_->WriteFrame(pData, frameNfo.PvFrameNr()); if (ret != DEVICE_OK) - { - StopSequenceAcquisition(); - // TODO: Display an error message? return ret; - } // Send the frame to MMCore if custom streaming to disk is not active, // otherwise send only every Nth frame to reduce unnecessary CPU/memory load @@ -3977,13 +3975,11 @@ int Universal::ProcessFrame(const void* pData, size_t dataSz, const PvFrameInfo& SetProperty(MM::g_Keyword_ActualInterval_ms, CDeviceUtils::ConvertToString(actualInterval)); unsigned char* pOutBuf = nullptr; + const size_t dataSz = (acqCfgCur_.CircBufEnabled) + ? circBuf_.FrameSize() : singleFrameBufRawSz_; ret = postProcessSingleFrame(&pOutBuf, (unsigned char*)pData, dataSz); if (ret != DEVICE_OK) - { - StopSequenceAcquisition(); - // TODO: Display an error message? return ret; - } // The post-processing done above also decodes the frame metadata if supported if (acqCfgCur_.FrameMetadataEnabled) @@ -4123,17 +4119,12 @@ int Universal::ProcessFrame(const void* pData, size_t dataSz, const PvFrameInfo& ret = GetCoreCallback()->InsertImage(this, pOutBuf, GetImageWidth(), GetImageHeight(), GetImageBytesPerPixel(), md.Serialize()); if (ret != DEVICE_OK) - { - StopSequenceAcquisition(); - // TODO: Display an error message? return ret; - } } // If we already have all frames inserted tell the camera to stop if (imagesInserted_ + 1 >= imagesToAcquire_) { - // TODO: Can we replace this last occurrence with StopSequenceAcquisition()? abortAcquisitionInternal(); } diff --git a/DeviceAdapters/PVCAM/PvCircularBuffer.cpp b/DeviceAdapters/PVCAM/PvCircularBuffer.cpp index fbc1d6153..4eb1f4ef1 100644 --- a/DeviceAdapters/PVCAM/PvCircularBuffer.cpp +++ b/DeviceAdapters/PVCAM/PvCircularBuffer.cpp @@ -76,7 +76,8 @@ void PvCircularBuffer::Resize(size_t frameSize, int count) Reset(); } -void PvCircularBuffer::ReportFrameArrived(const PvFrameInfo& frameNfo, void* pFrameData) +void PvCircularBuffer::ReportFrameArrived( + const PvFrameInfo& frameNfo, const void* pFrameData) { // Calculate the index of the received frame in our circular buffer const int curFrameIdx = static_cast diff --git a/DeviceAdapters/PVCAM/PvCircularBuffer.h b/DeviceAdapters/PVCAM/PvCircularBuffer.h index 097d692c6..200170fcd 100644 --- a/DeviceAdapters/PVCAM/PvCircularBuffer.h +++ b/DeviceAdapters/PVCAM/PvCircularBuffer.h @@ -77,7 +77,7 @@ class PvCircularBuffer * This function notifies the buffer that a new frame has arrived and assigns * the frame metadata to correct location. */ - void ReportFrameArrived(const PvFrameInfo& frameNfo, void* pFrameData); + void ReportFrameArrived(const PvFrameInfo& frameNfo, const void* pFrameData); private: std::unique_ptr pBuffer_{ nullptr }; diff --git a/DeviceAdapters/PVCAM/Version.h b/DeviceAdapters/PVCAM/Version.h index cf7afa6ac..720d3edda 100644 --- a/DeviceAdapters/PVCAM/Version.h +++ b/DeviceAdapters/PVCAM/Version.h @@ -2,4 +2,4 @@ #define PVCAM_ADAPTER_VERSION_MAJOR 1 #define PVCAM_ADAPTER_VERSION_MINOR 4 -#define PVCAM_ADAPTER_VERSION_REVISION 0 +#define PVCAM_ADAPTER_VERSION_REVISION 1 From d84038a747875977e91a668240b14702852b1369 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tom=C3=A1=C5=A1=20Han=C3=A1k?= Date: Wed, 6 May 2026 18:36:53 +0200 Subject: [PATCH 4/4] PVCAM: Moved code composing metadata for MMCore to new function. --- DeviceAdapters/PVCAM/PVCAMAdapter.h | 4 + DeviceAdapters/PVCAM/PVCAMUniversal.cpp | 328 ++++++++++++------------ 2 files changed, 170 insertions(+), 162 deletions(-) diff --git a/DeviceAdapters/PVCAM/PVCAMAdapter.h b/DeviceAdapters/PVCAM/PVCAMAdapter.h index e41a20106..895346254 100644 --- a/DeviceAdapters/PVCAM/PVCAMAdapter.h +++ b/DeviceAdapters/PVCAM/PVCAMAdapter.h @@ -622,6 +622,10 @@ class Universal : public CCameraBase * Called from FrameAcquired(), inserts the frame to the MMCore. */ int ProcessFrame(const void* pData, const PvFrameInfo& frameNfo); + /** + * Called from ProcessFrame(), composes metadata for the MMCore. + */ + void BuildMetadata(MM::CameraImageMetadata& md, const PvFrameInfo& frameNfo); private: // Make object non-copyable diff --git a/DeviceAdapters/PVCAM/PVCAMUniversal.cpp b/DeviceAdapters/PVCAM/PVCAMUniversal.cpp index ddc36a0ca..01db204f4 100644 --- a/DeviceAdapters/PVCAM/PVCAMUniversal.cpp +++ b/DeviceAdapters/PVCAM/PVCAMUniversal.cpp @@ -1482,7 +1482,9 @@ bool Universal::GetErrorText(int errorCode, char* text) const int Universal::SnapImage() { int nRet = DEVICE_ERR; - MM::MMTime startTs = GetCurrentMMTime(); + MM::MMTime startTs; + MM::MMTime endTs; + { std::lock_guard acqGuard(acqLock_); START_METHOD("Universal::SnapImage"); @@ -1560,12 +1562,14 @@ int Universal::SnapImage() singleFrameModeReady_ = false; } - const MM::MMTime endTs = GetCurrentMMTime(); - LogTimeDiff(startTs, endTs, "SnapImage() took: ", true); - isAcquiring_ = false; - return nRet; + + endTs = GetCurrentMMTime(); } + + LogTimeDiff(startTs, endTs, "SnapImage() took: ", true); + + return nRet; } const unsigned char* Universal::GetImageBuffer() @@ -3781,8 +3785,7 @@ int Universal::FrameAcquired() int ret = DEVICE_OK; void* pCurrFramePtr = nullptr; - PvFrameInfo currFrameNfo; - currFrameNfo.SetTimestampMsec(GetCurrentMMTime().getMsec()); + const MM::MMTime frameTs = GetCurrentMMTime(); { std::lock_guard pvcamGuard(g_pvcamLock); @@ -3795,6 +3798,8 @@ int Universal::FrameAcquired() return ret; } + PvFrameInfo currFrameNfo; + currFrameNfo.SetTimestampMsec(frameTs.getMsec()); currFrameNfo.SetPvHCam(pFrameInfo_->hCam); currFrameNfo.SetPvFrameNr(pFrameInfo_->FrameNr); currFrameNfo.SetPvReadoutTime(pFrameInfo_->ReadoutTime); @@ -3948,29 +3953,8 @@ int Universal::ProcessFrame(const void* pData, const PvFrameInfo& frameNfo) || (imagesInserted_ % acqCfgCur_.DiskStreamingCoreSkipRatio == 0); if (sendFrameToCore) { - // Build the metadata - MM::CameraImageMetadata md; - md.AddTag(MM::g_Keyword_Metadata_CameraLabel, deviceLabel_); - md.AddTag("TimeStampMsec", CDeviceUtils::ConvertToString(frameNfo.TimeStampMsec())); - - md.AddTag( "PVCAM-CameraHandle", frameNfo.PvHCam() ); - md.AddTag( "PVCAM-FrameNr", frameNfo.PvFrameNr() ); - md.AddTag( "PVCAM-ReadoutTime", frameNfo.PvReadoutTime() ); - md.AddTag( "PVCAM-TimeStamp", frameNfo.PvTimeStamp() ); - md.AddTag( "PVCAM-TimeStampBOF", frameNfo.PvTimeStampBOF() ); - if (circBufFrameRecoveryEnabled_) - { - md.AddTag( "PVCAM-FrameRecovered", frameNfo.IsRecovered() ); - md.AddTag( "PVCAM-FramesRecoveredTotal", imagesRecovered_ ); - } - - const double startTimeMsec = startTime_.getMsec(); + const double startTimeMsec = startTime_.getMsec(); const double elapsedTimeMsec = frameNfo.TimeStampMsec() - startTimeMsec; - - // The time elapsed since start of the acquisition until current frame readout - // Now added by MM automatically, no need to do it here. - // md.AddTag(MM::g_Keyword_Elapsed_Time_ms, CDeviceUtils::ConvertToString(elapsedTimeMsec)); - const double actualInterval = elapsedTimeMsec / imagesInserted_; SetProperty(MM::g_Keyword_ActualInterval_ms, CDeviceUtils::ConvertToString(actualInterval)); @@ -3981,139 +3965,8 @@ int Universal::ProcessFrame(const void* pData, const PvFrameInfo& frameNfo) if (ret != DEVICE_OK) return ret; - // The post-processing done above also decodes the frame metadata if supported - if (acqCfgCur_.FrameMetadataEnabled) - { - // FMD stands for Frame-MetaData, we should somehow distinguish the embedded - // metadata from other metadata and keep them grouped or close together. - const md_frame_header* fHdr = metaFrameStruct_->header; - // Selected metadata from the frame header - md.AddTag("PVCAM-FMD-Version", fHdr->version); // Need to use uns16 because uns8 is displayed as char - md.AddTag("PVCAM-FMD-FrameNr", fHdr->frameNr); - md.AddTag("PVCAM-FMD-RoiCount", fHdr->roiCount); - md.AddTag("PVCAM-FMD-BitDepth", fHdr->bitDepth); // Need to use uns16 because uns8 is displayed as char - const char* cKeywordColorMask = "PVCAM-FMD-ColorMask"; - switch (fHdr->colorMask) - { - case COLOR_NONE: - md.AddTag(cKeywordColorMask, "None"); - break; - case COLOR_RGGB: - md.AddTag(cKeywordColorMask, "RGGB"); - break; - case COLOR_GRBG: - md.AddTag(cKeywordColorMask, "GRBG"); - break; - case COLOR_GBRG: - md.AddTag(cKeywordColorMask, "GBRG"); - break; - case COLOR_BGGR: - md.AddTag(cKeywordColorMask, "BGGR"); - break; - default: - md.AddTag(cKeywordColorMask, "Unknown"); - break; - } - md.AddTag("PVCAM-FMD-Flags", fHdr->flags); // Need to use uns16 because uns8 is displayed as char - if (fHdr->version >= 2) - { - md.AddTag("PVCAM-FMD-ImageFormat", getPvcamImageFormatString(fHdr->imageFormat)); - md.AddTag("PVCAM-FMD-ImageCompression", getPvcamImageCompressionString(fHdr->imageCompression)); - } - if (fHdr->version < 3) - { - md.AddTag( "PVCAM-FMD-ExposureTimeNs", - (ulong64)fHdr->exposureTime * fHdr->exposureTimeResNs ); - md.AddTag( "PVCAM-FMD-TimestampBofNs", - (ulong64)fHdr->timestampBOF * fHdr->timestampResNs ); - md.AddTag( "PVCAM-FMD-TimestampEofNs", - (ulong64)fHdr->timestampEOF * fHdr->timestampResNs ); - } - else - { - const md_frame_header_v3* fHdr3 = - reinterpret_cast(metaFrameStruct_->header); - // Version 3 of the metadata transfers the timestamps in picoseconds. - md.AddTag( "PVCAM-FMD-ExposureTimePs", - (ulong64)fHdr3->exposureTime ); - md.AddTag( "PVCAM-FMD-TimestampBofPs", - (ulong64)fHdr3->timestampBOF ); - md.AddTag( "PVCAM-FMD-TimestampEofPs", - (ulong64)fHdr3->timestampEOF ); - } - // Implied ROI - const rgn_type& iRoi = metaFrameStruct_->impliedRoi; - snprintf(metaRoiStr_, sizeof(metaRoiStr_), "[%u,%u,%u,%u,%u,%u]", - iRoi.s1, iRoi.s2, iRoi.sbin, iRoi.p1, iRoi.p2, iRoi.pbin); - md.AddTag("PVCAM-FMD-ImpliedRoi", metaRoiStr_); - // Per-ROI metadata - metaAllRoisStr_ = "["; - for (int i = 0; i < metaFrameStruct_->roiCount; ++i) - { - // Since we cannot add per-ROI metadata we will format the MD to a simple JSON array - // and add it as a per-Frame metadata TAG. Example: - // "[{"nr":1,"coords":[0,0,0,0,0,0]},{"nr":2,"coords":[0,0,0,0,0,0]}]" - const md_frame_roi& pRoi = metaFrameStruct_->roiArray[i]; - const md_frame_roi_header* rHdr = pRoi.header; - if (rHdr->flags & PL_MD_ROI_FLAG_INVALID) - continue; // Skip invalid regions - if (i > 0) - metaAllRoisStr_.append(","); - metaAllRoisStr_.append("{"); - snprintf(metaRoiStr_, sizeof(metaRoiStr_), - "\"nr\":%u,\"coords\":[%u,%u,%u,%u,%u,%u],\"flags\":%u", - rHdr->roiNr, - rHdr->roi.s1, rHdr->roi.s2, rHdr->roi.sbin, - rHdr->roi.p1, rHdr->roi.p2, rHdr->roi.pbin, - rHdr->flags); - metaAllRoisStr_.append(metaRoiStr_); - if (fHdr->flags & PL_MD_FRAME_FLAG_ROI_TS_SUPPORTED) - { - if (fHdr->version < 3) - { - snprintf(metaRoiStr_, sizeof(metaRoiStr_), - ",\"borNs\":%llu,\"eorNs\":%llu", - (ulong64)rHdr->timestampBOR * fHdr->roiTimestampResNs, - (ulong64)rHdr->timestampEOR * fHdr->roiTimestampResNs); - } - else - { - snprintf(metaRoiStr_, sizeof(metaRoiStr_), - ",\"bor\":%u,\"eor\":%u", - rHdr->timestampBOR, rHdr->timestampEOR); - } - metaAllRoisStr_.append(metaRoiStr_); - } - if (/*(rHdr->flags & PL_MD_ROI_FLAG_HEADER_ONLY) && */pRoi.extMdDataSize > 0) - { - const md_ext_item_collection& collection = metaFrameExtData_.at(rHdr->roiNr); - const md_ext_item* item_id = collection.map[PL_MD_EXT_TAG_PARTICLE_ID]; - if (item_id) - { - snprintf(metaRoiStr_, sizeof(metaRoiStr_), - ",\"tagParticleId\":%u", *((uint32_t*)item_id->value)); - metaAllRoisStr_.append(metaRoiStr_); - } - const md_ext_item* item_m0 = collection.map[PL_MD_EXT_TAG_PARTICLE_M0]; - if (item_m0) - { - snprintf(metaRoiStr_, sizeof(metaRoiStr_), - ",\"tagParticleM0\":%u", *((uint32_t*)item_m0->value)); - metaAllRoisStr_.append(metaRoiStr_); - } - const md_ext_item* item_m2 = collection.map[PL_MD_EXT_TAG_PARTICLE_M2]; - if (item_m2) - { - snprintf(metaRoiStr_, sizeof(metaRoiStr_), - ",\"tagParticleM2\":%u", *((uint32_t*)item_m2->value)); - metaAllRoisStr_.append(metaRoiStr_); - } - } - metaAllRoisStr_.append("}"); - } - metaAllRoisStr_.append("]"); - md.AddTag("PVCAM-FMD-RoiMD", metaAllRoisStr_); - } + MM::CameraImageMetadata md; + BuildMetadata(md, frameNfo); // This method inserts a new image into the circular buffer (residing in MMCore) ret = GetCoreCallback()->InsertImage(this, pOutBuf, GetImageWidth(), @@ -4131,6 +3984,157 @@ int Universal::ProcessFrame(const void* pData, const PvFrameInfo& frameNfo) return ret; } +void Universal::BuildMetadata(MM::CameraImageMetadata& md, const PvFrameInfo& frameNfo) +{ + md.AddTag(MM::g_Keyword_Metadata_CameraLabel, deviceLabel_); + md.AddTag("TimeStampMsec", CDeviceUtils::ConvertToString(frameNfo.TimeStampMsec())); + + md.AddTag("PVCAM-CameraHandle", frameNfo.PvHCam()); + md.AddTag("PVCAM-FrameNr", frameNfo.PvFrameNr()); + md.AddTag("PVCAM-ReadoutTime", frameNfo.PvReadoutTime()); + md.AddTag("PVCAM-TimeStamp", frameNfo.PvTimeStamp()); + md.AddTag("PVCAM-TimeStampBOF", frameNfo.PvTimeStampBOF()); + + if (circBufFrameRecoveryEnabled_) + { + md.AddTag("PVCAM-FrameRecovered", frameNfo.IsRecovered()); + md.AddTag("PVCAM-FramesRecoveredTotal", imagesRecovered_); + } + + // The post-processing done above also decodes the frame metadata if supported + if (acqCfgCur_.FrameMetadataEnabled) + { + // FMD stands for Frame-MetaData, we should somehow distinguish the embedded + // metadata from other metadata and keep them grouped or close together. + const md_frame_header* fHdr = metaFrameStruct_->header; + // Selected metadata from the frame header + md.AddTag("PVCAM-FMD-Version", fHdr->version); // Use uns16, uns8 is displayed as char + md.AddTag("PVCAM-FMD-FrameNr", fHdr->frameNr); + md.AddTag("PVCAM-FMD-RoiCount", fHdr->roiCount); + md.AddTag("PVCAM-FMD-BitDepth", fHdr->bitDepth); // Use uns16, uns8 is displayed as char + const char* cKeywordColorMask = "PVCAM-FMD-ColorMask"; + switch (fHdr->colorMask) + { + case COLOR_NONE: + md.AddTag(cKeywordColorMask, "None"); + break; + case COLOR_RGGB: + md.AddTag(cKeywordColorMask, "RGGB"); + break; + case COLOR_GRBG: + md.AddTag(cKeywordColorMask, "GRBG"); + break; + case COLOR_GBRG: + md.AddTag(cKeywordColorMask, "GBRG"); + break; + case COLOR_BGGR: + md.AddTag(cKeywordColorMask, "BGGR"); + break; + default: + md.AddTag(cKeywordColorMask, "Unknown"); + break; + } + md.AddTag("PVCAM-FMD-Flags", fHdr->flags); // Use uns16, uns8 is displayed as char + if (fHdr->version >= 2) + { + md.AddTag("PVCAM-FMD-ImageFormat", + getPvcamImageFormatString(fHdr->imageFormat)); + md.AddTag("PVCAM-FMD-ImageCompression", + getPvcamImageCompressionString(fHdr->imageCompression)); + } + if (fHdr->version < 3) + { + md.AddTag("PVCAM-FMD-ExposureTimeNs", + (ulong64)fHdr->exposureTime * fHdr->exposureTimeResNs); + md.AddTag("PVCAM-FMD-TimestampBofNs", + (ulong64)fHdr->timestampBOF * fHdr->timestampResNs); + md.AddTag("PVCAM-FMD-TimestampEofNs", + (ulong64)fHdr->timestampEOF * fHdr->timestampResNs); + } + else + { + const md_frame_header_v3* fHdr3 = + reinterpret_cast(metaFrameStruct_->header); + // Version 3 of the metadata transfers the timestamps in picoseconds. + md.AddTag("PVCAM-FMD-ExposureTimePs", fHdr3->exposureTime); + md.AddTag("PVCAM-FMD-TimestampBofPs", fHdr3->timestampBOF); + md.AddTag("PVCAM-FMD-TimestampEofPs", fHdr3->timestampEOF); + } + // Implied ROI + const rgn_type& iRoi = metaFrameStruct_->impliedRoi; + snprintf(metaRoiStr_, sizeof(metaRoiStr_), "[%u,%u,%u,%u,%u,%u]", + iRoi.s1, iRoi.s2, iRoi.sbin, iRoi.p1, iRoi.p2, iRoi.pbin); + md.AddTag("PVCAM-FMD-ImpliedRoi", metaRoiStr_); + // Per-ROI metadata + metaAllRoisStr_ = "["; + for (int i = 0; i < metaFrameStruct_->roiCount; ++i) + { + // Since we cannot add per-ROI metadata we will format the MD to a simple + // JSON array and add it as a per-Frame metadata TAG. Example: + // "[{"nr":1,"coords":[0,0,0,0,0,0]},{"nr":2,"coords":[0,0,0,0,0,0]}]" + const md_frame_roi& pRoi = metaFrameStruct_->roiArray[i]; + const md_frame_roi_header* rHdr = pRoi.header; + if (rHdr->flags & PL_MD_ROI_FLAG_INVALID) + continue; // Skip invalid regions + if (i > 0) + metaAllRoisStr_.append(","); + metaAllRoisStr_.append("{"); + snprintf(metaRoiStr_, sizeof(metaRoiStr_), + "\"nr\":%u,\"coords\":[%u,%u,%u,%u,%u,%u],\"flags\":%u", + rHdr->roiNr, + rHdr->roi.s1, rHdr->roi.s2, rHdr->roi.sbin, + rHdr->roi.p1, rHdr->roi.p2, rHdr->roi.pbin, + rHdr->flags); + metaAllRoisStr_.append(metaRoiStr_); + if (fHdr->flags & PL_MD_FRAME_FLAG_ROI_TS_SUPPORTED) + { + if (fHdr->version < 3) + { + snprintf(metaRoiStr_, sizeof(metaRoiStr_), + ",\"borNs\":%llu,\"eorNs\":%llu", + (ulong64)rHdr->timestampBOR * fHdr->roiTimestampResNs, + (ulong64)rHdr->timestampEOR * fHdr->roiTimestampResNs); + } + else + { + snprintf(metaRoiStr_, sizeof(metaRoiStr_), + ",\"bor\":%u,\"eor\":%u", + rHdr->timestampBOR, rHdr->timestampEOR); + } + metaAllRoisStr_.append(metaRoiStr_); + } + if (/*(rHdr->flags & PL_MD_ROI_FLAG_HEADER_ONLY) && */pRoi.extMdDataSize > 0) + { + const md_ext_item_collection& collection = metaFrameExtData_.at(rHdr->roiNr); + const md_ext_item* item_id = collection.map[PL_MD_EXT_TAG_PARTICLE_ID]; + if (item_id) + { + snprintf(metaRoiStr_, sizeof(metaRoiStr_), + ",\"tagParticleId\":%u", *((uint32_t*)item_id->value)); + metaAllRoisStr_.append(metaRoiStr_); + } + const md_ext_item* item_m0 = collection.map[PL_MD_EXT_TAG_PARTICLE_M0]; + if (item_m0) + { + snprintf(metaRoiStr_, sizeof(metaRoiStr_), + ",\"tagParticleM0\":%u", *((uint32_t*)item_m0->value)); + metaAllRoisStr_.append(metaRoiStr_); + } + const md_ext_item* item_m2 = collection.map[PL_MD_EXT_TAG_PARTICLE_M2]; + if (item_m2) + { + snprintf(metaRoiStr_, sizeof(metaRoiStr_), + ",\"tagParticleM2\":%u", *((uint32_t*)item_m2->value)); + metaAllRoisStr_.append(metaRoiStr_); + } + } + metaAllRoisStr_.append("}"); + } + metaAllRoisStr_.append("]"); + md.AddTag("PVCAM-FMD-RoiMD", metaAllRoisStr_); + } +} + //============================================================================= //===================================================================== PRIVATE