From 05b27d5c1492eb1f6509d8851afe6c64a41adaa1 Mon Sep 17 00:00:00 2001 From: Christoffer Fremling Date: Sun, 7 Jun 2026 22:29:08 -0700 Subject: [PATCH 1/3] Never drop out of guiding when applying a goal offset acamd's offset_goal() previously kicked the system from TARGET_GUIDE back into TARGET_ACQUIRE whenever an offset was applied without the "fineguiding" flag. Because the slicecam "put on slit" path and the sequencer's target_offset() (run right after fine acquisition) both send ACAMD_OFFSETGOAL without that flag, a guided "put on slit" and every post-fineacquire science offset would drop guiding and restart acquisition. There is no use case for re-acquiring on an offset while already acquired and guiding. Make offset_goal() always stay in TARGET_GUIDE while guiding, resetting the offset median-filter buffers so the new goal takes effect quickly (the same reset the fineguiding path already did). The "fineguiding" flag is no longer needed and is removed from acamd's offset_goal() and from slicecam's offset_acam_goal(). The fix is contained entirely to the ACAMD_OFFSETGOAL command handler. The acquisition loop is untouched, and behavior in non-guiding states (ACQUIRE, ACQUIRE_HERE, NOP) is unchanged. Co-Authored-By: Claude Opus 4.8 (1M context) --- acamd/acam_interface.cpp | 28 ++++++++-------------------- slicecamd/slicecam_interface.cpp | 10 ++-------- slicecamd/slicecam_interface.h | 2 +- 3 files changed, 11 insertions(+), 29 deletions(-) diff --git a/acamd/acam_interface.cpp b/acamd/acam_interface.cpp index cce50353..73c91911 100644 --- a/acamd/acam_interface.cpp +++ b/acamd/acam_interface.cpp @@ -5484,11 +5484,10 @@ logwrite( function, message.str() ); // if ( args == "?" ) { retstring = ACAMD_OFFSETGOAL; - retstring.append( " [ [ fineguiding ]\n" ); + retstring.append( " [ ]\n" ); retstring.append( " Apply offsets to the ACAM goal coordinates.\n" ); retstring.append( " These offsets are applied only while guiding. If omitted,\n" ); retstring.append( " the current offsets are returned. Units are in degrees.\n" ); - retstring.append( " The optional 'fineguiding' is used for slicecam fine acquisition.\n" ); return HELP; } @@ -5497,17 +5496,13 @@ logwrite( function, message.str() ); double dRA=NAN, dDEC=NAN; if (!(iss >> dRA >> dDEC) || (std::isnan(dRA) || std::isnan(dDEC)) ) { - logwrite( function, "ERROR expected [ fineguiding ]" ); + logwrite( function, "ERROR expected " ); retstring="invalid_argument"; return ERROR; } this->target.dRA = dRA; this->target.dDEC = dDEC; - // optional fineguiding flag used for slicecam fineacquisition mode - std::string flag; - bool is_fineguiding = (iss >> flag && flag == "fineguiding"); - // Apply any dRA, dDEC goal offsets from the "put on slit" action to // acam_ra_goal, acam_dec_goal. These dRA,dDEC offsets can come from // either the ACAM or slicecam GUIs and are stored in the Target class. @@ -5521,20 +5516,13 @@ logwrite( function, message.str() ); message.str(""); message << this->target.dRA << " " << this->target.dDEC; retstring = message.str(); + // Applying an offset to the goal while guiding must never drop out of + // guiding -- there is no use case for re-acquiring on an offset. Stay in + // TARGET_GUIDE and reset the offset filter so the new goal takes effect + // quickly. This covers GUI "put on slit" and sequencer target offsets. + // if ( this->target.acquire_mode == Acam::TARGET_GUIDE ) { - // for slicecam fine aquisition/guiding, stay in TARGET_GUIDE but - // reset the filtering so the goal takes effect quickly - if ( is_fineguiding ) { - this->target.reset_offset_params(); - } - else { - this->target.acquire_mode = Acam::TARGET_ACQUIRE; - this->target.nacquired = 0; - this->target.attempts = 0; - this->target.sequential_failures = 0; - this->target.timeout_time = std::chrono::steady_clock::now() - + std::chrono::duration(this->target.timeout); - } + this->target.reset_offset_params(); } this->publish_status(); diff --git a/slicecamd/slicecam_interface.cpp b/slicecamd/slicecam_interface.cpp index 01604a4b..2b1bdc1e 100644 --- a/slicecamd/slicecam_interface.cpp +++ b/slicecamd/slicecam_interface.cpp @@ -535,7 +535,7 @@ 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 ) { + if ( this->offset_acam_goal( { cmd_dra, cmd_ddec } ) != NO_ERROR ) { logwrite( function, "ERROR failed to send offset to ACAM" ); this->is_fineacquire_running.store( false, std::memory_order_release ); this->publish_status(); @@ -2496,14 +2496,11 @@ namespace Slicecam { * @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) { const char* function = "Slicecam::Interface::offset_acam_goal"; auto [ra_off, dec_off] = offsets; // local copy - bool is_fineacquire=false; - if (fineacquire) is_fineacquire = *fineacquire; - // If ACAM is guiding then slicecam must not move the telescope, // but must allow ACAM to perform the offset. // @@ -2517,9 +2514,6 @@ namespace Slicecam { std::ostringstream cmd; cmd << ACAMD_OFFSETGOAL << " " << std::fixed << std::setprecision(6) << ra_off << " " << dec_off; - // add fineguiding arg when used for fine acquisition mode - if (is_fineacquire) cmd << " fineguiding"; - if (this->acamd.command( cmd.str() ) != NO_ERROR) { logwrite( function, "ERROR adding offset to acam goal" ); return ERROR; diff --git a/slicecamd/slicecam_interface.h b/slicecamd/slicecam_interface.h index f8d3920f..51f00e4b 100644 --- a/slicecamd/slicecam_interface.h +++ b/slicecamd/slicecam_interface.h @@ -336,7 +336,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); long collect_header_info( std::unique_ptr &slicecam ); From fcc0c3ebe12e782765db5c1f338dfcbf2c30edaf Mon Sep 17 00:00:00 2001 From: Christoffer Fremling Date: Sun, 7 Jun 2026 23:20:31 -0700 Subject: [PATCH 2/3] Cap deliberate guiding offsets at 300", ordinary corrections at 60" A deliberate goal offset applied while guiding -- put-on-slit, offset-star acquisition, the end-of-fineacquire target offset, and the pyGUI 'Offset' button (all routed through offset_goal/ACAMD_OFFSETGOAL) -- can legitimately be much larger than an ordinary guiding correction. The 60" ACQUIRE_TCS_MAX_OFFSET gate would reject these. The previous mechanism enlarged the gate by the frame-to-frame change in a putonslit_offset (maxoffset = tcs_max_offset + |putonslit_offset - last_putonslit_offset|). That was fragile: it only widened the cap when the offset *changed*, the one-shot widening was consumed on the first frame even if median_filter deferred the send, and it was never reset between targets, so a second similar-magnitude offset was rejected at 60". Replace it with an explicit one-shot allowance: offset_goal sets allow_large_offset while guiding, and do_acquire permits that one correction up to PUTONSLIT_TCS_MAX_OFFSET (300"). The allowance is consumed only when the offset is actually sent to the TCS, so a median_filter deferral can no longer waste it. Ordinary guiding corrections keep the 60" cap so a bad solution can't slew the telescope, and the ACQUIRE path (which uses tcs_max_offset, raised to 300" by the acquire wrapper during acquisition) is unchanged. Co-Authored-By: Claude Opus 4.8 (1M context) --- acamd/acam_interface.cpp | 59 ++++++++++++++++++---------------------- acamd/acam_interface.h | 5 ++-- 2 files changed, 29 insertions(+), 35 deletions(-) diff --git a/acamd/acam_interface.cpp b/acamd/acam_interface.cpp index 73c91911..f3267af9 100644 --- a/acamd/acam_interface.cpp +++ b/acamd/acam_interface.cpp @@ -16,6 +16,11 @@ namespace Acam { constexpr int OFFSETRATE=40; constexpr float GAIN1=0.71; ///< e-/ADU for unity CCD gain + /// Max offset (arcsec) allowed for a deliberate goal offset applied while + /// guiding (put-on-slit, offset-star, end-of-fineacquire, pyGUI 'Offset'). + /// Ordinary guiding corrections use the smaller ACQUIRE_TCS_MAX_OFFSET. + constexpr double PUTONSLIT_TCS_MAX_OFFSET=300.; + int npreserve=0; ///< counter used for Interface::preserve_framegrab() /***** Acam::Camera::emulator ***********************************************/ @@ -3371,6 +3376,7 @@ logwrite( function, message.str() ); this->nacquired = 0; this->attempts = 0; this->sequential_failures = 0; + this->allow_large_offset.store(false); // no stale deliberate-offset allowance this->is_acquired.store( false, std::memory_order_release ); // Start the timeout clock, initialized as the time now plus the @@ -3610,40 +3616,22 @@ logwrite( function, message.str() ); message.str(""); message << "[ACQUIRE] offset=" << offset << " (arcsec)"; logwrite( function,message.str() ); - // There is a maximum offset allowed to the TCS. - // This is not a TCS limit (their limit is very large). - // This is our limit so that we don't accidentally move too far off the - // slit. However, "putonslit" can include a desired offset which is - // outside this limit, so when checking the calculated offset, include a - // delta which is the change introduced by putonslit. - // - - // this will be the solution plus dRA, dDEC - // start by initializing with acam_ra,acam_dec - // - double acam_ra_dRA = acam_ra; - double acam_dec_dDEC = acam_dec; - - // Then acam_ra_dRA, acam_dec_dDEC will be modified by applying dRA, dDEC - // - iface->fpoffsets.apply_offset( acam_ra_dRA, iface->target.dRA, - acam_dec_dDEC, iface->target.dDEC ); - - // the offset introduced by putonslit is therefore the separation between - // acam_ra,acam_dec and acam_ra_dRA,acam_dec_dDEC + // There is a maximum offset we send to the TCS. This is not a TCS limit + // (theirs is very large); it is our safety limit so that a bad solution + // can't move us far off the slit. Ordinary guiding corrections use the + // normal tcs_max_offset (ACQUIRE_TCS_MAX_OFFSET). A deliberate goal offset + // applied while guiding -- via offset_goal, which covers put-on-slit, + // offset-star acquisition, the end-of-fineacquire target offset, and the + // pyGUI 'Offset' button -- is intentionally larger, so for the one + // correction that consumes it we allow up to PUTONSLIT_TCS_MAX_OFFSET. + // The ACQUIRE path uses tcs_max_offset and is unaffected. // - this->putonslit_offset = angular_separation( acam_ra_dRA, acam_dec_dDEC, acam_ra, acam_dec ); - - // and the delta is the difference between this and the last time, - // which gets added to the tcs_max_offset. - // - double maxoffset = this->tcs_max_offset + std::fabs(this->putonslit_offset - this->last_putonslit_offset); - - // so remember this for next time - // - this->last_putonslit_offset = this->putonslit_offset; + double maxoffset = this->tcs_max_offset; + if ( this->acquire_mode == Acam::TARGET_GUIDE && this->allow_large_offset.load() ) { + maxoffset = PUTONSLIT_TCS_MAX_OFFSET; + } - // Finally, check the requested offset against this putonslit-modified max allowed offset + // Check the requested offset against the applicable max allowed offset // if ( offset >= maxoffset ) { message.str(""); message << "[WARNING] calculated offset " << offset << " not below max " @@ -3685,6 +3673,7 @@ logwrite( function, message.str() ); if ( should_offset ) { // 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 std::this_thread::sleep_for( std::chrono::seconds(1) ); } @@ -5521,7 +5510,13 @@ logwrite( function, message.str() ); // TARGET_GUIDE and reset the offset filter so the new goal takes effect // quickly. This covers GUI "put on slit" and sequencer target offsets. // + // This is a deliberate offset (put-on-slit, offset-star, end-of-fineacquire, + // or the pyGUI 'Offset' button) and may exceed the normal guiding cap, so + // allow the next correction up to PUTONSLIT_TCS_MAX_OFFSET. The allowance is + // one-shot: do_acquire consumes it when that correction is actually sent. + // if ( this->target.acquire_mode == Acam::TARGET_GUIDE ) { + this->target.allow_large_offset.store(true); this->target.reset_offset_params(); } diff --git a/acamd/acam_interface.h b/acamd/acam_interface.h index 1034e836..dfb041a2 100644 --- a/acamd/acam_interface.h +++ b/acamd/acam_interface.h @@ -472,7 +472,7 @@ namespace Acam { double angle; } acam_goal; - double putonslit_offset, last_putonslit_offset; + std::atomic allow_large_offset{false}; ///< one-shot: allow the next guiding correction up to PUTONSLIT_TCS_MAX_OFFSET Target() : iface(nullptr), timeout(10), max_attempts(-1), min_repeat(1), is_acquired(false), @@ -482,8 +482,7 @@ namespace Acam { tcs_offset_period(1), pointmode(Acam::POINTMODE_SLIT), acquire_mode(Acam::TARGET_NOP), - dRA(0), dDEC(0), - putonslit_offset(0), last_putonslit_offset(0) { } + dRA(0), dDEC(0) { } }; /***** Acam::Target *********************************************************/ From 14a141a6281aa242d7f5ee2c1dc6403473fccf5b Mon Sep 17 00:00:00 2001 From: Christoffer Fremling Date: Sun, 7 Jun 2026 23:32:49 -0700 Subject: [PATCH 3/3] Make the deliberate-offset cap a config parameter The 300" deliberate-offset cap was a hard-coded constant; the 60" guiding cap is the config parameter ACQUIRE_TCS_MAX_OFFSET. Make them consistent: add ACQUIRE_TCS_MAX_PUTONSLIT_OFFSET (default 300") to acamd.cfg, loaded the same way as ACQUIRE_TCS_MAX_OFFSET, stored in Target::tcs_max_putonslit_offset with a matching setter/getter. The member defaults to 300" so existing configs that predate the new line still behave correctly. --- Config/acamd.cfg.in | 3 ++- acamd/acam_interface.cpp | 37 +++++++++++++++++++++++++++---------- acamd/acam_interface.h | 13 ++++++++++++- 3 files changed, 41 insertions(+), 12 deletions(-) diff --git a/Config/acamd.cfg.in b/Config/acamd.cfg.in index 3007f567..619954a2 100644 --- a/Config/acamd.cfg.in +++ b/Config/acamd.cfg.in @@ -159,7 +159,8 @@ ACQUIRE_TIMEOUT=90 # seconds before ACAM acquisition sequence aborts ACQUIRE_RETRYS=5 # max number of retrys before acquisition fails (optional, can be left blank to disable) ACQUIRE_OFFSET_THRESHOLD=0.5 # computed offset below this threshold (in arcsec) defines successful acquisition ACQUIRE_MIN_REPEAT=2 # minimum number of sequential successful acquires -ACQUIRE_TCS_MAX_OFFSET=60 # the maximum allowable offset sent to the TCS, in arcsec +ACQUIRE_TCS_MAX_OFFSET=60 # max offset (arcsec) for an ordinary guiding correction sent to the TCS +ACQUIRE_TCS_MAX_PUTONSLIT_OFFSET=300 # max offset (arcsec) for a deliberate goal offset (put-on-slit, offset-star, end-of-fineacquire, pyGUI 'Offset') applied while guiding # SkySimulator options: # SKYSIM_IMAGE_SIZE= where is integer diff --git a/acamd/acam_interface.cpp b/acamd/acam_interface.cpp index f3267af9..4bd01fb1 100644 --- a/acamd/acam_interface.cpp +++ b/acamd/acam_interface.cpp @@ -16,11 +16,6 @@ namespace Acam { constexpr int OFFSETRATE=40; constexpr float GAIN1=0.71; ///< e-/ADU for unity CCD gain - /// Max offset (arcsec) allowed for a deliberate goal offset applied while - /// guiding (put-on-slit, offset-star, end-of-fineacquire, pyGUI 'Offset'). - /// Ordinary guiding corrections use the smaller ACQUIRE_TCS_MAX_OFFSET. - constexpr double PUTONSLIT_TCS_MAX_OFFSET=300.; - int npreserve=0; ///< counter used for Interface::preserve_framegrab() /***** Acam::Camera::emulator ***********************************************/ @@ -1952,6 +1947,27 @@ namespace Acam { applied++; } + if ( config.param[entry] == "ACQUIRE_TCS_MAX_PUTONSLIT_OFFSET" ) { + double offset; + try { + offset = std::stod( config.arg[entry] ); + } catch ( std::invalid_argument &e ) { + message.str(""); message << "ERROR bad ACQUIRE_TCS_MAX_PUTONSLIT_OFFSET " << config.param[entry] << ": " << e.what(); + logwrite( function, message.str() ); + return(ERROR); + } catch ( std::out_of_range &e ) { + message.str(""); message << "ERROR bad ACQUIRE_TCS_MAX_PUTONSLIT_OFFSET " << config.param[entry] << ": " << e.what(); + logwrite( function, message.str() ); + return(ERROR); + } + if ( this->target.set_tcs_max_putonslit_offset( offset ) != NO_ERROR ) { + message.str(""); message << "ERROR bad ACQUIRE_TCS_MAX_PUTONSLIT_OFFSET \"" << config.param[entry] << "\" must be >= 0"; + logwrite( function, message.str() ); + return ERROR; + } + applied++; + } + if ( config.param[entry] == "ACQUIRE_MIN_REPEAT" ) { int repeat; try { @@ -3623,12 +3639,13 @@ logwrite( function, message.str() ); // applied while guiding -- via offset_goal, which covers put-on-slit, // offset-star acquisition, the end-of-fineacquire target offset, and the // pyGUI 'Offset' button -- is intentionally larger, so for the one - // correction that consumes it we allow up to PUTONSLIT_TCS_MAX_OFFSET. - // The ACQUIRE path uses tcs_max_offset and is unaffected. + // correction that consumes it we allow up to tcs_max_putonslit_offset + // (ACQUIRE_TCS_MAX_PUTONSLIT_OFFSET). The ACQUIRE path uses tcs_max_offset + // and is unaffected. // double maxoffset = this->tcs_max_offset; if ( this->acquire_mode == Acam::TARGET_GUIDE && this->allow_large_offset.load() ) { - maxoffset = PUTONSLIT_TCS_MAX_OFFSET; + maxoffset = this->tcs_max_putonslit_offset; } // Check the requested offset against the applicable max allowed offset @@ -5512,8 +5529,8 @@ logwrite( function, message.str() ); // // This is a deliberate offset (put-on-slit, offset-star, end-of-fineacquire, // or the pyGUI 'Offset' button) and may exceed the normal guiding cap, so - // allow the next correction up to PUTONSLIT_TCS_MAX_OFFSET. The allowance is - // one-shot: do_acquire consumes it when that correction is actually sent. + // allow the next correction up to ACQUIRE_TCS_MAX_PUTONSLIT_OFFSET. The + // allowance is one-shot: do_acquire consumes it when the offset is sent. // if ( this->target.acquire_mode == Acam::TARGET_GUIDE ) { this->target.allow_large_offset.store(true); diff --git a/acamd/acam_interface.h b/acamd/acam_interface.h index dfb041a2..108894fa 100644 --- a/acamd/acam_interface.h +++ b/acamd/acam_interface.h @@ -351,6 +351,7 @@ namespace Acam { std::atomic stop_acquisition; ///< set if the acquisition sequence should stop 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 double offset_cal_offset, offset_cal_raoff, offset_cal_decoff; @@ -413,6 +414,16 @@ namespace Acam { inline double get_tcs_max_offset() { return this->tcs_max_offset; } + inline long set_tcs_max_putonslit_offset( const double _offset ) { + if ( std::isnan( _offset ) || _offset <= 0 ) return ERROR; + else { + this->tcs_max_putonslit_offset = _offset; + return NO_ERROR; + } + } + + inline double get_tcs_max_putonslit_offset() { return this->tcs_max_putonslit_offset; } + inline void set_max_attempts( int _max ) { this->max_attempts = _max; } inline void set_min_repeat( int _repeat ) { this->min_repeat = _repeat; } @@ -472,7 +483,7 @@ namespace Acam { double angle; } acam_goal; - std::atomic allow_large_offset{false}; ///< one-shot: allow the next guiding correction up to PUTONSLIT_TCS_MAX_OFFSET + std::atomic allow_large_offset{false}; ///< one-shot: allow the next guiding correction up to tcs_max_putonslit_offset Target() : iface(nullptr), timeout(10), max_attempts(-1), min_repeat(1), is_acquired(false),