From 591ec67b438a51a2d89cdc77307fa0fd454b9e27 Mon Sep 17 00:00:00 2001 From: Christoffer Fremling Date: Thu, 13 Aug 2026 22:26:43 -0700 Subject: [PATCH] fineacquire: anchor the settle to the executed TCS move, not the send While ACAM guides, a fine-acquire correction only shifts acamd's goal; the telescope moves at acamd's next guide solve. The settle was timed from the send, so with short slicecam exposures the next sample median could be built from pre-move frames and the same correction applied twice (UT 2026-08-06, BD+28d4211: exposed 2.6 arcsec off slit). acamd publishes a counter incremented after each executed pt_offset. do_fineacquire waits for it to advance before settling; a stop interrupts the wait; on timeout (FINE_ACQUIRE_MOVE_TIMEOUT, default 60 s, 0 disables) it proceeds on the old timer. Also fixes the offset_acam_goal log line, which printed degrees labelled arcsec on the guiding path. Co-Authored-By: Claude Fable 5 --- Config/slicecamd.cfg.in | 7 ++++ acamd/acam_interface.cpp | 7 +++- acamd/acam_interface.h | 2 + common/message_keys.h | 1 + slicecamd/slicecam_interface.cpp | 66 ++++++++++++++++++++++++++++---- slicecamd/slicecam_interface.h | 4 +- 6 files changed, 77 insertions(+), 10 deletions(-) diff --git a/Config/slicecamd.cfg.in b/Config/slicecamd.cfg.in index 3bd6316a..1683331e 100644 --- a/Config/slicecamd.cfg.in +++ b/Config/slicecamd.cfg.in @@ -98,6 +98,13 @@ FINE_ACQUIRE_SETTLE_FRAMES=2 # FINE_ACQUIRE_SETTLE_SEC=3.0 +# FINE_ACQUIRE_MOVE_TIMEOUT= +# Max time to wait for acamd to report a goal-shift correction executed +# before the settle begins. On timeout the settle proceeds anyway. Set to 0 +# to disable the wait. +# +FINE_ACQUIRE_MOVE_TIMEOUT=60.0 + # FINE_ACQUIRE_GAIN= # Proportional gain = {0..1} applied to the commanded offset when the residual # is at or below FINE_ACQUIRE_GAIN_THRESHOLD arcsec. diff --git a/acamd/acam_interface.cpp b/acamd/acam_interface.cpp index f3a0daf1..4d166f61 100644 --- a/acamd/acam_interface.cpp +++ b/acamd/acam_interface.cpp @@ -1443,6 +1443,7 @@ namespace Acam { const int attempts = this->target.attempts; const std::string filter = this->motion.get_current_filtername(); const std::string cover = this->motion.get_current_coverpos(); + const int64_t ptoffset_seq = this->target.ptoffset_seq.load(); // unless forced, only publish if there was a change in any one of these // @@ -1452,7 +1453,8 @@ namespace Acam { nacquired == this->last_status.nacquired && attempts == this->last_status.attempts && filter == this->last_status.filter && - cover == this->last_status.cover ) return; + cover == this->last_status.cover && + ptoffset_seq == this->last_status.ptoffset_seq ) return; this->last_status.acquire_mode = acquire_mode; this->last_status.is_acquired = is_acquired; @@ -1460,6 +1462,7 @@ namespace Acam { this->last_status.attempts = attempts; this->last_status.filter = filter; this->last_status.cover = cover; + this->last_status.ptoffset_seq = ptoffset_seq; // assemble the telemetry into a json message // @@ -1473,6 +1476,7 @@ namespace Acam { jmessage_out[Key::Acamd::BACKGROUND] = this->astrometry.get_background(); jmessage_out[Key::Acamd::FILTER] = filter; jmessage_out[Key::Acamd::COVER] = cover; + jmessage_out[Key::Acamd::PTOFFSET_SEQ] = ptoffset_seq; jmessage_out[Key::PUBTIME] = get_time_us(); try { @@ -3692,6 +3696,7 @@ logwrite( function, message.str() ); // send offset to TCS here (returns when offset is complete) if ( iface->tcsd.pt_offset( ra_off*3600., dec_off*3600., OFFSETRATE )==ERROR) break; this->allow_large_offset.store(false); // deliberate-offset allowance consumed + this->ptoffset_seq.fetch_add(1, std::memory_order_release); std::this_thread::sleep_for( std::chrono::seconds(1) ); } diff --git a/acamd/acam_interface.h b/acamd/acam_interface.h index 4c2f59fd..e9941034 100644 --- a/acamd/acam_interface.h +++ b/acamd/acam_interface.h @@ -349,6 +349,7 @@ namespace Acam { std::atomic is_acquired; ///< set if target acquired successfully std::atomic stop_acquisition; ///< set if the acquisition sequence should stop + std::atomic ptoffset_seq{0}; ///< count of pt_offsets executed by do_acquire double tcs_max_offset; double tcs_max_putonslit_offset{300.}; ///< max offset (arcsec) for a deliberate goal offset (put-on-slit etc.) applied while guiding; defaults 300 if ACQUIRE_TCS_MAX_PUTONSLIT_OFFSET absent @@ -530,6 +531,7 @@ namespace Acam { int attempts = 0; std::string filter = ""; std::string cover = ""; + int64_t ptoffset_seq = 0; } last_status; public: diff --git a/common/message_keys.h b/common/message_keys.h index 0888bc52..03caead8 100644 --- a/common/message_keys.h +++ b/common/message_keys.h @@ -106,6 +106,7 @@ namespace Key { inline const std::string ATTEMPTS = "attempts"; inline const std::string SEEING = "seeing"; inline const std::string BACKGROUND = "background"; + inline const std::string PTOFFSET_SEQ = "ptoffset_seq"; ///< count of pt_offsets executed by the acquire/guide loop } namespace Slicecamd { diff --git a/slicecamd/slicecam_interface.cpp b/slicecamd/slicecam_interface.cpp index ec21083e..c562f077 100644 --- a/slicecamd/slicecam_interface.cpp +++ b/slicecamd/slicecam_interface.cpp @@ -562,17 +562,40 @@ namespace Slicecam { const double cmd_dra = effective_gain * med_dra; const double cmd_ddec = effective_gain * med_ddec; - if ( this->offset_acam_goal( { cmd_dra, cmd_ddec }, true ) != NO_ERROR ) { + // record the executed-move counter before sending so nothing is missed + const int64_t seq_before = this->acam_ptoffset_seq.load(std::memory_order_acquire); + + bool sent_to_acam = false; + if ( this->offset_acam_goal( { cmd_dra, cmd_ddec }, true, &sent_to_acam ) != NO_ERROR ) { logwrite( function, "ERROR failed to send offset to ACAM" ); this->is_fineacquire_running.store( false, std::memory_order_release ); this->publish_status(); return; } - // time-based settle: wait for the TCS to physically finish the move before - // sampling resumes. The frame-count settle (settle_frames) is too short in - // wall-clock when autoexpose shortens the exposure (e.g. bright targets), so - // apply a configurable time-based settle on top of it; settle_sec=0 disables. + // On the guiding path the correction only shifts acamd's goal; the + // telescope moves at acamd's next guide solve. Wait for that executed + // move so the settle below is anchored to the move, not the send. A + // stop interrupts the wait; on timeout proceed on the old timer. + if ( sent_to_acam && this->fineacquire_state.move_timeout_sec > 0.0 ) { + std::unique_lock lock(this->acam_mtx); + const bool moved = this->acam_cv.wait_for( lock, + std::chrono::duration( this->fineacquire_state.move_timeout_sec ), + [this, seq_before]() { + return this->acam_ptoffset_seq.load(std::memory_order_acquire) > seq_before + || !this->is_fineacquire_running.load(std::memory_order_acquire); + }); + if ( !this->is_fineacquire_running.load(std::memory_order_acquire) ) return; + if ( !moved ) { + oss.str(""); + oss << "WARNING goal shift not executed within " + << this->fineacquire_state.move_timeout_sec + << " s; proceeding on the old timer"; + logwrite( function, oss.str() ); + } + } + + // time-based settle after the executed move; settle_sec=0 disables if ( this->fineacquire_state.settle_sec > 0.0 ) { std::this_thread::sleep_for( std::chrono::duration( this->fineacquire_state.settle_sec ) ); } @@ -924,6 +947,10 @@ namespace Slicecam { Common::extract_telemetry_value( jmessage, Key::PUBTIME, pubtime ); this->last_acam_pubtime.store( pubtime, std::memory_order_relaxed ); + int64_t seq=0; + Common::extract_telemetry_value( jmessage, Key::Acamd::PTOFFSET_SEQ, seq ); + this->acam_ptoffset_seq.store( seq, std::memory_order_relaxed ); + // wake any thread waiting on ACAM state (e.g. fineacquire) std::lock_guard lock(this->acam_mtx); this->acam_cv.notify_all(); @@ -1418,6 +1445,19 @@ namespace Slicecam { applied++; } else + if ( config.param[entry] == "FINE_ACQUIRE_MOVE_TIMEOUT" ) { + try { this->fineacquire_state.move_timeout_sec = std::stod( config.arg[entry] ); } + catch ( const std::exception &e ) { + message.str(""); message << "ERROR invalid FINE_ACQUIRE_MOVE_TIMEOUT " + << config.arg[entry] << ": " << e.what(); + logwrite( function, message.str() ); + return ERROR; + } + message.str(""); message << "SLICECAMD:config:" << config.param[entry] << "=" << config.arg[entry]; + logwrite( function, message.str() ); + applied++; + } + else if ( config.param[entry] == "FINE_ACQUIRE_GAIN" ) { try { this->fineacquire_state.gain = std::stod( config.arg[entry] ); } catch ( const std::exception &e ) { @@ -2579,11 +2619,16 @@ namespace Slicecam { * @details When guiding is enabled, the offsets will be applied to the ACAM * goal so that the ACAM will guide on the offset position. When * not guiding, the offsets are sent directly to the TCS as PT offsets. - * @param[in] offsets pair { dRA, dDEC } + * @param[in] offsets pair { dRA, dDEC } + * @param[in] fineacquire optional: add the fineguiding token for acamd + * @param[out] sent_to_acam optional: set true when the guiding path was + * taken (goal shift sent to acamd, executed at + * its next guide solve), false when the offset + * went directly (and synchronously) to the TCS * @return ERROR | NO_ERROR * */ - long Interface::offset_acam_goal(const std::pair &offsets, std::optional fineacquire) { + long Interface::offset_acam_goal(const std::pair &offsets, std::optional fineacquire, bool *sent_to_acam) { const char* function = "Slicecam::Interface::offset_acam_goal"; auto [ra_off, dec_off] = offsets; // local copy @@ -2595,6 +2640,11 @@ namespace Slicecam { // but must allow ACAM to perform the offset. // bool is_guiding = this->is_acam_guiding.load(); + if (sent_to_acam) *sent_to_acam = is_guiding; + + // arcsec copies for the log line (the guiding path sends degrees) + const double ra_off_arcsec = ra_off * 3600.; + const double dec_off_arcsec = dec_off * 3600.; // send the offsets now // @@ -2632,7 +2682,7 @@ namespace Slicecam { } std::ostringstream message; - message << "requested offsets dRA=" << ra_off << " dDEC=" << dec_off << " arcsec"; + message << "requested offsets dRA=" << ra_off_arcsec << " dDEC=" << dec_off_arcsec << " arcsec"; logwrite(function, message.str()); return NO_ERROR; diff --git a/slicecamd/slicecam_interface.h b/slicecamd/slicecam_interface.h index 90a7fdb1..cd6a2ad9 100644 --- a/slicecamd/slicecam_interface.h +++ b/slicecamd/slicecam_interface.h @@ -95,6 +95,7 @@ namespace Slicecam { int settle_frames = 0; ///< countdown of frames to discard while telescope settles int settle_count = 2; ///< configured: frames to discard after each move double settle_sec = 3.0; ///< time-based settle (sec) after each move; 0 disables (needed when autoexpose shortens frames so the frame-count settle is too brief in wall-clock) + double move_timeout_sec = 60.0; ///< max wait (sec) for acamd to report the goal-shift move executed; 0 disables the wait int consecutive_centroid_failures = 0; ///< counts consecutive centroid failures // exposure compensation (shared by the reactive trim and, later, autoexpose) double exptime_min = 0.1; ///< clamp: minimum auto-adjusted exposure (sec) @@ -193,6 +194,7 @@ namespace Slicecam { std::atomic is_acam_guiding; ///< is acam guiding? std::atomic last_acam_pubtime{0}; ///< pubtime (us) of latest received acamd status + std::atomic acam_ptoffset_seq{0}; ///< pt_offset execution counter from latest acamd status // Latest target (goal) coords published on Topic::TARGETINFO // NAN until a TARGETINFO arrives, so manual runs with no sequencer target log nan. @@ -355,7 +357,7 @@ namespace Slicecam { long fan_mode( std::string args, std::string &retstring ); long gain( std::string args, std::string &retstring ); - long offset_acam_goal(const std::pair &offsets, std::optional fineacquire=std::nullopt); + long offset_acam_goal(const std::pair &offsets, std::optional fineacquire=std::nullopt, bool *sent_to_acam=nullptr); long collect_header_info( std::unique_ptr &slicecam );