From aad8c37dc876262c6df62fcab203a35f737b1a11 Mon Sep 17 00:00:00 2001 From: David Hale Date: Mon, 30 Mar 2026 15:27:29 -0700 Subject: [PATCH 1/7] first steps making helpers that will aid in sequencer scripting --- sequencerd/sequence.cpp | 448 ++++++++++++++++++++++++---------------- sequencerd/sequence.h | 15 +- 2 files changed, 286 insertions(+), 177 deletions(-) diff --git a/sequencerd/sequence.cpp b/sequencerd/sequence.cpp index 5db8147d..fc3de240 100644 --- a/sequencerd/sequence.cpp +++ b/sequencerd/sequence.cpp @@ -18,6 +18,199 @@ namespace Sequencer { constexpr long CAMERA_PROLOG_TIMEOUT = 6000; ///< timeout msec to send camera prolog command + /***** Sequencer::Sequence::run ********************************************/ + /** + * @brief + * @param[in] op Operation + * @param[in] function function name of operation for logging + * + */ + long Sequence::run( const Operation &op, + const std::string &function ) { + long error=NO_ERROR; + try { + error = op.func(); + + if (error != NO_ERROR) { + this->async.enqueue_and_log(function, "ERROR in "+op.name); + } + } + catch (const std::exception &e) { + logwrite(function, "ERROR in "+op.name+": "+e.what()); + } + return error; + } + /***** Sequencer::Sequence::run ********************************************/ + + + /***** Sequencer::Sequence::run_parallel ***********************************/ + /** + * @brief + * @param[in] op Operation + * @param[in] function function name of operation for logging + * + */ + long Sequence::run_parallel( const std::vector &ops, + const std::string &function ) { + + // start a thread for each operation + // + std::vector> futures; + for (const auto &op : ops) { + futures.emplace_back(std::async(std::launch::async, [this, &op, function]() { + try { + return op.func(); + } + catch (const std::exception &e) { + logwrite(function, "ERROR in "+op.name+": "+e.what()); + return ERROR; + } + })); + } + + long error=NO_ERROR; + + // wait for each thread to complete + // + for (size_t i=0; i ops; + + if (this->target.pointmode == Acam::POINTMODE_ACAM) { + this->dotype("ONE"); + ops = { { "move_to_target", THR_MOVE_TO_TARGET, [this]{ return move_to_target(); } } }; + } + else { + this->target.pointmode = Acam::POINTMODE_SLIT; + + // these are the default operations prior to exposure, + // they can be done in parallel + ops = { { "move_to_target", THR_MOVE_TO_TARGET, [this]{ return move_to_target(); } }, + { "camera_set", THR_CAMERA_SET, [this]{ return camera_set(); } }, + { "focus_set", THR_FOCUS_SET, [this]{ return focus_set(); } }, + { "flexure_set", THR_FLEXURE_SET, [this]{ return flexure_set(); } }, + { "calib_set", THR_CALIB_SET, [this]{ return calib_set(); } }, + { "slit_set", THR_SLIT_SET, + [this]{ return slit_set(this->target.iscal ? VSM_DATABASE : VSM_ACQUIRE); } } }; + } + + // wait for threads + error = run_parallel(ops, caller); + + // early exit on error + if (error != NO_ERROR) return error; + + if (this->cancel_flag.load()) return ABORT; + + // ---------- POINTMODE-ACAM EXIT ---------------------- + + // If pointmode is ACAM then the user has chosen to put the star on ACAM, in + // which case the assumption is made that nothing else matters. This special + // mode of operation only points the telescope. + // + if ( this->target.pointmode == Acam::POINTMODE_ACAM ) { + this->async.enqueue_and_log(caller, "NOTICE: target list processing has stopped"); + return NO_ERROR; + } + + // if not a calibration target then acquire, first acam then slicecam + if (!this->target.iscal) { + + // ---------- TARGET ACQUISITION --------------------- + + // start ACAM acquisition. If it fails then wait for user to continue or cancel. + if ( this->do_acam_acquire() != NO_ERROR ) { + + this->async.enqueue_and_log( caller, "WARNING acam acquisition failed" ); + + if (this->wait_for_user(caller)==ABORT) { + this->async.enqueue_and_log( caller, "NOTICE: cancelled" ); + return ABORT; + } + } + else + // start SLICECAM fine acquisition + if ( this->do_slicecam_fineacquire() != NO_ERROR ) { + + this->async.enqueue_and_log( caller, "WARNING slicecam fine acquisition failed" ); + } + + // ---------- TARGET OFFSETS ------------------------- + + // send offsets. wait for user if that fails or cancelled + if (this->target_offset() == ERROR) { + + if (this->wait_for_user(caller)==ABORT) { + this->async.enqueue_and_log(caller, "NOTICE: cancelled"); + return ABORT; + } + } + + // ---------- SLIT POSITON FOR EXPOSE ---------------- + + // ensure slit offset is in "expose" position when needed + try { + error |= this->slit_set(Sequencer::VSM_EXPOSE); + } + catch (const std::exception &e) { + logwrite(caller, "ERROR slit offset exception: "+std::string(e.what())); + return ERROR; + } + } + + // ---------- EXPOSURE --------------------------------- + + logwrite(caller, "starting exposure"); + + // Start the exposure in a thread... + // set the EXPOSE bit here, outside of the trigger_exposure function, because that + // function only triggers the exposure -- it doesn't block waiting for the exposure. + // + this->wait_state_manager.set( Sequencer::SEQ_WAIT_EXPOSE ); // set EXPOSE bit + auto start_exposure = std::async(std::launch::async, &Sequence::trigger_exposure, this); + try { + error = start_exposure.get(); + } + catch (const std::exception& e) { + logwrite( caller, "ERROR start_exposure exception: "+std::string(e.what()) ); + return ERROR; + } + + // wait for the exposure to end (naturally or cancelled) + // + logwrite( caller, "waiting for exposure" ); + if (error==NO_ERROR) error = this->wait_for_exposure(caller); + + // If not using frame transfer then wait for readout, too + // + if (error==NO_ERROR && !this->is_science_frame_transfer) { + logwrite( caller, "waiting for readout" ); + error = this->wait_for_readout(caller); + } + + return error; + } + /***** Sequencer::Sequence::run_default_sequence ***************************/ + + /***** Sequencer::Sequence::handletopic_snapshot ***************************/ /** * @brief publishes snapshot of my telemetry @@ -412,40 +605,88 @@ namespace Sequencer { * @brief waits for the user to click a button, or cancel * @details Use this when you just want to slow things down or get a * cup of coffee instead of observing. + * @param[in] caller reference to caller's name for logging * @return NO_ERROR on continue | ABORT on cancel * */ - long Sequence::wait_for_user() { - const std::string function("Sequencer::Sequence::wait_for_user"); + long Sequence::wait_for_user(const std::string &caller) { { ScopedState wait_state( wait_state_manager, Sequencer::SEQ_WAIT_USER ); - this->async.enqueue_and_log( function, "NOTICE: waiting for USER to send \"continue\" signal" ); + this->async.enqueue_and_log( caller, "NOTICE: waiting for USER to send 'continue' signal" ); while ( !this->cancel_flag.load() && !this->is_usercontinue.load() ) { std::unique_lock lock(cv_mutex); this->cv.wait( lock, [this]() { return( this->is_usercontinue.load() || this->cancel_flag.load() ); } ); } - this->async.enqueue_and_log( function, "NOTICE: received " + this->async.enqueue_and_log( caller, "NOTICE: received " +(this->cancel_flag.load() ? std::string("cancel") : std::string("continue")) +" signal!" ); } // end scope for wait_state = WAIT_USER if ( this->cancel_flag.load() ) { - this->async.enqueue_and_log( function, "NOTICE: sequence cancelled" ); + this->async.enqueue_and_log( caller, "NOTICE: sequence cancelled" ); return ABORT; } this->is_usercontinue.store(false); - this->async.enqueue_and_log( function, "NOTICE: received USER continue signal!" ); - return NO_ERROR; } /***** Sequencer::Sequence::wait_for_user ***********************************/ + /***** Sequencer::Sequence::wait_for_exposure *******************************/ + /** + * @brief waits for exposure completion, or cancel + * @param[in] caller reference to caller's name for logging + * @return NO_ERROR on continue | ABORT on cancel + * + */ + long Sequence::wait_for_exposure(const std::string &caller) { + while (!this->cancel_flag.load() && + wait_state_manager.is_set(Sequencer::SEQ_WAIT_EXPOSE)) { + std::unique_lock lock(cv_mutex); + this->cv.wait( lock, [this]() { return(!wait_state_manager.is_set(SEQ_WAIT_EXPOSE) || + this->cancel_flag.load()); } ); + } + + if (this->cancel_flag.load()) { + this->async.enqueue_and_log(caller, "NOTICE: exposure cancelled"); + return ABORT; + } + + return NO_ERROR; + } + /***** Sequencer::Sequence::wait_for_exposure *******************************/ + + + /***** Sequencer::Sequence::wait_for_readout ********************************/ + /** + * @brief waits for readout completion, or cancel + * @param[in] caller reference to caller's name for logging + * @return NO_ERROR on continue | ABORT on cancel + * + */ + long Sequence::wait_for_readout(const std::string &caller) { + while (!this->cancel_flag.load() && + wait_state_manager.is_set(Sequencer::SEQ_WAIT_READOUT)) { + std::unique_lock lock(cv_mutex); + this->cv.wait( lock, [this]() { return(!wait_state_manager.is_set(SEQ_WAIT_READOUT) || + this->cancel_flag.load()); } ); + } + + if (this->cancel_flag.load()) { + this->async.enqueue_and_log(caller, "NOTICE: wait for readout cancelled"); + return ABORT; + } + + return NO_ERROR; + } + /***** Sequencer::Sequence::wait_for_readout ********************************/ + + /***** Sequencer::Sequence::sequence_start **********************************/ /** * @brief main sequence start thread @@ -569,174 +810,17 @@ namespace Sequencer { break; } - // get the threads going -- - // - // These things can all be done in parallel, just have to sync up at the end. - // - - // threads to start, pair their ThreadStatusBit with the function to call - std::vector>> worker_threads; - - // If pointmode is ACAM then the user has chosen to put the star on ACAM, in - // which case the assumption is made that nothing else matters. This special - // mode of operation only points the telescope. - // - if ( this->target.pointmode == Acam::POINTMODE_ACAM ) { - this->dotype( "ONE" ); - worker_threads = { { THR_MOVE_TO_TARGET, std::bind(&Sequence::move_to_target, this) } }; - - } - else { - - // For any other pointmode (SLIT, or empty, which assumes SLIT), all - // subsystems are readied. - // - // set pointmode explicitly, in case it's empty - this->target.pointmode = Acam::POINTMODE_SLIT; - - // threads to start, pair their ThreadStatusBit with the function to call - // - 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, - this->target.iscal ? Sequencer::VSM_DATABASE : Sequencer::VSM_ACQUIRE) } - }; - } - - // pair their ThreadStatusBit with their future - std::vector>> worker_futures; - - // start all of the threads - // - for ( const auto &[thr, func] : worker_threads ) { - worker_futures.emplace_back( thr, std::async(std::launch::async, func) ); - } - - // wait for the threads to complete. these can be cancelled. + // default observation sequence // - for ( auto &[thr, future] : worker_futures) { - try { - error |= future.get(); // wait for this worker to finish - logwrite( function, "NOTICE: worker "+Sequencer::thread_names.at(thr)+" completed"); - } - catch (const std::exception& e) { - logwrite( function, "ERROR: worker "+Sequencer::thread_names.at(thr)+" exception: "+std::string(e.what()) ); - return; - } - } - - logwrite(function, "DONE waiting on threads"); + error = run_default_sequence(function); - if ( this->cancel_flag.load() ) { - this->async.enqueue_and_log( function, "NOTICE: sequence cancelled" ); - return; - } - - // For pointmode ACAM, there is nothing to be done so get out - // - if ( this->target.pointmode == Acam::POINTMODE_ACAM ) { - this->async.enqueue_and_log( function, "NOTICE: target list processing has stopped" ); + if (error != NO_ERROR) { + this->thread_error_manager.set(THR_SEQUENCE_START); break; } - // If not a calibration target then acquire, first acam then slicecam - // - if ( !this->target.iscal ) { - - // start ACAM acquisition. If it fails then wait for user to continue or cancel. - if ( this->do_acam_acquire() != NO_ERROR ) { - this->async.enqueue_and_log( function, "WARNING acam acquisition failed" ); - if (this->wait_for_user()==ABORT) { - this->async.enqueue_and_log( function, "NOTICE: cancelled" ); - return; - } - } - else - // start SLICECAM fine acquisition - if ( this->do_slicecam_fineacquire() != NO_ERROR ) { - this->async.enqueue_and_log( function, "WARNING slicecam fine acquisition failed" ); - } - } - - if ( !this->target.iscal ) { - // send offsets. wait for user if that fails to continue or cancel. - if ( this->target_offset() == ERROR ) { - if (this->wait_for_user()==ABORT) { - this->async.enqueue_and_log( function, "NOTICE: cancelled" ); - return; - } - } - // ensure slit offset is in "expose" position when needed - try { - error |= this->slit_set(Sequencer::VSM_EXPOSE); - } - catch (const std::exception& e) { - logwrite( function, "ERROR slit offset exception: "+std::string(e.what()) ); - return; - } - } - - logwrite( function, "starting exposure" ); ///< TODO @todo log to telemetry! - - // Start the exposure in a thread... - // set the EXPOSE bit here, outside of the trigger_exposure function, because that - // function only triggers the exposure -- it doesn't block waiting for the exposure. - // - this->wait_state_manager.set( Sequencer::SEQ_WAIT_EXPOSE ); // set EXPOSE bit - auto start_exposure = std::async(std::launch::async, &Sequence::trigger_exposure, this); - try { - error |= start_exposure.get(); - } - catch (const std::exception& e) { - logwrite( function, "ERROR repeat_exposure exception: "+std::string(e.what()) ); - return; - } - - // wait for the exposure to end (naturally or cancelled) - // - logwrite( function, "waiting for exposure" ); - while ( !this->cancel_flag.load() && wait_state_manager.is_set( Sequencer::SEQ_WAIT_EXPOSE ) ) { - std::unique_lock lock(cv_mutex); - this->cv.wait( lock, [this]() { return( !wait_state_manager.is_set(SEQ_WAIT_EXPOSE) || this->cancel_flag.load() ); } ); - } - - // When an exposure is aborted then it will be marked as UNASSIGNED - // - if ( this->cancel_flag.load() ) { - this->async.enqueue_and_log( function, "NOTICE: exposure cancelled" ); - 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 - << " as " << Sequencer::TARGET_UNASSIGNED; - logwrite( function, message.str() ); - return; - } - - this->async.enqueue_and_log( function, "NOTICE: done waiting for expose" ); - message.str(""); message << "exposure complete for target " << this->target.name - << " id " << this->target.obsid << " order " << this->target.obsorder; - logwrite( function, message.str() ); - - // If not using frame transfer then wait for readout, too - // - if (!this->is_science_frame_transfer) { - logwrite( function, "waiting for readout" ); - while ( !this->cancel_flag.load() && wait_state_manager.is_set( Sequencer::SEQ_WAIT_READOUT ) ) { - std::unique_lock lock(cv_mutex); - this->cv.wait( lock, [this]() { return( !wait_state_manager.is_set(SEQ_WAIT_READOUT) || this->cancel_flag.load() ); } ); - } - } - - // Now that we're done waiting, check for errors or abort - // - if ( this->thread_error_manager.are_any_set() ) { - message.str(""); message << "ERROR stopping sequencer because the following thread(s) had an error: " - << this->thread_error_manager.get_set_states(); - logwrite( function, message.str() ); + if (this->cancel_flag.load()) { + this->async.enqueue_and_log(function, "NOTICE: sequence cancelled"); break; } @@ -747,14 +831,24 @@ namespace Sequencer { // Update this target's state in the database // - error = this->target.update_state( Sequencer::TARGET_COMPLETE ); // update the active target table + if (error==NO_ERROR) error = this->target.update_state( Sequencer::TARGET_COMPLETE ); + else + if (error==ABORT) error = this->target.update_state( Sequencer::TARGET_UNASSIGNED ); + if (error==NO_ERROR) error = this->target.insert_completed(); // insert into the completed table if (error!=NO_ERROR) this->thread_error_manager.set( THR_SEQUENCE_START ); // report any error // let the world know of the state change // - message.str(""); message << "TARGETSTATE:" << this->target.state << " TARGET:" << this->target.name << " OBSID:" << this->target.obsid; - this->async.enqueue( message.str() ); + std::ostringstream oss; + oss << "TARGETSTATE:" << this->target.state + << " TARGET:" << this->target.name + << " OBSID:" << this->target.obsid; + this->async.enqueue_and_log(function, oss.str()); + + // abort sequence on error + // + if ( this->thread_error_manager.are_any_set() ) break; // Check the "dotype" -- // If this was "do one" then do_once is set and get out now. @@ -767,9 +861,11 @@ namespace Sequencer { } // end while true if ( this->thread_error_manager.are_any_set() ) { - logwrite( function, "requesting stop because an error was detected" ); - if ( this->target.get_next( Sequencer::TARGET_ACTIVE, targetstatus ) == TargetInfo::TARGET_FOUND ) { // If this target was flagged as active, - this->target.update_state( Sequencer::TARGET_UNASSIGNED ); // then change it to unassigned on error. + logwrite(function, "ERROR stopping sequencer due to error in: "+ + this->thread_error_manager.get_set_states()); + // If this target was flagged as active, then change it to unassigned on error. + if ( this->target.get_next( Sequencer::TARGET_ACTIVE, targetstatus ) == TargetInfo::TARGET_FOUND ) { + this->target.update_state( Sequencer::TARGET_UNASSIGNED ); } this->thread_error_manager.clear_all(); // clear the thread error state this->do_once.store(true); diff --git a/sequencerd/sequence.h b/sequencerd/sequence.h index ab86cc45..de3975b2 100644 --- a/sequencerd/sequence.h +++ b/sequencerd/sequence.h @@ -291,6 +291,12 @@ namespace Sequencer { std::atomic is_fineacquire_locked{false}; ///< is slicecam fine acquisition locked? std::atomic is_acam_guiding{false}; ///< is acam guiding? + struct Operation { + std::string name; + ThreadStatusBits thr; + std::function func; + }; + /** @brief safely runs function in a detached thread using lambda to catch exceptions */ void safe_thread(long (Sequence::*method)(), const std::string &function) { @@ -456,6 +462,11 @@ namespace Sequencer { float slitoffsetacquire; ///< "virtual slit mode" offset for acquire float slitwidthacquire; ///< "virtual slit mode" width for acquire + // new stuff + long run(const Operation &op, const std::string &function); + long run_parallel(const std::vector &ops, const std::string &function); + long run_default_sequence(const std::string &caller); + // publish/subscribe functions // long init_pubsub(const std::initializer_list &topics={}) { @@ -560,7 +571,9 @@ namespace Sequencer { void modify_exptime( double exptime_in ); ///< modify exptime while exposure running void dothread_test(); - long wait_for_user(); ///< wait for the user or cancel + long wait_for_user(const std::string &function); ///< wait for the user or cancel + long wait_for_exposure(const std::string &function); ///< wait for exposure completion or cancel + long wait_for_readout(const std::string &function); ///< wait for readout completion or cancel void sequence_start(std::string obsid_in); ///< main sequence start thread. optional obsid_in for single target obs long calib_set(); ///< sets calib according to target entry params long camera_set(); ///< sets camera according to target entry params From d3ae5861f7e8ababb20ca77535473f024b664b99 Mon Sep 17 00:00:00 2001 From: David Hale Date: Tue, 31 Mar 2026 09:02:33 -0700 Subject: [PATCH 2/7] adds a sequencial operations wrapper --- sequencerd/sequence.cpp | 222 ++++++++++++++++++++++++++-------------- sequencerd/sequence.h | 1 + 2 files changed, 144 insertions(+), 79 deletions(-) diff --git a/sequencerd/sequence.cpp b/sequencerd/sequence.cpp index fc3de240..41dd9d52 100644 --- a/sequencerd/sequence.cpp +++ b/sequencerd/sequence.cpp @@ -20,49 +20,88 @@ namespace Sequencer { /***** Sequencer::Sequence::run ********************************************/ /** - * @brief - * @param[in] op Operation - * @param[in] function function name of operation for logging + * @brief executes a single operation + * @param[in] op Operation + * @param[in] caller name of calling function for logging + * @return ERROR|NO_ERROR|ABORT * */ long Sequence::run( const Operation &op, - const std::string &function ) { + const std::string &caller ) { long error=NO_ERROR; try { error = op.func(); if (error != NO_ERROR) { - this->async.enqueue_and_log(function, "ERROR in "+op.name); + this->async.enqueue_and_log(caller, "ERROR in "+op.name); } } catch (const std::exception &e) { - logwrite(function, "ERROR in "+op.name+": "+e.what()); + logwrite(caller, "ERROR in "+op.name+": "+e.what()); } return error; } /***** Sequencer::Sequence::run ********************************************/ + /***** Sequencer::Sequence::run_sequence ***********************************/ + /** + * @brief executes operations in sequence, one at a time + * @param[in] op vector of Operations to execute + * @param[in] caller name of calling function for logging + * @return ERROR|NO_ERROR|ABORT + * + */ + long Sequence::run_sequence( const std::vector &ops, + const std::string &caller ) { + + for (const auto &op : ops) { + + if (this->cancel_flag.load()) return ABORT; + + logwrite(caller, "starting "+op.name); + + try { + long error; + if ( (error = op.func()) != NO_ERROR ) { + std::ostringstream oss; + oss << (error==ABORT ? "cancelled" : "ERROR") << " in " << op.name; + logwrite(caller, oss.str()); + return error; + } + } + catch (const std::exception &e) { + logwrite(caller, "ERROR in "+op.name+": "+std::string(e.what())); + return ERROR; + } + } + return NO_ERROR; + } + /***** Sequencer::Sequence::run_sequence ***********************************/ + + /***** Sequencer::Sequence::run_parallel ***********************************/ /** - * @brief - * @param[in] op Operation - * @param[in] function function name of operation for logging + * @brief executes operations in parallel threads + * @details This will return only when all have completed. + * @param[in] op vector of Operations to execute + * @param[in] caller name of calling function for logging + * @return ERROR|NO_ERROR|ABORT * */ long Sequence::run_parallel( const std::vector &ops, - const std::string &function ) { + const std::string &caller ) { // start a thread for each operation // std::vector> futures; for (const auto &op : ops) { - futures.emplace_back(std::async(std::launch::async, [this, &op, function]() { + futures.emplace_back(std::async(std::launch::async, [this, &op, caller]() { try { return op.func(); } catch (const std::exception &e) { - logwrite(function, "ERROR in "+op.name+": "+e.what()); + logwrite(caller, "ERROR in "+op.name+": "+e.what()); return ERROR; } })); @@ -75,10 +114,10 @@ namespace Sequencer { for (size_t i=0; i ops; + std::vector par_ops; + // If pointmode is ACAM then the user has chosen to put the star on ACAM, in + // which case the assumption is made that nothing else matters. This special + // mode of operation only points the telescope. + // if (this->target.pointmode == Acam::POINTMODE_ACAM) { this->dotype("ONE"); - ops = { { "move_to_target", THR_MOVE_TO_TARGET, [this]{ return move_to_target(); } } }; + par_ops = { { "move_to_target", THR_MOVE_TO_TARGET, [this]{ return move_to_target(); } } }; } else { this->target.pointmode = Acam::POINTMODE_SLIT; + // ---------- RUN THESE IN PARALLEL ------------------ + // these are the default operations prior to exposure, // they can be done in parallel - ops = { { "move_to_target", THR_MOVE_TO_TARGET, [this]{ return move_to_target(); } }, - { "camera_set", THR_CAMERA_SET, [this]{ return camera_set(); } }, - { "focus_set", THR_FOCUS_SET, [this]{ return focus_set(); } }, - { "flexure_set", THR_FLEXURE_SET, [this]{ return flexure_set(); } }, - { "calib_set", THR_CALIB_SET, [this]{ return calib_set(); } }, - { "slit_set", THR_SLIT_SET, - [this]{ return slit_set(this->target.iscal ? VSM_DATABASE : VSM_ACQUIRE); } } }; + par_ops = { { "move_to_target", THR_MOVE_TO_TARGET, [this]{ return move_to_target(); } }, + { "camera_set", THR_CAMERA_SET, [this]{ return camera_set(); } }, + { "focus_set", THR_FOCUS_SET, [this]{ return focus_set(); } }, + { "flexure_set", THR_FLEXURE_SET, [this]{ return flexure_set(); } }, + { "calib_set", THR_CALIB_SET, [this]{ return calib_set(); } }, + { "slit_set", THR_SLIT_SET, + [this]{ return slit_set(this->target.iscal ? VSM_DATABASE : VSM_ACQUIRE); } } }; } - // wait for threads - error = run_parallel(ops, caller); + // execute in parallel threads and wait for completion + error = run_parallel(par_ops, caller); // early exit on error if (error != NO_ERROR) return error; @@ -131,80 +182,89 @@ namespace Sequencer { return NO_ERROR; } + // ---------- RUN THESE IN SERIES ---------------------- + + std::vector seq_ops; + // if not a calibration target then acquire, first acam then slicecam if (!this->target.iscal) { // ---------- TARGET ACQUISITION --------------------- - // start ACAM acquisition. If it fails then wait for user to continue or cancel. - if ( this->do_acam_acquire() != NO_ERROR ) { + seq_ops.push_back( { "acam_acquire", THR_ACQUISITION, + [this,caller]() { - this->async.enqueue_and_log( caller, "WARNING acam acquisition failed" ); + // start ACAM acquisition. + if ( this->do_acam_acquire() != NO_ERROR ) { - if (this->wait_for_user(caller)==ABORT) { - this->async.enqueue_and_log( caller, "NOTICE: cancelled" ); - return ABORT; - } - } - else - // start SLICECAM fine acquisition - if ( this->do_slicecam_fineacquire() != NO_ERROR ) { + this->async.enqueue_and_log( caller, "WARNING acam acquisition failed" ); - this->async.enqueue_and_log( caller, "WARNING slicecam fine acquisition failed" ); - } + // If acquisition fails, wait for user to continue or cancel. + if (this->wait_for_user(caller)==ABORT) { + this->async.enqueue_and_log( caller, "NOTICE: cancelled" ); + return ABORT; + } + } + else + // ACAM acquire success, start SLICECAM fine acquisition + if ( this->do_slicecam_fineacquire() != NO_ERROR ) { + + // slicecam fine acquire failure is not fatal + this->async.enqueue_and_log( caller, "WARNING slicecam fine acquisition failed" ); + } + } } ); // ---------- TARGET OFFSETS ------------------------- - // send offsets. wait for user if that fails or cancelled - if (this->target_offset() == ERROR) { + seq_ops.push_back( { "target_offset", THR_MOVE_TO_TARGET, + [this,caller]() { - if (this->wait_for_user(caller)==ABORT) { - this->async.enqueue_and_log(caller, "NOTICE: cancelled"); - return ABORT; - } - } + // send offsets. wait for user if that fails or cancelled + if (this->target_offset() == ERROR) { + + if (this->wait_for_user(caller)==ABORT) { + this->async.enqueue_and_log(caller, "NOTICE: cancelled"); + return ABORT; + } + } + } } ); // ---------- SLIT POSITON FOR EXPOSE ---------------- - // ensure slit offset is in "expose" position when needed - try { - error |= this->slit_set(Sequencer::VSM_EXPOSE); - } - catch (const std::exception &e) { - logwrite(caller, "ERROR slit offset exception: "+std::string(e.what())); - return ERROR; - } + seq_ops.push_back( { "slit_expose", THR_SLIT_SET, + [this]() { + // This was moved to VSM_ACQUIRE initially, then VSM_EXPOSE after acquisition. + return this->slit_set(Sequencer::VSM_EXPOSE); + } } ); } + // end if iscal // ---------- EXPOSURE --------------------------------- - logwrite(caller, "starting exposure"); + seq_ops.push_back( { "trigger_exposure", THR_TRIGGER_EXPOSURE, + [this, caller]() { - // Start the exposure in a thread... - // set the EXPOSE bit here, outside of the trigger_exposure function, because that - // function only triggers the exposure -- it doesn't block waiting for the exposure. - // - this->wait_state_manager.set( Sequencer::SEQ_WAIT_EXPOSE ); // set EXPOSE bit - auto start_exposure = std::async(std::launch::async, &Sequence::trigger_exposure, this); - try { - error = start_exposure.get(); - } - catch (const std::exception& e) { - logwrite( caller, "ERROR start_exposure exception: "+std::string(e.what()) ); - return ERROR; - } + logwrite(caller, "starting exposure"); - // wait for the exposure to end (naturally or cancelled) - // - logwrite( caller, "waiting for exposure" ); - if (error==NO_ERROR) error = this->wait_for_exposure(caller); + // Start the exposure in a thread... + // set the EXPOSE bit here, outside of the trigger_exposure function, because that + // function only triggers the exposure -- it doesn't block waiting for the exposure. + // + this->wait_state_manager.set( Sequencer::SEQ_WAIT_EXPOSE ); // set EXPOSE bit + auto start_exposure = std::async(std::launch::async, &Sequence::trigger_exposure, this); + long ret = start_exposure.get(); - // If not using frame transfer then wait for readout, too - // - if (error==NO_ERROR && !this->is_science_frame_transfer) { - logwrite( caller, "waiting for readout" ); - error = this->wait_for_readout(caller); - } + if (ret==NO_ERROR) ret = this->wait_for_exposure(caller); + + if (ret==NO_ERROR && !this->is_science_frame_transfer) { + ret = this->wait_for_readout(caller); + } + return ret; + } } ); + + // ---------- RUN THE SEQUENCE NOW --------------------- + + error = run_sequence(seq_ops, caller); return error; } @@ -645,6 +705,7 @@ namespace Sequencer { * */ long Sequence::wait_for_exposure(const std::string &caller) { + logwrite(caller, "waiting for exposure"); while (!this->cancel_flag.load() && wait_state_manager.is_set(Sequencer::SEQ_WAIT_EXPOSE)) { std::unique_lock lock(cv_mutex); @@ -670,6 +731,7 @@ namespace Sequencer { * */ long Sequence::wait_for_readout(const std::string &caller) { + logwrite(caller, "waiting for readout"); while (!this->cancel_flag.load() && wait_state_manager.is_set(Sequencer::SEQ_WAIT_READOUT)) { std::unique_lock lock(cv_mutex); @@ -703,7 +765,7 @@ namespace Sequencer { */ void Sequence::sequence_start(std::string obsid_in="") { const std::string function("Sequencer::Sequence::sequence_start"); - std::stringstream message; + std::ostringstream message; std::string reply; std::string targetstatus; TargetInfo::TargetState targetstate; @@ -793,7 +855,9 @@ namespace Sequencer { // let the world know of the state change // - message.str(""); message << "TARGETSTATE:" << this->target.state << " TARGET:" << this->target.name << " OBSID:" << this->target.obsid; + message.str(""); message << "TARGETSTATE:" << this->target.state + << " TARGET:" << this->target.name + << " OBSID:" << this->target.obsid; this->async.enqueue( message.str() ); #ifdef LOGLEVEL_DEBUG logwrite( function, "[DEBUG] target found, starting threads" ); diff --git a/sequencerd/sequence.h b/sequencerd/sequence.h index de3975b2..8c954279 100644 --- a/sequencerd/sequence.h +++ b/sequencerd/sequence.h @@ -464,6 +464,7 @@ namespace Sequencer { // new stuff long run(const Operation &op, const std::string &function); + long run_sequence(const std::vector &ops, const std::string &function); long run_parallel(const std::vector &ops, const std::string &function); long run_default_sequence(const std::string &caller); From 856a24e3202e61df4b70b41f438702d2f32ea21a Mon Sep 17 00:00:00 2001 From: David Hale Date: Tue, 31 Mar 2026 11:47:57 -0700 Subject: [PATCH 3/7] adds one more level of abstraction, Operation Blocks, to aid in scripting --- sequencerd/sequence.cpp | 230 +++++++++++++++++++++++----------------- sequencerd/sequence.h | 22 +++- 2 files changed, 149 insertions(+), 103 deletions(-) diff --git a/sequencerd/sequence.cpp b/sequencerd/sequence.cpp index 41dd9d52..9110b1d2 100644 --- a/sequencerd/sequence.cpp +++ b/sequencerd/sequence.cpp @@ -29,6 +29,8 @@ namespace Sequencer { long Sequence::run( const Operation &op, const std::string &caller ) { long error=NO_ERROR; + logwrite(caller, "starting "+op.name); + try { error = op.func(); @@ -38,6 +40,7 @@ namespace Sequencer { } catch (const std::exception &e) { logwrite(caller, "ERROR in "+op.name+": "+e.what()); + error = ERROR; } return error; } @@ -92,40 +95,83 @@ namespace Sequencer { long Sequence::run_parallel( const std::vector &ops, const std::string &caller ) { + std::vector> futures; + // start a thread for each operation // - std::vector> futures; for (const auto &op : ops) { - futures.emplace_back(std::async(std::launch::async, [this, &op, caller]() { - try { - return op.func(); - } - catch (const std::exception &e) { - logwrite(caller, "ERROR in "+op.name+": "+e.what()); - return ERROR; - } - })); + futures.emplace_back(std::async(std::launch::async, op.func)); } - long error=NO_ERROR; + long error = NO_ERROR; - // wait for each thread to complete + // wait for all threads, collect errors // - for (size_t i=0; i &blocks, + const std::string &caller, + bool continue_on_error ) { + long error = NO_ERROR; + + for (const auto &block : blocks) { + if (this->cancel_flag.load()) return ABORT; + + // PARALLEL Blocks are executed in parallel threads + // + if (block.type == OperationType::PARALLEL) { + long ret = run_parallel(block.operations, caller); + error |= ret; + + if (ret != NO_ERROR && !continue_on_error) return error; + } + // SERIAL Blocks are executed one at a time + // + else { + for (const auto &op : block.operations) { + if (this->cancel_flag.load()) return ABORT; + + long ret = run(op, caller); + error |= ret; + + if (ret != NO_ERROR && !continue_on_error) return error; + } + } + } + + return error; + } + /***** Sequencer::Sequence::run_operation_blocks ***************************/ + + /***** Sequencer::Sequence::run_default_sequence ***************************/ /** * @brief executes a default observation sequence @@ -135,9 +181,9 @@ namespace Sequencer { */ long Sequence::run_default_sequence(const std::string &caller) { - long error = NO_ERROR; + std::vector blocks; - std::vector par_ops; + // ---------- RUN THESE IN PARALLEL -------------------- // If pointmode is ACAM then the user has chosen to put the star on ACAM, in // which case the assumption is made that nothing else matters. This special @@ -145,128 +191,113 @@ namespace Sequencer { // if (this->target.pointmode == Acam::POINTMODE_ACAM) { this->dotype("ONE"); - par_ops = { { "move_to_target", THR_MOVE_TO_TARGET, [this]{ return move_to_target(); } } }; + blocks.push_back( { OperationType::PARALLEL, + { { "move_to_target", THR_MOVE_TO_TARGET, [this]{ return move_to_target(); } } } } ); } else { this->target.pointmode = Acam::POINTMODE_SLIT; - // ---------- RUN THESE IN PARALLEL ------------------ - // these are the default operations prior to exposure, // they can be done in parallel - par_ops = { { "move_to_target", THR_MOVE_TO_TARGET, [this]{ return move_to_target(); } }, - { "camera_set", THR_CAMERA_SET, [this]{ return camera_set(); } }, - { "focus_set", THR_FOCUS_SET, [this]{ return focus_set(); } }, - { "flexure_set", THR_FLEXURE_SET, [this]{ return flexure_set(); } }, - { "calib_set", THR_CALIB_SET, [this]{ return calib_set(); } }, - { "slit_set", THR_SLIT_SET, - [this]{ return slit_set(this->target.iscal ? VSM_DATABASE : VSM_ACQUIRE); } } }; + blocks.push_back( { OperationType::PARALLEL, + { { "move_to_target", THR_MOVE_TO_TARGET, [this]{ return move_to_target(); } }, + { "camera_set", THR_CAMERA_SET, [this]{ return camera_set(); } }, + { "focus_set", THR_FOCUS_SET, [this]{ return focus_set(); } }, + { "flexure_set", THR_FLEXURE_SET, [this]{ return flexure_set(); } }, + { "calib_set", THR_CALIB_SET, [this]{ return calib_set(); } }, + { "slit_set", THR_SLIT_SET, + [this]{ return slit_set(this->target.iscal ? VSM_DATABASE : VSM_ACQUIRE); } } } } ); } - // execute in parallel threads and wait for completion - error = run_parallel(par_ops, caller); - - // early exit on error - if (error != NO_ERROR) return error; - - if (this->cancel_flag.load()) return ABORT; - - // ---------- POINTMODE-ACAM EXIT ---------------------- - - // If pointmode is ACAM then the user has chosen to put the star on ACAM, in - // which case the assumption is made that nothing else matters. This special - // mode of operation only points the telescope. + // Early Exit for pointmode=ACAM // - if ( this->target.pointmode == Acam::POINTMODE_ACAM ) { - this->async.enqueue_and_log(caller, "NOTICE: target list processing has stopped"); - return NO_ERROR; + if (this->target.pointmode == Acam::POINTMODE_ACAM) { + return run_operation_blocks(blocks, caller); } // ---------- RUN THESE IN SERIES ---------------------- - std::vector seq_ops; - // if not a calibration target then acquire, first acam then slicecam if (!this->target.iscal) { // ---------- TARGET ACQUISITION --------------------- - seq_ops.push_back( { "acam_acquire", THR_ACQUISITION, - [this,caller]() { - - // start ACAM acquisition. - if ( this->do_acam_acquire() != NO_ERROR ) { + blocks.push_back( { OperationType::SERIAL, + { { "acam_acquire", THR_ACQUISITION, [this,caller]() { - this->async.enqueue_and_log( caller, "WARNING acam acquisition failed" ); + // if ACAM acquisition fails, wait for user to continue or cancel + if ( this->do_acam_acquire() != NO_ERROR ) { + return this->wait_for_user(caller); + } + else return NO_ERROR; + } }, - // If acquisition fails, wait for user to continue or cancel. - if (this->wait_for_user(caller)==ABORT) { - this->async.enqueue_and_log( caller, "NOTICE: cancelled" ); - return ABORT; + { "slicecam_fineacquire", THR_ACQUISITION, [this,caller]() { + if ( this->do_slicecam_fineacquire() != NO_ERROR ) { + this->async.enqueue_and_log(caller, "WARNING slicecam fine acquisition failed"); } + return NO_ERROR; // slicecam fine acquire is never fatal + } } } - else - // ACAM acquire success, start SLICECAM fine acquisition - if ( this->do_slicecam_fineacquire() != NO_ERROR ) { + } ); + } - // slicecam fine acquire failure is not fatal - this->async.enqueue_and_log( caller, "WARNING slicecam fine acquisition failed" ); - } - } } ); + if (!this->target.iscal) { // ---------- TARGET OFFSETS ------------------------- - seq_ops.push_back( { "target_offset", THR_MOVE_TO_TARGET, - [this,caller]() { + blocks.push_back( { OperationType::SERIAL, + { { "target_offset", THR_MOVE_TO_TARGET, [this,caller]() { - // send offsets. wait for user if that fails or cancelled - if (this->target_offset() == ERROR) { - - if (this->wait_for_user(caller)==ABORT) { - this->async.enqueue_and_log(caller, "NOTICE: cancelled"); - return ABORT; + // if offsets fail, wait for user to continue or cancel + if (this->target_offset() != NO_ERROR) { + return this->wait_for_user(caller); } + else return NO_ERROR; + } } } - } } ); + } ); + } + + if (!this->target.iscal) { // ---------- SLIT POSITON FOR EXPOSE ---------------- - seq_ops.push_back( { "slit_expose", THR_SLIT_SET, - [this]() { - // This was moved to VSM_ACQUIRE initially, then VSM_EXPOSE after acquisition. - return this->slit_set(Sequencer::VSM_EXPOSE); - } } ); + blocks.push_back( { OperationType::SERIAL, + { { "slit_expose", THR_SLIT_SET, [this]() { + return this->slit_set(Sequencer::VSM_EXPOSE); } } + } + } ); } - // end if iscal // ---------- EXPOSURE --------------------------------- - seq_ops.push_back( { "trigger_exposure", THR_TRIGGER_EXPOSURE, - [this, caller]() { - - logwrite(caller, "starting exposure"); + blocks.push_back( { OperationType::SERIAL, + { { "trigger_exposure", THR_EXPOSURE, [this]() { - // Start the exposure in a thread... - // set the EXPOSE bit here, outside of the trigger_exposure function, because that - // function only triggers the exposure -- it doesn't block waiting for the exposure. - // - this->wait_state_manager.set( Sequencer::SEQ_WAIT_EXPOSE ); // set EXPOSE bit - auto start_exposure = std::async(std::launch::async, &Sequence::trigger_exposure, this); - long ret = start_exposure.get(); + // set the EXPOSE bit here, outside of the trigger_exposure function, because that + // function only triggers the exposure -- it doesn't block waiting for the exposure. + // + this->wait_state_manager.set( Sequencer::SEQ_WAIT_EXPOSE ); // set EXPOSE bit + return trigger_exposure(); + } }, - if (ret==NO_ERROR) ret = this->wait_for_exposure(caller); + { "wait_exposure", THR_EXPOSURE, [this,caller]() { + return this->wait_for_exposure(caller); + } }, - if (ret==NO_ERROR && !this->is_science_frame_transfer) { - ret = this->wait_for_readout(caller); + { "wait_readout", THR_EXPOSURE, [this,caller]() { + if (!this->is_science_frame_transfer) { + return this->wait_for_readout(caller); + } + else return NO_ERROR; + } } } - return ret; - } } ); + } ); // ---------- RUN THE SEQUENCE NOW --------------------- - error = run_sequence(seq_ops, caller); - - return error; + return run_operation_blocks(blocks, caller); } /***** Sequencer::Sequence::run_default_sequence ***************************/ @@ -4843,7 +4874,6 @@ namespace Sequencer { return( ERROR ); } - bool ispower = false; std::string reply; // power module must be initialized before any others. If this is not diff --git a/sequencerd/sequence.h b/sequencerd/sequence.h index 8c954279..c95b8dd3 100644 --- a/sequencerd/sequence.h +++ b/sequencerd/sequence.h @@ -188,6 +188,7 @@ namespace Sequencer { enum ThreadStatusBits : size_t { THR_SEQUENCER_ASYNC_LISTENER=0, THR_TRIGGER_EXPOSURE, + THR_EXPOSURE, THR_REPEAT_EXPOSURE, THR_STOP_EXPOSURE, THR_ABORT_PROCESS, @@ -232,6 +233,7 @@ namespace Sequencer { const std::map thread_names = { {THR_SEQUENCER_ASYNC_LISTENER, "async_listener"}, {THR_TRIGGER_EXPOSURE, "trigger_exposure"}, + {THR_EXPOSURE, "exposure"}, {THR_REPEAT_EXPOSURE, "repeat_exposure"}, {THR_STOP_EXPOSURE, "stop_exposure"}, {THR_ABORT_PROCESS, "abort_process"}, @@ -291,10 +293,21 @@ namespace Sequencer { std::atomic is_fineacquire_locked{false}; ///< is slicecam fine acquisition locked? std::atomic is_acam_guiding{false}; ///< is acam guiding? + enum class OperationType { + PARALLEL, + SERIAL + }; + struct Operation { - std::string name; - ThreadStatusBits thr; - std::function func; + std::string name; ///< name of this operation + ThreadStatusBits thr; ///< status bit of what is running + std::function func; ///< function that this operation calls + std::map params; ///< function parameters + }; + + struct OperationBlock { + OperationType type; + std::vector operations; }; /** @brief safely runs function in a detached thread using lambda to catch exceptions @@ -467,6 +480,9 @@ namespace Sequencer { long run_sequence(const std::vector &ops, const std::string &function); long run_parallel(const std::vector &ops, const std::string &function); long run_default_sequence(const std::string &caller); + long run_operation_blocks( const std::vector &blocks, + const std::string &caller, + bool continue_on_error=false ); // publish/subscribe functions // From 68cbf871e2f4b2ad647bd552cadaacd28c9e0303 Mon Sep 17 00:00:00 2001 From: David Hale Date: Fri, 3 Apr 2026 16:51:38 -0700 Subject: [PATCH 4/7] encapsulates more logic into blocks to aid scripting changes std::string to string_view where string not needed --- common/common.cpp | 10 +- common/common.h | 10 +- sequencerd/sequence.cpp | 282 +++++++++++++++------------- sequencerd/sequence.h | 25 ++- sequencerd/sequence_acquisition.cpp | 50 ++++- sequencerd/sequencer_server.cpp | 2 +- utils/logentry.cpp | 7 +- utils/logentry.h | 2 +- utils/network.cpp | 8 +- utils/network.h | 2 +- 10 files changed, 231 insertions(+), 167 deletions(-) diff --git a/common/common.cpp b/common/common.cpp index 3fc176eb..196d1fb0 100644 --- a/common/common.cpp +++ b/common/common.cpp @@ -59,7 +59,7 @@ namespace Common { * @param[in] message string to write * */ - void Queue::enqueue_and_log(std::string function, std::string message) { + void Queue::enqueue_and_log(std::string_view function, std::string_view message) { std::lock_guard lock(queue_mutex); message_queue.push(message); notifier.notify_one(); @@ -77,9 +77,9 @@ namespace Common { * @param[in] message string to write * */ - void Queue::enqueue_and_log( std::string tag, std::string function, std::string message ) { + void Queue::enqueue_and_log( std::string_view tag, std::string_view function, std::string_view message ) { std::lock_guard lock(queue_mutex); - std::stringstream qmessage; + std::ostringstream qmessage; qmessage << tag << ":" << message; message_queue.push(qmessage.str()); notifier.notify_one(); @@ -98,12 +98,12 @@ namespace Common { * If the queue is empty, wait untill an element is avaiable. * */ - std::string Queue::dequeue(void) { + std::string_view Queue::dequeue(void) { std::unique_lock lock(queue_mutex); while(message_queue.empty()) { notifier.wait(lock); // release lock as long as the wait and reaquire it afterwards. } - std::string message = message_queue.front(); + std::string_view message = message_queue.front(); message_queue.pop(); return message; } diff --git a/common/common.h b/common/common.h index d540dbd3..49d4db66 100644 --- a/common/common.h +++ b/common/common.h @@ -1112,7 +1112,7 @@ namespace Common { */ class Queue { private: - std::queue message_queue; + std::queue message_queue; mutable std::mutex queue_mutex; std::condition_variable notifier; bool is_running; @@ -1123,10 +1123,10 @@ namespace Common { void service_running(bool state) { this->is_running = state; }; ///< set service running bool service_running() { return this->is_running; }; ///< is the service running? - void enqueue_and_log(std::string function, std::string message); - void enqueue_and_log(std::string tag, std::string function, std::string message); - void enqueue(std::string message); ///< push an element into the queue. - std::string dequeue(void); ///< pop an element from the queue + void enqueue_and_log(std::string_view function, std::string_view message); + void enqueue_and_log(std::string_view tag, std::string_view function, std::string_view message); + void enqueue(std::string message_view); ///< push an element into the queue. + std::string_view dequeue(void); ///< pop an element from the queue }; /**************** Common::Queue *********************************************/ diff --git a/sequencerd/sequence.cpp b/sequencerd/sequence.cpp index 9110b1d2..3c2a7641 100644 --- a/sequencerd/sequence.cpp +++ b/sequencerd/sequence.cpp @@ -27,7 +27,7 @@ namespace Sequencer { * */ long Sequence::run( const Operation &op, - const std::string &caller ) { + std::string_view caller ) { long error=NO_ERROR; logwrite(caller, "starting "+op.name); @@ -56,7 +56,7 @@ namespace Sequencer { * */ long Sequence::run_sequence( const std::vector &ops, - const std::string &caller ) { + std::string_view caller ) { for (const auto &op : ops) { @@ -93,7 +93,7 @@ namespace Sequencer { * */ long Sequence::run_parallel( const std::vector &ops, - const std::string &caller ) { + std::string_view caller ) { std::vector> futures; @@ -138,7 +138,7 @@ namespace Sequencer { * */ long Sequence::run_operation_blocks( const std::vector &blocks, - const std::string &caller, + std::string_view caller, bool continue_on_error ) { long error = NO_ERROR; @@ -179,7 +179,7 @@ namespace Sequencer { * @return ERROR|NO_ERROR|ABORT * */ - long Sequence::run_default_sequence(const std::string &caller) { + long Sequence::run_default_sequence(std::string_view caller) { std::vector blocks; @@ -200,13 +200,14 @@ namespace Sequencer { // these are the default operations prior to exposure, // they can be done in parallel blocks.push_back( { OperationType::PARALLEL, - { { "move_to_target", THR_MOVE_TO_TARGET, [this]{ return move_to_target(); } }, - { "camera_set", THR_CAMERA_SET, [this]{ return camera_set(); } }, - { "focus_set", THR_FOCUS_SET, [this]{ return focus_set(); } }, - { "flexure_set", THR_FLEXURE_SET, [this]{ return flexure_set(); } }, - { "calib_set", THR_CALIB_SET, [this]{ return calib_set(); } }, - { "slit_set", THR_SLIT_SET, - [this]{ return slit_set(this->target.iscal ? VSM_DATABASE : VSM_ACQUIRE); } } } } ); + { { "move_to_target", THR_MOVE_TO_TARGET, [this]{ return move_to_target(); } }, + { "camera_set", THR_CAMERA_SET, [this]{ return camera_set(); } }, + { "focus_set", THR_FOCUS_SET, [this]{ return focus_set(); } }, + { "flexure_set", THR_FLEXURE_SET, [this]{ return flexure_set(); } }, + { "calib_set", THR_CALIB_SET, [this]{ return calib_set(); } }, + { "slit_set", THR_SLIT_SET, [this]{ return slit_set(this->target.iscal ? VSM_DATABASE + : VSM_ACQUIRE); } } + } } ); } // Early Exit for pointmode=ACAM @@ -217,58 +218,17 @@ namespace Sequencer { // ---------- RUN THESE IN SERIES ---------------------- - // if not a calibration target then acquire, first acam then slicecam - if (!this->target.iscal) { - - // ---------- TARGET ACQUISITION --------------------- - - blocks.push_back( { OperationType::SERIAL, - { { "acam_acquire", THR_ACQUISITION, [this,caller]() { - - // if ACAM acquisition fails, wait for user to continue or cancel - if ( this->do_acam_acquire() != NO_ERROR ) { - return this->wait_for_user(caller); - } - else return NO_ERROR; - } }, - - { "slicecam_fineacquire", THR_ACQUISITION, [this,caller]() { - if ( this->do_slicecam_fineacquire() != NO_ERROR ) { - this->async.enqueue_and_log(caller, "WARNING slicecam fine acquisition failed"); - } - return NO_ERROR; // slicecam fine acquire is never fatal - } } - } - } ); - } - - if (!this->target.iscal) { - - // ---------- TARGET OFFSETS ------------------------- - - blocks.push_back( { OperationType::SERIAL, - { { "target_offset", THR_MOVE_TO_TARGET, [this,caller]() { - - // if offsets fail, wait for user to continue or cancel - if (this->target_offset() != NO_ERROR) { - return this->wait_for_user(caller); - } - else return NO_ERROR; - } } - } - } ); - } - - if (!this->target.iscal) { + blocks.push_back( { OperationType::SERIAL, + { { "target_acquisition", THR_ACQUISITION, + [this,caller]() { return this->do_target_acquisition(caller); } }, - // ---------- SLIT POSITON FOR EXPOSE ---------------- + { "target_offset", THR_MOVE_TO_TARGET, + [this]() { return this->target_offset(); } }, - blocks.push_back( { OperationType::SERIAL, - { { "slit_expose", THR_SLIT_SET, [this]() { - return this->slit_set(Sequencer::VSM_EXPOSE); } } - } - } ); - } + { "slit_expose", THR_SLIT_SET, + [this]() { return this->do_target_virtualslit(Sequencer::VSM_EXPOSE); } } + } + } ); // ---------- EXPOSURE --------------------------------- @@ -589,7 +549,7 @@ namespace Sequencer { * */ void Sequence::dothread_sequencer_async_listener( Sequencer::Sequence &seq, Network::UdpSocket udp ) { - const std::string function("Sequencer::Sequence::dothread_sequencer_async_listener"); + std::string_view function("Sequencer::Sequence::dothread_sequencer_async_listener"); ScopedState thr_state( seq.thread_state_manager, Sequencer::THR_SEQUENCER_ASYNC_LISTENER ); @@ -691,6 +651,40 @@ namespace Sequencer { } + /***** Sequencer::Sequence::wait_for_ontarget *******************************/ + /** + * @brief waits for the TCS Operator to click 'ontarget' + * @param[in] caller reference to caller's name for logging + * @return NO_ERROR on continue | ABORT on cancel + * + */ + long Sequence::wait_for_ontarget(std::string_view caller) { + // waiting for TCS Operator input (or cancel) + { + ScopedState wait_state( wait_state_manager, Sequencer::SEQ_WAIT_TCSOP ); + + this->async.enqueue_and_log(caller, "NOTICE: waiting for TCS operator to send 'ontarget' signal"); + + 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->async.enqueue_and_log(caller, "NOTICE: received " + +(this->cancel_flag.load() ? std::string("cancel") + : std::string("ontarget")) + +" signal!" ); + } + this->is_ontarget.store(false); + + return (this->cancel_flag.load() ? ABORT : NO_ERROR); + } + /***** Sequencer::Sequence::wait_for_ontarget *******************************/ + + /***** Sequencer::Sequence::wait_for_user ***********************************/ /** * @brief waits for the user to click a button, or cancel @@ -700,7 +694,7 @@ namespace Sequencer { * @return NO_ERROR on continue | ABORT on cancel * */ - long Sequence::wait_for_user(const std::string &caller) { + long Sequence::wait_for_user(std::string_view caller) { { ScopedState wait_state( wait_state_manager, Sequencer::SEQ_WAIT_USER ); @@ -735,7 +729,7 @@ namespace Sequencer { * @return NO_ERROR on continue | ABORT on cancel * */ - long Sequence::wait_for_exposure(const std::string &caller) { + long Sequence::wait_for_exposure(std::string_view caller) { logwrite(caller, "waiting for exposure"); while (!this->cancel_flag.load() && wait_state_manager.is_set(Sequencer::SEQ_WAIT_EXPOSE)) { @@ -761,7 +755,7 @@ namespace Sequencer { * @return NO_ERROR on continue | ABORT on cancel * */ - long Sequence::wait_for_readout(const std::string &caller) { + long Sequence::wait_for_readout(std::string_view caller) { logwrite(caller, "waiting for readout"); while (!this->cancel_flag.load() && wait_state_manager.is_set(Sequencer::SEQ_WAIT_READOUT)) { @@ -780,6 +774,36 @@ namespace Sequencer { /***** Sequencer::Sequence::wait_for_readout ********************************/ + /***** Sequencer::Sequence::wait_for_canexpose ******************************/ + /** + * @brief waits for camera to be ready to expose, or cancel + * @param[in] caller reference to caller's name for logging + * @return NO_ERROR on continue | ABORT on cancel + * + */ + long Sequence::wait_for_canexpose(std::string_view caller) { + logwrite(caller, "waiting for can_expose"); + + while ( !this->cancel_flag.load() && + !this->can_expose.load() ) { + + this->async.enqueue_and_log(caller, "NOTICE: waiting for camera to be ready to expose"); + + std::unique_lock lock(this->camerad_mtx); + this->camerad_cv.wait( lock, [this]() { return( this->can_expose.load() || + this->cancel_flag.load() ); } ); + } + + if (this->cancel_flag.load()) { + this->async.enqueue_and_log(caller, "NOTICE: wait for can_expose cancelled"); + return ABORT; + } + + return NO_ERROR; + } + /***** Sequencer::Sequence::wait_for_canexpose ******************************/ + + /***** Sequencer::Sequence::sequence_start **********************************/ /** * @brief main sequence start thread @@ -795,7 +819,7 @@ namespace Sequencer { * */ void Sequence::sequence_start(std::string obsid_in="") { - const std::string function("Sequencer::Sequence::sequence_start"); + std::string_view function("Sequencer::Sequence::sequence_start"); std::ostringstream message; std::string reply; std::string targetstatus; @@ -905,7 +929,7 @@ namespace Sequencer { break; } - // default observation sequence + // ---------- default observation sequence ----------- // error = run_default_sequence(function); @@ -982,27 +1006,14 @@ namespace Sequencer { * */ long Sequence::camera_set() { - const std::string function("Sequencer::Sequence::camera_set"); + std::string_view function("Sequencer::Sequence::camera_set"); std::string reply; std::stringstream camcmd; long error=NO_ERROR; // wait until camera is ready to expose // - std::unique_lock lock(this->camerad_mtx); - if (!this->can_expose.load()) { - - this->async.enqueue_and_log(function, "NOTICE: waiting for camera to be ready to expose"); - - this->camerad_cv.wait( lock, [this]() { - return( this->can_expose.load() || this->cancel_flag.load() ); - } ); - - if (this->cancel_flag.load()) { - logwrite(function, "sequence cancelled"); - return NO_ERROR; - } - } + this->wait_for_canexpose(function); logwrite( function, "setting camera parameters"); @@ -1083,7 +1094,7 @@ namespace Sequencer { * */ long Sequence::slit_set(VirtualSlitMode mode) { - const std::string function("Sequencer::Sequence::slit_set"); + std::string_view function("Sequencer::Sequence::slit_set"); std::string reply, modestr; std::stringstream slitcmd, message; @@ -1140,7 +1151,7 @@ namespace Sequencer { * */ long Sequence::power_init() { - const std::string function("Sequencer::Sequence::power_init"); + std::string_view function("Sequencer::Sequence::power_init"); ScopedState thr_state( thread_state_manager, Sequencer::THR_POWER_INIT ); ScopedState wait_state( wait_state_manager, Sequencer::SEQ_WAIT_POWER ); @@ -1167,7 +1178,7 @@ namespace Sequencer { * */ long Sequence::power_shutdown() { - const std::string function("Sequencer::Sequence::power_shutdown"); + std::string_view function("Sequencer::Sequence::power_shutdown"); ScopedState thr_state( this->thread_state_manager, Sequencer::THR_POWER_SHUTDOWN ); ScopedState wait_state( this->wait_state_manager, Sequencer::SEQ_WAIT_POWER ); @@ -1190,7 +1201,7 @@ namespace Sequencer { * */ long Sequence::slit_init() { - const std::string function("Sequencer::Sequence::slit_init"); + std::string_view function("Sequencer::Sequence::slit_init"); ScopedState thr_state( thread_state_manager, Sequencer::THR_SLIT_INIT ); ScopedState wait_state( wait_state_manager, Sequencer::SEQ_WAIT_SLIT ); @@ -1256,7 +1267,7 @@ namespace Sequencer { * */ long Sequence::slit_shutdown() { - const std::string function("Sequencer::Sequence::slit_shutdown"); + std::string_view function("Sequencer::Sequence::slit_shutdown"); std::stringstream message; std::string reply; long error=NO_ERROR; @@ -1324,7 +1335,7 @@ namespace Sequencer { * */ long Sequence::slicecam_init() { - const std::string function("Sequencer::Sequence::slicecam_init"); + std::string_view function("Sequencer::Sequence::slicecam_init"); this->daemon_manager.clear( Sequencer::DAEMON_SLICECAM ); // slicecamd not ready @@ -1364,7 +1375,7 @@ namespace Sequencer { * */ long Sequence::acam_init() { - const std::string function("Sequencer::Sequence::acam_init"); + std::string_view function("Sequencer::Sequence::acam_init"); this->daemon_manager.clear( Sequencer::DAEMON_ACAM ); // acamd not ready @@ -1424,7 +1435,7 @@ namespace Sequencer { * */ long Sequence::slicecam_shutdown() { - const std::string function("Sequencer::Sequence::slicecam_shutdown"); + std::string_view function("Sequencer::Sequence::slicecam_shutdown"); std::stringstream message; std::string reply; long error=NO_ERROR; @@ -1487,7 +1498,7 @@ namespace Sequencer { * */ long Sequence::acam_shutdown() { - const std::string function("Sequencer::Sequence::acam_shutdown"); + std::string_view function("Sequencer::Sequence::acam_shutdown"); std::stringstream message; std::string reply; long error=NO_ERROR; @@ -1559,7 +1570,7 @@ namespace Sequencer { * */ long Sequence::calib_init() { - const std::string function("Sequencer::Sequence::calib_init"); + std::string_view function("Sequencer::Sequence::calib_init"); this->daemon_manager.clear( Sequencer::DAEMON_CALIB ); @@ -1633,7 +1644,7 @@ namespace Sequencer { * */ long Sequence::calib_shutdown() { - const std::string function("Sequencer::Sequence::calib_shutdown"); + std::string_view function("Sequencer::Sequence::calib_shutdown"); long error=NO_ERROR; ScopedState thr_state( this->thread_state_manager, Sequencer::THR_CALIB_SHUTDOWN ); @@ -1755,7 +1766,7 @@ namespace Sequencer { * */ long Sequence::tcs_shutdown() { - const std::string function("Sequencer::Sequence::tcs_shutdown"); + std::string_view function("Sequencer::Sequence::tcs_shutdown"); std::stringstream message; ScopedState thr_state( this->thread_state_manager, Sequencer::THR_TCS_SHUTDOWN ); @@ -1797,7 +1808,7 @@ namespace Sequencer { * */ long Sequence::flexure_init() { - const std::string function("Sequencer::Sequence::flexure_init"); + std::string_view function("Sequencer::Sequence::flexure_init"); ScopedState thr_state( thread_state_manager, Sequencer::THR_FLEXURE_INIT ); ScopedState wait_state( wait_state_manager, Sequencer::SEQ_WAIT_FLEXURE ); @@ -1836,7 +1847,7 @@ namespace Sequencer { * */ long Sequence::flexure_shutdown() { - const std::string function("Sequencer::Sequence::flexure_shutdown"); + std::string_view function("Sequencer::Sequence::flexure_shutdown"); std::string reply; long error=NO_ERROR; @@ -1897,7 +1908,7 @@ namespace Sequencer { * */ long Sequence::focus_init() { - const std::string function("Sequencer::Sequence::focus_init"); + std::string_view function("Sequencer::Sequence::focus_init"); ScopedState thr_state( thread_state_manager, Sequencer::THR_FOCUS_INIT ); ScopedState wait_state( wait_state_manager, Sequencer::SEQ_WAIT_FOCUS ); @@ -1965,7 +1976,7 @@ namespace Sequencer { * */ long Sequence::focus_shutdown() { - const std::string function("Sequencer::Sequence::focus_shutdown"); + std::string_view function("Sequencer::Sequence::focus_shutdown"); std::string reply; long error=NO_ERROR; @@ -2026,7 +2037,7 @@ namespace Sequencer { * */ long Sequence::camera_init() { - const std::string function("Sequencer::Sequence::camera_init"); + std::string_view function("Sequencer::Sequence::camera_init"); ScopedState thr_state( thread_state_manager, Sequencer::THR_CAMERA_INIT ); ScopedState wait_state( wait_state_manager, Sequencer::SEQ_WAIT_CAMERA ); @@ -2080,7 +2091,7 @@ namespace Sequencer { * */ long Sequence::camera_shutdown() { - const std::string function("Sequencer::Sequence::camera_shutdown"); + std::string_view function("Sequencer::Sequence::camera_shutdown"); ScopedState thr_state( this->thread_state_manager, Sequencer::THR_CAMERA_SHUTDOWN ); ScopedState wait_state( this->wait_state_manager, Sequencer::SEQ_WAIT_CAMERA ); @@ -2145,7 +2156,7 @@ namespace Sequencer { * */ long Sequence::move_to_target() { - const std::string function("Sequencer::Sequence::move_to_target"); + std::string_view function("Sequencer::Sequence::move_to_target"); std::stringstream message; long error=NO_ERROR; @@ -2299,7 +2310,7 @@ namespace Sequencer { * */ void Sequence::dothread_notify_tcs( Sequencer::Sequence &seq ) { - const std::string function("Sequencer::Sequence::dothread_notify_tcs"); + std::string_view function("Sequencer::Sequence::dothread_notify_tcs"); std::stringstream message; ScopedState thr_state( seq.thread_state_manager, Sequencer::THR_NOTIFY_TCS ); @@ -2371,7 +2382,7 @@ namespace Sequencer { * */ long Sequence::focus_set() { - const std::string function("Sequencer::Sequence::focus_set"); + std::string_view function("Sequencer::Sequence::focus_set"); ScopedState thr_state( thread_state_manager, Sequencer::THR_FOCUS_SET ); @@ -2390,7 +2401,7 @@ namespace Sequencer { * */ long Sequence::flexure_set() { - const std::string function("Sequencer::Sequence::flexure_set"); + std::string_view function("Sequencer::Sequence::flexure_set"); ScopedState thr_state( thread_state_manager, Sequencer::THR_FLEXURE_SET ); @@ -2409,7 +2420,7 @@ namespace Sequencer { * */ long Sequence::calib_set() { - const std::string function("Sequencer::Sequence::calib_set"); + std::string_view function("Sequencer::Sequence::calib_set"); std::stringstream message; ScopedState thr_state( thread_state_manager, Sequencer::THR_CALIBRATOR_SET ); @@ -2494,7 +2505,7 @@ namespace Sequencer { * */ void Sequence::abort_process() { - const std::string function("Sequencer::Sequence::abort_process"); + std::string_view function("Sequencer::Sequence::abort_process"); ScopedState thr_state( this->thread_state_manager, Sequencer::THR_ABORT_PROCESS ); @@ -2529,7 +2540,7 @@ namespace Sequencer { * */ void Sequence::stop_exposure() { - const std::string function("Sequencer::Sequence::stop_exposure"); + std::string_view function("Sequencer::Sequence::stop_exposure"); ScopedState thr_state( this->thread_state_manager, Sequencer::THR_STOP_EXPOSURE ); @@ -2577,7 +2588,7 @@ namespace Sequencer { * */ long Sequence::repeat_exposure() { - const std::string function("Sequencer::Sequence::repeat_exposure"); + std::string_view function("Sequencer::Sequence::repeat_exposure"); std::stringstream message; long error = NO_ERROR; @@ -2667,7 +2678,7 @@ namespace Sequencer { * */ long Sequence::trigger_exposure() { - const std::string function("Sequencer::Sequence::trigger_exposure"); + std::string_view function("Sequencer::Sequence::trigger_exposure"); std::stringstream message; std::string reply; long error=NO_ERROR; @@ -2721,7 +2732,7 @@ namespace Sequencer { * */ void Sequence::modify_exptime( double exptime_in ) { - const std::string function("Sequencer::Sequence::modify_exptime"); + std::string_view function("Sequencer::Sequence::modify_exptime"); std::stringstream message; std::string reply=""; long error = NO_ERROR; @@ -2775,7 +2786,7 @@ namespace Sequencer { * */ long Sequence::startup() { - const std::string function("Sequencer::Sequence::startup"); + std::string_view function("Sequencer::Sequence::startup"); std::stringstream message; long error=NO_ERROR; @@ -2984,7 +2995,7 @@ namespace Sequencer { * */ long Sequence::shutdown() { - const std::string function("Sequencer::Sequence::shutdown"); + std::string_view function("Sequencer::Sequence::shutdown"); long error=ERROR; ScopedState thr_state( this->thread_state_manager, Sequencer::THR_SHUTDOWN ); // this thread is running @@ -3075,8 +3086,8 @@ namespace Sequencer { * @return ERROR or NO_ERROR * */ - long Sequence::parse_state( std::string whoami, std::string reply, bool &state ) { - const std::string function("Sequencer::Sequence::parse_state"); + long Sequence::parse_state( std::string_view whoami, std::string reply, bool &state ) { + std::string_view function("Sequencer::Sequence::parse_state"); std::stringstream message; // Tokenize the reply -- @@ -3133,7 +3144,7 @@ namespace Sequencer { * */ long Sequence::extract_tcs_value( std::string reply, int &value ) { - const std::string function("Sequencer::Sequence::extract_tcs_value"); + std::string_view function("Sequencer::Sequence::extract_tcs_value"); std::stringstream message; std::vector tokens; long error = ERROR; @@ -3221,7 +3232,7 @@ namespace Sequencer { * */ long Sequence::parse_tcs_generic( int value ) { - const std::string function("Sequencer::Sequence::parse_tcs_generic"); + std::string_view function("Sequencer::Sequence::parse_tcs_generic"); std::stringstream message; std::string tcsreply; std::vector tokens; @@ -3270,7 +3281,7 @@ namespace Sequencer { * */ long Sequence::dotype( std::string args ) { - const std::string function("Sequencer::Sequence::dotype"); + std::string_view function("Sequencer::Sequence::dotype"); std::stringstream message; std::string dontcare; return this->dotype( args, dontcare ); @@ -3294,7 +3305,7 @@ namespace Sequencer { * */ long Sequence::dotype( std::string args, std::string &retstring ) { - const std::string function("Sequencer::Sequence::dotype"); + std::string_view function("Sequencer::Sequence::dotype"); std::stringstream message; long error = NO_ERROR; @@ -3341,7 +3352,7 @@ namespace Sequencer { return this->get_dome_position( false, domeazi, telazi ); } long Sequence::get_dome_position( bool poll, double &domeazi, double &telazi ) { - const std::string function("Sequencer::Sequence::get_dome_position"); + std::string_view function("Sequencer::Sequence::get_dome_position"); std::stringstream message; std::string tcsreply; @@ -3403,7 +3414,7 @@ namespace Sequencer { return this->get_tcs_motion( false, state_out ); } long Sequence::get_tcs_motion( bool poll, std::string &state_out ) { - const std::string function("Sequencer::Sequence::get_tcs_motion"); + std::string_view function("Sequencer::Sequence::get_tcs_motion"); std::stringstream message; std::string tcsreply; @@ -3445,7 +3456,7 @@ namespace Sequencer { return this->get_tcs_coords_type( TCSD_WEATHER_COORDS, ra_h, dec_d ); } long Sequence::get_tcs_coords_type( std::string cmd, double &ra_h, double &dec_d ) { - const std::string function("Sequencer::Sequence::get_tcs_coords"); + std::string_view function("Sequencer::Sequence::get_tcs_coords"); std::stringstream message; std::string coordstring; @@ -3498,7 +3509,7 @@ namespace Sequencer { * */ long Sequence::get_tcs_cass( double &cass ) { - const std::string function("Sequencer::Sequencer::get_tcs_cass"); + std::string_view function("Sequencer::Sequencer::get_tcs_cass"); std::stringstream message; std::string tcsreply; @@ -3549,11 +3560,14 @@ namespace Sequencer { * */ long Sequence::target_offset() { - const std::string function("Sequencer::Sequence::target_offset"); + std::string_view function("Sequencer::Sequence::target_offset"); + + bool is_ra_zero = std::abs(this->target.offset_ra) < std::numeric_limits::epsilon(); + bool is_dec_zero = std::abs(this->target.offset_dec) < std::numeric_limits::epsilon(); - // nothing to do if both ra and dec offsets are zero - if (this->target.offset_ra == 0.0 && - this->target.offset_dec == 0.0) return NO_ERROR; + // nothing to do for calibrator or if both ra and dec offsets are zero + if ( this->target.iscal || + (is_ra_zero && is_dec_zero) ) return NO_ERROR; // zero TCS offsets before applying target offset long error = this->tcsd.command( TCSD_ZERO_OFFSETS ); @@ -3661,7 +3675,7 @@ namespace Sequencer { * */ long Sequence::handle_json_message( const std::string message_in ) { - const std::string function("Sequencer::Sequence::handle_json_message"); + std::string_view function("Sequencer::Sequence::handle_json_message"); std::stringstream message; if ( message_in.empty() ) { @@ -3744,7 +3758,7 @@ namespace Sequencer { * */ void Sequence::dothread_test_fpoffset() { - const std::string function("Sequencer::Sequence::dothread_fpoffset"); + std::string_view function("Sequencer::Sequence::dothread_fpoffset"); std::stringstream message; message.str(""); message << "calling fpoffsets.compute_offset() from thread: PyGILState=" << PyGILState_Check(); @@ -3779,7 +3793,7 @@ namespace Sequencer { } long Sequence::set_power_switch( PowerState reqstate, const std::string which, std::chrono::seconds delay ) { - const std::string function("Sequencer::Sequence::set_power_switch"); + std::string_view function("Sequencer::Sequence::set_power_switch"); long error=NO_ERROR; bool need_delay=false; @@ -3885,7 +3899,7 @@ namespace Sequencer { long Sequence::open_hardware( Common::DaemonClient &daemon, const std::string opencmd, const int opentimeout, bool &was_opened, bool forceopen ) { - const std::string function("Sequencer::Sequence::open_hardware"); + std::string_view function("Sequencer::Sequence::open_hardware"); const int maxattempts=3; ///< allow retries connecting to daemon bool isopen=false; std::string reply; @@ -3945,7 +3959,7 @@ namespace Sequencer { * */ long Sequence::connect_to_daemon( Common::DaemonClient &daemon ) { - const std::string function("Sequencer::Sequence::connect_to_daemon"); + std::string_view function("Sequencer::Sequence::connect_to_daemon"); // if not connected to the daemon then connect // @@ -3973,7 +3987,7 @@ namespace Sequencer { * */ long Sequence::daemon_restart(Common::DaemonClient &daemon) { - const std::string function("Sequencer::Sequence::daemon_restart"); + std::string_view function("Sequencer::Sequence::daemon_restart"); std::string command; // the daemon control script must have been specified in the config file @@ -4028,7 +4042,7 @@ namespace Sequencer { * */ long Sequence::test( std::string args, std::string &retstring ) { - const std::string function("Sequencer::Sequence::test"); + std::string_view function("Sequencer::Sequence::test"); std::stringstream message; std::vector tokens; long error = NO_ERROR; diff --git a/sequencerd/sequence.h b/sequencerd/sequence.h index c95b8dd3..92b2baa6 100644 --- a/sequencerd/sequence.h +++ b/sequencerd/sequence.h @@ -312,7 +312,7 @@ namespace Sequencer { /** @brief safely runs function in a detached thread using lambda to catch exceptions */ - void safe_thread(long (Sequence::*method)(), const std::string &function) { + void safe_thread(long (Sequence::*method)(), std::string_view function) { std::thread([this, method, function]() { try { (this->*method)(); @@ -476,12 +476,12 @@ namespace Sequencer { float slitwidthacquire; ///< "virtual slit mode" width for acquire // new stuff - long run(const Operation &op, const std::string &function); - long run_sequence(const std::vector &ops, const std::string &function); - long run_parallel(const std::vector &ops, const std::string &function); - long run_default_sequence(const std::string &caller); + long run(const Operation &op, std::string_view function); + long run_sequence(const std::vector &ops, std::string_view function); + long run_parallel(const std::vector &ops, std::string_view function); + long run_default_sequence(std::string_view caller); long run_operation_blocks( const std::vector &blocks, - const std::string &caller, + std::string_view caller, bool continue_on_error=false ); // publish/subscribe functions @@ -543,7 +543,7 @@ namespace Sequencer { bool is_ready() { return this->ready_to_start; } ///< returns the ready_to_start state, set true only after nightly startup long parse_calibration_target(); - long parse_state( std::string whoami, std::string reply, bool &state ); ///< parse true|false state from reply string + long parse_state( std::string_view whoami, std::string reply, bool &state ); ///< parse true|false state from reply string void dothread_test_fpoffset(); ///< for testing, calls Python function from thread long test( std::string args, std::string &retstring ); ///< handles test commands long extract_tcs_value( std::string reply, int &value ); ///< extract value returned by the TCS via tcsd @@ -588,9 +588,12 @@ namespace Sequencer { void modify_exptime( double exptime_in ); ///< modify exptime while exposure running void dothread_test(); - long wait_for_user(const std::string &function); ///< wait for the user or cancel - long wait_for_exposure(const std::string &function); ///< wait for exposure completion or cancel - long wait_for_readout(const std::string &function); ///< wait for readout completion or cancel + long wait_for_ontarget(std::string_view caller); ///< wait for TCS Operator + long wait_for_user(std::string_view caller); ///< wait for the user or cancel + long wait_for_exposure(std::string_view caller); ///< wait for exposure completion or cancel + long wait_for_readout(std::string_view caller); ///< wait for readout completion or cancel + long wait_for_canexpose(std::string_view caller); ///< wait for camera can_expose + void sequence_start(std::string obsid_in); ///< main sequence start thread. optional obsid_in for single target obs long calib_set(); ///< sets calib according to target entry params long camera_set(); ///< sets camera according to target entry params @@ -605,6 +608,8 @@ namespace Sequencer { */ long do_acam_acquire(); long do_slicecam_fineacquire(); + long do_target_acquisition(std::string_view caller); + long do_target_virtualslit(VirtualSlitMode mode); long acam_init(); ///< initializes connection to acamd diff --git a/sequencerd/sequence_acquisition.cpp b/sequencerd/sequence_acquisition.cpp index 7beab093..695c0423 100644 --- a/sequencerd/sequence_acquisition.cpp +++ b/sequencerd/sequence_acquisition.cpp @@ -16,7 +16,7 @@ namespace Sequencer { * */ long Sequence::do_acam_acquire() { - const std::string function("Sequencer::Sequence::do_acam_acquire"); + std::string_view function("Sequencer::Sequence::do_acam_acquire"); std::string reply; ScopedState thr_state( thread_state_manager, Sequencer::THR_ACQUISITION ); @@ -75,7 +75,7 @@ namespace Sequencer { * */ long Sequence::do_slicecam_fineacquire() { - const std::string function("Sequencer::Sequence::do_slicecam_fineacquire"); + std::string_view function("Sequencer::Sequence::do_slicecam_fineacquire"); ScopedState wait_state(wait_state_manager, Sequencer::SEQ_WAIT_ACQUIRE); @@ -117,4 +117,50 @@ namespace Sequencer { } /***** Sequencer::Sequence::do_slicecam_fineacquire **************************/ + + /***** Sequencer::Sequence::do_target_acquisition ****************************/ + /** + * @brief performs target acquisition + * @details First acquire on ACAM, then run slicecam fineacquire + * @return NO_ERROR | ABORT + * + */ + long Sequence::do_target_acquisition(std::string_view caller) { + + if (this->target.iscal) return NO_ERROR; + + // ---------- ACAM acquire ----------------------------- + // + if ( this->do_acam_acquire() != NO_ERROR ) { + this->async.enqueue_and_log(caller, "WARNING acam acquisition failed"); + + // on ACAM acquisition failure wait for user to continue or cancel + if ( this->wait_for_user(caller) == ABORT ) return ABORT; + + return NO_ERROR; // user chose to continue + } + + // ---------- SLICECAM fineacquire --------------------- + // + if ( this->do_slicecam_fineacquire() != NO_ERROR ) { + this->async.enqueue_and_log(caller, "WARNING slicecam fine acquisition failed"); + } + + return NO_ERROR; + } + /***** Sequencer::Sequence::do_target_acquisition ****************************/ + + + /***** Sequencer::Sequence::do_target_virtualslit ****************************/ + /** + * @brief move to virtual slit position + * @param[in] mode VirtualSlitMode + * @return NO_ERROR | NO_ERROR + * + */ + long Sequence::do_target_virtualslit(VirtualSlitMode mode) { + if (this->target.iscal) return NO_ERROR; + return this->slit_set(mode); + } + /***** Sequencer::Sequence::do_target_virtualslit ****************************/ } diff --git a/sequencerd/sequencer_server.cpp b/sequencerd/sequencer_server.cpp index 06514cef..75a0f436 100644 --- a/sequencerd/sequencer_server.cpp +++ b/sequencerd/sequencer_server.cpp @@ -1043,7 +1043,7 @@ namespace Sequencer { } while (1) { - std::string message = seq.sequence.async.dequeue(); // get the latest message from the queue (blocks) + auto message = seq.sequence.async.dequeue(); // get the latest message from the queue (blocks) retval = sock.Send(message); // transmit the message if (retval < 0) { std::stringstream errstm; diff --git a/utils/logentry.cpp b/utils/logentry.cpp index 0b95650e..6ab2ba79 100644 --- a/utils/logentry.cpp +++ b/utils/logentry.cpp @@ -175,13 +175,12 @@ void close_log() { * log filestream isn't open. * */ -void logwrite( const std::string &function, std::string message ) { - std::stringstream logmsg; - std::string timestamp = get_timestamp(); // get the current time (defined in utilities.h) +void logwrite( std::string_view function, std::string_view message ) { + std::ostringstream logmsg; std::lock_guard lock(loglock); // lock mutex to protect from multiple access - logmsg << timestamp << " (" << function << ") " << message << "\n"; + logmsg << get_timestamp() << " (" << function << ") " << message << "\n"; if (filestream.is_open()) { filestream << logmsg.str(); // send to the file stream (if open) diff --git a/utils/logentry.h b/utils/logentry.h index ddf95890..f296bcbf 100644 --- a/utils/logentry.h +++ b/utils/logentry.h @@ -21,6 +21,6 @@ extern unsigned int nextday; /// number of seconds long init_log( std::string logpath, std::string name ); /// initialize the logging system long init_log( std::string logpath, std::string name, bool stderr_in ); /// initialize the logging system void close_log(); /// close the log file stream -void logwrite(const std::string &function, std::string message); /// create a time-stamped log entry "message" from "function" +void logwrite(std::string_view function, std::string_view message); /// create a time-stamped log entry "message" from "function" #endif diff --git a/utils/network.cpp b/utils/network.cpp index 5b6a2402..caf3d36e 100644 --- a/utils/network.cpp +++ b/utils/network.cpp @@ -174,14 +174,14 @@ namespace Network { * @return 0 on success, -1 on error * */ - int UdpSocket::Send(std::string message) { - std::string function = "Network::UdpSocket::Send"; - std::stringstream errstm; + int UdpSocket::Send(std::string_view message) { + std::string_view function = "Network::UdpSocket::Send"; + std::ostringstream errstm; ssize_t nbytes; if ( !this->is_running() ) return 0; // silently do nothing if the UDP multicast socket isn't running - if ( ( nbytes = sendto( this->fd, message.c_str(), (size_t)message.length(), 0, + if ( ( nbytes = sendto( this->fd, std::string(message).c_str(), (size_t)message.length(), 0, (struct sockaddr*) &this->addr, (socklen_t)sizeof(this->addr) ) ) < 0 ) { errstm << "error " << errno << " calling sendto: " << strerror(errno); logwrite(function, errstm.str()); diff --git a/utils/network.h b/utils/network.h index f2b5637a..0fea2000 100644 --- a/utils/network.h +++ b/utils/network.h @@ -168,7 +168,7 @@ namespace Network { std::string getgroup() { return this->group; }; ///< use to get group int Create(); ///< create a UDP multi-cast socket - int Send(std::string message); ///< transmit the message to the UDP socket + int Send(std::string_view message); ///< transmit the message to the UDP socket int Close(); ///< close the UDP socket connection int Listener(); ///< creates a UDP listener, returns a file descriptor ssize_t Receive( std::string &message ); ///< receive a UDP message from the Listener fd From 5095456963004b4733dee0afcfcfb07061ed1338 Mon Sep 17 00:00:00 2001 From: David Hale Date: Mon, 6 Apr 2026 10:07:54 -0700 Subject: [PATCH 5/7] adds signatures for scripting functions moves wait_for_XXXX wrappers to a separate file moves sequence building tools to a separate file --- common/sequencerd_commands.h | 4 + sequencerd/CMakeLists.txt | 2 + sequencerd/sequence.cpp | 378 ++++++++++---------------------- sequencerd/sequence.h | 70 +++++- sequencerd/sequence_builder.cpp | 65 ++++++ sequencerd/sequence_wait.cpp | 168 ++++++++++++++ sequencerd/sequencer_server.cpp | 16 ++ 7 files changed, 432 insertions(+), 271 deletions(-) create mode 100644 sequencerd/sequence_builder.cpp create mode 100644 sequencerd/sequence_wait.cpp diff --git a/common/sequencerd_commands.h b/common/sequencerd_commands.h index cf38c57d..adce01ee 100644 --- a/common/sequencerd_commands.h +++ b/common/sequencerd_commands.h @@ -16,9 +16,11 @@ const std::string SEQUENCERD_GUIDE = "guide"; const std::string SEQUENCERD_MODEXPTIME = "modexptime"; const std::string SEQUENCERD_ONTARGET = "ontarget"; const std::string SEQUENCERD_USERCONTINUE = "usercontinue"; +const std::string SEQUENCERD_OP = "op"; const std::string SEQUENCERD_PAUSE = "pause"; const std::string SEQUENCERD_REPEAT = "repeat"; const std::string SEQUENCERD_RESUME = "resume"; +const std::string SEQUENCERD_SCRIPT = "script"; const std::string SEQUENCERD_SHUTDOWN = "shutdown"; const std::string SEQUENCERD_START = "start"; const std::string SEQUENCERD_STARTONE = "startone"; @@ -54,11 +56,13 @@ const std::vector SEQUENCERD_SYNTAX = { SEQUENCERD_GUIDE, SEQUENCERD_MODEXPTIME+" ", SEQUENCERD_ONTARGET, + SEQUENCERD_OP, SEQUENCERD_PAUSE, SEQUENCERD_REPEAT, SEQUENCERD_RESUME, TELEMREQUEST+" [?]", SEQUENCERD_USERCONTINUE, + SEQUENCERD_SCRIPT, SEQUENCERD_SHUTDOWN, SEQUENCERD_START, SEQUENCERD_STARTONE, diff --git a/sequencerd/CMakeLists.txt b/sequencerd/CMakeLists.txt index dda2f133..b5804c76 100644 --- a/sequencerd/CMakeLists.txt +++ b/sequencerd/CMakeLists.txt @@ -38,6 +38,8 @@ add_executable(sequencerd ${SEQUENCER_DIR}/sequencer_server.cpp ${SEQUENCER_DIR}/sequencer_interface.cpp ${SEQUENCER_DIR}/sequence_acquisition.cpp + ${SEQUENCER_DIR}/sequence_wait.cpp + ${SEQUENCER_DIR}/sequence_builder.cpp ${SEQUENCER_DIR}/sequence.cpp ${MYSQL_INCLUDES} ${PYTHON_DEV} diff --git a/sequencerd/sequence.cpp b/sequencerd/sequence.cpp index 3c2a7641..598092ac 100644 --- a/sequencerd/sequence.cpp +++ b/sequencerd/sequence.cpp @@ -47,42 +47,6 @@ namespace Sequencer { /***** Sequencer::Sequence::run ********************************************/ - /***** Sequencer::Sequence::run_sequence ***********************************/ - /** - * @brief executes operations in sequence, one at a time - * @param[in] op vector of Operations to execute - * @param[in] caller name of calling function for logging - * @return ERROR|NO_ERROR|ABORT - * - */ - long Sequence::run_sequence( const std::vector &ops, - std::string_view caller ) { - - for (const auto &op : ops) { - - if (this->cancel_flag.load()) return ABORT; - - logwrite(caller, "starting "+op.name); - - try { - long error; - if ( (error = op.func()) != NO_ERROR ) { - std::ostringstream oss; - oss << (error==ABORT ? "cancelled" : "ERROR") << " in " << op.name; - logwrite(caller, oss.str()); - return error; - } - } - catch (const std::exception &e) { - logwrite(caller, "ERROR in "+op.name+": "+std::string(e.what())); - return ERROR; - } - } - return NO_ERROR; - } - /***** Sequencer::Sequence::run_sequence ***********************************/ - - /***** Sequencer::Sequence::run_parallel ***********************************/ /** * @brief executes operations in parallel threads @@ -123,40 +87,40 @@ namespace Sequencer { /***** Sequencer::Sequence::run_parallel ***********************************/ - /***** Sequencer::Sequence::run_operation_blocks ***************************/ + /***** Sequencer::Sequence::run_sequence ***********************************/ /** - * @brief executes operation blocks - * @details An operation block contains a vector of operations paired - * with a type, SERIAL|PARALLEL which specifies how that block - * is to be executed. This executes a vector of Operation Blocks. - * The optional continue_on_error allows a block to continue, or - * stop immediately when any operation within the block fails. - * @param[in] blocks vector of OperationBlocks + * @brief executes a sequence, a collection of operation groups + * @details An operation group contains a vector of operations paired + * with a type, SERIAL|PARALLEL which specifies how that group + * is to be executed. This executes a vector of Operation Groups. + * The optional continue_on_error allows a group to continue, or + * stop immediately when any operation within the group fails. + * @param[in] groups vector of OperationGroups * @param[in] caller name of calling function for logging - * @param[in] continue_on_error continue or stop block execution on error + * @param[in] continue_on_error continue or stop group execution on error * @return ERROR|NO_ERROR|ABORT * */ - long Sequence::run_operation_blocks( const std::vector &blocks, - std::string_view caller, - bool continue_on_error ) { + long Sequence::run_sequence( const std::vector &groups, + std::string_view caller, + bool continue_on_error ) { long error = NO_ERROR; - for (const auto &block : blocks) { + for (const auto &group : groups) { if (this->cancel_flag.load()) return ABORT; - // PARALLEL Blocks are executed in parallel threads + // PARALLEL Groups are executed in parallel threads // - if (block.type == OperationType::PARALLEL) { - long ret = run_parallel(block.operations, caller); + if (group.type == OperationType::PARALLEL) { + long ret = run_parallel(group.operations, caller); error |= ret; if (ret != NO_ERROR && !continue_on_error) return error; } - // SERIAL Blocks are executed one at a time + // SERIAL Groups are executed one at a time // else { - for (const auto &op : block.operations) { + for (const auto &op : group.operations) { if (this->cancel_flag.load()) return ABORT; long ret = run(op, caller); @@ -169,7 +133,7 @@ namespace Sequencer { return error; } - /***** Sequencer::Sequence::run_operation_blocks ***************************/ + /***** Sequencer::Sequence::run_sequence ***********************************/ /***** Sequencer::Sequence::run_default_sequence ***************************/ @@ -181,17 +145,18 @@ namespace Sequencer { */ long Sequence::run_default_sequence(std::string_view caller) { - std::vector blocks; + std::vector groups; // ---------- RUN THESE IN PARALLEL -------------------- // If pointmode is ACAM then the user has chosen to put the star on ACAM, in // which case the assumption is made that nothing else matters. This special - // mode of operation only points the telescope. + // mode of operation only points the telescope so this is the only operation + // added to the sequence. // if (this->target.pointmode == Acam::POINTMODE_ACAM) { this->dotype("ONE"); - blocks.push_back( { OperationType::PARALLEL, + groups.push_back( { OperationType::PARALLEL, { { "move_to_target", THR_MOVE_TO_TARGET, [this]{ return move_to_target(); } } } } ); } else { @@ -199,7 +164,7 @@ namespace Sequencer { // these are the default operations prior to exposure, // they can be done in parallel - blocks.push_back( { OperationType::PARALLEL, + groups.push_back( { OperationType::PARALLEL, { { "move_to_target", THR_MOVE_TO_TARGET, [this]{ return move_to_target(); } }, { "camera_set", THR_CAMERA_SET, [this]{ return camera_set(); } }, { "focus_set", THR_FOCUS_SET, [this]{ return focus_set(); } }, @@ -210,56 +175,83 @@ namespace Sequencer { } } ); } - // Early Exit for pointmode=ACAM - // - if (this->target.pointmode == Acam::POINTMODE_ACAM) { - return run_operation_blocks(blocks, caller); - } - // ---------- RUN THESE IN SERIES ---------------------- - blocks.push_back( { OperationType::SERIAL, - { { "target_acquisition", THR_ACQUISITION, - [this,caller]() { return this->do_target_acquisition(caller); } }, + if (this->target.pointmode != Acam::POINTMODE_ACAM) { + groups.push_back( { OperationType::SERIAL, + { { "target_acquisition", THR_ACQUISITION, + [this,caller]() { return this->do_target_acquisition(caller); } }, - { "target_offset", THR_MOVE_TO_TARGET, - [this]() { return this->target_offset(); } }, + { "target_offset", THR_MOVE_TO_TARGET, + [this]() { return this->target_offset(); } }, - { "slit_expose", THR_SLIT_SET, - [this]() { return this->do_target_virtualslit(Sequencer::VSM_EXPOSE); } } - } - } ); + { "slit_expose", THR_SLIT_SET, + [this]() { return this->do_target_virtualslit(Sequencer::VSM_EXPOSE); } }, - // ---------- EXPOSURE --------------------------------- + { "science_exposure", THR_EXPOSURE, + [this,caller]() { return this->do_exposure(caller); } } + } + } ); + } - blocks.push_back( { OperationType::SERIAL, - { { "trigger_exposure", THR_EXPOSURE, [this]() { + // ---------- RUN THE SEQUENCE NOW --------------------- - // set the EXPOSE bit here, outside of the trigger_exposure function, because that - // function only triggers the exposure -- it doesn't block waiting for the exposure. - // - this->wait_state_manager.set( Sequencer::SEQ_WAIT_EXPOSE ); // set EXPOSE bit - return trigger_exposure(); - } }, + return run_sequence(groups, caller); + } + /***** Sequencer::Sequence::run_default_sequence ***************************/ - { "wait_exposure", THR_EXPOSURE, [this,caller]() { - return this->wait_for_exposure(caller); - } }, - { "wait_readout", THR_EXPOSURE, [this,caller]() { - if (!this->is_science_frame_transfer) { - return this->wait_for_readout(caller); - } - else return NO_ERROR; - } } - } - } ); + /***** Sequencer::Sequence::run_script *************************************/ + /** + * @brief executes a user script + * @param[in] filename filename of script + * @return ERROR|NO_ERROR|ABORT + * + */ + long Sequence::run_script(const std::string &filename) { + return NO_ERROR; + } + /***** Sequencer::Sequence::run_script *************************************/ - // ---------- RUN THE SEQUENCE NOW --------------------- - return run_operation_blocks(blocks, caller); + /***** Sequencer::Sequence::parse_script ***********************************/ + /** + * @brief parses a user script + * @param[in] filename filename of script + * @return ERROR|NO_ERROR|ABORT + * + */ + long Sequence::parse_script(const std::string &filename, + std::vector &out) { + return NO_ERROR; } - /***** Sequencer::Sequence::run_default_sequence ***************************/ + /***** Sequencer::Sequence::parse_script ***********************************/ + + + /***** Sequencer::Sequence::validate_sequence ******************************/ + /** + * @brief + * @param[in] + * @return ERROR|NO_ERROR|ABORT + * + */ + long Sequence::validate_sequence(const std::vector &groups) { + return NO_ERROR; + } + /***** Sequencer::Sequence::validate_sequence ******************************/ + + + /***** Sequencer::Sequence::handle_cli_operation ***************************/ + /** + * @brief handle incoming operation request + * @param[in] op the name of an operation + * @return ERROR|NO_ERROR|ABORT + * + */ + long Sequence::handle_cli_operation(const std::string &op) { + return NO_ERROR; + } + /***** Sequencer::Sequence::handle_cli_operation ***************************/ /***** Sequencer::Sequence::handletopic_snapshot ***************************/ @@ -642,168 +634,6 @@ namespace Sequencer { /***** Sequencer::Sequence::dothread_sequencer_async_listener ***************/ - void Sequence::dothread_test() { - logwrite( "Sequencer::Sequence::dothread_test", "here I am" ); - std::string targetstatus; - this->target.get_specified_target( "4430", targetstatus ); - logwrite( "Sequencer::Sequence::dothread_test", targetstatus ); - return; - } - - - /***** Sequencer::Sequence::wait_for_ontarget *******************************/ - /** - * @brief waits for the TCS Operator to click 'ontarget' - * @param[in] caller reference to caller's name for logging - * @return NO_ERROR on continue | ABORT on cancel - * - */ - long Sequence::wait_for_ontarget(std::string_view caller) { - // waiting for TCS Operator input (or cancel) - { - ScopedState wait_state( wait_state_manager, Sequencer::SEQ_WAIT_TCSOP ); - - this->async.enqueue_and_log(caller, "NOTICE: waiting for TCS operator to send 'ontarget' signal"); - - 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->async.enqueue_and_log(caller, "NOTICE: received " - +(this->cancel_flag.load() ? std::string("cancel") - : std::string("ontarget")) - +" signal!" ); - } - this->is_ontarget.store(false); - - return (this->cancel_flag.load() ? ABORT : NO_ERROR); - } - /***** Sequencer::Sequence::wait_for_ontarget *******************************/ - - - /***** Sequencer::Sequence::wait_for_user ***********************************/ - /** - * @brief waits for the user to click a button, or cancel - * @details Use this when you just want to slow things down or get a - * cup of coffee instead of observing. - * @param[in] caller reference to caller's name for logging - * @return NO_ERROR on continue | ABORT on cancel - * - */ - long Sequence::wait_for_user(std::string_view caller) { - { - ScopedState wait_state( wait_state_manager, Sequencer::SEQ_WAIT_USER ); - - this->async.enqueue_and_log( caller, "NOTICE: waiting for USER to send 'continue' signal" ); - - while ( !this->cancel_flag.load() && !this->is_usercontinue.load() ) { - std::unique_lock lock(cv_mutex); - this->cv.wait( lock, [this]() { return( this->is_usercontinue.load() || this->cancel_flag.load() ); } ); - } - - this->async.enqueue_and_log( caller, "NOTICE: received " - +(this->cancel_flag.load() ? std::string("cancel") : std::string("continue")) - +" signal!" ); - } // end scope for wait_state = WAIT_USER - - if ( this->cancel_flag.load() ) { - this->async.enqueue_and_log( caller, "NOTICE: sequence cancelled" ); - return ABORT; - } - - this->is_usercontinue.store(false); - - return NO_ERROR; - } - /***** Sequencer::Sequence::wait_for_user ***********************************/ - - - /***** Sequencer::Sequence::wait_for_exposure *******************************/ - /** - * @brief waits for exposure completion, or cancel - * @param[in] caller reference to caller's name for logging - * @return NO_ERROR on continue | ABORT on cancel - * - */ - long Sequence::wait_for_exposure(std::string_view caller) { - logwrite(caller, "waiting for exposure"); - while (!this->cancel_flag.load() && - wait_state_manager.is_set(Sequencer::SEQ_WAIT_EXPOSE)) { - std::unique_lock lock(cv_mutex); - this->cv.wait( lock, [this]() { return(!wait_state_manager.is_set(SEQ_WAIT_EXPOSE) || - this->cancel_flag.load()); } ); - } - - if (this->cancel_flag.load()) { - this->async.enqueue_and_log(caller, "NOTICE: exposure cancelled"); - return ABORT; - } - - return NO_ERROR; - } - /***** Sequencer::Sequence::wait_for_exposure *******************************/ - - - /***** Sequencer::Sequence::wait_for_readout ********************************/ - /** - * @brief waits for readout completion, or cancel - * @param[in] caller reference to caller's name for logging - * @return NO_ERROR on continue | ABORT on cancel - * - */ - long Sequence::wait_for_readout(std::string_view caller) { - logwrite(caller, "waiting for readout"); - while (!this->cancel_flag.load() && - wait_state_manager.is_set(Sequencer::SEQ_WAIT_READOUT)) { - std::unique_lock lock(cv_mutex); - this->cv.wait( lock, [this]() { return(!wait_state_manager.is_set(SEQ_WAIT_READOUT) || - this->cancel_flag.load()); } ); - } - - if (this->cancel_flag.load()) { - this->async.enqueue_and_log(caller, "NOTICE: wait for readout cancelled"); - return ABORT; - } - - return NO_ERROR; - } - /***** Sequencer::Sequence::wait_for_readout ********************************/ - - - /***** Sequencer::Sequence::wait_for_canexpose ******************************/ - /** - * @brief waits for camera to be ready to expose, or cancel - * @param[in] caller reference to caller's name for logging - * @return NO_ERROR on continue | ABORT on cancel - * - */ - long Sequence::wait_for_canexpose(std::string_view caller) { - logwrite(caller, "waiting for can_expose"); - - while ( !this->cancel_flag.load() && - !this->can_expose.load() ) { - - this->async.enqueue_and_log(caller, "NOTICE: waiting for camera to be ready to expose"); - - std::unique_lock lock(this->camerad_mtx); - this->camerad_cv.wait( lock, [this]() { return( this->can_expose.load() || - this->cancel_flag.load() ); } ); - } - - if (this->cancel_flag.load()) { - this->async.enqueue_and_log(caller, "NOTICE: wait for can_expose cancelled"); - return ABORT; - } - - return NO_ERROR; - } - /***** Sequencer::Sequence::wait_for_canexpose ******************************/ - - /***** Sequencer::Sequence::sequence_start **********************************/ /** * @brief main sequence start thread @@ -840,6 +670,8 @@ namespace Sequencer { return; } + // ---------- SEQUENCER IS RUNNING --------------------- + // ScopedState thr_state( thread_state_manager, Sequencer::THR_SEQUENCE_START ); // this thread is running ScopedState seq_state( seq_state_manager, Sequencer::SEQ_RUNNING, true ); // state = RUNNING (only) seq_state.destruct_set( Sequencer::SEQ_READY ); // set state=READY on exit @@ -1011,8 +843,6 @@ namespace Sequencer { std::stringstream camcmd; long error=NO_ERROR; - // wait until camera is ready to expose - // this->wait_for_canexpose(function); logwrite( function, "setting camera parameters"); @@ -2683,6 +2513,8 @@ namespace Sequencer { std::string reply; long error=NO_ERROR; + this->wait_for_canexpose(function); + ScopedState thr_state( thread_state_manager, Sequencer::THR_TRIGGER_EXPOSURE ); // Check tcs_preauth_time and set notify_tcs_next_target -- @@ -2721,6 +2553,30 @@ namespace Sequencer { /***** Sequencer::Sequence::trigger_exposure ********************************/ + /***** Sequencer::Sequence::do_exposure *************************************/ + /** + * @brief wrapper for performing science exposure + * @details Triggers an exposure and waits for the exposure and readout. + * This blocks until + * @param[in] caller name of calling function + * @return ERROR|NO_ERROR + * + */ + long Sequence::do_exposure(std::string_view caller) { + + this->wait_state_manager.set( Sequencer::SEQ_WAIT_EXPOSE ); + + if ( this->trigger_exposure() != NO_ERROR ) return ERROR; + + if ( this->wait_for_exposure(caller) != NO_ERROR ) return ERROR; + + if ( this->wait_for_readout(caller) != NO_ERROR ) return ERROR; + + return NO_ERROR; + } + /***** Sequencer::Sequence::do_exposure *************************************/ + + /***** Sequencer::Sequence::modify_exptime **********************************/ /** * @brief modify the exposure time while an exposure is running diff --git a/sequencerd/sequence.h b/sequencerd/sequence.h index 92b2baa6..6292cd77 100644 --- a/sequencerd/sequence.h +++ b/sequencerd/sequence.h @@ -193,6 +193,7 @@ namespace Sequencer { THR_STOP_EXPOSURE, THR_ABORT_PROCESS, THR_SEQUENCE_START, + THR_RUN_SCRIPT, THR_MONITOR_READY_STATE, THR_CALIB_SET, THR_CAMERA_SET, @@ -238,6 +239,7 @@ namespace Sequencer { {THR_STOP_EXPOSURE, "stop_exposure"}, {THR_ABORT_PROCESS, "abort_process"}, {THR_SEQUENCE_START, "sequence_start"}, + {THR_RUN_SCRIPT, "run_script"}, {THR_MONITOR_READY_STATE, "monitor_ready_state"}, {THR_CALIB_SET, "calib_set"}, {THR_CAMERA_SET, "camera_set"}, @@ -293,23 +295,62 @@ namespace Sequencer { std::atomic is_fineacquire_locked{false}; ///< is slicecam fine acquisition locked? std::atomic is_acam_guiding{false}; ///< is acam guiding? + /** @brief operation type can be SERIAL or PARALLEL + */ enum class OperationType { PARALLEL, SERIAL }; + /** @brief map of parameter key=value pairs associated with operation + */ + struct OperationParams { + std::unordered_map map; + + bool has(const std::string &key) const { + return map.find(key) != map.end(); + } + + template + T get(const std::string &key, const T &default_val) const { + auto it = map.find(key); + if (it == map.end()) return default_val; + + if constexpr (std::is_same_v) { + return it->second; + } + else { + std::istringstream iss(it->second); + T val; + iss >> val; + return iss.fail() ? default_val : val; + } + } + }; + + /** @brief sequencer operation contains name, status bit, function and params + */ struct Operation { - std::string name; ///< name of this operation - ThreadStatusBits thr; ///< status bit of what is running - std::function func; ///< function that this operation calls - std::map params; ///< function parameters + std::string name; + ThreadStatusBits thr; + std::function func; + OperationParams params; }; - struct OperationBlock { + /** @brief a group of operations stored in a vector with the operation type + */ + struct OperationGroup { OperationType type; std::vector operations; }; + /** @brief associates a sequencer command with its parameters + */ + struct ParsedCommand { + std::string name; + OperationParams params; + }; + /** @brief safely runs function in a detached thread using lambda to catch exceptions */ void safe_thread(long (Sequence::*method)(), std::string_view function) { @@ -475,14 +516,22 @@ namespace Sequencer { float slitoffsetacquire; ///< "virtual slit mode" offset for acquire float slitwidthacquire; ///< "virtual slit mode" width for acquire - // new stuff + // ---------- sequencer scripting and execution tools -------------------- + // long run(const Operation &op, std::string_view function); - long run_sequence(const std::vector &ops, std::string_view function); long run_parallel(const std::vector &ops, std::string_view function); long run_default_sequence(std::string_view caller); - long run_operation_blocks( const std::vector &blocks, - std::string_view caller, - bool continue_on_error=false ); + long run_sequence( const std::vector &groups, + std::string_view caller, + bool continue_on_error=false ); + + long run_script(const std::string &filename); ///< run user script + long parse_script(const std::string &filename, + std::vector &out); ///< parse script into commands/args + long build_sequence(const std::vector &commands, + std::vector &sequence_out); ///< build sequence from parsed commands + long validate_sequence(const std::vector &groups); ///< validate sequence + long handle_cli_operation(const std::string &op); ///< handle incoming operation request // publish/subscribe functions // @@ -582,6 +631,7 @@ namespace Sequencer { // These are various jobs that are done in their own threads // long trigger_exposure(); ///< trigger and wait for exposure + long do_exposure(std::string_view caller); ///< wrapper performs and waits for science exposure void abort_process(); ///< tries to abort everything void stop_exposure(); ///< stop exposure timer in progress long repeat_exposure(); ///< repeat the last exposure diff --git a/sequencerd/sequence_builder.cpp b/sequencerd/sequence_builder.cpp new file mode 100644 index 00000000..4a7489a7 --- /dev/null +++ b/sequencerd/sequence_builder.cpp @@ -0,0 +1,65 @@ +/** + * @file sequence_builder.cpp + * @brief implementation for building sequences from operations + * @author David Hale + * + */ + +#include "sequence.h" + +namespace Sequencer { + + /***** Sequencer::Sequence::build_sequence *********************************/ + /** + * @brief build a sequence from parsed commands + * @param[in] commands vector of ParsedCommands + * @param[out] sequence_out the operation group to execute + * @return ERROR|NO_ERROR|ABORT + * + */ + long Sequence::build_sequence(const std::vector &commands, + std::vector &sequence_out) { + OperationGroup group; + + group.type = OperationType::SERIAL; // default is serial + + for (const auto &command : commands) { + + if (command.name == "begin_parallel") { + if (!group.operations.empty()) sequence_out.push_back(group); + group = { OperationType::PARALLEL, {} }; + continue; + } + else + if (command.name == "end_parallel") { + sequence_out.push_back(group); + group = { OperationType::SERIAL, {} }; // back to default + continue; + } + else + if (command.name == "move_to_target") { + group.operations.emplace_back( Operation { + "move_to_target", THR_MOVE_TO_TARGET, + [this,params=command.params]() { + if (params.has("ra") && params.has("dec")) { + this->target.ra_hms = params.get(std::string("ra"),std::string("")); + this->target.dec_dms = params.get(std::string("dec"),std::string("")); + } + return move_to_target(); + }, + command.params + }); + } + + else { + this->async.enqueue_and_log("Sequencer::Sequence::build_sequence", + "ERROR unknown command '"+command.name+"'"); + } + } + if (!group.operations.empty()) sequence_out.push_back(group); + + return NO_ERROR; + } + /***** Sequencer::Sequence::build_sequence *********************************/ + +} diff --git a/sequencerd/sequence_wait.cpp b/sequencerd/sequence_wait.cpp new file mode 100644 index 00000000..078c1046 --- /dev/null +++ b/sequencerd/sequence_wait.cpp @@ -0,0 +1,168 @@ +/** + * @file sequence_wait.cpp + * @brief wait wrappers used in the Sequence class + * @author David Hale + * + */ + +#include "sequence.h" + +namespace Sequencer { + + /***** Sequencer::Sequence::wait_for_ontarget *******************************/ + /** + * @brief waits for the TCS Operator to click 'ontarget' + * @param[in] caller reference to caller's name for logging + * @return NO_ERROR on continue | ABORT on cancel + * + */ + long Sequence::wait_for_ontarget(std::string_view caller) { + // waiting for TCS Operator input (or cancel) + { + ScopedState wait_state( wait_state_manager, Sequencer::SEQ_WAIT_TCSOP ); + + this->async.enqueue_and_log(caller, "NOTICE: waiting for TCS operator to send 'ontarget' signal"); + + 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->async.enqueue_and_log(caller, "NOTICE: received " + +(this->cancel_flag.load() ? std::string("cancel") + : std::string("ontarget")) + +" signal!" ); + } + this->is_ontarget.store(false); + + return (this->cancel_flag.load() ? ABORT : NO_ERROR); + } + /***** Sequencer::Sequence::wait_for_ontarget *******************************/ + + + /***** Sequencer::Sequence::wait_for_user ***********************************/ + /** + * @brief waits for the user to click a button, or cancel + * @details Use this when you just want to slow things down or get a + * cup of coffee instead of observing. + * @param[in] caller reference to caller's name for logging + * @return NO_ERROR on continue | ABORT on cancel + * + */ + long Sequence::wait_for_user(std::string_view caller) { + { + ScopedState wait_state( wait_state_manager, Sequencer::SEQ_WAIT_USER ); + + this->async.enqueue_and_log( caller, "NOTICE: waiting for USER to send 'continue' signal" ); + + while ( !this->cancel_flag.load() && !this->is_usercontinue.load() ) { + std::unique_lock lock(cv_mutex); + this->cv.wait( lock, [this]() { return( this->is_usercontinue.load() || this->cancel_flag.load() ); } ); + } + + this->async.enqueue_and_log( caller, "NOTICE: received " + +(this->cancel_flag.load() ? std::string("cancel") : std::string("continue")) + +" signal!" ); + } // end scope for wait_state = WAIT_USER + + if ( this->cancel_flag.load() ) { + this->async.enqueue_and_log( caller, "NOTICE: sequence cancelled" ); + return ABORT; + } + + this->is_usercontinue.store(false); + + return NO_ERROR; + } + /***** Sequencer::Sequence::wait_for_user ***********************************/ + + + /***** Sequencer::Sequence::wait_for_exposure *******************************/ + /** + * @brief waits for exposure completion, or cancel + * @param[in] caller reference to caller's name for logging + * @return NO_ERROR on continue | ABORT on cancel + * + */ + long Sequence::wait_for_exposure(std::string_view caller) { + logwrite(caller, "waiting for exposure"); + while (!this->cancel_flag.load() && + wait_state_manager.is_set(Sequencer::SEQ_WAIT_EXPOSE)) { + std::unique_lock lock(cv_mutex); + this->cv.wait( lock, [this]() { return(!wait_state_manager.is_set(SEQ_WAIT_EXPOSE) || + this->cancel_flag.load()); } ); + } + + if (this->cancel_flag.load()) { + this->async.enqueue_and_log(caller, "NOTICE: exposure cancelled"); + return ABORT; + } + + return NO_ERROR; + } + /***** Sequencer::Sequence::wait_for_exposure *******************************/ + + + /***** Sequencer::Sequence::wait_for_readout ********************************/ + /** + * @brief waits for readout completion, or cancel + * @param[in] caller reference to caller's name for logging + * @return NO_ERROR on continue | ABORT on cancel + * + */ + long Sequence::wait_for_readout(std::string_view caller) { + + // don't have to wait for readout when using frame transfer + if ( this->is_science_frame_transfer ) return NO_ERROR; + + logwrite(caller, "waiting for readout"); + + while (!this->cancel_flag.load() && + wait_state_manager.is_set(Sequencer::SEQ_WAIT_READOUT)) { + std::unique_lock lock(cv_mutex); + this->cv.wait( lock, [this]() { return(!wait_state_manager.is_set(SEQ_WAIT_READOUT) || + this->cancel_flag.load()); } ); + } + + if (this->cancel_flag.load()) { + this->async.enqueue_and_log(caller, "NOTICE: wait for readout cancelled"); + return ABORT; + } + + return NO_ERROR; + } + /***** Sequencer::Sequence::wait_for_readout ********************************/ + + + /***** Sequencer::Sequence::wait_for_canexpose ******************************/ + /** + * @brief waits for camera to be ready to expose, or cancel + * @param[in] caller reference to caller's name for logging + * @return NO_ERROR on continue | ABORT on cancel + * + */ + long Sequence::wait_for_canexpose(std::string_view caller) { + + this->async.enqueue_and_log(caller, "NOTICE: waiting for camera to be ready to expose"); + + while ( !this->cancel_flag.load() && + !this->can_expose.load() ) { + + std::unique_lock lock(this->camerad_mtx); + this->camerad_cv.wait( lock, [this]() { return( this->can_expose.load() || + this->cancel_flag.load() ); } ); + } + + if (this->cancel_flag.load()) { + this->async.enqueue_and_log(caller, "NOTICE: wait for can_expose cancelled"); + return ABORT; + } + + return NO_ERROR; + } + /***** Sequencer::Sequence::wait_for_canexpose ******************************/ + +} diff --git a/sequencerd/sequencer_server.cpp b/sequencerd/sequencer_server.cpp index 75a0f436..e0ed8961 100644 --- a/sequencerd/sequencer_server.cpp +++ b/sequencerd/sequencer_server.cpp @@ -1376,6 +1376,22 @@ namespace Sequencer { } else + // handle incoming CLI operation request + // + if ( cmd == SEQUENCERD_OP ) { + std::thread( &Sequencer::Sequence::handle_cli_operation, std::ref(this->sequence), args ).detach(); + ret = NO_ERROR; + } + else + + // run sequencer script + // + if ( cmd == SEQUENCERD_SCRIPT ) { + std::thread( &Sequencer::Sequence::run_script, std::ref(this->sequence), args ).detach(); + ret = NO_ERROR; + } + else + // Sequence "start" // if ( cmd == SEQUENCERD_START ) { From ae7e8dfe420579ae3cf4b9f092ca2b76ddb3a13f Mon Sep 17 00:00:00 2001 From: David Hale Date: Mon, 6 Apr 2026 14:41:04 -0700 Subject: [PATCH 6/7] implements the functions for script parsing and execution --- sequencerd/sequence.cpp | 165 +++++++++++++++++++++++++++----- sequencerd/sequence.h | 7 +- sequencerd/sequence_builder.cpp | 28 +++++- 3 files changed, 174 insertions(+), 26 deletions(-) diff --git a/sequencerd/sequence.cpp b/sequencerd/sequence.cpp index 598092ac..f1c719d9 100644 --- a/sequencerd/sequence.cpp +++ b/sequencerd/sequence.cpp @@ -101,12 +101,14 @@ namespace Sequencer { * @return ERROR|NO_ERROR|ABORT * */ - long Sequence::run_sequence( const std::vector &groups, + long Sequence::run_sequence( const std::vector &sequence, std::string_view caller, bool continue_on_error ) { long error = NO_ERROR; - for (const auto &group : groups) { + logwrite(caller, "starting sequence"); + + for (const auto &group : sequence) { if (this->cancel_flag.load()) return ABORT; // PARALLEL Groups are executed in parallel threads @@ -131,6 +133,8 @@ namespace Sequencer { } } + logwrite(caller, "sequence complete"); + return error; } /***** Sequencer::Sequence::run_sequence ***********************************/ @@ -145,7 +149,7 @@ namespace Sequencer { */ long Sequence::run_default_sequence(std::string_view caller) { - std::vector groups; + std::vector sequence; // ---------- RUN THESE IN PARALLEL -------------------- @@ -156,15 +160,15 @@ namespace Sequencer { // if (this->target.pointmode == Acam::POINTMODE_ACAM) { this->dotype("ONE"); - groups.push_back( { OperationType::PARALLEL, - { { "move_to_target", THR_MOVE_TO_TARGET, [this]{ return move_to_target(); } } } } ); + sequence.push_back( { OperationType::PARALLEL, + { { "move_to_target", THR_MOVE_TO_TARGET, [this]{ return move_to_target(); } } } } ); } else { this->target.pointmode = Acam::POINTMODE_SLIT; // these are the default operations prior to exposure, // they can be done in parallel - groups.push_back( { OperationType::PARALLEL, + sequence.push_back( { OperationType::PARALLEL, { { "move_to_target", THR_MOVE_TO_TARGET, [this]{ return move_to_target(); } }, { "camera_set", THR_CAMERA_SET, [this]{ return camera_set(); } }, { "focus_set", THR_FOCUS_SET, [this]{ return focus_set(); } }, @@ -178,17 +182,17 @@ namespace Sequencer { // ---------- RUN THESE IN SERIES ---------------------- if (this->target.pointmode != Acam::POINTMODE_ACAM) { - groups.push_back( { OperationType::SERIAL, - { { "target_acquisition", THR_ACQUISITION, + sequence.push_back( { OperationType::SERIAL, + { { "target_acquire", THR_ACQUISITION, [this,caller]() { return this->do_target_acquisition(caller); } }, { "target_offset", THR_MOVE_TO_TARGET, [this]() { return this->target_offset(); } }, - { "slit_expose", THR_SLIT_SET, + { "slit_set", THR_SLIT_SET, [this]() { return this->do_target_virtualslit(Sequencer::VSM_EXPOSE); } }, - { "science_exposure", THR_EXPOSURE, + { "expose", THR_EXPOSURE, [this,caller]() { return this->do_exposure(caller); } } } } ); @@ -196,7 +200,7 @@ namespace Sequencer { // ---------- RUN THE SEQUENCE NOW --------------------- - return run_sequence(groups, caller); + return run_sequence(sequence, caller); } /***** Sequencer::Sequence::run_default_sequence ***************************/ @@ -209,7 +213,26 @@ namespace Sequencer { * */ long Sequence::run_script(const std::string &filename) { - return NO_ERROR; + std::string_view function("Sequencer::Sequence::run_script"); + + std::vector commands; + if ( parse_script(filename, commands) != NO_ERROR ) { + logwrite(function, "ERROR parsing '"+filename+"'"); + return ERROR; + } + + std::vector sequence; + if ( build_sequence(commands, sequence) != NO_ERROR ) { + logwrite(function, "ERROR building sequence from '"+filename+"'"); + return ERROR; + } + + if ( validate_sequence(sequence) != NO_ERROR ) { + logwrite(function, "ERROR validating sequence from '"+filename+"'"); + return ERROR; + } + + return run_sequence(sequence, function); } /***** Sequencer::Sequence::run_script *************************************/ @@ -217,25 +240,97 @@ namespace Sequencer { /***** Sequencer::Sequence::parse_script ***********************************/ /** * @brief parses a user script - * @param[in] filename filename of script - * @return ERROR|NO_ERROR|ABORT + * @details This parses a script, makes a ParsedCommand struct from each + * line, returning a vector of ParsedCommands. No validation is + * done here, only parsing. + * @param[in] filename filename of script + * @param[out] commands_out reference to vector of ParsedCommands + * @return ERROR|NO_ERROR * */ long Sequence::parse_script(const std::string &filename, - std::vector &out) { + std::vector &commands_out) { + std::ifstream file(filename); + if (!file.is_open()) { + logwrite("Sequencer::Sequence::parse_script", "ERROR opening '"+filename+"'"); + return ERROR; + } + + std::string line; + + while (std::getline(file, line)) { + + auto command = parse_command(line); + + if (command) commands_out.push_back(*command); + } + return NO_ERROR; } /***** Sequencer::Sequence::parse_script ***********************************/ + /***** Sequencer::Sequence::parse_command **********************************/ + /** + * @brief parses a single command line + * @details This parses a command and any parameters as key=val pairs + * from the supplied string and returns a ParsedCommand struct. + * @param[in] args string containing command and any optional arguments + * @return nullptr | ParsedCommand + * + */ + std::optional Sequence::parse_command(std::string &args) { + + // strip comments, everything after '#' + auto pos = args.find('#'); + if (pos != std::string::npos) args = args.substr(0, pos); + + std::istringstream iss(args); + std::string word; + + // first word is the command + if (!(iss >> word)) return std::nullopt; + + ParsedCommand command; + command.name = word; + + // any additional words are parameters, expected to be key=val pairs + while (iss >> word) { + auto eq = word.find('='); + if (eq != std::string::npos) { + std::string key = word.substr(0, eq); + std::string val = word.substr(eq+1); + command.params.map[key] = val; + } + } + + return command; + } + /***** Sequencer::Sequence::parse_command **********************************/ + + /***** Sequencer::Sequence::validate_sequence ******************************/ /** - * @brief - * @param[in] - * @return ERROR|NO_ERROR|ABORT + * @brief applies validation rules to sequence + * @param[in] sequence vector of OperationGroups + * @return ERROR|NO_ERROR * */ - long Sequence::validate_sequence(const std::vector &groups) { + long Sequence::validate_sequence(const std::vector &sequence) { + + // sequence is a vector of OperationGroups + for (const auto &group : sequence) { + + // group is a vector of Operations + for (const auto &op : group.operations) { + + if (op.name == "expose") { + } + else + if (op.name == "slit_set") { + } + } + } return NO_ERROR; } /***** Sequencer::Sequence::validate_sequence ******************************/ @@ -244,12 +339,38 @@ namespace Sequencer { /***** Sequencer::Sequence::handle_cli_operation ***************************/ /** * @brief handle incoming operation request - * @param[in] op the name of an operation + * @details This performs all the same steps for a single command as a + * sequence of one operation. This is inefficient for performing + * multiple steps. + * @param[in] args string containing command and any arguments * @return ERROR|NO_ERROR|ABORT * */ - long Sequence::handle_cli_operation(const std::string &op) { - return NO_ERROR; + long Sequence::handle_cli_operation(std::string args) { + std::string_view function("Sequencer::Sequence::handle_cli_operation"); + + if (args.empty()) return ERROR; + + // build a mini-sequence of one command in order to validate it + // + auto commands = { *parse_command(args) }; + + std::vector sequence; + + if ( build_sequence(commands, sequence) != NO_ERROR ) return ERROR; + + if ( validate_sequence(sequence) != NO_ERROR ) return ERROR; + + if ( sequence.empty() || sequence[0].operations.empty() ) { + logwrite(function, "ERROR invalid command '"+args+"'"); + return ERROR; + } + + Operation op = sequence[0].operations[0]; + + // ---------- RUN THE COMMAND -------------------------- + // + return run(op, function); } /***** Sequencer::Sequence::handle_cli_operation ***************************/ diff --git a/sequencerd/sequence.h b/sequencerd/sequence.h index 6292cd77..dcac10f0 100644 --- a/sequencerd/sequence.h +++ b/sequencerd/sequence.h @@ -527,11 +527,12 @@ namespace Sequencer { long run_script(const std::string &filename); ///< run user script long parse_script(const std::string &filename, - std::vector &out); ///< parse script into commands/args + std::vector &commands_out); ///< parse script into commands/args + std::optional parse_command(std::string &args); long build_sequence(const std::vector &commands, std::vector &sequence_out); ///< build sequence from parsed commands - long validate_sequence(const std::vector &groups); ///< validate sequence - long handle_cli_operation(const std::string &op); ///< handle incoming operation request + long validate_sequence(const std::vector &sequence); ///< validate sequence + long handle_cli_operation(std::string command); ///< handle incoming operation request // publish/subscribe functions // diff --git a/sequencerd/sequence_builder.cpp b/sequencerd/sequence_builder.cpp index 4a7489a7..6b2d36ba 100644 --- a/sequencerd/sequence_builder.cpp +++ b/sequencerd/sequence_builder.cpp @@ -31,12 +31,14 @@ namespace Sequencer { continue; } else + if (command.name == "end_parallel") { sequence_out.push_back(group); - group = { OperationType::SERIAL, {} }; // back to default + group = { OperationType::SERIAL, {} }; continue; } else + if (command.name == "move_to_target") { group.operations.emplace_back( Operation { "move_to_target", THR_MOVE_TO_TARGET, @@ -50,6 +52,30 @@ namespace Sequencer { command.params }); } + else + + if (command.name == "slit_set") { + group.operations.emplace_back( Operation { + "slit_set", THR_SLIT_SET, + [this,params=command.params]() { + size_t mode = params.get("mode", VSM_DATABASE); + return slit_set(static_cast(mode)); + }, + command.params + }); + } + + else + + if (command.name == "expose") { + group.operations.emplace_back( Operation { + "expose", THR_SLIT_SET, + [this]() { + return do_exposure("placeholder"); + }, + {} + }); + } else { this->async.enqueue_and_log("Sequencer::Sequence::build_sequence", From ec93b5de414b7e0bc64c36b81d240b00413b97d9 Mon Sep 17 00:00:00 2001 From: David Hale Date: Tue, 7 Apr 2026 16:13:12 -0700 Subject: [PATCH 7/7] * updates status publishing in slitd * adds subscriber topic handlers to sequencerd * introduces a command handler header-only lib (WIP, not implemented) --- camerad/astrocam.cpp | 3 +- camerad/camerad.cpp | 2 +- common/message_keys.h | 22 +++- sequencerd/command.h | 134 ++++++++++++++++++++++ sequencerd/command_rules.h | 37 ++++++ sequencerd/sequence.cpp | 189 ++++++++----------------------- sequencerd/sequence.h | 25 +++- sequencerd/sequence_builder.cpp | 2 +- sequencerd/sequencer_interface.h | 13 +++ sequencerd/sequencer_server.h | 2 +- sequencerd/sequencerd.cpp | 2 + slitd/slit_interface.cpp | 88 ++++++-------- slitd/slit_interface.h | 18 ++- slitd/slit_server.cpp | 18 +-- slitd/slitd.cpp | 2 +- 15 files changed, 332 insertions(+), 225 deletions(-) create mode 100644 sequencerd/command.h create mode 100644 sequencerd/command_rules.h diff --git a/camerad/astrocam.cpp b/camerad/astrocam.cpp index 23d4db37..fb2ddea2 100644 --- a/camerad/astrocam.cpp +++ b/camerad/astrocam.cpp @@ -29,8 +29,9 @@ namespace AstroCam { nlohmann::json jmessage_out; // build JSON message with my telemetry - jmessage_out[Key::SOURCE] = "camerad"; + jmessage_out[Key::SOURCE] = Topic::CAMERAD; jmessage_out[Key::Camerad::READY] = this->can_expose.load(); + jmessage_out[Key::Camerad::SHUTTERTIME] = this->camera.shutter.get_duration(); // publish JSON message try { diff --git a/camerad/camerad.cpp b/camerad/camerad.cpp index 2d7e0449..551a9a70 100644 --- a/camerad/camerad.cpp +++ b/camerad/camerad.cpp @@ -339,7 +339,7 @@ void async_main(Network::UdpSocket sock) { } while (1) { - std::string message = server.camera.async.dequeue(); // get the latest message from the queue (blocks) + std::string_view message = server.camera.async.dequeue(); // get the latest message from the queue (blocks) retval = sock.Send(message); // transmit the message if (retval < 0) { std::stringstream errstm; diff --git a/common/message_keys.h b/common/message_keys.h index ab7d8a46..13a4d7ff 100644 --- a/common/message_keys.h +++ b/common/message_keys.h @@ -31,7 +31,8 @@ namespace Key { } namespace Camerad { - inline const std::string READY = "ready"; + inline const std::string READY = "ready"; + inline const std::string SHUTTERTIME = "shuttime_sec"; } namespace Acamd { @@ -50,4 +51,23 @@ namespace Key { inline const std::string FINEACQUIRE_LOCKED = "fineacquire_locked"; inline const std::string FINEACQUIRE_RUNNING = "fineacquire_running"; } + + namespace Slitd { + inline const std::string SLITPOSA = "slitposa"; + inline const std::string SLITPOSB = "slitposb"; + inline const std::string SLITW = "slitw"; + inline const std::string SLITO = "slito"; + inline const std::string ISOPEN = "isopen"; + inline const std::string ISHOME = "ishome"; + } + + namespace Tcsd { + inline const std::string TELRA = "telra"; + inline const std::string TELDEC = "teldec"; + inline const std::string ALT = "alt"; + inline const std::string AZ = "az"; + inline const std::string AIRMASS = "airmass"; + inline const std::string CASANGLE = "casangle"; + } + } diff --git a/sequencerd/command.h b/sequencerd/command.h new file mode 100644 index 00000000..ebacb4bc --- /dev/null +++ b/sequencerd/command.h @@ -0,0 +1,134 @@ +/** + * @file command.h + * @brief header-only library for handling commands to daemons + * @details This provides a wrapper to form command and arg list strings. + * Also a wrapper that stores transition states, and validates that + * a command is allowed to be sent while in the current state. + * @author David Hale + * + */ + +#pragma once + +#include +#include +#include +#include + +/***** Sequencer **************************************************************/ +/** + * @brief namespace for the observation sequencer + * + */ +namespace Sequencer { + + /** + * @brief wrapper to form string from command and arglist + */ + struct Command { + std::string name; + std::vector arglist; + + std::string str() const { + std::string strung = name; + for (const auto &arg : arglist) { + strung += " " + arg; + } + return strung; + } + }; + + + /** + * @brief command specs right now just holds min/max number of allowed args + */ + struct CommandSpec { + int min_args; + int max_args; + }; + + using CommandSpecMap = std::unordered_map; + + + /** + * @brief structure contains command and states it can transition from->to + */ + template + struct Transition { + State from; + std::string command; + State to; + }; + + + /** + * @brief contains the functionality of the library + * @details This pairs specs and transitions with a command and holds + * the client object used to communiate with the daemon. + */ + template + class CommandClient { + private: + Common::DaemonClient &client; + const CommandSpecMap &specs; + State state; + const std::vector> &transitions; + + void validate_args( const Command &cmd ) const { + auto it = specs.find( cmd.name ); + if (it == specs.end()) throw std::runtime_error("unknown command: "+cmd.name); + int nargs = cmd.arglist.size(); + if (nargs < it->second.min_args || nargs > it->second.max_args) { + throw std::runtime_error("invalid arg count for "+cmd.name); + } + } + + void validate_order( const Command &cmd ) const { + for (const auto &transition : transitions) { + if (transition.from == state && transition.command == cmd.name) { + return; + } + } + throw std::runtime_error("invalid command order: "+cmd.name); + } + + void advance_state( const Command &cmd ) { + for (const auto &transition : transitions) { + if (transition.from == state && transition.command == cmd.name) { + state = transition.to; + return; + } + } + } + + public: + CommandClient( Common::DaemonClient &client, + const CommandSpecMap &specs, + State initial_state, + const std::vector> &transitions ) + : client(client), + specs(specs), + state(initial_state), + transitions(transitions) { } + + /** + * @brief primary interface to sending commands + * @details This validates the number of args, validates the + * transition state, that this command is allowed to be used + * in the current state, and sends the command. + * @param[in] cmd Command struct contains command and arglist + * @return return value from the client + * + */ + long send( const Command &cmd ) { + validate_args( cmd ); + validate_order( cmd ); + long ret = client.command( cmd.str() ); + advance_state( cmd ); + return ret; + } + + State get_state() const { return state; } + }; +} +/***** Sequencer **************************************************************/ diff --git a/sequencerd/command_rules.h b/sequencerd/command_rules.h new file mode 100644 index 00000000..5ec60136 --- /dev/null +++ b/sequencerd/command_rules.h @@ -0,0 +1,37 @@ + +#pragma once + +#include "command.h" + +#include + +namespace Sequencer { + + enum class CameraState { + IDLE, + READY, + EXPOSING, + READING + }; + + const CommandSpecMap camerad_specs = { + { CAMERAD_ACTIVATE, {0, 4} }, + { CAMERAD_DEACTIVATE, {1, 4} }, + { CAMERAD_OPEN, {0, 1} }, + { CAMERAD_CLOSE, {0, 0} }, + { CAMERAD_EXPTIME, {0, 1} }, + { CAMERAD_EXPOSE, {0, 0} }, + { CAMERAD_READOUT, {0, 2} } + }; + + const std::vector> camerad_transitions = { + { CameraState::IDLE, CAMERAD_OPEN, CameraState::READY }, + { CameraState::READY, CAMERAD_ACTIVATE, CameraState::READY }, + { CameraState::READY, CAMERAD_DEACTIVATE, CameraState::READY }, + { CameraState::READY, CAMERAD_EXPTIME, CameraState::READY }, + { CameraState::READY, CAMERAD_EXPOSE, CameraState::EXPOSING }, + { CameraState::EXPOSING, CAMERAD_READOUT, CameraState::READING }, + { CameraState::READING, CAMERAD_READOUT, CameraState::READY } + }; + +} diff --git a/sequencerd/sequence.cpp b/sequencerd/sequence.cpp index f1c719d9..564156f3 100644 --- a/sequencerd/sequence.cpp +++ b/sequencerd/sequence.cpp @@ -405,16 +405,34 @@ namespace Sequencer { * */ void Sequence::handletopic_camerad(const nlohmann::json &jmessage) { + this->target.column_from_json( DBCol::EXPTIME, Key::Camerad::SHUTTERTIME, jmessage ); if (jmessage.contains(Key::Camerad::READY)) { int isready = jmessage[Key::Camerad::READY].get(); this->can_expose.store(isready, std::memory_order_relaxed); - std::lock_guard lock(camerad_mtx); - this->camerad_cv.notify_all(); } + + std::lock_guard lock(camerad_mtx); + this->camerad_cv.notify_all(); } /***** Sequencer::Sequence::handletopic_camerad ****************************/ + /***** Sequencer::Sequence::handletopic_slitd ******************************/ + /** + * @brief handles Topic::SLITD telemetry + * @param[in] jmessage subscribed-received JSON message + * + */ + void Sequence::handletopic_slitd(const nlohmann::json &jmessage) { + this->target.column_from_json( DBCol::SLITWIDTH, Key::Slitd::SLITW, jmessage ); + this->target.column_from_json( DBCol::SLITOFFSET, Key::Slitd::SLITO, jmessage ); + + std::lock_guard lock(slitd_mtx); + this->slitd_cv.notify_all(); + } + /***** Sequencer::Sequence::handletopic_slitd ******************************/ + + /***** Sequencer::Sequence::handletopic_slicecamd **************************/ /** * @brief handles Topic::SLICECAMD telemetry @@ -426,12 +444,33 @@ namespace Sequencer { bool fineacquirelocked; Common::extract_telemetry_value( jmessage, Key::Slicecamd::FINEACQUIRE_LOCKED, fineacquirelocked ); this->is_fineacquire_locked.store(fineacquirelocked, std::memory_order_relaxed); + std::lock_guard lock(this->fineacquire_mtx); this->fineacquire_cv.notify_all(); } /***** Sequencer::Sequence::handletopic_slicecamd **************************/ + /***** Sequencer::Sequence::handletopic_tcsd *******************************/ + /** + * @brief handles Topic::TCSD telemetry + * @param[in] jmessage subscribed-received JSON message + * + */ + void Sequence::handletopic_tcsd(const nlohmann::json &jmessage) { + this->target.column_from_json( DBCol::TELRA, Key::Tcsd::TELRA, jmessage ); + this->target.column_from_json( DBCol::TELDECL, Key::Tcsd::TELDEC, jmessage ); + this->target.column_from_json( DBCol::ALT, Key::Tcsd::ALT, jmessage ); + this->target.column_from_json( DBCol::AZ, Key::Tcsd::AZ, jmessage ); + this->target.column_from_json( DBCol::AIRMASS, Key::Tcsd::AIRMASS, jmessage ); + this->target.column_from_json( DBCol::CASANGLE, Key::Tcsd::CASANGLE, jmessage ); + + std::lock_guard lock(tcsd_mtx); + this->tcsd_cv.notify_all(); + } + /***** Sequencer::Sequence::handletopic_tcsd *******************************/ + + /***** Sequencer::Sequence::handletopic_acamd ******************************/ /** * @brief handles Topic::ACAMD telemetry @@ -443,6 +482,7 @@ namespace Sequencer { bool acquired; Common::extract_telemetry_value( jmessage, Key::Acamd::IS_ACQUIRED, acquired ); this->is_acam_guiding.store(acquired, std::memory_order_relaxed); + std::lock_guard lock(this->acam_mtx); this->acam_cv.notify_all(); } @@ -769,7 +809,7 @@ namespace Sequencer { * @param[in] obsid_in optional obsid, specify for single-target observation * */ - void Sequence::sequence_start(std::string obsid_in="") { + void Sequence::sequence_start(std::string obsid_in) { std::string_view function("Sequencer::Sequence::sequence_start"); std::ostringstream message; std::string reply; @@ -896,10 +936,7 @@ namespace Sequencer { break; } - // before writing to the completed database table, get current - // telemetry from other daemons. - // - this->get_external_telemetry(); +// this->request_status(tcsd); // force tcsd to publish his status TODO WORK-IN-PROGRESS // Update this target's state in the database // @@ -989,6 +1026,7 @@ namespace Sequencer { // send two commands, one for each if (!activechans.str().empty()) { std::string cmd = CAMERAD_ACTIVATE + activechans.str(); +/*** if ( camerad_cmd.send( { CAMERAD_ACTIVATE, { activechans.str() } } ) != NO_ERROR ) { WIP ***/ if (this->camerad.send(cmd, reply)!=NO_ERROR) { this->async.enqueue_and_log(function, "ERROR sending \""+cmd+"\": "+reply); throw std::runtime_error("camera returned "+reply); @@ -3013,8 +3051,8 @@ namespace Sequencer { { THR_CAMERA_SHUTDOWN, std::bind(&Sequence::camera_shutdown, this) }, { THR_FLEXURE_SHUTDOWN, std::bind(&Sequence::flexure_shutdown, this) }, { THR_FOCUS_SHUTDOWN, std::bind(&Sequence::focus_shutdown, this) }, - { THR_SLICECAM_SHUTDOWN, std::bind(&Sequence::slit_shutdown, this) }, - { THR_SLIT_SHUTDOWN, std::bind(&Sequence::slicecam_shutdown, this) }, + { THR_SLIT_SHUTDOWN, std::bind(&Sequence::slit_shutdown, this) }, + { THR_SLICECAM_SHUTDOWN, std::bind(&Sequence::slicecam_shutdown, this) }, { THR_TCS_SHUTDOWN, std::bind(&Sequence::tcs_shutdown, this) } }; @@ -3615,120 +3653,6 @@ namespace Sequencer { /***** Sequencer::Sequence::make_telemetry_message **************************/ - /***** Sequencer::Sequence::get_external_telemetry **************************/ - /** - * @brief collect telemetry from other daemon(s) - * @details This is used for any telemetry that I need to collect from - * another daemon. Common::collect_telemetry() sends a command - * to the daemon, which will respond with a JSON message. The - * daemon(s) to contact are configured with the TELEM_PROVIDER - * key in the config file. - * - */ - void Sequence::get_external_telemetry() { - // Loop through each configured telemetry provider. This requests - // their telemetry which is returned as a serialized json string - // held in retstring. - // - // handle_json_message() will parse the serialized json string. - // - std::string retstring; - for ( const auto &provider : this->telemetry_providers ) { - Common::collect_telemetry( provider, retstring ); - handle_json_message(retstring); - } - return; - } - /***** Sequencer::Sequence::get_external_telemetry **************************/ - - - /***** Sequencer::Sequence::handle_json_message *****************************/ - /** - * @brief parses incoming telemetry messages - * @details Requesting telemetry from another daemon returns a serialized - * JSON message which needs to be passed in here to parse it. - * @param[in] message_in incoming serialized JSON message (as a string) - * @return ERROR | NO_ERROR - * - */ - long Sequence::handle_json_message( const std::string message_in ) { - std::string_view function("Sequencer::Sequence::handle_json_message"); - std::stringstream message; - - if ( message_in.empty() ) { - logwrite( function, "ERROR empty JSON message" ); - return ERROR; - } - - try { - nlohmann::json jmessage = nlohmann::json::parse( message_in ); - std::string messagetype; - - // jmessage must not contain key "error" and must contain key "messagetype" - // - if ( !jmessage.contains("error") ) { - if ( jmessage.contains("messagetype") && jmessage["messagetype"].is_string() ) { - messagetype = jmessage["messagetype"]; - } - else { - logwrite( function, "ERROR received JSON message with missing or invalid messagetype" ); - return ERROR; - } - } - else { - logwrite( function, "ERROR in JSON message" ); - return ERROR; - } - - // No errors, so disseminate the message contents based on the message type. - // - // column_from_json( colname, jkey, jmessage ) will extract the value of - // expected type with key jkey from json string jmessage, and assign it - // to this->target.external_telemetry[colname] map. It is expected that - // "colname" is the column name in the database. - // - if ( messagetype == "camerainfo" ) { - this->target.column_from_json( "EXPTIME", "SHUTTIME_SEC", jmessage ); - } - else - if ( messagetype == "slitinfo" ) { - this->target.column_from_json( "SLITWIDTH", "SLITW", jmessage ); - this->target.column_from_json( "SLITOFFSET", "SLITO", jmessage ); - } - else - if ( messagetype == "tcsinfo" ) { - this->target.column_from_json( "TELRA", "TELRA", jmessage ); - this->target.column_from_json( "TELDECL", "TELDEC", jmessage ); - this->target.column_from_json( "ALT", "ALT", jmessage ); - this->target.column_from_json( "AZ", "AZ", jmessage ); - this->target.column_from_json( "AIRMASS", "AIRMASS", jmessage ); - this->target.column_from_json( "CASANGLE", "CASANGLE", jmessage ); - } - else - if ( messagetype == "test" ) { - } - else { - message.str(""); message << "ERROR received unhandled JSON message type \"" << messagetype << "\""; - logwrite( function, message.str() ); - return ERROR; - } - } - catch ( const nlohmann::json::parse_error &e ) { - message.str(""); message << "ERROR json exception parsing message: " << e.what(); - logwrite( function, message.str() ); - return ERROR; - } - catch ( const std::exception &e ) { - message.str(""); message << "ERROR parsing message: " << e.what(); - logwrite( function, message.str() ); - return ERROR; - } - - return NO_ERROR; - } - /***** Sequencer::Sequence::handle_json_message *****************************/ - - /***** Sequencer::Sequence::dothread_test_fpoffset **************************/ /** * @brief for testing, calls a Python function from a thread @@ -4054,7 +3978,6 @@ namespace Sequencer { retstring.append( " fpoffset ? | \n" ); retstring.append( " getnext [ ? ]\n" ); retstring.append( " getobsid [ ? ]\n" ); - retstring.append( " gettelem [ ? ]\n" ); retstring.append( " isready [ ? ]\n" ); retstring.append( " moveto [ ? | ]\n" ); retstring.append( " notify [ ? ]\n" ); @@ -4437,24 +4360,6 @@ namespace Sequencer { retstring = rts.str(); } else - // ---------------------------------------------------- - // gettelem -- get external telemetry - // ---------------------------------------------------- - // - if ( testname == "gettelem" ) { - if ( tokens.size() > 1 && tokens[1] == "?" ) { - retstring = "test gettelem\n"; - retstring.append( " Get external telemetry from other daemons.\n" ); - return HELP; - } - this->get_external_telemetry(); - message.str(""); - for ( const auto &[name,data] : this->target.external_telemetry ) { - message << "name=" << name << " valid=" << (data.valid?"T":"F") << " value=" << data.value << "\n"; - } - retstring = message.str(); - } - else // ---------------------------------------------------- // addrow -- insert a (fixed, hard-coded) row into the database diff --git a/sequencerd/sequence.h b/sequencerd/sequence.h index dcac10f0..fa5e7420 100644 --- a/sequencerd/sequence.h +++ b/sequencerd/sequence.h @@ -34,6 +34,10 @@ #include "tcsd_commands.h" #include "sequencerd_commands.h" #include "message_keys.h" +/*** Work-In-Progress + * #include "command.h" + * #include "command_rules.h" + */ #include "tcs_constants.h" #include "acam_interface_shared.h" @@ -405,6 +409,10 @@ namespace Sequencer { [this](const nlohmann::json &msg) { handletopic_acamd(msg); } ) }, { Topic::SLICECAMD, std::function( [this](const nlohmann::json &msg) { handletopic_slicecamd(msg); } ) }, + { Topic::SLITD, std::function( + [this](const nlohmann::json &msg) { handletopic_slitd(msg); } ) }, + { Topic::TCSD, std::function( + [this](const nlohmann::json &msg) { handletopic_tcsd(msg); } ) }, { Topic::CAMERAD, std::function( [this](const nlohmann::json &msg) { handletopic_camerad(msg); } ) } }; @@ -456,6 +464,10 @@ namespace Sequencer { std::condition_variable acam_cv; std::mutex camerad_mtx; std::condition_variable camerad_cv; + std::mutex slitd_mtx; + std::condition_variable slitd_cv; + std::mutex tcsd_mtx; + std::condition_variable tcsd_cv; std::mutex wait_mtx; std::condition_variable cv; std::mutex cv_mutex; @@ -510,6 +522,13 @@ namespace Sequencer { Common::DaemonClient slitd { "slitd" }; Common::DaemonClient tcsd { "tcsd" }; +/**** Work-In-Progress + CommandClient camerad_cmd { camerad, + camerad_specs, + CameraState::IDLE, + camerad_transitions}; +*****/ + std::map power_switch; ///< STL map of PowerSwitch objects maps all plugnames to each subsystem float slitoffsetexpose; ///< "virtual slit mode" offset for expose @@ -546,6 +565,8 @@ namespace Sequencer { void handletopic_camerad( const nlohmann::json &jmessage ); void handletopic_acamd( const nlohmann::json &jmessage ); void handletopic_slicecamd( const nlohmann::json &jmessage ); + void handletopic_slitd( const nlohmann::json &jmessage ); + void handletopic_tcsd( const nlohmann::json &jmessage ); void publish_snapshot(); void publish_snapshot(std::string &retstring); void publish_seqstate(); @@ -616,8 +637,6 @@ namespace Sequencer { long target_offset(); void make_telemetry_message( std::string &retstring ); ///< assembles my telemetry message - void get_external_telemetry(); ///< collect telemetry from another daemon - long handle_json_message( const std::string message_in ); ///< parses incoming telemetry messages long set_power_switch( PowerState state, const std::string which, std::chrono::seconds delay ); long check_power_switch( PowerState checkstate, const std::string which, bool &is_set ); @@ -645,7 +664,7 @@ namespace Sequencer { long wait_for_readout(std::string_view caller); ///< wait for readout completion or cancel long wait_for_canexpose(std::string_view caller); ///< wait for camera can_expose - void sequence_start(std::string obsid_in); ///< main sequence start thread. optional obsid_in for single target obs + void sequence_start(std::string obsid_in=""); ///< main sequence start thread. optional obsid_in for single target obs long calib_set(); ///< sets calib according to target entry params long camera_set(); ///< sets camera according to target entry params long slit_set(VirtualSlitMode mode=VSM_DATABASE); ///< sets slit according to target entry params and mode diff --git a/sequencerd/sequence_builder.cpp b/sequencerd/sequence_builder.cpp index 6b2d36ba..cf1ba269 100644 --- a/sequencerd/sequence_builder.cpp +++ b/sequencerd/sequence_builder.cpp @@ -69,7 +69,7 @@ namespace Sequencer { if (command.name == "expose") { group.operations.emplace_back( Operation { - "expose", THR_SLIT_SET, + "expose", THR_EXPOSURE, [this]() { return do_exposure("placeholder"); }, diff --git a/sequencerd/sequencer_interface.h b/sequencerd/sequencer_interface.h index f4569e7c..fd59948b 100644 --- a/sequencerd/sequencer_interface.h +++ b/sequencerd/sequencer_interface.h @@ -26,6 +26,19 @@ #define ERROR_TARGETLIST_BAD_HEADER 1001 ///< TODO change this +namespace DBCol { + inline const std::string EXPTIME = "EXPTIME"; + inline const std::string SLITWIDTH = "SLITWIDTH"; + inline const std::string SLITOFFSET = "SLITOFFSET"; + + inline const std::string TELRA = "TELRA"; + inline const std::string TELDECL = "TELDECL"; + inline const std::string ALT = "ALT"; + inline const std::string AZ = "AZ"; + inline const std::string AIRMASS = "AIRMASS"; + inline const std::string CASANGLE = "CASANGLE"; +} + /***** Sequencer **************************************************************/ /** * @namespace Sequencer diff --git a/sequencerd/sequencer_server.h b/sequencerd/sequencer_server.h index f785d42d..aa3440a8 100644 --- a/sequencerd/sequencer_server.h +++ b/sequencerd/sequencer_server.h @@ -82,7 +82,7 @@ namespace Sequencer { // are initialized here. The names are useful just for logging. // this->sequence.calibd.name = "calibd"; - this->sequence.camerad.name = "camerad"; +// this->sequence.camerad.name = "camerad"; this->sequence.filterd.name = "filterd"; this->sequence.flexured.name = "flexured"; this->sequence.focusd.name = "focusd"; diff --git a/sequencerd/sequencerd.cpp b/sequencerd/sequencerd.cpp index 3b9ba63f..26f43523 100644 --- a/sequencerd/sequencerd.cpp +++ b/sequencerd/sequencerd.cpp @@ -131,6 +131,8 @@ int main(int argc, char **argv) { // if ( sequencerd.sequence.init_pubsub( { Topic::CAMERAD, Topic::ACAMD, + Topic::TCSD, + Topic::SLITD, Topic::SLICECAMD } ) == ERROR ) { logwrite(function, "ERROR initializing publisher-subscriber handler"); sequencerd.exit_cleanly(); diff --git a/slitd/slit_interface.cpp b/slitd/slit_interface.cpp index 2569478c..96edc275 100644 --- a/slitd/slit_interface.cpp +++ b/slitd/slit_interface.cpp @@ -104,10 +104,10 @@ namespace Slit { std::string retstring; this->is_open( "", retstring ); - snapshot.isopen = ( retstring=="true" ? true : false ); - if ( snapshot.isopen ) { + status.isopen = ( retstring=="true" ? true : false ); + if ( status.isopen ) { this->is_home( "", retstring ); - snapshot.ishome = ( retstring=="true" ? true : false ); + status.ishome = ( retstring=="true" ? true : false ); } this->get( retstring ); @@ -285,14 +285,14 @@ namespace Slit { return HELP; } - if ( std::isnan(snapshot.width.arcsec()) ) { + if ( std::isnan(status.width.arcsec()) ) { logwrite( "Slit::Interface::offset", "ERROR width not previously set" ); retstring="undefined_width"; return ERROR; } std::stringstream cmd; - cmd << snapshot.width.arcsec() << " " << args; + cmd << status.width.arcsec() << " " << args; return this->set( cmd.str(), retstring ); } @@ -374,7 +374,7 @@ namespace Slit { else fval = std::round( fval * 10.0 ) / 10.0; // round to nearest tenth } reqwidth = SlitDimension( fval, unit ); - reqoffset = snapshot.offset; + reqoffset = status.offset; } if ( tokens.size() == 2 ) { if ( tokens.at(1).find("mm") != std::string::npos ) unit=Unit::MM; else unit=Unit::ARCSEC; @@ -502,30 +502,27 @@ namespace Slit { // this call reads the controller and returns the numeric values // - error = this->read_positions( poswidth, posoffset, snapshot.posA, snapshot.posB ); + error = this->read_positions( poswidth, posoffset, status.posA, status.posB ); // store the current readings in the class // - snapshot.width = SlitDimension( poswidth, Unit::MM ); - snapshot.offset = SlitDimension( posoffset, Unit::MM ); + status.width = SlitDimension( poswidth, Unit::MM ); + status.offset = SlitDimension( posoffset, Unit::MM ); // form the return value // std::stringstream s; if ( args=="mm" ) { - s << std::setprecision(2) << std::fixed << snapshot.width.mm() << " " - << std::setprecision(3) << snapshot.offset.mm() << " mm"; + s << std::setprecision(2) << std::fixed << status.width.mm() << " " + << std::setprecision(3) << status.offset.mm() << " mm"; } else { - s << std::setprecision(2) << std::fixed << snapshot.width.arcsec() << " " - << std::setprecision(3) << snapshot.offset.arcsec(); + s << std::setprecision(2) << std::fixed << status.width.arcsec() << " " + << std::setprecision(3) << status.offset.arcsec(); } retstring = s.str(); - message.str(""); message << "NOTICE:" << Slit::DAEMON_NAME << " " << retstring; - this->async.enqueue( message.str() ); - - this->publish_snapshot(); + this->publish_status(); return error; } @@ -713,55 +710,42 @@ namespace Slit { * @param[in] jmessage_in subscribed-received JSON message * */ - void Interface::handletopic_snapshot( const nlohmann::json &jmessage_in ) { - // If my name is in the jmessage then publish my snapshot - // - if ( jmessage_in.contains( Slit::DAEMON_NAME ) ) { - this->publish_snapshot(); - } - else - if ( jmessage_in.contains( "test" ) ) { - logwrite( "Slit::Interface::handletopic_snapshot", jmessage_in.dump() ); - } + void Interface::handletopic_snapshot( const nlohmann::json &jmessage ) { + if ( jmessage.contains(Topic::SLITD) ) this->publish_status(); } /***** Slit::Interface::handletopic_snapshot ********************************/ - /***** Slit::Interface::publish_snapshot ************************************/ + /***** Slit::Interface::publish_status **************************************/ /** - * @brief publishes snapshot of my telemetry - * @details This publishes a JSON message containing a snapshot of my - * telemetry. + * @brief publishes my status on change + * @param[in] force optional (default=false) force publish irrespective of change * */ - void Interface::publish_snapshot() { - std::string dontcare; - this->publish_snapshot(dontcare); - } - void Interface::publish_snapshot(std::string &retstring) { + void Interface::publish_status(bool force) { + + // unless forced, only publish if there was a change + if ( !force && this->status == this->last_published_status ) return; + nlohmann::json jmessage_out; - jmessage_out["source"] = "slitd"; - jmessage_out["ISOPEN"] = snapshot.isopen; - jmessage_out["ISHOME"] = snapshot.ishome; - jmessage_out["SLITW"] = snapshot.width.arcsec(); - jmessage_out["SLITO"] = snapshot.offset.arcsec(); - jmessage_out["SLITPOSA"] = snapshot.posA; - jmessage_out["SLITPOSB"] = snapshot.posB; - - // for backwards compatibility - jmessage_out["messagetype"] = "slitinfo"; - retstring=jmessage_out.dump(); - retstring.append(JEOF); + jmessage_out[Key::SOURCE] = Topic::SLITD; + jmessage_out[Key::Slitd::ISOPEN] = this->status.isopen; + jmessage_out[Key::Slitd::ISHOME] = this->status.ishome; + jmessage_out[Key::Slitd::SLITW] = this->status.width.arcsec(); + jmessage_out[Key::Slitd::SLITO] = this->status.offset.arcsec(); + jmessage_out[Key::Slitd::SLITPOSA] = this->status.posA; + jmessage_out[Key::Slitd::SLITPOSB] = this->status.posB; + + this->last_published_status = this->status; try { this->publisher->publish( jmessage_out ); } catch ( const std::exception &e ) { - logwrite( "Slit::Interface::publish_snapshot", - "ERROR publishing message: "+std::string(e.what()) ); - return; + logwrite( "Slit::Interface::publish_status", + "ERROR publishing status: "+std::string(e.what()) ); } } - /***** Slit::Interface::publish_snapshot ************************************/ + /***** Slit::Interface::publish_status **************************************/ } diff --git a/slitd/slit_interface.h b/slitd/slit_interface.h index 02111f81..235c2455 100644 --- a/slitd/slit_interface.h +++ b/slitd/slit_interface.h @@ -8,6 +8,7 @@ #pragma once +#include "message_keys.h" #include "network.h" #include "pi.h" #include "logentry.h" @@ -207,16 +208,24 @@ namespace Slit { SlitDimension minwidth; ///< set by config file SlitDimension center; ///< position of center in actuator units - typedef struct { + struct Status { SlitDimension width; SlitDimension offset; float posA=NAN; float posB=NAN; bool ishome=false; bool isopen=false; - } snapshot_t; - snapshot_t snapshot; + bool operator==(const Status& other) const { + return std::tie(width, offset, posA, posB, ishome, isopen) == + std::tie(other.width, other.offset, other.posA, other.posB, other.ishome, other.isopen); + } + + bool operator!=(const Status& other) const { return !(*this == other); } + }; + + Status status; + Status last_published_status; Common::Queue async; @@ -233,8 +242,7 @@ namespace Slit { void stop_subscriber_thread() { Common::PubSubHandler::stop_subscriber_thread(*this); } void handletopic_snapshot( const nlohmann::json &jmessage ); - void publish_snapshot(); - void publish_snapshot(std::string &retstring); + void publish_status(bool force=false); long initialize_class(); long open(); ///< opens the PI socket connection diff --git a/slitd/slit_server.cpp b/slitd/slit_server.cpp index 3a298599..a24960cf 100644 --- a/slitd/slit_server.cpp +++ b/slitd/slit_server.cpp @@ -376,7 +376,7 @@ namespace Slit { } while (1) { - std::string message = slit.interface.async.dequeue(); // get the latest message from the queue (blocks) + std::string_view message = slit.interface.async.dequeue(); // get the latest message from the queue (blocks) retval = sock.Send(message); // transmit the message if (retval < 0) { std::stringstream errstm; @@ -597,22 +597,6 @@ namespace Slit { if ( cmd == SLITD_NATIVE ) { ret = this->interface.send_command( args, retstring ); } - else - - // send telemetry on request - // - if ( cmd == TELEMREQUEST ) { - if ( args=="?" || args=="help" ) { - retstring=TELEMREQUEST+"\n"; - retstring.append( " Returns a serialized JSON message containing telemetry\n" ); - retstring.append( " information, terminated with \"EOF\\n\".\n" ); - ret=HELP; - } - else { - this->interface.publish_snapshot(retstring); - ret = JSON; - } - } // unknown commands generate an error // diff --git a/slitd/slitd.cpp b/slitd/slitd.cpp index 23daaa3e..4fd6a1fb 100644 --- a/slitd/slitd.cpp +++ b/slitd/slitd.cpp @@ -127,7 +127,7 @@ int main(int argc, char **argv) { } std::this_thread::sleep_for( std::chrono::milliseconds(100) ); - slitd.interface.publish_snapshot(); + slitd.interface.publish_status(true); // This will pre-thread N_THREADS threads. // The 0th thread is reserved for the blocking port, and the rest are for the non-blocking port.