diff --git a/Config/sequencerd.cfg.in b/Config/sequencerd.cfg.in index e665eeb6..d8db2de4 100644 --- a/Config/sequencerd.cfg.in +++ b/Config/sequencerd.cfg.in @@ -150,6 +150,8 @@ TCS_OFFSET_RATE_RA=45 # TCS offset rate for RA ("MRATE") in arcse TCS_OFFSET_RATE_DEC=45 # TCS offset rate for DEC ("MRATE") in arcsec per second TCS_SETTLE_TIMEOUT=20 # seconds to wait for telescope to settle (for "TRACKING_STABLY") TCS_SETTLE_STABLE=1.5 # time in fractional seconds (to 0.1) that TCS must report TRACKING before it is really stable +TCS_AUTO_ONTARGET=no # yes: TCS ?ONTARGET=1 + tracking + sep < TCS_AUTO_ONTARGET_SEP (2 consecutive 1 Hz reads) satisfies the ontarget wait; operator press always works +TCS_AUTO_ONTARGET_SEP=5.0 # arcsec: max |telescope-target| for auto-ontarget TCS_DOMEAZI_READY=10 # max degrees azimuth that dome and telescope can differ before ready to observe TCS_PREAUTH_TIME=10 # seconds before end of exposure to notify TCS of next target's coords (0 to disable) diff --git a/sequencerd/sequence.cpp b/sequencerd/sequence.cpp index 338b6077..864048f7 100644 --- a/sequencerd/sequence.cpp +++ b/sequencerd/sequence.cpp @@ -2149,6 +2149,88 @@ namespace Sequencer { /***** Sequencer::Sequence::camera_shutdown *********************************/ + /***** Sequencer::Sequence::dothread_auto_ontarget **************************/ + /** + * @brief satisfies the TCSOP wait when the TCS itself reports arrival + * @details Spawned by move_to_target when TCS_AUTO_ONTARGET is enabled and + * lives only while that wait is open (auto_ontarget_active). + * Acknowledges when two consecutive 1 Hz reads show + * ?ONTARGET==1 AND motion tracking AND |telescope-commanded| < + * tcs_auto_ontarget_sep. All three predicates are required: + * the flag alone is position-agnostic, the separation alone + * cannot see a sub-threshold hop in progress, and motion alone + * says nothing about settling. The coords are the SCOPE-frame + * position move_to_target actually commanded. A wrong + * acknowledgment is self-limiting -- ACAM cannot solve a field + * that is not there and the acquisition retry machinery + * handles the failure -- so the conditions make wrong acks + * rare, not impossible. The operator's press works unchanged; + * whichever arrives first wins. The acknowledgment + * deliberately does not go through ontarget(): that clears + * cancel_flag, and a concurrent cancel must always win + * against this automatic path. + */ + void Sequence::dothread_auto_ontarget( double tgt_ra_deg, double tgt_dec_deg ) { + const std::string function("Sequencer::Sequence::dothread_auto_ontarget"); + std::stringstream message; + + if ( std::isnan(tgt_ra_deg) || std::isnan(tgt_dec_deg) ) { + logwrite( function, "ERROR invalid commanded coords; auto-ontarget sits this wait out" ); + return; + } + + int consecutive = 0; + + while ( this->auto_ontarget_active.load() && + !this->cancel_flag.load() && !this->is_ontarget.load() ) { + + std::this_thread::sleep_for( std::chrono::seconds(1) ); + + // the TCS's own settled flag (0|1), returned verbatim + // + std::string reply; + if ( this->tcsd.send( TCSD_NATIVE+" ?ONTARGET", reply ) != NO_ERROR ) { consecutive=0; continue; } + bool flag_on = ( reply.rfind( "1", 0 ) == 0 ); + + // motion state and separation from the cached 1 Hz tcsd telemetry + // + bool fresh = false; + bool tracking = false; + double sep_as = NAN; + { + std::lock_guard lock( tcsd_mtx ); + fresh = this->tcsinfo.is_fresh(); + if ( fresh ) { + tracking = ( this->tcsinfo.motion == TCS_MOTION_TRACKING_STR ); + double dra_deg = this->tcsinfo.ra_h * 15.0 - tgt_ra_deg; + dra_deg = std::fmod( dra_deg + 540.0, 360.0 ) - 180.0; // shortest arc across RA wrap + double dra_as = dra_deg * cos( tgt_dec_deg * M_PI / 180.0 ) * 3600.0; + double ddec_as = ( this->tcsinfo.dec_d - tgt_dec_deg ) * 3600.0; + sep_as = std::hypot( dra_as, ddec_as ); + } + } + + if ( !fresh || !flag_on || !tracking || + !( sep_as < this->tcs_auto_ontarget_sep ) ) { consecutive=0; continue; } + + if ( ++consecutive < 2 ) continue; // debounce: two reads in a row + + // Re-check at the last instant; do NOT clear cancel_flag (see @details). + // + if ( !this->auto_ontarget_active.load() || this->cancel_flag.load() ) return; + + message.str(""); message << "TCS reports on-target (?ONTARGET=1, tracking, sep=" + << std::fixed << std::setprecision(1) << sep_as + << " arcsec x2) -- sending ontarget"; + this->broadcast.notice( function, message.str() ); + this->is_ontarget.store( true ); + this->cv.notify_all(); + return; + } + } + /***** Sequencer::Sequence::dothread_auto_ontarget **************************/ + + /***** Sequencer::Sequence::move_to_target **********************************/ /** * @brief send request to TCS to move to target coordinates @@ -2297,10 +2379,24 @@ namespace Sequencer { this->broadcast.notice( function, "waiting for TCS operator to send \"ontarget\" signal" ); + // AUTO-ONTARGET (config TCS_AUTO_ONTARGET, default no): the TCS itself can + // satisfy this wait through the same ontarget() the operator's press uses. + // The watcher lives exactly as long as this wait (auto_ontarget_active). + // + this->auto_ontarget_active.store( this->tcs_auto_ontarget ); + if ( this->tcs_auto_ontarget ) { + // ra_out/dec_out are the SCOPE-frame coordinates this function actually + // commanded, which is the position REQPOS telemetry will report -- the + // database coordinates differ by the pointmode focal-plane offset. + std::thread( &Sequencer::Sequence::dothread_auto_ontarget, this, + ra_out, dec_out ).detach(); + } + while ( !this->cancel_flag.load() && !this->is_ontarget.load() ) { std::unique_lock lock(cv_mutex); this->cv.wait( lock, [this]() { return( this->is_ontarget.load() || this->cancel_flag.load() ); } ); } + this->auto_ontarget_active.store( false ); this->broadcast.notice( function, "received " +(this->cancel_flag.load() ? std::string("cancel") : std::string("ontarget")) diff --git a/sequencerd/sequence.h b/sequencerd/sequence.h index 8df8f33b..e7a53023 100644 --- a/sequencerd/sequence.h +++ b/sequencerd/sequence.h @@ -399,6 +399,12 @@ namespace Sequencer { this->is_ontarget.store(false); } + /** @brief satisfies the TCSOP wait when the TCS itself reports arrival + * (config TCS_AUTO_ONTARGET); coords are the commanded SCOPE + * frame position in degrees; see sequence.cpp */ + void dothread_auto_ontarget( double tgt_ra_deg, double tgt_dec_deg ); + std::atomic auto_ontarget_active{false}; ///< true only while move_to_target's TCSOP wait is open + /** @brief sends usercontinue signal, clears after 3 seconds */ inline void usercontinue() { this->cancel_flag.store(false); @@ -421,6 +427,8 @@ namespace Sequencer { double tcs_domeazi_ready; ///< max degrees azimuth that dome and telescope can differ before ready to observe double tcs_preauth_time; ///< seconds before end of exposure to notify TCS of next target's coords (0 to disable) double offset_settle_sec; ///< sec to wait after a target offset before exposing (config OFFSET_SETTLE_SEC; 0 disables) + bool tcs_auto_ontarget = false; ///< TCS ?ONTARGET may satisfy the TCSOP wait (config TCS_AUTO_ONTARGET; default no) + double tcs_auto_ontarget_sep = 5.0; ///< max |telescope-target| arcsec for auto-ontarget (config TCS_AUTO_ONTARGET_SEP) /// std::mutex tcs_ontarget_mtx; /// std::condition_variable tcs_ontarget_cv; diff --git a/sequencerd/sequencer_server.cpp b/sequencerd/sequencer_server.cpp index 405b43f3..56ea79d7 100644 --- a/sequencerd/sequencer_server.cpp +++ b/sequencerd/sequencer_server.cpp @@ -362,6 +362,31 @@ namespace Sequencer { applied++; } + // TCS_AUTO_ONTARGET + if (config.param[entry] == "TCS_AUTO_ONTARGET") { + this->sequence.tcs_auto_ontarget = ( config.arg[entry] == "yes" || + config.arg[entry] == "true" || + config.arg[entry] == "1" ); + message.str(""); message << "SEQUENCERD:config:" << config.param[entry] << "=" << config.arg[entry]; + logwrite( function, message.str() ); + applied++; + } + + // TCS_AUTO_ONTARGET_SEP + if (config.param[entry] == "TCS_AUTO_ONTARGET_SEP") { + try { + this->sequence.tcs_auto_ontarget_sep = std::stod( config.arg[entry] ); + } + catch (const std::exception &e) { + message.str(""); message << "ERROR parsing TCS_AUTO_ONTARGET_SEP: " << e.what(); + logwrite( function, message.str() ); + return ERROR; + } + message.str(""); message << "SEQUENCERD:config:" << config.param[entry] << "=" << config.arg[entry]; + logwrite( function, message.str() ); + applied++; + } + // OFFSET_SETTLE_SEC if (config.param[entry] == "OFFSET_SETTLE_SEC") { try { diff --git a/tcsd/tcs_interface.cpp b/tcsd/tcs_interface.cpp index 1fe8bee7..c05d566c 100644 --- a/tcsd/tcs_interface.cpp +++ b/tcsd/tcs_interface.cpp @@ -1857,6 +1857,9 @@ namespace TCS { if ( cmd == "?NAME" || cmd == "LAMPS?" || cmd == "NPS" || + cmd == "?ONTARGET" || // reply IS the flag (0|1), not a status code: + // 0 collides with TCS_SUCCESS ("success") and + // 1 falls through to "tcs_undefined" ERROR cmd == "?PARALLACTIC" || cmd == "?WEATHER" || cmd == "RAWDEC" ||