From 7a22f56f6046b7c098773b0987988fef15f5b9c0 Mon Sep 17 00:00:00 2001 From: Christoffer Fremling Date: Thu, 29 Oct 2026 16:07:10 +0200 Subject: [PATCH] camerad, sequencerd: stop and abort an NEXP>1 series stop: the sequencer accepts it for the whole series of the current target (the EXPOSE wait covers only the first sub-exposure; camerad not READY covers the rest) and no longer clears a cancel. camerad ends the series after the frame in progress, cut short if it is integrating with 5 s or more left, instead of starting the next sub-exposure. abort no longer starts the next sub-exposure either. A cancelled exposure's remaining PIXELCOUNT messages cannot set the READOUT wait later. camerad waits for the last frame's FITS write before acting on the next command after READY. Co-Authored-By: Claude Opus 5.5 --- camerad/astrocam.cpp | 44 +++++++++++++++++++++++++++++++---------- camerad/astrocam.h | 22 ++++++++++++++++++++- sequencerd/sequence.cpp | 18 +++++++++++++---- 3 files changed, 69 insertions(+), 15 deletions(-) diff --git a/camerad/astrocam.cpp b/camerad/astrocam.cpp index 6a5792d1..13a12dd8 100644 --- a/camerad/astrocam.cpp +++ b/camerad/astrocam.cpp @@ -957,6 +957,10 @@ namespace AstroCam { long Interface::do_abort() { std::string function = "AstroCam::Interface::do_abort"; std::stringstream message; + // No sub-exposure follows an abort: clearing exposure_pending below wakes + // dothread_monitor_exposure_pending, which starts the next sub-exposure + // whenever nexp is still > 0 after its decrement. + this->nexp.store(0); // int this_expbuf = this->get_expbuf(); for ( const auto &dev : this->active_devnums ) { this->exposure_pending( dev, false ); @@ -1019,9 +1023,10 @@ namespace AstroCam { return ERROR; } - // Don't make any changes while frames are still being written. + // Don't make any changes while frames are still being written: give a + // write that is finishing (ms after READY) up to 2 s, then refuse. // - if ( this->any_writes_pending() ) { + if ( !this->wait_writes_done( 2000 ) ) { logwrite( function, "ERROR: cannot change binning while frame write is in progress" ); retstring="write_in_progress"; return ERROR; @@ -2713,7 +2718,7 @@ namespace AstroCam { if ( interface.nexp.load() > 0 ) { message.str(""); message << "starting next exposure. " << interface.nexp << " remaining"; interface.broadcast.notice( function, message.str() ); - interface.do_expose(interface.nexp); + interface.do_expose(interface.nexp, true); // continuation: nexp already holds what is left } else { interface.status.can_expose.store(true); @@ -2768,7 +2773,7 @@ namespace AstroCam { * @return ERROR or NO_ERROR * */ - long Interface::do_expose(int nexp_in) { + long Interface::do_expose(int nexp_in, bool continuation) { const std::string function("AstroCam::Interface::do_expose"); std::stringstream message; std::string devstr; @@ -2792,8 +2797,6 @@ namespace AstroCam { } } - this->nexp.store(nexp_in); - // Different controllers may have different rules about when to set and clear their // exposure_pending flag (depending on whether or not they support frame transfer), but // if an exposure is pending for any controller then do not start a new exposure. @@ -2807,6 +2810,11 @@ namespace AstroCam { return(ERROR); } + // Only now, and only for a new expose command: a refused expose must not + // change how many sub-exposures the running series has left, and a + // continuation must not undo a stop that lowered nexp since it was read. + if ( !continuation ) this->nexp.store(nexp_in); + // When using the internal ARC shutter control you cannot overlap exposures // with readout because it would require sending a command which cannot // be handled during readout. @@ -3602,9 +3610,10 @@ for ( const auto &dev : selectdev ) { return( ERROR ); } - // Don't remap the DMA buffer while frames are still being written from it. + // Don't remap the DMA buffer while frames are still being written from it: + // give a write that is finishing (ms after READY) up to 2 s, then refuse. // - if ( this->any_writes_pending() ) { + if ( !this->wait_writes_done( 2000 ) ) { logwrite( function, "ERROR: cannot remap DMA buffer while frame write is in progress" ); retstring="write_in_progress"; return ERROR; @@ -4427,9 +4436,18 @@ logwrite(function, message.str()); const std::string function("AstroCam::Interface::stop_exposure"); std::stringstream message; - // nothing to do if not exposing - // + // An NEXP>1 series ends after the frame in progress, whatever that frame + // is doing -- integrating, reading out, or about to start -- so no further + // sub-exposure begins. That frame is also cut short below when it is + // integrating with >= 5 s left. + const bool ends_series = ( this->nexp.load() > 1 ); + if ( ends_series ) this->nexp.store(1); if ( ! this->exposure_pending() ) { + if ( ends_series ) { + logwrite( function, "between sub-exposures: the series ends after the next frame" ); + retstring="series_ends"; + return NO_ERROR; + } logwrite( function, "not exposing" ); return NOTHING; } @@ -4440,6 +4458,12 @@ logwrite(function, message.str()); // long remaining_time = this->camera.shutter_timer.get_remaining(); + if ( remaining_time < 5000 && ends_series ) { + logwrite( function, "too late to cut this frame short: the series ends after it" ); + retstring="series_ends"; + return NO_ERROR; + } + if ( remaining_time < 5000 ) { message << "ERROR cannot stop exposure time with less than 5 s exptime remaining" << " (" << remaining_time << ")"; diff --git a/camerad/astrocam.h b/camerad/astrocam.h index 0901b248..f225ab9a 100644 --- a/camerad/astrocam.h +++ b/camerad/astrocam.h @@ -964,6 +964,26 @@ std::vector> fitsinfo; /***** Interface::any_writes_pending ************************************/ + /***** Interface::wait_writes_done **************************************/ + /** + * @brief wait up to timeout_ms for every frame write to finish + * @return true if no writes are pending, false on timeout + * @details READY is published when the last frame has arrived, a few ms + * before its FITS write ends, so a command sent right after READY + * can find a write still in progress. + */ + inline bool wait_writes_done( int timeout_ms ) { + std::unique_lock lock( this->write_lock ); + return this->write_condition.wait_for( lock, std::chrono::milliseconds( timeout_ms ), [this]() { + for ( const auto &pending : this->writes_pending ) { + if ( !pending.empty() ) return false; + } + return true; + } ); + } + /***** Interface::wait_writes_done **************************************/ + + /***** Interface::write_pending *****************************************/ /** * @brief Set or clear the write pending state for a given exposure @@ -1240,7 +1260,7 @@ std::vector> fitsinfo; void set_imagesize(int rowsin, int colsin, int* status); long expose(std::string nexp_in); - long do_expose(int nexp_in); + long do_expose(int nexp_in, bool continuation=false); ///< continuation: the next sub-exposure of a running series double get_live_airmass(); ///< latest airmass from cached tcsd telemetry, or NAN void get_live_provenance( std::string &githash, std::string &buildtime ); ///< build provenance of all running daemons long native(std::string cmdstr); diff --git a/sequencerd/sequence.cpp b/sequencerd/sequence.cpp index aa24ba62..5aa8d9d3 100644 --- a/sequencerd/sequence.cpp +++ b/sequencerd/sequence.cpp @@ -1054,6 +1054,7 @@ namespace Sequencer { // if ( this->cancel_flag.load() ) { this->broadcast.notice( function, "exposure cancelled" ); + this->arm_readout_flag = false; // the stopped frame's PIXELCOUNTs must not set READOUT later error = this->target.update_state( Sequencer::TARGET_UNASSIGNED ); message.str(""); message << ( error==NO_ERROR ? "" : "ERROR " ) << "marking target " << this->target.name << " id " << this->target.obsid << " order " << this->target.obsorder @@ -3209,15 +3210,24 @@ namespace Sequencer { ScopedState thr_state( this->thread_state_manager, Sequencer::THR_STOP_EXPOSURE ); - // This function is only used while exposing + // Only while an exposure command of the current target is running. For + // NEXP>1 that is the whole series: the EXPOSE bit covers only the first + // sub-exposure; after it, camerad not READY (and not the previous + // target's last readout after a pick, which has nothing left to stop) + // covers the rest. A stale READOUT bit is not evidence. camerad ends the + // series after the frame in progress, cut short if it is integrating + // with >= 5 s left. A stop never clears a cancel. // - if ( ! this->wait_state_manager.is_set( Sequencer::SEQ_WAIT_EXPOSE ) ) { + bool series_running = false; + { + std::lock_guard lock( this->camerad_mtx ); + series_running = !this->can_expose.load() && !this->readout_gate_armed; + } + if ( ! this->wait_state_manager.is_set( Sequencer::SEQ_WAIT_EXPOSE ) && !series_running ) { this->broadcast.notice(function, "not currently exposing"); return; } - this->cancel_flag.store(false); - // Send command to the camera to stop the exposure. // std::string reply;