From 3eb9e3a4f875820ce58bbba696874541ff1341c4 Mon Sep 17 00:00:00 2001 From: Christoffer Fremling Date: Fri, 6 Nov 2026 17:43:41 -0800 Subject: [PATCH] sequencerd, camerad: set the flexure compensation for the pointing being exposed flexure_set ran once per target, in parallel with the slew, so flexured computed the compensation from telescope telemetry read before or during the slew, for the previous pointing. It was never refreshed during an NEXP>1 series, or for a REPEAT, while the telescope tracks. flexure_set now runs for every exposure, unconditionally: - as each exposure is initiated, for every target, however it was acquired (automatically, by hand, or not at all) and after any target offset. After a slew, the slit move to the expose position covers it; - at each shutter close camerad reports (not a pause) that another sub-exposure of the expose command follows, until camerad is ready again. The shutter is closed between any two integrations, also with frame transfer; camerad's INREADOUT flag is not used, since with frame transfer two readouts can give it one rising edge; - for a REPEAT, once camera_set has seen the camera ready. When the telescope has not moved, flexured recomputes the positions the actuators already hold, so they do not move. Each set runs in a detached thread, in the std::thread( ..., this ).detach() form main uses for its other detached Sequence calls, and nothing waits for it. A failure is reported and no longer stops the sequence; flexure_set no longer throws, and after a cancel it no longer reports a set it skipped. camerad takes the flexure positions (FLEX_X/Y/Z) once, at the end of each exposure, instead of at its start: in each channel's readout thread, after the shutter has closed and before the readout. The compensation can change after an exposure starts, and with frame transfer while the next sub-exposure is already integrating. Co-Authored-By: Claude Opus 5.5 --- camerad/astrocam.cpp | 15 ++++++++++- sequencerd/sequence.cpp | 58 ++++++++++++++++++++++++++++++++++------- sequencerd/sequence.h | 3 +++ 3 files changed, 66 insertions(+), 10 deletions(-) diff --git a/camerad/astrocam.cpp b/camerad/astrocam.cpp index 6a5792d1..14a82f02 100644 --- a/camerad/astrocam.cpp +++ b/camerad/astrocam.cpp @@ -2185,6 +2185,20 @@ namespace AstroCam { logwrite(function, message.str()); #endif + // The flexure compensation can change after the exposure starts, so this + // channel's flexure positions are taken at its end, with SHUTTIME, before + // its readout. + // + { + std::lock_guard lock( server.live_telemetry_mtx ); + auto flex = server.live_telemetry.find( Topic::FLEXURED ); + if ( flex != server.live_telemetry.end() ) { + for ( const auto &k : FitsHeaderKeys::FlexureInfoKeys ) { + if ( con.channel == k.chan ) con.expinfo[expbuf].telemkeys.add_json_key( flex->second, k.jkey, k.keyword, k.comment, k.type, EXT, k.chan ); + } + } + } + // Wait for any shutter delay before reading out. // if ( cam.get_shutter_delay() > 0 ) cam.wait_shutter_delay(); @@ -2858,7 +2872,6 @@ namespace AstroCam { add( Topic::SLITD, FitsHeaderKeys::SlitInfoKeys ); // primary add( Topic::TARGETINFO, FitsHeaderKeys::TargetInfoKeys ); // primary add( Topic::TCSD, FitsHeaderKeys::TcsInfoKeys ); // primary - add( Topic::FLEXURED, FitsHeaderKeys::FlexureInfoKeys ); // extension add( Topic::FOCUSD, FitsHeaderKeys::FocusInfoKeys ); // extension add( Topic::THERMALD, FitsHeaderKeys::ThermalInfoKeys ); // extension diff --git a/sequencerd/sequence.cpp b/sequencerd/sequence.cpp index 338b6077..4efbc1af 100644 --- a/sequencerd/sequence.cpp +++ b/sequencerd/sequence.cpp @@ -50,8 +50,23 @@ namespace Sequencer { std::lock_guard lock(camerad_mtx); if (jmessage.contains(Key::Camerad::READY)) { int isready = jmessage[Key::Camerad::READY].get(); + // ready again: the expose command's series has ended, re-apply no more + if ( isready && !this->can_expose.load() ) this->expose_nexp = 0; this->can_expose.store(isready); } + + // The shutter closing, other than to pause, ends an integration. The + // telescope keeps tracking, so while another sub-exposure of the expose + // command follows, re-apply the flexure compensation. + // + if (jmessage.contains(Key::Camerad::SHUTTEROPEN)) { + bool open = jmessage[Key::Camerad::SHUTTEROPEN].get(); + bool paused = jmessage.contains(Key::Camerad::PAUSED) && jmessage[Key::Camerad::PAUSED].get(); + if ( !open && !paused && this->last_shutteropen && ++this->shutter_closes < this->expose_nexp ) { + std::thread( &Sequencer::Sequence::flexure_set, this ).detach(); + } + this->last_shutteropen = open; + } this->camerad_cv.notify_all(); } /***** Sequencer::Sequence::handletopic_camerad ****************************/ @@ -742,7 +757,6 @@ namespace Sequencer { worker_threads = { { THR_MOVE_TO_TARGET, std::bind(&Sequence::move_to_target, this) }, { THR_CAMERA_SET, std::bind(&Sequence::camera_set, this) }, { THR_FOCUS_SET, std::bind(&Sequence::focus_set, this) }, - { THR_FLEXURE_SET, std::bind(&Sequence::flexure_set, this) }, { THR_CALIB_SET, std::bind(&Sequence::calib_set, this) }, // for CAL targets, slit comes from database, otherwise use VSM acquire position { THR_SLIT_SET, std::bind(&Sequence::slit_set, this, @@ -840,6 +854,15 @@ namespace Sequencer { return; } } + } + + // Set the flexure compensation for the pointing to be exposed, every time, + // however the target was acquired (automatically, by hand, or not at + // all) and after any target offset. Nothing waits for it. + // + std::thread( &Sequencer::Sequence::flexure_set, this ).detach(); + + if ( !this->target.iscal ) { // ensure slit offset is in "expose" position when needed try { error |= this->slit_set(Sequencer::VSM_EXPOSE); @@ -2426,7 +2449,9 @@ namespace Sequencer { * compensation origin (flexured adds the computed delta to defpos), * so this is the neutral state; it prevents a previous target's * compensation from being left applied to every subsequent target. - * @return NO_ERROR + * Runs in a detached thread and nothing waits for it, so it does + * not throw: a failure is reported and the sequence goes on. + * @return ERROR | NO_ERROR * */ long Sequence::flexure_set() { @@ -2452,22 +2477,28 @@ namespace Sequencer { return NO_ERROR; } + if ( this->cancel_flag.load() ) return NO_ERROR; + // build up list of activate chans std::ostringstream activechans; const std::string calname = std::string(this->target.iscal ? this->target.name : "SCIENCE"); - const auto &calinfo = this->caltarget.get_info(calname); - for (const auto &[chan,active] : calinfo.channel_active) { - if (active) activechans << " " << chan; + try { + const auto &calinfo = this->caltarget.get_info(calname); + for (const auto &[chan,active] : calinfo.channel_active) { + if (active) activechans << " " << chan; + } + } + catch ( const std::exception &e ) { + this->broadcast.error( function, "setting flexure compensator: "+std::string(e.what()) ); + return ERROR; } std::ostringstream comp; comp << FLEXURED_COMPENSATE << activechans.str(); - if ( !this->cancel_flag.load() && - this->flexured.command( comp.str() ) != NO_ERROR ) { + if ( this->flexured.command( comp.str() ) != NO_ERROR ) { this->broadcast.error( function, "setting flexure compensator" ); - this->thread_error_manager.set( THR_FLEXURE_SET ); - throw std::runtime_error("setting flexure compensator"); + return ERROR; } this->is_flexure_default.store(false); @@ -2840,6 +2871,12 @@ namespace Sequencer { return NO_ERROR; } + // Set the flexure compensation for the pointing now, once camera_set has + // seen the camera ready (the previous exposure may still have been + // integrating). Nothing waits for it. + // + std::thread( &Sequencer::Sequence::flexure_set, this ).detach(); + // Start the exposure in a thread... // auto start_exposure = std::async(std::launch::async, &Sequence::trigger_exposure, this); @@ -2890,6 +2927,9 @@ namespace Sequencer { // if ( this->tcs_preauth_time > 0 ) this->notify_tcs_next_target = true; else this->notify_tcs_next_target = false; + this->shutter_closes = 0; // counted per expose command + this->expose_nexp = this->target.nexp; + this->arm_readout_flag = true; // enables the async_listener to look for the readout and clear the EXPOSE bit this->set_imgtype(); diff --git a/sequencerd/sequence.h b/sequencerd/sequence.h index 8df8f33b..f8e637b3 100644 --- a/sequencerd/sequence.h +++ b/sequencerd/sequence.h @@ -295,6 +295,8 @@ namespace Sequencer { std::atomic is_science_frame_transfer; ///< is frame transfer enabled for science cameras std::atomic notify_tcs_next_target; ///< notify TCS of next target when remaining time within TCS_PREAUTH_TIME std::atomic arm_readout_flag; ///< + std::atomic shutter_closes{0}; ///< shutter closes since the last expose command + std::atomic expose_nexp{0}; ///< sub-exposures of the last expose command std::atomic cancel_flag{false}; std::atomic is_ontarget{false}; ///< remotely set by the TCS operator to indicate that the target is ready std::atomic is_usercontinue{false}; ///< remotely set by the user to continue @@ -431,6 +433,7 @@ namespace Sequencer { std::condition_variable acam_cv; std::mutex camerad_mtx; std::condition_variable camerad_cv; + bool last_shutteropen = false; ///< SHUTTEROPEN in camerad's latest report (camerad_mtx) std::mutex slitd_mtx; std::condition_variable slitd_cv; std::mutex tcsd_mtx;