From c1ce7f3792d2b78d65fe750ffc7a01c2887e810c Mon Sep 17 00:00:00 2001 From: David Hale Date: Fri, 24 Apr 2026 18:36:20 -0700 Subject: [PATCH 1/8] updates sequencerd published messages for fineacquire --- sequencerd/sequence.cpp | 101 ++++++++++++++++++++++------ sequencerd/sequence.h | 48 ++++++++----- sequencerd/sequence_acquisition.cpp | 24 +++---- 3 files changed, 121 insertions(+), 52 deletions(-) diff --git a/sequencerd/sequence.cpp b/sequencerd/sequence.cpp index 5db8147d..034f7573 100644 --- a/sequencerd/sequence.cpp +++ b/sequencerd/sequence.cpp @@ -257,15 +257,13 @@ namespace Sequencer { /***** Sequencer::Sequence::broadcast_seqstate ******************************/ /** - * @brief writes string of seq_state to the async port - * @details This broadcasts the seqstate as a string with the "SEQSTATE:" - * message tag. + * @brief publishes seq_state on the SEQ_SEQSTATE topic + * @details Legacy UDP "SEQSTATE:" async strings have been removed. + * Seqstate is now broadcast only via PUB-SUB. * */ void Sequence::broadcast_seqstate() { this->publish_seqstate(); - this->async.enqueue_and_log( "Sequencer::Sequence::broadcast_seqstate", - "SEQSTATE: "+seq_state_manager.get_set_states() ); this->cv.notify_all(); } /***** Sequencer::Sequence::broadcast_seqstate ******************************/ @@ -273,20 +271,51 @@ namespace Sequencer { /***** Sequencer::Sequence::broadcast_waitstate *****************************/ /** - * @brief writes string of all set wait_state bits to the asyn port - * @details This broadcasts the seqstate as a string with the "WAITSTATE:" - * message tag. + * @brief publishes wait_state on the SEQ_WAITSTATE topic + * @details Legacy UDP "WAITSTATE:" async strings have been removed. + * Waitstate is now broadcast only via PUB-SUB. * */ void Sequence::broadcast_waitstate() { this->publish_waitstate(); - this->async.enqueue_and_log( "Sequencer::Sequence::broadcast_waitstate", - "WAITSTATE: "+wait_state_manager.get_set_states() ); this->cv.notify_all(); } /***** Sequencer::Sequence::broadcast_waitstate *****************************/ + /***** Sequencer::Sequence::broadcast ***************************************/ + /** + * @brief logs a narrative message and publishes it on Topic::BROADCAST + * @param[in] function name of caller (used for log line) + * @param[in] severity one of Severity::NOTICE, Severity::WARNING, Severity::ERROR + * @param[in] message operator-facing narrative text + * @details Replaces the legacy pattern of enqueuing narrative strings onto + * the UDP async queue. Narrative messages are now routed through + * the PUB-SUB broadcast topic. Logging is preserved. + * + */ + void Sequence::broadcast( const std::string &function, + const std::string &severity, + const std::string &message ) { + logwrite( function, severity+": "+message ); + + if ( ! this->publisher ) return; + + nlohmann::json jmessage; + jmessage[Key::SOURCE] = Sequencer::DAEMON_NAME; + jmessage[Key::Broadcast::SEVERITY] = severity; + jmessage[Key::Broadcast::MESSAGE] = message; + + try { + this->publisher->publish( jmessage, Topic::BROADCAST ); + } + catch ( const std::exception &e ) { + logwrite( function, "ERROR publishing broadcast: "+std::string(e.what()) ); + } + } + /***** Sequencer::Sequence::broadcast ***************************************/ + + /***** Sequencer::Sequence::dothread_sequencer_async_listener ***************/ /** * @brief async message listening thread @@ -1960,6 +1989,7 @@ namespace Sequencer { ScopedState thr_state( thread_state_manager, Sequencer::THR_MOVE_TO_TARGET ); ScopedState wait_state( wait_state_manager, Sequencer::SEQ_WAIT_TCS ); + ScopedState wait_moveto( wait_state_manager, Sequencer::SEQ_WAIT_MOVETO ); // If RA and DEC fields are both empty then no telescope move // @@ -1972,7 +2002,7 @@ namespace Sequencer { // if ( this->target.ra_hms == this->last_ra_hms && this->target.dec_dms == this->last_dec_dms ) { - this->async.enqueue_and_log( function, "NOTICE: no move required for repeat target" ); + this->broadcast( function, Severity::NOTICE, "no move required for repeat target" ); return NO_ERROR; } @@ -1996,7 +2026,7 @@ namespace Sequencer { if ( ra_isnan ) { message << " RA=\"" << this->target.ra_hms << "\""; } if ( dec_isnan ) { message << " DEC=\"" << this->target.dec_dms << "\""; } message << " to decimal"; - this->async.enqueue_and_log( function, "ERROR "+message.str() ); + this->broadcast( function, Severity::ERROR, message.str() ); this->thread_error_manager.set( THR_MOVE_TO_TARGET ); throw std::runtime_error(message.str()); } @@ -2014,9 +2044,9 @@ namespace Sequencer { double _solved_angle = ( angle_out < 0 ? angle_out + 360.0 : angle_out ); if ( std::abs(_solved_angle) - std::abs(this->target.casangle) > 0.01 ) { - message.str(""); message << "NOTICE: Calculated angle " << angle_out + message.str(""); message << "Calculated angle " << angle_out << " is not equivalent to casangle " << this->target.casangle; - this->async.enqueue_and_log( function, message.str() ); + this->broadcast( function, Severity::NOTICE, message.str() ); } // Send coordinates using TCS-native COORDS command. @@ -2046,8 +2076,8 @@ namespace Sequencer { error = this->tcsd.send( coords_cmd.str(), coords_reply ); // send to the TCS // second failure return error if ( error != NO_ERROR || coords_reply.compare( 0, strlen(TCS_SUCCESS_STR), TCS_SUCCESS_STR ) != 0 ) { - message.str(""); message << "ERROR sending COORDS command. TCS reply: " << coords_reply; - this->async.enqueue_and_log( function, message.str() ); + message.str(""); message << "sending COORDS command. TCS reply: " << coords_reply; + this->broadcast( function, Severity::ERROR, message.str() ); this->thread_error_manager.set( THR_MOVE_TO_TARGET ); throw std::runtime_error("sending COORDS to TCS: "+coords_reply); } @@ -2060,7 +2090,7 @@ namespace Sequencer { std::stringstream ringgo_cmd; std::string noreply("DONTWAIT"); // indicates don't wait for reply ringgo_cmd << TCSD_RINGGO << " " << angle_out; // this is calculated cass angle - this->async.enqueue_and_log( function, "sending "+ringgo_cmd.str()+" to TCS" ); + this->broadcast( function, Severity::NOTICE, "sending "+ringgo_cmd.str()+" to TCS" ); error = this->tcsd.send( ringgo_cmd.str(), noreply ); } @@ -2068,16 +2098,16 @@ namespace Sequencer { { ScopedState wait_state( wait_state_manager, Sequencer::SEQ_WAIT_TCSOP ); - this->async.enqueue_and_log( function, "NOTICE: waiting for TCS operator to send \"ontarget\" signal" ); + this->broadcast( function, Severity::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( function, "NOTICE: received " - +(this->cancel_flag.load() ? std::string("cancel") : std::string("ontarget")) - +" signal!" ); + this->broadcast( function, Severity::NOTICE, "received " + +(this->cancel_flag.load() ? std::string("cancel") : std::string("ontarget")) + +" signal!" ); } // If waiting for TCS operator was cancelled then don't continue @@ -2300,6 +2330,13 @@ namespace Sequencer { /***** Sequencer::Sequence::abort_process *********************************/ /** * @brief tries to abort everything happening + * @details Sets SEQ_ABORTING via RAII for the duration of the abort, + * then on exit: + * - if aborting during RUNNING or PAUSED, restores SEQ_READY + * - if aborting during STARTING or STOPPING, sets SEQ_FAILED + * (indeterminate lifecycle state; requires startup/shutdown + * to clear) + * - otherwise leaves seqstate unchanged on exit * */ void Sequence::abort_process() { @@ -2307,6 +2344,26 @@ namespace Sequencer { ScopedState thr_state( this->thread_state_manager, Sequencer::THR_ABORT_PROCESS ); + // Decide post-abort seqstate before entering SEQ_ABORTING. + // + const bool abort_during_run = this->seq_state_manager.are_any_set( + Sequencer::SEQ_RUNNING, + Sequencer::SEQ_PAUSED ); + const bool abort_during_lifecycle = this->seq_state_manager.are_any_set( + Sequencer::SEQ_STARTING, + Sequencer::SEQ_STOPPING ); + + // RAII: SEQ_ABORTING set on entry, cleared on scope exit. + // + ScopedState seq_state( this->seq_state_manager, Sequencer::SEQ_ABORTING ); + + if ( abort_during_run ) { + seq_state.destruct_set( Sequencer::SEQ_READY ); + } + else if ( abort_during_lifecycle ) { + seq_state.destruct_set( Sequencer::SEQ_FAILED ); + } + this->cancel_flag.store(false); // stop any exposure that may be in progress @@ -2328,7 +2385,7 @@ namespace Sequencer { // this->do_once.store(true); - this->async.enqueue_and_log( function, "NOTICE: cancel signal sent" ); + this->broadcast( function, Severity::NOTICE, "cancel signal sent" ); } /***** Sequencer::Sequence::stop_exposure *********************************/ diff --git a/sequencerd/sequence.h b/sequencerd/sequence.h index ab86cc45..9e5bf84e 100644 --- a/sequencerd/sequence.h +++ b/sequencerd/sequence.h @@ -125,6 +125,8 @@ namespace Sequencer { SEQ_STOPPING, ///< set when sequencer is shutting down SEQ_PAUSED, ///< set when sequencer is paused SEQ_STARTING, ///< set when sequencer is starting up + SEQ_FAILED, ///< set on a fatal/indeterminate failure; cleared only by startup or shutdown + SEQ_ABORTING, ///< transitory; set/cleared via RAII in abort_process() NUM_SEQ_STATES }; @@ -134,7 +136,9 @@ namespace Sequencer { {SEQ_RUNNING, "RUNNING"}, {SEQ_STOPPING, "STOPPING"}, {SEQ_PAUSED, "PAUSED"}, - {SEQ_STARTING, "STARTING"} + {SEQ_STARTING, "STARTING"}, + {SEQ_FAILED, "FAILED"}, + {SEQ_ABORTING, "ABORTING"} }; /** @@ -153,7 +157,9 @@ namespace Sequencer { SEQ_WAIT_SLIT, ///< set when waiting for slit SEQ_WAIT_TCS, ///< set when waiting for tcs // states - SEQ_WAIT_ACQUIRE, ///< set when waiting for acquire + SEQ_WAIT_MOVETO, ///< set when waiting for move-to-target + SEQ_WAIT_ACAM_ACQUIRE, ///< set when waiting for ACAM acquire + SEQ_WAIT_FINEACQUIRE, ///< set when waiting for slicecam fineacquire SEQ_WAIT_EXPOSE, ///< set when waiting for camera exposure SEQ_WAIT_READOUT, ///< set when waiting for camera readout SEQ_WAIT_TCSOP, ///< set when waiting specifically for tcs operator @@ -163,21 +169,23 @@ namespace Sequencer { const std::map wait_state_names = { // daemons - {SEQ_WAIT_ACAM, "ACAM"}, - {SEQ_WAIT_CALIB, "CALIB"}, - {SEQ_WAIT_CAMERA, "CAMERA"}, - {SEQ_WAIT_FLEXURE, "FLEXURE"}, - {SEQ_WAIT_FOCUS, "FOCUS"}, - {SEQ_WAIT_POWER, "POWER"}, - {SEQ_WAIT_SLICECAM, "SLICECAM"}, - {SEQ_WAIT_SLIT, "SLIT"}, - {SEQ_WAIT_TCS, "TCS"}, + {SEQ_WAIT_ACAM, "ACAM"}, + {SEQ_WAIT_CALIB, "CALIB"}, + {SEQ_WAIT_CAMERA, "CAMERA"}, + {SEQ_WAIT_FLEXURE, "FLEXURE"}, + {SEQ_WAIT_FOCUS, "FOCUS"}, + {SEQ_WAIT_POWER, "POWER"}, + {SEQ_WAIT_SLICECAM, "SLICECAM"}, + {SEQ_WAIT_SLIT, "SLIT"}, + {SEQ_WAIT_TCS, "TCS"}, // states - {SEQ_WAIT_ACQUIRE, "ACQUIRE"}, - {SEQ_WAIT_EXPOSE, "EXPOSE"}, - {SEQ_WAIT_READOUT, "READOUT"}, - {SEQ_WAIT_TCSOP, "TCSOP"}, - {SEQ_WAIT_USER, "USER"} + {SEQ_WAIT_MOVETO, "MOVETO"}, + {SEQ_WAIT_ACAM_ACQUIRE, "ACAM_ACQUIRE"}, + {SEQ_WAIT_FINEACQUIRE, "FINEACQUIRE"}, + {SEQ_WAIT_EXPOSE, "EXPOSE"}, + {SEQ_WAIT_READOUT, "READOUT"}, + {SEQ_WAIT_TCSOP, "TCSOP"}, + {SEQ_WAIT_USER, "USER"} }; /** @@ -499,8 +507,12 @@ namespace Sequencer { /// void set_seqstate_bit( uint32_t mb ); ///< set the specified masked bit in the seqstate word void broadcast_daemonstate(); ///< void broadcast_threadstate(); ///< - void broadcast_seqstate(); ///< writes the seqstate string to the async port - void broadcast_waitstate(); ///< writes the waitstate string to the async port + void broadcast_seqstate(); ///< publishes the seqstate on the seq_seqstate topic + void broadcast_waitstate(); ///< publishes the waitstate on the seq_waitstate topic + + void broadcast( const std::string &function, + const std::string &severity, + const std::string &message ); ///< logs and publishes a narrative message on Topic::BROADCAST uint32_t get_reqstate(); ///< get the reqstate word diff --git a/sequencerd/sequence_acquisition.cpp b/sequencerd/sequence_acquisition.cpp index 7beab093..ea023be9 100644 --- a/sequencerd/sequence_acquisition.cpp +++ b/sequencerd/sequence_acquisition.cpp @@ -20,7 +20,7 @@ namespace Sequencer { std::string reply; ScopedState thr_state( thread_state_manager, Sequencer::THR_ACQUISITION ); - ScopedState wait_state( wait_state_manager, Sequencer::SEQ_WAIT_ACQUIRE ); + ScopedState wait_state( wait_state_manager, Sequencer::SEQ_WAIT_ACAM_ACQUIRE ); // form and send the ACQUIRE command to ACAM // @@ -29,17 +29,17 @@ namespace Sequencer { double angle_in = this->target.slitangle; if ( std::isnan(ra_in) || std::isnan(dec_in) ) { - this->async.enqueue_and_log( function, "ERROR converting target coordinates to decimal" ); + this->broadcast( function, Severity::ERROR, "converting target coordinates to decimal" ); return ERROR; } std::ostringstream cmd; cmd << ACAMD_ACQUIRE << " " << ra_in << " " << dec_in << " " << angle_in; - this->async.enqueue_and_log( function, "NOTICE: starting ACAM acquisition" ); + this->broadcast( function, Severity::NOTICE, "starting ACAM acquisition" ); if ( this->acamd.command( cmd.str(), reply ) != NO_ERROR ) { - this->async.enqueue_and_log( function, "ERROR sending acquire command to acamd" ); + this->broadcast( function, Severity::ERROR, "sending acquire command to acamd" ); return ERROR; } @@ -58,11 +58,11 @@ namespace Sequencer { if (this->cancel_flag.load()) return ABORT; if (use_timeout && !this->is_acam_guiding.load()) { - this->async.enqueue_and_log(function, "ERROR ACAM acquisition timed out!"); + this->broadcast( function, Severity::ERROR, "ACAM acquisition timed out!" ); return TIMEOUT; } - this->async.enqueue_and_log(function, "ACAM target acquired"); + this->broadcast( function, Severity::NOTICE, "ACAM target acquired" ); return NO_ERROR; } /***** Sequencer::Sequence::do_acam_acquire **********************************/ @@ -77,21 +77,21 @@ namespace Sequencer { long Sequence::do_slicecam_fineacquire() { const std::string function("Sequencer::Sequence::do_slicecam_fineacquire"); - ScopedState wait_state(wait_state_manager, Sequencer::SEQ_WAIT_ACQUIRE); + ScopedState wait_state(wait_state_manager, Sequencer::SEQ_WAIT_FINEACQUIRE); // TODO don't hard-code the arguments here: std::string reply; if (this->slicecamd.command( SLICECAMD_FINEACQUIRE+" start L", reply ) != NO_ERROR) { - this->async.enqueue_and_log(function, "ERROR starting slicecam fine acquisition"); + this->broadcast( function, Severity::ERROR, "starting slicecam fine acquisition" ); return ERROR; } if ( reply.find("ERROR") != std::string::npos ) { - this->async.enqueue_and_log(function, "slicecam fine acquisition mode: "+reply); + this->broadcast( function, Severity::ERROR, "slicecam fine acquisition mode: "+reply ); return ERROR; } - this->async.enqueue_and_log(function, "NOTICE: slicecam fine acquisition started"); + this->broadcast( function, Severity::NOTICE, "slicecam fine acquisition started" ); const bool use_timeout = ( this->acquisition_timeout > 0 ); const auto timeout_time = std::chrono::steady_clock::now() @@ -108,11 +108,11 @@ namespace Sequencer { if (this->cancel_flag.load()) return ABORT; if (use_timeout && !this->is_fineacquire_locked.load()) { - this->async.enqueue_and_log(function, "ERROR slicecam fine acquisition timed out!"); + this->broadcast( function, Severity::ERROR, "slicecam fine acquisition timed out!" ); return TIMEOUT; } - this->async.enqueue_and_log(function, "slicecam fine acquisition target acquired"); + this->broadcast( function, Severity::NOTICE, "slicecam fine acquisition target acquired" ); return NO_ERROR; } /***** Sequencer::Sequence::do_slicecam_fineacquire **************************/ From 2c57e985366181f655fc2a313e997dea24bde9d6 Mon Sep 17 00:00:00 2001 From: David Hale Date: Fri, 24 Apr 2026 18:49:40 -0700 Subject: [PATCH 2/8] adds updated message_keys.h missing from last commit --- common/message_keys.h | 58 ++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 57 insertions(+), 1 deletion(-) diff --git a/common/message_keys.h b/common/message_keys.h index ab7d8a46..12ef41c7 100644 --- a/common/message_keys.h +++ b/common/message_keys.h @@ -8,13 +8,39 @@ #include +namespace Daemon { + inline const std::string ACAMD = "acamd"; + inline const std::string CALIBD = "calibd"; + inline const std::string CAMERAD = "camerad"; + inline const std::string FLEXURED = "flexured"; + inline const std::string FOCUSD = "focusd"; + inline const std::string POWERD = "powerd"; + inline const std::string SEQUENCER = "sequencerd"; + inline const std::string SLICECAMD = "slicecamd"; + inline const std::string SLITD = "slitd"; + inline const std::string TCSD = "tcsd"; + inline const std::string THERMALD = "thermald"; +} + +namespace Severity { + inline const std::string NOTICE = "NOTICE"; + inline const std::string WARNING = "WARNING"; + inline const std::string ERROR = "ERROR"; +} + namespace Topic { inline const std::string SNAPSHOT = "_snapshot"; inline const std::string TARGETINFO = "targetinfo"; + inline const std::string BROADCAST = "broadcast"; inline const std::string TCSD = "tcsd"; inline const std::string SLITD = "slitd"; inline const std::string CAMERAD = "camerad"; inline const std::string ACAMD = "acamd"; + inline const std::string CALIBD = "calibd"; + inline const std::string FLEXURED = "flexured"; + inline const std::string FOCUSD = "focusd"; + inline const std::string POWERD = "powerd"; + inline const std::string THERMALD = "thermald"; inline const std::string SEQ_DAEMONSTATE = "seq_daemonstate"; inline const std::string SEQ_SEQSTATE = "seq_seqstate"; inline const std::string SEQ_THREADSTATE = "seq_threadstate"; @@ -26,12 +52,23 @@ namespace Key { inline const std::string SOURCE = "source"; + namespace Broadcast { + inline const std::string SEVERITY = "severity"; + inline const std::string MESSAGE = "message"; + } + namespace Sequencer { inline const std::string SEQSTATE = "seqstate"; } namespace Camerad { - inline const std::string READY = "ready"; + inline const std::string READY = "ready"; + inline const std::string SHUTTERTIME = "shuttime_sec"; + inline const std::string EXPTIME = "exptime"; + inline const std::string IMNUM = "imnum"; + inline const std::string IMNAME = "imname"; + inline const std::string FRAMECOUNT = "framecount"; + inline const std::string FRAMETRANSFER = "frametransfer"; } namespace Acamd { @@ -50,4 +87,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"; + } + } From 1e6c49629a21de1d4736a41e7e72756ef65b088d Mon Sep 17 00:00:00 2001 From: David Hale Date: Sat, 25 Apr 2026 07:20:00 -0700 Subject: [PATCH 3/8] replaces more async UDP broadcasts with new broadcast call (zmq-pub) adds FALIED state adds seqmon utility (as proxy for GUI) --- sequencerd/sequence.cpp | 193 +++++++++++++------------- utils/CMakeLists.txt | 12 ++ utils/seqmon.cpp | 290 ++++++++++++++++++++++++++++++++++++++++ 3 files changed, 400 insertions(+), 95 deletions(-) create mode 100644 utils/seqmon.cpp diff --git a/sequencerd/sequence.cpp b/sequencerd/sequence.cpp index 034f7573..0ec00272 100644 --- a/sequencerd/sequence.cpp +++ b/sequencerd/sequence.cpp @@ -449,26 +449,26 @@ namespace Sequencer { { 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->broadcast( function, Severity::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->broadcast( function, Severity::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->broadcast( function, Severity::NOTICE, "sequence cancelled" ); return ABORT; } this->is_usercontinue.store(false); - this->async.enqueue_and_log( function, "NOTICE: received USER continue signal!" ); + this->broadcast( function, Severity::NOTICE, "received USER continue signal!" ); return NO_ERROR; } @@ -500,14 +500,14 @@ namespace Sequencer { // The Sequencer can only be started once // if ( thread_state_manager.is_set( Sequencer::THR_SEQUENCE_START ) ) { - this->async.enqueue_and_log( function, "ERROR sequencer already running" ); + this->broadcast( function, Severity::ERROR, "sequencer already running" ); return; } // The Sequencer can only be started when state is READY // if ( ! seq_state_manager.is_set( Sequencer::SEQ_READY ) ) { - this->async.enqueue_and_log( function, "ERROR cannot start: system not ready" ); + this->broadcast( function, Severity::ERROR, "cannot start: system not ready" ); return; } @@ -565,9 +565,9 @@ namespace Sequencer { // if ( ! this->daemon_manager.is_set( Sequencer::DAEMON_TCS ) ) { if ( ! this->target.ra_hms.empty() || ! this->target.dec_dms.empty() ) { - message.str(""); message << "ERROR cannot move to target " << this->target.name + message.str(""); message << "cannot move to target " << this->target.name << " because TCS is not connected"; - this->async.enqueue_and_log( function, message.str() ); + this->broadcast( function, Severity::ERROR, message.str() ); this->thread_error_manager.set( THR_SEQUENCE_START ); // report error break; } @@ -594,7 +594,7 @@ namespace Sequencer { } else if ( targetstate == TargetInfo::TARGET_ERROR ) { // request stop on error - this->async.enqueue_and_log( function, "ERROR getting next target. stopping" ); + this->broadcast( function, Severity::ERROR, "getting next target. stopping" ); break; } @@ -661,14 +661,14 @@ namespace Sequencer { logwrite(function, "DONE waiting on threads"); if ( this->cancel_flag.load() ) { - this->async.enqueue_and_log( function, "NOTICE: sequence cancelled" ); + this->broadcast( function, Severity::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" ); + this->broadcast( function, Severity::NOTICE, "target list processing has stopped" ); break; } @@ -678,16 +678,16 @@ namespace Sequencer { // 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" ); + this->broadcast( function, Severity::WARNING, "acam acquisition failed" ); if (this->wait_for_user()==ABORT) { - this->async.enqueue_and_log( function, "NOTICE: cancelled" ); + this->broadcast( function, Severity::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" ); + this->broadcast( function, Severity::WARNING, "slicecam fine acquisition failed" ); } } @@ -695,7 +695,7 @@ namespace Sequencer { // 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" ); + this->broadcast( function, Severity::NOTICE, "cancelled" ); return; } } @@ -736,7 +736,7 @@ namespace Sequencer { // 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" ); + this->broadcast( function, Severity::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 @@ -745,7 +745,7 @@ namespace Sequencer { return; } - this->async.enqueue_and_log( function, "NOTICE: done waiting for expose" ); + this->broadcast( function, Severity::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() ); @@ -830,7 +830,7 @@ namespace Sequencer { 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->broadcast(function, Severity::NOTICE, "waiting for camera to be ready to expose"); this->camerad_cv.wait( lock, [this]() { return( this->can_expose.load() || this->cancel_flag.load() ); @@ -866,14 +866,14 @@ namespace Sequencer { if (!activechans.str().empty()) { std::string cmd = CAMERAD_ACTIVATE + activechans.str(); if (this->camerad.send(cmd, reply)!=NO_ERROR) { - this->async.enqueue_and_log(function, "ERROR sending \""+cmd+"\": "+reply); + this->broadcast(function, Severity::ERROR, "sending \""+cmd+"\": "+reply); throw std::runtime_error("camera returned "+reply); } } if (!deactivechans.str().empty()) { std::string cmd = CAMERAD_DEACTIVATE + deactivechans.str(); if (this->camerad.send(cmd, reply)!=NO_ERROR) { - this->async.enqueue_and_log(function, "ERROR sending \""+cmd+"\": "+reply); + this->broadcast(function, Severity::ERROR, "sending \""+cmd+"\": "+reply); throw std::runtime_error("camera returned "+reply); } } @@ -886,7 +886,7 @@ namespace Sequencer { long exptime_msec = (long)( this->target.exptime_req * 1000 ); camcmd.str(""); camcmd << CAMERAD_EXPTIME << " " << exptime_msec; if (error==NO_ERROR && (error=this->camerad.send( camcmd.str(), reply ))!=NO_ERROR) { - this->async.enqueue_and_log( function, "ERROR sending \""+camcmd.str()+"\": "+reply ); + this->broadcast( function, Severity::ERROR, "sending \""+camcmd.str()+"\": "+reply ); throw std::runtime_error( "camera returned "+reply ); } @@ -894,12 +894,12 @@ namespace Sequencer { // camcmd.str(""); camcmd << CAMERAD_BIN << " spat " << this->target.binspat; if (error==NO_ERROR && (error=this->camerad.send( camcmd.str(), reply ))!=NO_ERROR) { - this->async.enqueue_and_log( function, "ERROR sending \""+camcmd.str()+"\": "+reply ); + this->broadcast( function, Severity::ERROR, "sending \""+camcmd.str()+"\": "+reply ); throw std::runtime_error( "camera returned "+reply ); } camcmd.str(""); camcmd << CAMERAD_BIN << " spec " << this->target.binspect; if (error==NO_ERROR && (error=this->camerad.send( camcmd.str(), reply ))!=NO_ERROR) { - this->async.enqueue_and_log( function, "ERROR sending \""+camcmd.str()+"\": "+reply ); + this->broadcast( function, Severity::ERROR, "sending \""+camcmd.str()+"\": "+reply ); throw std::runtime_error( "camera returned "+reply ); } @@ -960,7 +960,7 @@ namespace Sequencer { logwrite( function, "moving slit to "+slitcmd.str()+" for "+modestr+"position" ); if ( this->slitd.command_timeout( slitcmd.str(), reply, SLITD_SET_TIMEOUT ) != NO_ERROR ) { - this->async.enqueue_and_log( function, "ERROR setting slit" ); + this->broadcast( function, Severity::ERROR, "setting slit" ); this->thread_error_manager.set( THR_SLIT_SET ); throw std::runtime_error("slit returned: "+reply); } @@ -986,7 +986,7 @@ namespace Sequencer { this->daemon_manager.clear( Sequencer::DAEMON_POWER ); // powerd not ready if ( this->reopen_hardware(this->powerd, POWERD_REOPEN, 10000 ) != NO_ERROR ) { - this->async.enqueue_and_log( function, "ERROR initializing power control" ); + this->broadcast( function, Severity::ERROR, "initializing power control" ); throw std::runtime_error("could not initialize power control"); } @@ -1038,13 +1038,13 @@ namespace Sequencer { this->thread_error_manager.set( THR_SLIT_INIT ); // assume the worst, clear on success if ( this->set_power_switch(ON, POWER_SLIT, std::chrono::seconds(5)) != NO_ERROR ) { - this->async.enqueue_and_log( function, "ERROR powering slit hardware" ); + this->broadcast( function, Severity::ERROR, "powering slit hardware" ); throw std::runtime_error("could not power slit hardware"); } bool was_opened=false; if ( this->open_hardware(this->slitd, was_opened) != NO_ERROR ) { - this->async.enqueue_and_log( function, "ERROR connecting to slit" ); + this->broadcast( function, Severity::ERROR, "connecting to slit" ); throw std::runtime_error("could not open connection to slit hardware"); } @@ -1053,7 +1053,7 @@ namespace Sequencer { bool ishomed=false; std::string reply; if ( this->slitd.command( SLITD_ISHOME, reply ) ) { - this->async.enqueue_and_log( function, "ERROR communicating with slit hardware" ); + this->broadcast( function, Severity::ERROR, "communicating with slit hardware" ); throw std::runtime_error("could not communicate with slit hardware: "+reply); } this->parse_state( function, reply, ishomed ); @@ -1063,7 +1063,7 @@ namespace Sequencer { if ( !ishomed ) { logwrite( function, "sending home command" ); if ( this->slitd.command_timeout( SLITD_HOME, reply, SLITD_HOME_TIMEOUT ) != NO_ERROR ) { - this->async.enqueue_and_log( function, "ERROR communicating with slit hardware" ); + this->broadcast( function, Severity::ERROR, "communicating with slit hardware" ); throw std::runtime_error("could not home slit hardware: "+reply); } } @@ -1073,7 +1073,7 @@ namespace Sequencer { if ( was_opened && !this->config_init["SLIT"].empty() ) { std::string cmd = SLITD_SET+" "+this->config_init["SLIT"]; if ( this->slitd.command_timeout( cmd, reply, SLITD_SET_TIMEOUT ) != NO_ERROR ) { - this->async.enqueue_and_log( function, "ERROR sending \""+cmd+"\" to slit" ); + this->broadcast( function, Severity::ERROR, "sending \""+cmd+"\" to slit" ); throw std::runtime_error("slit "+cmd+" returned: "+reply); } } @@ -1127,7 +1127,7 @@ namespace Sequencer { if (error==NO_ERROR && !this->config_shutdown["SLIT"].empty() ) { std::string cmd = SLITD_SET+" "+this->config_shutdown["SLIT"]; if ( this->slitd.command_timeout( cmd, reply, SLITD_SET_TIMEOUT ) != NO_ERROR ) { - this->async.enqueue_and_log( function, "ERROR sending \""+cmd+"\" to slit" ); + this->broadcast( function, Severity::ERROR, "sending \""+cmd+"\" to slit" ); throw std::runtime_error(cmd+" returned: "+reply); } } @@ -1137,7 +1137,7 @@ namespace Sequencer { logwrite( function, "closing slit hardware" ); error = this->slitd.command( SLITD_CLOSE, reply ); if ( error != NO_ERROR ) { - this->async.enqueue_and_log( function, "ERROR closing connection to slit hardware" ); + this->broadcast( function, Severity::ERROR, "closing connection to slit hardware" ); throw std::runtime_error("closing slit connection returned: "+reply); } @@ -1174,14 +1174,14 @@ namespace Sequencer { // make sure hardware is powered on // if ( this->set_power_switch(ON, POWER_SLICECAM, std::chrono::seconds(10)) != NO_ERROR ) { - this->async.enqueue_and_log( function, "ERROR initializing slicecam control" ); + this->broadcast( function, Severity::ERROR, "initializing slicecam control" ); throw std::runtime_error("could not power slicecam hardware"); } // open connection is all that is needed, slicecamd takes care of everything // if ( this->open_hardware(this->slicecamd, SLICECAMD_OPEN, SLICECAMD_OPEN_TIMEOUT) != NO_ERROR ) { - this->async.enqueue_and_log( function, "ERROR starting slicecam" ); + this->broadcast( function, Severity::ERROR, "starting slicecam" ); throw SlicecamException("could not start slicecam"); } @@ -1214,7 +1214,7 @@ namespace Sequencer { // make sure hardware is powered on // if ( this->set_power_switch(ON, POWER_ACAM, std::chrono::seconds(10)) != NO_ERROR ) { - this->async.enqueue_and_log( function, "ERROR powering acam hardware" ); + this->broadcast( function, Severity::ERROR, "powering acam hardware" ); throw std::runtime_error("could not power acam hardware"); } @@ -1222,7 +1222,7 @@ namespace Sequencer { // bool was_opened=false; if ( this->open_hardware(this->acamd, ACAMD_OPEN, ACAMD_OPEN_TIMEOUT, was_opened) != NO_ERROR ) { - this->async.enqueue_and_log( function, "ERROR opening acam camera" ); + this->broadcast( function, Severity::ERROR, "opening acam camera" ); throw AcamException(ErrorCode::ERROR_ACAM_CAMERA, "could not open acam camera"); } @@ -1233,14 +1233,14 @@ namespace Sequencer { if ( ! this->config_init["ACAM_FILTER"].empty() ) { cmd = ACAMD_FILTER+" "+this->config_init["ACAM_FILTER"]; if ( this->acamd.command_timeout( cmd, reply, ACAMD_MOVE_TIMEOUT ) != NO_ERROR ) { - this->async.enqueue_and_log( function, "ERROR sending \""+cmd+"\" to acamd: "+reply ); + this->broadcast( function, Severity::ERROR, "sending \""+cmd+"\" to acamd: "+reply ); throw std::runtime_error("acam "+cmd+" returned: "+reply); } } if ( ! this->config_init["ACAM_COVER"].empty() ) { cmd = ACAMD_COVER+" "+this->config_init["ACAM_COVER"]; if ( this->acamd.command_timeout( cmd, reply, ACAMD_MOVE_TIMEOUT ) != NO_ERROR ) { - this->async.enqueue_and_log( function, "ERROR sending \""+cmd+"\" to acamd: "+reply ); + this->broadcast( function, Severity::ERROR, "sending \""+cmd+"\" to acamd: "+reply ); throw std::runtime_error("acam "+cmd+" returned: "+reply); } } @@ -1289,14 +1289,14 @@ namespace Sequencer { } if ( (error=this->connect_to_daemon(this->slicecamd)) != NO_ERROR ) { - this->async.enqueue_and_log(function, "ERROR connecting to slicecamd"); + this->broadcast(function, Severity::ERROR, "connecting to slicecamd"); } // close connections between slicecamd and the hardware with which it communicates // logwrite( function, "closing slicecam hardware" ); if ( (error=this->slicecamd.command_timeout( SLICECAMD_SHUTDOWN, reply, SLICECAMD_SHUTDOWN_TIMEOUT )) != NO_ERROR ) { - this->async.enqueue_and_log( function, "ERROR closing connection to slicecam hardware" ); + this->broadcast( function, Severity::ERROR, "closing connection to slicecam hardware" ); } // disconnect me from slicecamd, irrespective of any previous error @@ -1307,7 +1307,7 @@ namespace Sequencer { // Turn off power to slicecam hardware. // if ( this->set_power_switch(OFF, POWER_SLICECAM, std::chrono::seconds(0)) != NO_ERROR ) { - this->async.enqueue_and_log( function, "ERROR switching off slicecam" ); + this->broadcast( function, Severity::ERROR, "switching off slicecam" ); throw std::runtime_error("could not power off slicecam hardware"); } @@ -1347,14 +1347,14 @@ namespace Sequencer { if ( ! this->config_shutdown["ACAM_FILTER"].empty() ) { cmd = ACAMD_FILTER+" "+this->config_shutdown["ACAM_FILTER"]; if ( this->acamd.command_timeout( cmd, reply, ACAMD_MOVE_TIMEOUT ) != NO_ERROR ) { - this->async.enqueue_and_log( function, "ERROR sending \""+cmd+"\" to acamd: "+reply ); + this->broadcast( function, Severity::ERROR, "sending \""+cmd+"\" to acamd: "+reply ); throw std::runtime_error("acam "+cmd+" returned: "+reply); } } if ( ! this->config_shutdown["ACAM_COVER"].empty() ) { cmd = ACAMD_COVER+" "+this->config_shutdown["ACAM_COVER"]; if ( this->acamd.command_timeout( cmd, reply, ACAMD_MOVE_TIMEOUT ) != NO_ERROR ) { - this->async.enqueue_and_log( function, "ERROR sending \""+cmd+"\" to acamd: "+reply ); + this->broadcast( function, Severity::ERROR, "sending \""+cmd+"\" to acamd: "+reply ); throw std::runtime_error("acam "+cmd+" returned: "+reply); } } @@ -1367,7 +1367,7 @@ namespace Sequencer { if ( error==NO_ERROR ) { logwrite( function, "closing acam hardware" ); error = this->acamd.command_timeout( ACAMD_SHUTDOWN, ACAMD_SHUTDOWN_TIMEOUT ); - if ( error != NO_ERROR ) this->async.enqueue_and_log( function, "ERROR shutting down acam" ); + if ( error != NO_ERROR ) this->broadcast( function, Severity::ERROR, "shutting down acam" ); } // disconnect me from acamd, irrespective of any previous error @@ -1378,7 +1378,7 @@ namespace Sequencer { // Turn off power to acam hardware. // if ( this->set_power_switch(OFF, POWER_ACAM, std::chrono::seconds(0)) != NO_ERROR ) { - this->async.enqueue_and_log( function, "ERROR switching off acam" ); + this->broadcast( function, Severity::ERROR, "switching off acam" ); throw std::runtime_error("could not switch off acam"); } @@ -1408,14 +1408,14 @@ namespace Sequencer { // make sure calib hardware is powered if ( this->set_power_switch(ON, POWER_CALIB, std::chrono::seconds(5)) != NO_ERROR ) { - this->async.enqueue_and_log( function, "ERROR powering focus control" ); + this->broadcast( function, Severity::ERROR, "powering focus control" ); throw std::runtime_error("could not power focus control"); } // connect to calibd bool was_opened=false; if ( this->open_hardware(this->calibd, was_opened) != NO_ERROR ) { - this->async.enqueue_and_log( function, "ERROR initializing calib control" ); + this->broadcast( function, Severity::ERROR, "initializing calib control" ); throw std::runtime_error("could not power calib control"); } @@ -1427,14 +1427,14 @@ namespace Sequencer { std::string reply; long error = this->calibd.command( CALIBD_ISHOME, reply ); if ( error!=NO_ERROR || this->parse_state( function, reply, ishomed ) != NO_ERROR ) { - this->async.enqueue_and_log( function, "ERROR communicating with calib hardware" ); + this->broadcast( function, Severity::ERROR, "communicating with calib hardware" ); throw std::runtime_error("could not communicate with calib hardware: "+reply); } // home calib actuators if not already homed if ( !ishomed ) { logwrite( function, "sending home command" ); if ( this->calibd.command_timeout( CALIBD_HOME, reply, CALIBD_HOME_TIMEOUT ) != NO_ERROR ) { - this->async.enqueue_and_log( function, "ERROR communicating with calib hardware" ); + this->broadcast( function, Severity::ERROR, "communicating with calib hardware" ); throw std::runtime_error("could not communicate with calib hardware: "+reply); } } @@ -1447,7 +1447,7 @@ namespace Sequencer { if ( !this->config_init["CALIB_DOOR"].empty() ) cmd << " door=" << this->config_init["CALIB_DOOR"]; logwrite( function, "calib default: "+cmd.str() ); if ( this->calibd.command_timeout( cmd.str(), CALIBD_SET_TIMEOUT ) != NO_ERROR ) { - this->async.enqueue_and_log( function, "ERROR moving calib door and/or cover" ); + this->broadcast( function, Severity::ERROR, "moving calib door and/or cover" ); throw std::runtime_error("could not move calib door and/or cover"); } } @@ -1484,7 +1484,7 @@ namespace Sequencer { // bool poweron=false; if ( check_power_switch(ON, POWER_CALIB, poweron ) != NO_ERROR ) { - this->async.enqueue_and_log( function, "ERROR checking calib power switch" ); + this->broadcast( function, Severity::ERROR, "checking calib power switch" ); throw std::runtime_error("checking calib power switch"); } @@ -1502,7 +1502,7 @@ namespace Sequencer { if ( !this->config_shutdown["CALIB_DOOR"].empty() ) cmd << " door=" << this->config_shutdown["CALIB_DOOR"]; logwrite( function, "calib default: "+cmd.str() ); if ( this->calibd.command_timeout( cmd.str(), CALIBD_SET_TIMEOUT ) != NO_ERROR ) { - this->async.enqueue_and_log( function, "ERROR moving calib door and/or cover" ); + this->broadcast( function, Severity::ERROR, "moving calib door and/or cover" ); throw std::runtime_error("moving calib door and/or cover"); } } @@ -1515,7 +1515,7 @@ namespace Sequencer { std::string reply; logwrite( function, "closing calib hardware" ); error = this->calibd.send( CALIBD_CLOSE, reply ); - if ( error != NO_ERROR ) this->async.enqueue_and_log( function, "ERROR closing connection to calib hardware" ); + if ( error != NO_ERROR ) this->broadcast( function, Severity::ERROR, "closing connection to calib hardware" ); } // disconnect me from calibd, irrespective of any previous error @@ -1526,14 +1526,14 @@ namespace Sequencer { // Turn off power to calib hardware. // if ( this->set_power_switch(OFF, POWER_CALIB, std::chrono::seconds(0)) != NO_ERROR ) { - this->async.enqueue_and_log( function, "ERROR switching off calib hardware" ); + this->broadcast( function, Severity::ERROR, "switching off calib hardware" ); error=ERROR; } // always turn off power to lamps // if ( this->set_power_switch(OFF, POWER_LAMP, std::chrono::seconds(5)) != NO_ERROR ) { - this->async.enqueue_and_log( function, "ERROR powering off lamps" ); + this->broadcast( function, Severity::ERROR, "powering off lamps" ); error=ERROR; } @@ -1567,7 +1567,7 @@ namespace Sequencer { this->daemon_manager.clear( Sequencer::DAEMON_TCS ); // tcsd not ready if ( this->open_hardware(this->tcsd) != NO_ERROR ) { - this->async.enqueue_and_log( "Sequencer::Sequence::tcs_init", "ERROR initializing TCS" ); + this->broadcast( "Sequencer::Sequence::tcs_init", Severity::ERROR, "initializing TCS" ); this->thread_error_manager.set( THR_TCS_INIT ); throw std::runtime_error("could not initialize TCS"); } @@ -1611,7 +1611,7 @@ namespace Sequencer { std::string reply; error = this->tcsd.send( TCSD_CLOSE, reply ); if ( error != NO_ERROR ) { - this->async.enqueue_and_log( function, "ERROR: closing connection to TCS" ); + this->broadcast( function, Severity::ERROR, "closing connection to TCS" ); throw std::runtime_error("closing TCS connection: "+reply); } } @@ -1645,13 +1645,13 @@ namespace Sequencer { // make sure hardware is powered on // if ( this->set_power_switch(ON, POWER_FLEXURE, std::chrono::seconds(21)) != NO_ERROR ) { - this->async.enqueue_and_log( function, "ERROR powering flexure control" ); + this->broadcast( function, Severity::ERROR, "powering flexure control" ); this->thread_error_manager.set( THR_FLEXURE_INIT ); throw std::runtime_error("could not power flexure control"); } if ( this->open_hardware(this->flexured) != NO_ERROR ) { - this->async.enqueue_and_log( function, "ERROR initializing flexure control" ); + this->broadcast( function, Severity::ERROR, "initializing flexure control" ); this->thread_error_manager.set( THR_FLEXURE_INIT ); throw std::runtime_error("could not initialize flexure control"); } @@ -1698,7 +1698,7 @@ namespace Sequencer { } if ( this->connect_to_daemon(this->flexured) != NO_ERROR ) { - this->async.enqueue_and_log( function, "ERROR connecting to flexure hardware" ); + this->broadcast( function, Severity::ERROR, "connecting to flexure hardware" ); error=ERROR; } @@ -1707,7 +1707,7 @@ namespace Sequencer { // logwrite( function, "closing flexure hardware" ); if (error==NO_ERROR && (error=this->flexured.command( FLEXURED_CLOSE, reply )) != NO_ERROR) { - this->async.enqueue_and_log( function, "ERROR closing connection to flexure hardware" ); + this->broadcast( function, Severity::ERROR, "closing connection to flexure hardware" ); } // disconnect me from flexured, irrespective of any previous error @@ -1718,7 +1718,7 @@ namespace Sequencer { // Turn off power to flexure hardware. // if ( this->set_power_switch(OFF, POWER_FLEXURE, std::chrono::seconds(0)) != NO_ERROR ) { - this->async.enqueue_and_log( function, "ERROR switching off flexure" ); + this->broadcast( function, Severity::ERROR, "switching off flexure" ); throw std::runtime_error("switching off flexure hardware"); } @@ -1745,14 +1745,14 @@ namespace Sequencer { this->thread_error_manager.set( THR_FOCUS_INIT ); // assume failure, clear on success if ( this->set_power_switch(ON, POWER_FOCUS, std::chrono::seconds(5)) != NO_ERROR ) { - this->async.enqueue_and_log( function, "ERROR powering focus control" ); + this->broadcast( function, Severity::ERROR, "powering focus control" ); throw std::runtime_error("could not power focus control"); } // connect to focusd bool was_opened=false; if ( this->open_hardware(this->focusd, was_opened) != NO_ERROR ) { - this->async.enqueue_and_log( function, "ERROR initializing focus control" ); + this->broadcast( function, Severity::ERROR, "initializing focus control" ); throw std::runtime_error("could not open focus hardware"); } @@ -1764,14 +1764,14 @@ namespace Sequencer { std::string reply; long error = this->focusd.command( FOCUSD_ISHOME, reply ); if ( error!=NO_ERROR || this->parse_state( function, reply, ishomed ) != NO_ERROR ) { - this->async.enqueue_and_log( function, "ERROR communicating with focus hardware" ); + this->broadcast( function, Severity::ERROR, "communicating with focus hardware" ); throw std::runtime_error("focus "+FOCUSD_ISHOME+" returned: "+reply); } // home focus actuators if not already homed if ( !ishomed ) { logwrite( function, "sending home command" ); if ( this->focusd.command_timeout( FOCUSD_HOME, reply, FOCUSD_HOME_TIMEOUT ) != NO_ERROR ) { - this->async.enqueue_and_log( function, "ERROR communicating with focus hardware" ); + this->broadcast( function, Severity::ERROR, "communicating with focus hardware" ); throw std::runtime_error("focus "+FOCUSD_HOME+" returned: "+reply); } } @@ -1781,7 +1781,7 @@ namespace Sequencer { for ( const auto &chan : chans ) { std::string command = "set " + chan + " nominal"; if ( this->focusd.command_timeout( command, reply, FOCUSD_SET_TIMEOUT ) != NO_ERROR ) { - this->async.enqueue_and_log( function, "ERROR setting focus "+chan ); + this->broadcast( function, Severity::ERROR, "setting focus "+chan ); throw std::runtime_error("focus "+command+" returned: "+reply); } } @@ -1827,7 +1827,7 @@ namespace Sequencer { } if ( this->connect_to_daemon(this->focusd) != NO_ERROR ) { - this->async.enqueue_and_log( function, "ERROR connecting to focus hardware" ); + this->broadcast( function, Severity::ERROR, "connecting to focus hardware" ); error=ERROR; } @@ -1836,7 +1836,7 @@ namespace Sequencer { // logwrite( function, "closing focus hardware" ); if (error==NO_ERROR && (error=this->focusd.command( FOCUSD_CLOSE, reply )) != NO_ERROR) { - this->async.enqueue_and_log( function, "ERROR closing connection to focus hardware" ); + this->broadcast( function, Severity::ERROR, "closing connection to focus hardware" ); } // disconnect me from focusd, irrespective of any previous error @@ -1847,7 +1847,7 @@ namespace Sequencer { // Turn off power to focus hardware. // if ( this->set_power_switch(OFF, POWER_FOCUS, std::chrono::seconds(0)) != NO_ERROR ) { - this->async.enqueue_and_log( function, "ERROR switching off focus" ); + this->broadcast( function, Severity::ERROR, "switching off focus" ); throw std::runtime_error("switching off focus hardware"); } @@ -1875,13 +1875,13 @@ namespace Sequencer { // make sure hardware is powered on // if ( this->set_power_switch(ON, POWER_CAMERA, std::chrono::seconds(5)) != NO_ERROR ) { - this->async.enqueue_and_log( function, "ERROR powering camera" ); + this->broadcast( function, Severity::ERROR, "powering camera" ); throw std::runtime_error("switching on camera"); } bool was_opened=false; if ( this->open_hardware(this->camerad, "open", 12000, was_opened) != NO_ERROR ) { - this->async.enqueue_and_log( function, "ERROR initializing camera" ); + this->broadcast( function, Severity::ERROR, "initializing camera" ); throw std::runtime_error("initializing camera"); } @@ -1891,7 +1891,7 @@ namespace Sequencer { if ( was_opened) { for ( const auto &cmd : this->camera_prologue ) { if ( this->camerad.command_timeout( cmd, reply, CAMERA_PROLOG_TIMEOUT ) != NO_ERROR ) { - this->async.enqueue_and_log( function, "ERROR sending \""+cmd+"\" to camera" ); + this->broadcast( function, Severity::ERROR, "sending \""+cmd+"\" to camera" ); throw std::runtime_error("sending \""+cmd+"\" to camera"); } } @@ -1962,7 +1962,7 @@ namespace Sequencer { // turn off power to camera hardware // if ( this->set_power_switch(OFF, POWER_CAMERA, std::chrono::seconds(5)) != NO_ERROR ) { - this->async.enqueue_and_log( function, "ERROR powering off camera" ); + this->broadcast( function, Severity::ERROR, "powering off camera" ); throw std::runtime_error("switching off camera"); } @@ -2263,7 +2263,7 @@ namespace Sequencer { // const auto &calinfo = this->caltarget.get_info(calname); - this->async.enqueue_and_log(function, "NOTICE: configuring calibrator for "+calname); + this->broadcast(function, Severity::NOTICE, "configuring calibrator for "+calname); // set the calib door and cover // @@ -2275,7 +2275,7 @@ namespace Sequencer { logwrite( function, "calib: "+cmd.str() ); if ( !this->cancel_flag.load() && this->calibd.command_timeout( cmd.str(), CALIBD_SET_TIMEOUT ) != NO_ERROR ) { - this->async.enqueue_and_log( function, "ERROR moving calib door and/or cover" ); + this->broadcast( function, Severity::ERROR, "moving calib door and/or cover" ); throw std::runtime_error("moving calib door and/or cover"); } @@ -2288,7 +2288,7 @@ namespace Sequencer { logwrite( function, message.str() ); std::string reply; if ( this->powerd.send( cmd.str(), reply ) != NO_ERROR ) { - this->async.enqueue_and_log( function, "ERROR "+message.str() ); + this->broadcast( function, Severity::ERROR, message.str() ); throw std::runtime_error("setting lamp "+message.str()); } } @@ -2312,13 +2312,13 @@ namespace Sequencer { if ( this->cancel_flag.load() ) break; cmd.str(""); cmd << CALIBD_LAMPMOD << " " << mod << " " << (state?1:0) << " 1000"; if ( this->calibd.command( cmd.str() ) != NO_ERROR ) { - this->async.enqueue_and_log( function, "ERROR "+cmd.str() ); + this->broadcast( function, Severity::ERROR, cmd.str() ); throw std::runtime_error("setting lamp modulator "+cmd.str()); } } if ( this->cancel_flag.load() ) { - this->async.enqueue_and_log( function, "NOTICE: abort may have left calib system partially set" ); + this->broadcast( function, Severity::NOTICE, "abort may have left calib system partially set" ); } this->thread_error_manager.clear( THR_CALIBRATOR_SET ); // success @@ -2645,10 +2645,12 @@ namespace Sequencer { std::stringstream message; long error=NO_ERROR; - if ( ! seq_state_manager.are_any_set( Sequencer::SEQ_READY, Sequencer::SEQ_NOTREADY ) ) { - message << "ERROR cannot perform system startup while " + if ( ! seq_state_manager.are_any_set( Sequencer::SEQ_READY, + Sequencer::SEQ_NOTREADY, + Sequencer::SEQ_FAILED ) ) { + message << "cannot perform system startup while " << seq_state_manager.get_set_states(); - this->async.enqueue_and_log( function, message.str() ); + this->broadcast( function, Severity::ERROR, message.str() ); return ERROR; } @@ -2672,7 +2674,7 @@ namespace Sequencer { error = start_power.get(); if ( error != NO_ERROR ) { - this->async.enqueue_and_log( function, "ERROR starting power control. Will try to continue (but don't hold your breath)" ); + this->broadcast( function, Severity::ERROR, "starting power control. Will try to continue (but don't hold your breath)" ); } // threads to start, pair their ThreadStatusBit with the function to call @@ -2738,7 +2740,7 @@ namespace Sequencer { // restart the slicecam daemon, then loop to try again. if (attempt < maxattempts) { if ( set_power_switch(OFF, POWER_SLICECAM, std::chrono::seconds(5)) != NO_ERROR ) { - async.enqueue_and_log( function, "ERROR switching off slicecams" ); + this->broadcast( function, Severity::ERROR, "switching off slicecams" ); __error=ERROR; break; } @@ -2752,7 +2754,7 @@ namespace Sequencer { continue; } else { - async.enqueue_and_log( function, "ERROR exceeded max attempts starting slicecam" ); + this->broadcast( function, Severity::ERROR, "exceeded max attempts starting slicecam" ); __error=ERROR; } } @@ -2768,7 +2770,7 @@ namespace Sequencer { } } // end while if (__error == ERROR) { - async.enqueue_and_log( function, "ERROR slicecam not initialized" ); + this->broadcast( function, Severity::ERROR, "slicecam not initialized" ); error=ERROR; } } @@ -2792,7 +2794,7 @@ namespace Sequencer { if (e.code == ErrorCode::ERROR_ACAM_CAMERA) { if (attempt < maxattempts) { if ( set_power_switch(OFF, POWER_ACAM_CAM, std::chrono::seconds(5)) != NO_ERROR ) { - async.enqueue_and_log( function, "ERROR switching off acam camera" ); + this->broadcast( function, Severity::ERROR, "switching off acam camera" ); __error=ERROR; } logwrite(function, "acam camera powered off"); @@ -2805,7 +2807,7 @@ namespace Sequencer { continue; } else { - async.enqueue_and_log( function, "ERROR exceeded max attempts starting acam" ); + this->broadcast( function, Severity::ERROR, "exceeded max attempts starting acam" ); __error=ERROR; } } @@ -2823,7 +2825,7 @@ namespace Sequencer { } } // end while if (__error == ERROR) { - async.enqueue_and_log( function, "ERROR acam not initialized" ); + this->broadcast( function, Severity::ERROR, "acam not initialized" ); error=ERROR; } } @@ -2833,7 +2835,7 @@ namespace Sequencer { seq_state_manager.set_only( {Sequencer::SEQ_READY} ); } else { - seq_state_manager.set_only( {Sequencer::SEQ_NOTREADY} ); + seq_state_manager.set_only( {Sequencer::SEQ_FAILED} ); } return error; @@ -2879,7 +2881,7 @@ namespace Sequencer { // auto start_power = std::async(std::launch::async, &Sequence::power_init, this); if ( start_power.get() != NO_ERROR ) { - this->async.enqueue_and_log( function, "ERROR from power control. Will try to continue (but don't hold your breath)" ); + this->broadcast( function, Severity::ERROR, "from power control. Will try to continue (but don't hold your breath)" ); } // container of shutdown threads to launch, @@ -2923,6 +2925,7 @@ namespace Sequencer { } else { message << "ERROR occurred during shutdown and may not have completed"; + seq_state.destruct_set( Sequencer::SEQ_FAILED ); // override exit state on failure } this->async.enqueue_and_log( function, message.str() ); @@ -3770,7 +3773,7 @@ namespace Sequencer { } // connection failed too many times if (attempt > maxattempts) { - async.enqueue_and_log(function, "ERROR exceeded max attempts connecting to " + daemon.name); + this->broadcast(function, Severity::ERROR, "exceeded max attempts connecting to " + daemon.name); return ERROR; } @@ -3779,7 +3782,7 @@ namespace Sequencer { error |= daemon.send( "isopen", reply ); error |= this->parse_state( function, reply, isopen ); if ( error != NO_ERROR ) { - this->async.enqueue_and_log( function, "ERROR opening "+daemon.name+" hardware" ); + this->broadcast( function, Severity::ERROR, "opening "+daemon.name+" hardware" ); return ERROR; } @@ -3789,7 +3792,7 @@ namespace Sequencer { logwrite( function, "opening "+daemon.name+" hardware connections with " +std::to_string(opentimeout)+" ms timeout" ); if ( daemon.command_timeout( opencmd, reply, opentimeout ) != NO_ERROR ) { - this->async.enqueue_and_log( function, "ERROR opening connection to "+daemon.name+" hardware" ); + this->broadcast( function, Severity::ERROR, "opening connection to "+daemon.name+" hardware" ); return ERROR; } was_opened=true; @@ -3818,7 +3821,7 @@ namespace Sequencer { if ( !daemon.socket.isconnected() ) { logwrite( function, "connecting to "+daemon.name+" daemon" ); if ( daemon.connect() != NO_ERROR ) { - this->async.enqueue_and_log( function, "ERROR connecting to "+daemon.name ); + this->broadcast( function, Severity::ERROR, "connecting to "+daemon.name ); return ERROR; } } diff --git a/utils/CMakeLists.txt b/utils/CMakeLists.txt index baa453ba..d8992d58 100644 --- a/utils/CMakeLists.txt +++ b/utils/CMakeLists.txt @@ -66,3 +66,15 @@ target_link_libraries( listener PRIVATE utilities ) find_library( ZMQPP_LIB zmqpp NAMES libzmqpp PATHS /usr/local/lib ) find_library( ZMQ_LIB zmq NAMES libzmq PATHS /usr/local/lib ) +add_executable( seqmon + ${PROJECT_UTILS_DIR}/seqmon.cpp + ) +target_include_directories( seqmon PRIVATE + ${PROJECT_BASE_DIR}/utils + ${PROJECT_BASE_DIR}/common + ) +target_link_libraries( seqmon PRIVATE + ${ZMQPP_LIB} + ${ZMQ_LIB} + ) + diff --git a/utils/seqmon.cpp b/utils/seqmon.cpp new file mode 100644 index 00000000..22232f34 --- /dev/null +++ b/utils/seqmon.cpp @@ -0,0 +1,290 @@ +/** + * @file seqmon.cpp + * @brief terminal-based subscriber that mimics the sequencer operator display + * @author David Hale + * + * Subscribes to three sequencer topics on the ZMQ broker and renders a simple + * color-coded terminal display: + * + * seq_seqstate lifecycle state (top line, color by value) + * seq_waitstate active wait bits (second line, list of set labels) + * broadcast operator messages (scrolling log, color by severity) + * + * This is a standalone utility intended as a stand-in for the GUI message + * window during testing. It has no dependencies on the sequencer sources; + * it only shares the message_keys.h contract. + * + */ + +#include +#include +#include "message_keys.h" + +#include +#include +#include +#include +#include +#include + +namespace { + + constexpr const char *BROKER_ENDPOINT = "tcp://localhost:5556"; + + // ANSI escape codes + // + constexpr const char *ANSI_RESET = "\033[0m"; + constexpr const char *ANSI_GRAY = "\033[90m"; + constexpr const char *ANSI_RED = "\033[31m"; + constexpr const char *ANSI_GREEN = "\033[32m"; + constexpr const char *ANSI_YELLOW = "\033[33m"; + constexpr const char *ANSI_BLUE = "\033[34m"; + constexpr const char *ANSI_CYAN = "\033[36m"; + constexpr const char *ANSI_BOLDYEL = "\033[1;33m"; + + // cursor control: save/restore, move up 3 lines, clear entire line + // + constexpr const char *CURSOR_SAVE = "\033[s"; + constexpr const char *CURSOR_RESTORE = "\033[u"; + constexpr const char *CURSOR_HOME = "\033[H"; + constexpr const char *CLEAR_SCREEN = "\033[2J"; + constexpr const char *CLEAR_LINE = "\033[2K"; + + std::atomic running{true}; + + // Cached last-known state so a redraw after a broadcast message can + // reprint the fixed top lines. + // + std::string last_seqstate = "(unknown)"; + std::string last_waitstate; + + /***** color_for_state ******************************************************/ + /** + * @brief map a lifecycle state label to an ANSI color + * @param state state string (e.g. "NOTREADY", "RUNNING") + * @return ANSI escape sequence + */ + const char *color_for_state( const std::string &state ) { + if ( state.find("FAILED") != std::string::npos ) return ANSI_RED; + if ( state.find("ABORTING") != std::string::npos ) return ANSI_BOLDYEL; + if ( state.find("RUNNING") != std::string::npos ) return ANSI_GREEN; + if ( state.find("READY") != std::string::npos ) return ANSI_YELLOW; + if ( state.find("STARTING") != std::string::npos ) return ANSI_BLUE; + if ( state.find("STOPPING") != std::string::npos ) return ANSI_BLUE; + if ( state.find("PAUSED") != std::string::npos ) return ANSI_CYAN; + if ( state.find("NOTREADY") != std::string::npos ) return ANSI_GRAY; + return ANSI_RESET; + } + /***** color_for_state ******************************************************/ + + + /***** color_for_severity ***************************************************/ + /** + * @brief map a broadcast severity label to an ANSI color + * @param severity severity string (NOTICE|WARNING|ERROR) + * @return ANSI escape sequence + */ + const char *color_for_severity( const std::string &severity ) { + if ( severity == Severity::ERROR ) return ANSI_RED; + if ( severity == Severity::WARNING ) return ANSI_YELLOW; + return ANSI_RESET; + } + /***** color_for_severity ***************************************************/ + + + /***** timestamp ************************************************************/ + /** + * @brief local time as HH:MM:SS + * @return formatted string + */ + std::string timestamp() { + std::time_t now = std::time(nullptr); + std::tm tm_local{}; + localtime_r( &now, &tm_local ); + char buf[16]; + std::strftime( buf, sizeof(buf), "%H:%M:%S", &tm_local ); + return std::string(buf); + } + /***** timestamp ************************************************************/ + + + /***** redraw_status ********************************************************/ + /** + * @brief reprint the fixed status header (lines 1 and 2) in place + * @details Uses ANSI save/restore so the current scroll position for + * broadcast messages is not disturbed. + */ + void redraw_status() { + std::cout << CURSOR_SAVE + << CURSOR_HOME + << CLEAR_LINE + << "STATE: " << color_for_state(last_seqstate) + << last_seqstate << ANSI_RESET << "\n" + << CLEAR_LINE + << "WAITING: " << last_waitstate << "\n" + << CURSOR_RESTORE + << std::flush; + } + /***** redraw_status ********************************************************/ + + + /***** handle_seqstate ******************************************************/ + /** + * @brief parse a seq_seqstate payload and update the top status line + * @param payload JSON payload as string + */ + void handle_seqstate( const std::string &payload ) { + try { + auto j = nlohmann::json::parse( payload ); + if ( j.contains(Key::Sequencer::SEQSTATE) ) { + last_seqstate = j[Key::Sequencer::SEQSTATE].get(); + if ( last_seqstate.empty() ) last_seqstate = "(none)"; + redraw_status(); + } + } + catch ( const std::exception &e ) { + std::cerr << "seqstate parse error: " << e.what() << "\n"; + } + } + /***** handle_seqstate ******************************************************/ + + + /***** handle_waitstate *****************************************************/ + /** + * @brief parse a seq_waitstate payload and update the wait-bit line + * @param payload JSON payload as string + * @details Payload contains one boolean per wait-bit label; collect the + * labels whose value is true. + */ + void handle_waitstate( const std::string &payload ) { + try { + auto j = nlohmann::json::parse( payload ); + std::ostringstream oss; + bool first = true; + for ( auto it = j.begin(); it != j.end(); ++it ) { + if ( ! it.value().is_boolean() ) continue; + if ( it.value().get() ) { + if ( !first ) oss << " "; + oss << it.key(); + first = false; + } + } + last_waitstate = oss.str(); + if ( last_waitstate.empty() ) last_waitstate = "(idle)"; + redraw_status(); + } + catch ( const std::exception &e ) { + std::cerr << "waitstate parse error: " << e.what() << "\n"; + } + } + /***** handle_waitstate *****************************************************/ + + + /***** handle_broadcast *****************************************************/ + /** + * @brief parse a broadcast payload and append a colored line to the log + * @param payload JSON payload as string + */ + void handle_broadcast( const std::string &payload ) { + try { + auto j = nlohmann::json::parse( payload ); + std::string severity = j.value( Key::Broadcast::SEVERITY, std::string("NOTICE") ); + std::string message = j.value( Key::Broadcast::MESSAGE, std::string("") ); + std::string source = j.value( Key::SOURCE, std::string("?") ); + std::cout << color_for_severity(severity) + << "[" << timestamp() << "] " + << "[" << source << "] " + << severity << ": " << message + << ANSI_RESET << "\n" + << std::flush; + } + catch ( const std::exception &e ) { + std::cerr << "broadcast parse error: " << e.what() << "\n"; + } + } + /***** handle_broadcast *****************************************************/ + + + /***** signal_handler *******************************************************/ + /** + * @brief SIGINT handler; triggers clean exit of the main loop + */ + void signal_handler( int /*signum*/ ) { + running.store(false); + } + /***** signal_handler *******************************************************/ + +} // anonymous namespace + + +void usage( const char *exe ) { + std::cout << "usage: " << exe << " [endpoint]\n" + << " endpoint ZMQ broker address (default " << BROKER_ENDPOINT << ")\n"; +} + + +int main( int argc, char *argv[] ) { + + std::string endpoint = BROKER_ENDPOINT; + + if ( argc > 1 ) { + std::string arg = argv[1]; + if ( arg == "-h" || arg == "--help" ) { + usage(argv[0]); + return 0; + } + endpoint = arg; + } + + std::signal( SIGINT, signal_handler ); + std::signal( SIGTERM, signal_handler ); + + // set up ZMQ subscriber + // + zmqpp::context context; + zmqpp::socket subscriber( context, zmqpp::socket_type::subscribe ); + + try { + subscriber.connect( endpoint ); + subscriber.subscribe( Topic::BROADCAST ); + subscriber.subscribe( Topic::SEQ_SEQSTATE ); + subscriber.subscribe( Topic::SEQ_WAITSTATE ); + } + catch ( const std::exception &e ) { + std::cerr << "ERROR connecting to " << endpoint << ": " << e.what() << "\n"; + return 1; + } + + // initial screen: clear, draw status header, leave room for scrolling log + // + std::cout << CLEAR_SCREEN << CURSOR_HOME + << "STATE: " << last_seqstate << "\n" + << "WAITING: " << last_waitstate << "\n" + << std::string(60, '-') << "\n" + << "seqmon subscribed to " << endpoint + << " (Ctrl-C to exit)\n" + << std::flush; + + // poll so Ctrl-C can break out promptly + // + zmqpp::poller poller; + poller.add( subscriber, zmqpp::poller::poll_in ); + + while ( running.load() ) { + if ( poller.poll(500) == 0 ) continue; + if ( ! poller.has_input(subscriber) ) continue; + + zmqpp::message zmsg; + subscriber.receive( zmsg ); + std::string topic, payload; + zmsg >> topic >> payload; + + if ( topic == Topic::SEQ_SEQSTATE ) handle_seqstate( payload ); + else if ( topic == Topic::SEQ_WAITSTATE ) handle_waitstate( payload ); + else if ( topic == Topic::BROADCAST ) handle_broadcast( payload ); + // unrecognized topics silently ignored + } + + std::cout << "\nseqmon exiting\n" << std::flush; + return 0; +} From c5d0c8f5a4765f93120f1ff56c20b507c4b29033 Mon Sep 17 00:00:00 2001 From: David Hale Date: Sun, 26 Apr 2026 16:29:47 -0700 Subject: [PATCH 4/8] * fixes state-change prob in sequencer * fixes bug in camerad emulator * updates seqmon utility --- camerad/simulator-arc.cpp | 30 +++--- sequencerd/sequence.cpp | 109 +++++++++++++++----- sequencerd/sequence.h | 5 +- sequencerd/sequencerd.cpp | 2 +- utils/seqmon.cpp | 210 +++++++++++++++++++++++++++++++++++--- 5 files changed, 294 insertions(+), 62 deletions(-) diff --git a/camerad/simulator-arc.cpp b/camerad/simulator-arc.cpp index a85f4a6c..dece6c3b 100644 --- a/camerad/simulator-arc.cpp +++ b/camerad/simulator-arc.cpp @@ -43,7 +43,7 @@ namespace AstroCam { // If no string is given then use vector of configured devices // if ( devices_in.empty() ) { - this->devnums = this->configured_devnums; + this->connected_devnums = this->configured_devnums; } else { // Otherwise, tokenize the device list string and build devnums from the tokens @@ -53,8 +53,8 @@ namespace AstroCam { for ( const auto &n : tokens ) { // For each token in the devices_in string, try { int dev = std::stoi( n ); // convert to int - if ( std::find( this->devnums.begin(), this->devnums.end(), dev ) == this->devnums.end() ) { // If it's not already in the vector, - this->devnums.push_back( dev ); // then push into devnums vector. + if ( std::find( this->connected_devnums.begin(), this->connected_devnums.end(), dev ) == this->connected_devnums.end() ) { // If it's not already in the vector, + this->connected_devnums.push_back( dev ); // then push into devnums vector. } } catch (std::invalid_argument &) { @@ -76,7 +76,7 @@ namespace AstroCam { // For each requested dev in devnums, if there is a matching controller in the config file, // then get the devname and store it in the controller map. // - for ( const auto &dev : this->devnums ) { + for ( const auto &dev : this->connected_devnums ) { if ( this->controller.find( dev ) != this->controller.end() ) { this->controller[ dev ].devname = "sim"+std::to_string(dev); } @@ -84,7 +84,7 @@ namespace AstroCam { // set the controller connected state true // - for ( const auto &dev : this->devnums ) { + for ( const auto &dev : this->connected_devnums ) { this->controller[dev].connected = true; } @@ -110,7 +110,7 @@ namespace AstroCam { // clear the controller connected state // - for ( const auto &dev : this->devnums ) { + for ( const auto &dev : this->connected_devnums ) { this->controller[dev].connected = false; } @@ -150,7 +150,7 @@ namespace AstroCam { std::stringstream lodfilestream; // But only use it if the device is open // - if ( std::find( this->devnums.begin(), this->devnums.end(), fw->first ) != this->devnums.end() ) { + if ( std::find( this->connected_devnums.begin(), this->connected_devnums.end(), fw->first ) != this->connected_devnums.end() ) { lodfilestream << fw->first << " " << fw->second; // Call do_load_firmware with the built up string. @@ -452,14 +452,14 @@ namespace AstroCam { } /***** AstroCam::Interface::native ******************************************/ - - long Interface::_image_size( std::string args, std::string &retstring, const bool save_as_default ) { - std::string function = "AstroCam::Interface::_image_size"; - std::stringstream message; - logwrite( function, "NOP" ); - return( NO_ERROR ); - } - +/* + *long Interface::_image_size( std::string args, std::string &retstring, const bool save_as_default ) { + * std::string function = "AstroCam::Interface::_image_size"; + * std::stringstream message; + * logwrite( function, "NOP" ); + * return( NO_ERROR ); + *} + */ /***** AstroCam::Simulator::dothread_expose *********************************/ /** diff --git a/sequencerd/sequence.cpp b/sequencerd/sequence.cpp index 0ec00272..92eae521 100644 --- a/sequencerd/sequence.cpp +++ b/sequencerd/sequence.cpp @@ -232,23 +232,25 @@ namespace Sequencer { /***** Sequencer::Sequence::broadcast_daemonstate ***************************/ /** * @brief publishes daemonstate and can control seqstate - * @details If not STARTING or STOPPING and not all daemons ready then - * this ensures that the seqstate drops into NOTREADY. + * @details Daemon-readiness changes may only force the sequencer into + * NOTREADY when the current seqstate is itself READY or + * NOTREADY. Lifecycle states (STARTING, STOPPING, RUNNING, + * PAUSED, ABORTING, FAILED) are owned by the lifecycle + * functions and are never overridden here. The one-hot + * seqstate contract is preserved. * */ void Sequence::broadcast_daemonstate() { // always publish daemonstate when called this->publish_daemonstate(); - // If any daemon isn't ready then the sequencer can't be ready, - // but don't override STARTING or STOPPING, unless none are ready. - if ( daemon_manager.are_all_clear() ) { - seq_state_manager.set_only( {Sequencer::SEQ_NOTREADY} ); - } - else - if ( ! seq_state_manager.is_set(SEQ_STARTING) && - ! seq_state_manager.is_set(SEQ_STOPPING) && - ! daemon_manager.are_all_set() ) { + // Only degrade seqstate to NOTREADY when the sequencer is currently + // READY or NOTREADY. Never override an active lifecycle transition + // (STARTING, STOPPING, RUNNING, PAUSED, ABORTING) or FAILED. + // + if ( ! daemon_manager.are_all_set() && + seq_state_manager.are_any_set( Sequencer::SEQ_READY, + Sequencer::SEQ_NOTREADY ) ) { seq_state_manager.set_only( {Sequencer::SEQ_NOTREADY} ); } } @@ -263,8 +265,23 @@ namespace Sequencer { * */ void Sequence::broadcast_seqstate() { + const std::string function("Sequencer::Sequence::broadcast_seqstate"); + + // publish the structured seqstate topic + // this->publish_seqstate(); this->cv.notify_all(); + + // emit a NOTICE on Topic::BROADCAST only when the lifecycle state has + // actually changed, so operators (and logs) get a breadcrumb trail of + // state transitions without noise from repeated identical callbacks. + // + std::string current( this->seq_state_manager.get_set_states() ); + rtrim( current ); + if ( current != this->last_seqstate_str ) { + this->last_seqstate_str = current; + this->broadcast( function, Severity::NOTICE, "sequencer state: "+current ); + } } /***** Sequencer::Sequence::broadcast_seqstate ******************************/ @@ -2344,7 +2361,9 @@ namespace Sequencer { ScopedState thr_state( this->thread_state_manager, Sequencer::THR_ABORT_PROCESS ); - // Decide post-abort seqstate before entering SEQ_ABORTING. + // Decide post-abort seqstate before entering SEQ_ABORTING. These snapshots + // must be taken before any seqstate mutation below, because set_only() + // clears all other lifecycle bits. // const bool abort_during_run = this->seq_state_manager.are_any_set( Sequencer::SEQ_RUNNING, @@ -2353,16 +2372,9 @@ namespace Sequencer { Sequencer::SEQ_STARTING, Sequencer::SEQ_STOPPING ); - // RAII: SEQ_ABORTING set on entry, cleared on scope exit. + // Enter SEQ_ABORTING as a strict one-hot state. // - ScopedState seq_state( this->seq_state_manager, Sequencer::SEQ_ABORTING ); - - if ( abort_during_run ) { - seq_state.destruct_set( Sequencer::SEQ_READY ); - } - else if ( abort_during_lifecycle ) { - seq_state.destruct_set( Sequencer::SEQ_FAILED ); - } + this->seq_state_manager.set_only( {Sequencer::SEQ_ABORTING} ); this->cancel_flag.store(false); @@ -2386,7 +2398,24 @@ namespace Sequencer { this->do_once.store(true); this->broadcast( function, Severity::NOTICE, "cancel signal sent" ); + + // Exit SEQ_ABORTING to a strict one-hot terminal state chosen from the + // snapshot taken at entry. If neither condition applies (e.g. abort + // invoked while READY/NOTREADY/FAILED) we leave the state at NOTREADY + // so callers never see SEQ_ABORTING linger and no prior bit is retained. + // + if ( abort_during_run ) { + this->seq_state_manager.set_only( {Sequencer::SEQ_READY} ); + } + else if ( abort_during_lifecycle ) { + this->seq_state_manager.set_only( {Sequencer::SEQ_FAILED} ); + } + else { + this->seq_state_manager.set_only( {Sequencer::SEQ_NOTREADY} ); + } } + /***** Sequencer::Sequence::abort_process *********************************/ + /***** Sequencer::Sequence::stop_exposure *********************************/ /** @@ -2855,16 +2884,34 @@ namespace Sequencer { const std::string function("Sequencer::Sequence::shutdown"); long error=ERROR; - ScopedState thr_state( this->thread_state_manager, Sequencer::THR_SHUTDOWN ); // this thread is running + // Reject if a conflicting lifecycle transition is already in progress. + // All other states (READY, NOTREADY, FAILED, RUNNING, PAUSED) are valid + // starting points for a shutdown. + // + if ( seq_state_manager.are_any_set( Sequencer::SEQ_STOPPING, + Sequencer::SEQ_STARTING, + Sequencer::SEQ_ABORTING ) ) { + std::stringstream message; + message << "cannot perform system shutdown while " + << seq_state_manager.get_set_states(); + this->broadcast( function, Severity::ERROR, message.str() ); + return ERROR; + } - // set only STOPPING (and clear everything else) - ScopedState seq_state( seq_state_manager, Sequencer::SEQ_STOPPING, true ); // state=STOPPING (only) + // stop everything first + // + this->abort_process(); - seq_state.destruct_set( Sequencer::SEQ_NOTREADY ); // set state=NOTREADY on exit + ScopedState thr_state( this->thread_state_manager, Sequencer::THR_SHUTDOWN ); // this thread is running - // stop everything + // Enter SEQ_STOPPING as a strict one-hot state. Explicit management (not + // ScopedState RAII) is used here because abort_process() below independently + // transitions seqstate, and an RAII destructor using set_and_clear would + // re-add NOTREADY on top of any FAILED bit left by abort_process, producing + // a non-one-hot state. The terminal transition is made explicitly before + // every return from this function. // - this->abort_process(); + seq_state_manager.set_only( {Sequencer::SEQ_STOPPING} ); // clear stop flags // @@ -2925,9 +2972,15 @@ namespace Sequencer { } else { message << "ERROR occurred during shutdown and may not have completed"; - seq_state.destruct_set( Sequencer::SEQ_FAILED ); // override exit state on failure } + // Always end in NOTREADY regardless of worker errors. SEQ_FAILED is + // reserved for startup failures and aborted lifecycle transitions. + // Worker errors during shutdown are logged above but do not prevent + // the instrument from being considered shut down (not ready). + // + seq_state_manager.set_only( {Sequencer::SEQ_NOTREADY} ); + this->async.enqueue_and_log( function, message.str() ); return error; diff --git a/sequencerd/sequence.h b/sequencerd/sequence.h index 9e5bf84e..5ce078a0 100644 --- a/sequencerd/sequence.h +++ b/sequencerd/sequence.h @@ -414,9 +414,6 @@ namespace Sequencer { std::atomic do_once; ///< set if "do one" selected, clear if "do all" selected - std::mutex seqstate_mtx; - std::condition_variable seqstate_cv; - ImprovedStateManager(Sequencer::NUM_THREAD_STATES)> thread_error_manager{ Sequencer::thread_names }; ImprovedStateManager(Sequencer::NUM_SEQ_STATES)> seq_state_manager{Sequencer::seq_state_names}; ImprovedStateManager(Sequencer::NUM_WAIT_STATES)> wait_state_manager{Sequencer::wait_state_names}; @@ -514,6 +511,8 @@ namespace Sequencer { const std::string &severity, const std::string &message ); ///< logs and publishes a narrative message on Topic::BROADCAST + std::string last_seqstate_str; ///< last seqstate string announced via broadcast_seqstate() (for change detection) + uint32_t get_reqstate(); ///< get the reqstate word static void dothread_monitor_ready_state( Sequencer::Sequence &seq ); diff --git a/sequencerd/sequencerd.cpp b/sequencerd/sequencerd.cpp index 3b9ba63f..1f27a76d 100644 --- a/sequencerd/sequencerd.cpp +++ b/sequencerd/sequencerd.cpp @@ -135,7 +135,7 @@ int main(int argc, char **argv) { logwrite(function, "ERROR initializing publisher-subscriber handler"); sequencerd.exit_cleanly(); } - sequencerd.sequence.seq_state_manager.set(Sequencer::SEQ_NOTREADY); + sequencerd.sequence.seq_state_manager.set_only({Sequencer::SEQ_NOTREADY}); std::this_thread::sleep_for( std::chrono::milliseconds(100) ); sequencerd.sequence.publish_snapshot(); diff --git a/utils/seqmon.cpp b/utils/seqmon.cpp index 22232f34..3db2987a 100644 --- a/utils/seqmon.cpp +++ b/utils/seqmon.cpp @@ -3,12 +3,19 @@ * @brief terminal-based subscriber that mimics the sequencer operator display * @author David Hale * - * Subscribes to three sequencer topics on the ZMQ broker and renders a simple + * Subscribes to sequencer topics on the ZMQ broker and renders a simple * color-coded terminal display: * - * seq_seqstate lifecycle state (top line, color by value) - * seq_waitstate active wait bits (second line, list of set labels) - * broadcast operator messages (scrolling log, color by severity) + * seq_seqstate lifecycle state (top line, color by value) + * seq_waitstate active wait bits (second line, list of set labels) + * seq_daemonstate daemon online/offline status + * acamd acquisition mode, acquired flag, seeing + * slicecamd fine-acquisition locked/running flags + * broadcast operator messages (scrolling log, color by severity) + * + * On startup a Topic::SNAPSHOT request is published so that sequencerd, + * acamd, and slicecamd immediately re-publish their current telemetry, + * avoiding a stale display until the next natural update. * * This is a standalone utility intended as a stand-in for the GUI message * window during testing. It has no dependencies on the sequencer sources; @@ -17,19 +24,23 @@ */ #include -#include +#include #include "message_keys.h" #include +#include #include #include +#include #include #include #include +#include namespace { - constexpr const char *BROKER_ENDPOINT = "tcp://localhost:5556"; + constexpr const char *BROKER_ENDPOINT = "tcp://localhost:5556"; // subscribers connect here + constexpr const char *BROKER_PUB_ENDPOINT = "tcp://localhost:5555"; // publishers connect here // ANSI escape codes // @@ -56,7 +67,13 @@ namespace { // reprint the fixed top lines. // std::string last_seqstate = "(unknown)"; - std::string last_waitstate; + std::string last_waitstate = "(none)"; + std::string last_daemonstate = "(none)"; + std::string last_acq_mode = "?"; // "stopped" | "guiding" | "acquiring" + bool last_is_acquired = false; // Key::Acamd::IS_ACQUIRED + bool last_fa_locked = false; // Key::Slicecamd::FINEACQUIRE_LOCKED + bool last_fa_running = false; // Key::Slicecamd::FINEACQUIRE_RUNNING + double last_seeing = 0.0; // Key::Acamd::SEEING /***** color_for_state ******************************************************/ /** @@ -92,6 +109,32 @@ namespace { /***** color_for_severity ***************************************************/ + /***** color_for_daemonstate ************************************************/ + /** + * @brief map a broadcast severity label to an ANSI color + * @param severity severity string (NOTICE|WARNING|ERROR) + * @return ANSI escape sequence + */ + const char *color_for_daemonstate( bool state) { + return ( state ? ANSI_GREEN : ANSI_RED ); + } + /***** color_for_daemonstate ************************************************/ + + + /***** color_for_acqmode ****************************************************/ + /** + * @brief map an acquisition mode string to an ANSI color + * @param mode "acquiring" | "guiding" | "stopped" + * @return ANSI escape sequence + */ + const char *color_for_acqmode( const std::string &mode ) { + if ( mode == "acquiring" ) return ANSI_GREEN; + if ( mode == "guiding" ) return ANSI_CYAN; + return ANSI_GRAY; // "stopped" or unknown + } + /***** color_for_acqmode ****************************************************/ + + /***** timestamp ************************************************************/ /** * @brief local time as HH:MM:SS @@ -118,10 +161,26 @@ namespace { std::cout << CURSOR_SAVE << CURSOR_HOME << CLEAR_LINE - << "STATE: " << color_for_state(last_seqstate) + << "STATE: " << color_for_state(last_seqstate) << last_seqstate << ANSI_RESET << "\n" << CLEAR_LINE - << "WAITING: " << last_waitstate << "\n" + << "WAITING: " << last_waitstate << ANSI_RESET << "\n" + << CLEAR_LINE + << "SUBSYSTEMS: " << last_daemonstate << "\n" + << CLEAR_LINE + << "ACQUISITION: " + << "ACAM: " << color_for_acqmode(last_acq_mode) + << last_acq_mode << ANSI_RESET + << " " << ( last_is_acquired ? ANSI_GREEN : ANSI_GRAY ) + << "[acquired]" << ANSI_RESET + << " SLICECAM: " + << ( last_fa_locked ? ANSI_GREEN : ANSI_GRAY ) << "locked" << ANSI_RESET + << " " + << ( last_fa_running ? ANSI_GREEN : ANSI_GRAY ) << "running" << ANSI_RESET + << " SEEING: " << std::fixed << std::setprecision(2) + << ANSI_BLUE << last_seeing << ANSI_RESET + << "\n" + << std::string(60, '-') << "\n" << CURSOR_RESTORE << std::flush; } @@ -180,6 +239,83 @@ namespace { /***** handle_waitstate *****************************************************/ + void handle_daemonstate( const std::string &payload ) { + try { + auto j = nlohmann::json::parse(payload); + std::ostringstream oss; + for (auto it = j.begin(); it != j.end(); ++it) { + if (!it.value().is_boolean()) continue; + bool daemonstate = it.value().get(); + oss << color_for_daemonstate(daemonstate) + << it.key() << ANSI_RESET << " "; + } + last_daemonstate = oss.str(); + redraw_status(); + } + catch (const std::exception &e) { + std::cerr << "daemonstate parse error: " << e.what() << "\n"; + } + } + + + /***** handle_acamd *********************************************************/ + /** + * @brief parse an acamd payload and update acquisition mode/status + * @param payload JSON payload as string + * @details Handles partial payloads; only keys present are updated. + */ + void handle_acamd( const std::string &payload ) { + try { + auto j = nlohmann::json::parse( payload ); + bool changed = false; + if ( j.contains( Key::Acamd::ACQUIRE_MODE ) ) { + last_acq_mode = j[Key::Acamd::ACQUIRE_MODE].get(); + changed = true; + } + if ( j.contains( Key::Acamd::IS_ACQUIRED ) ) { + last_is_acquired = j[Key::Acamd::IS_ACQUIRED].get(); + changed = true; + } + if ( j.contains( Key::Acamd::SEEING ) && !j[Key::Acamd::SEEING].is_null() ) { + last_seeing = j[Key::Acamd::SEEING].get(); + changed = true; + } + if ( changed ) redraw_status(); + } + catch ( const std::exception &e ) { + std::cerr << "acamd parse error: " << e.what() << "\n"; + } + } + /***** handle_acamd *********************************************************/ + + + /***** handle_slicecamd *****************************************************/ + /** + * @brief parse a slicecamd payload and update fine-acquisition status + * @param payload JSON payload as string + * @details Handles partial payloads; only keys present are updated. + */ + void handle_slicecamd( const std::string &payload ) { + try { + auto j = nlohmann::json::parse( payload ); + bool changed = false; + if ( j.contains( Key::Slicecamd::FINEACQUIRE_LOCKED ) ) { + last_fa_locked = j[Key::Slicecamd::FINEACQUIRE_LOCKED].get(); + changed = true; + } + if ( j.contains( Key::Slicecamd::FINEACQUIRE_RUNNING ) ) { + last_fa_running = j[Key::Slicecamd::FINEACQUIRE_RUNNING].get(); + changed = true; + } + if ( changed ) redraw_status(); + } + catch ( const std::exception &e ) { + std::cerr << "slicecamd parse error: " << e.what() << "\n"; + } + } + /***** handle_slicecamd *****************************************************/ + + /***** handle_broadcast *****************************************************/ /** * @brief parse a broadcast payload and append a colored line to the log @@ -205,6 +341,28 @@ namespace { /***** handle_broadcast *****************************************************/ + /***** request_snapshot *****************************************************/ + /** + * @brief publish a Topic::SNAPSHOT request to named daemons + * @details Each daemon whose name appears as a key in the payload will + * respond by re-publishing its current telemetry snapshot, allowing + * seqmon to populate the status header immediately on startup rather + * than waiting for the next natural update from each daemon. + * @param publisher connected ZMQ publish socket + */ + void request_snapshot( zmqpp::socket &publisher ) { + nlohmann::json j; + j[Daemon::SEQUENCER] = true; + j[Daemon::ACAMD] = true; + j[Daemon::SLICECAMD] = true; + zmqpp::message zmsg; + zmsg.add( Topic::SNAPSHOT ); + zmsg.add( j.dump() ); + publisher.send( zmsg ); + } + /***** request_snapshot *****************************************************/ + + /***** signal_handler *******************************************************/ /** * @brief SIGINT handler; triggers clean exit of the main loop @@ -239,16 +397,22 @@ int main( int argc, char *argv[] ) { std::signal( SIGINT, signal_handler ); std::signal( SIGTERM, signal_handler ); - // set up ZMQ subscriber + // set up ZMQ subscriber and publisher // zmqpp::context context; zmqpp::socket subscriber( context, zmqpp::socket_type::subscribe ); + zmqpp::socket publisher ( context, zmqpp::socket_type::publish ); try { subscriber.connect( endpoint ); subscriber.subscribe( Topic::BROADCAST ); subscriber.subscribe( Topic::SEQ_SEQSTATE ); subscriber.subscribe( Topic::SEQ_WAITSTATE ); + subscriber.subscribe( Topic::SEQ_DAEMONSTATE ); + subscriber.subscribe( Topic::ACAMD ); + subscriber.subscribe( Topic::SLICECAMD ); + + publisher.connect( BROKER_PUB_ENDPOINT ); } catch ( const std::exception &e ) { std::cerr << "ERROR connecting to " << endpoint << ": " << e.what() << "\n"; @@ -258,13 +422,25 @@ int main( int argc, char *argv[] ) { // initial screen: clear, draw status header, leave room for scrolling log // std::cout << CLEAR_SCREEN << CURSOR_HOME - << "STATE: " << last_seqstate << "\n" - << "WAITING: " << last_waitstate << "\n" + << "STATE: " << last_seqstate << "\n" + << "WAITING: " << last_waitstate << "\n" + << "SUBSYSTEMS: " << last_daemonstate << "\n" + << "ACQUISITION: ACAM: " << last_acq_mode + << " SLICECAM: locked running" + << " SEEING: " << std::fixed << std::setprecision(2) << last_seeing << "\n" << std::string(60, '-') << "\n" << "seqmon subscribed to " << endpoint << " (Ctrl-C to exit)\n" << std::flush; + // ZMQ PUB sockets have a slow-joiner problem: messages sent immediately + // after connect are dropped before the broker registers the connection. + // A brief sleep ensures the publisher is ready before we send the snapshot + // request. + // + std::this_thread::sleep_for( std::chrono::milliseconds(200) ); + request_snapshot( publisher ); + // poll so Ctrl-C can break out promptly // zmqpp::poller poller; @@ -276,12 +452,16 @@ int main( int argc, char *argv[] ) { zmqpp::message zmsg; subscriber.receive( zmsg ); + if ( zmsg.parts() < 2 ) continue; // guard against malformed/partial messages std::string topic, payload; zmsg >> topic >> payload; - if ( topic == Topic::SEQ_SEQSTATE ) handle_seqstate( payload ); - else if ( topic == Topic::SEQ_WAITSTATE ) handle_waitstate( payload ); - else if ( topic == Topic::BROADCAST ) handle_broadcast( payload ); + if ( topic == Topic::SEQ_SEQSTATE ) handle_seqstate( payload ); + else if ( topic == Topic::SEQ_WAITSTATE ) handle_waitstate( payload ); + else if ( topic == Topic::SEQ_DAEMONSTATE ) handle_daemonstate( payload ); + else if ( topic == Topic::ACAMD ) handle_acamd( payload ); + else if ( topic == Topic::SLICECAMD ) handle_slicecamd( payload ); + else if ( topic == Topic::BROADCAST ) handle_broadcast( payload ); // unrecognized topics silently ignored } From 9be38d59f4dfe2af65a6d75e1f6f76c56b8dfcd6 Mon Sep 17 00:00:00 2001 From: David Hale Date: Sun, 26 Apr 2026 20:45:02 -0700 Subject: [PATCH 5/8] slitd publishes on change, tcsd publishes at 1 Hz --- common/tcsd_commands.h | 2 + slitd/slit_interface.cpp | 88 ++++++++++++++++------------------------ slitd/slit_interface.h | 18 +++++--- slitd/slit_server.cpp | 16 -------- slitd/slitd.cpp | 2 +- tcsd/tcs_interface.cpp | 67 +++++++++++++++++++++++++++++- tcsd/tcs_interface.h | 6 ++- tcsd/tcs_server.cpp | 7 ++++ tcsd/tcsd.cpp | 3 ++ 9 files changed, 131 insertions(+), 78 deletions(-) diff --git a/common/tcsd_commands.h b/common/tcsd_commands.h index e8cffebc..01fa1f85 100644 --- a/common/tcsd_commands.h +++ b/common/tcsd_commands.h @@ -26,6 +26,7 @@ const std::string TCSD_NATIVE = "native"; const std::string TCSD_OFFSETRATE = "offsetrate"; const std::string TCSD_OPEN = "open"; const std::string TCSD_PTOFFSET = "offset"; +const std::string TCSD_PUBLISHSTATE = "publishstate"; const std::string TCSD_RETOFFSETS = "retoffsets"; const std::string TCSD_RINGGO = "ringgo"; const std::string TCSD_SET_FOCUS = "setfocus"; @@ -51,6 +52,7 @@ const std::vector TCSD_SYNTAX = { TCSD_OFFSETRATE+" [ ? | ]", TCSD_OPEN+" ? | ", TCSD_PTOFFSET+" ? | ", + TCSD_PUBLISHSTATE+" ? | on | off", TCSD_RETOFFSETS+" [ ? ]", TCSD_RINGGO+" ? | ", TCSD_SET_FOCUS+" ? | ", 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..1c39d8f4 100644 --- a/slitd/slit_server.cpp +++ b/slitd/slit_server.cpp @@ -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. diff --git a/tcsd/tcs_interface.cpp b/tcsd/tcs_interface.cpp index ed59ae1f..3fef6afc 100644 --- a/tcsd/tcs_interface.cpp +++ b/tcsd/tcs_interface.cpp @@ -42,7 +42,10 @@ namespace TCS { this->get_tcs_info(); nlohmann::json jmessage_out; - jmessage_out["source"] = "tcsd"; + jmessage_out[Key::SOURCE] = Daemon::TCSD; + + { + std::lock_guard lock(tcs_info_mtx); jmessage_out["ISOPEN"] = this->tcs_info.isopen; jmessage_out["TCSNAME"] = this->tcs_info.tcsname; @@ -64,6 +67,7 @@ namespace TCS { jmessage_out["TELFOCUS"] = this->tcs_info.focus; jmessage_out["AIRMASS"] = this->tcs_info.airmass; jmessage_out["MOTION"] = this->tcs_info.motion; + } // for backwards compatibility jmessage_out["messagetype"] = "tcsinfo"; @@ -82,6 +86,58 @@ namespace TCS { /***** TCS::Interface::publish_snapshot *************************************/ + /***** TCS::Interface::do_continuous_snapshot *******************************/ + /** + * @brief publish snapshot at 1 Hz when connected + * + */ + void Interface::do_continuous_snapshot() { + auto next = std::chrono::steady_clock::now(); + while (should_publish.load()) { + bool isopen = false + { + std::lock_guard lock(tcs_info_mtx); + isopen = this->tcs_info.isopen; + } + if (isopen) publish_snapshot(); + next += std::chrono::seconds(1); + std::this_thread::sleep_until(next); + } + } + /***** TCS::Interface::do_continuous_snapshot *******************************/ + + + /***** TCS::Interface::publish_state ****************************************/ + /** + * @brief set | get snapshot publish state + * @param[in] arg on|off + * @param[out] retstring reference to string to contain the state + * @return NO_ERROR | HELP + * + */ + long Interface::publish_state( const std::string &arg, std::string &retstring ) { + const std::string function = "TCS::Interface::publish_state"; + + // help + if ( arg == "?" || arg == "help" ) { + retstring = TCSD_PUBLISHSTATE; + retstring.append( " on | off\n" ); + retstring.append( " set | get continuous snapshot publish state\n" ); + return HELP; + } + // on + else if ( arg == "on" ) should_publish.store(true); + + // off + else if ( arg == "off" ) should_publish.store(false); + + retstring = should_publish.load() ? "on" : "off"; + + return NO_ERROR; + } + /***** TCS::Interface::publish_state ****************************************/ + + /***** TCS::Interface::get_tcs_info *****************************************/ /** * @brief fills the tcs_info class @@ -92,6 +148,8 @@ namespace TCS { long error = NO_ERROR; std::string retstring; + std::lock_guard lock(tcs_info_mtx); + // erase the class because it's all or nothing. If something fails partway // through, we don't want to mix values from a command now with values from // an earlier command. E.G. if reqpos fails here but reqstat and weather @@ -371,10 +429,13 @@ namespace TCS { } } + { + std::lock_guard lock(tcs_info_mtx); this->tcs_info.isopen = ( ! _name.empty() ? true : false ); this->tcs_info.tcsname = _name; retstring = ( this->tcs_info.isopen ? "true" : "false" ); // return string is the state + } asyncmsg.str(""); asyncmsg << "TCSD:open:" << retstring; this->async.enqueue( asyncmsg.str() ); // broadcast the state @@ -1137,10 +1198,12 @@ namespace TCS { // parse the reply which stores it in the TcsInfo class // + { + std::lock_guard lock(tcs_info_mtx); this->tcs_info.parse_pa(tcsreply); - std::ostringstream oss; oss << this->tcs_info.pa; + } retstring = oss.str(); if ( !retstring.empty() && !silent ) logwrite( function, retstring ); diff --git a/tcsd/tcs_interface.h b/tcsd/tcs_interface.h index 9c76b03f..1c351acf 100644 --- a/tcsd/tcs_interface.h +++ b/tcsd/tcs_interface.h @@ -445,13 +445,13 @@ logwrite(function,message.str()); std::condition_variable publish_condition; std::condition_variable collect_condition; - std::atomic publish_enable; + std::atomic should_publish; std::atomic collect_enable; Interface() : context(), offsetrate(0), - publish_enable(false), + should_publish(true), collect_enable(false), subscriber(std::make_unique(context, Common::PubSub::Mode::SUB)), is_subscriber_thread_running(false), @@ -487,6 +487,7 @@ logwrite(function,message.str()); void publish_snapshot(); void publish_snapshot(std::string &retstring); + void do_continuous_snapshot(); /** * These are the functions for communicating with the TCS @@ -499,6 +500,7 @@ logwrite(function,message.str()); long isopen( std::string &retstring ); long isopen( const std::string &arg, std::string &retstring ); long close(); + long publish_state( const std::string &arg, std::string &retstring ); long get_name( const std::string &arg, std::string &retstring ); long get_weather_coords( const std::string &arg, std::string &retstring ); long get_coords( const std::string &arg, std::string &retstring ); diff --git a/tcsd/tcs_server.cpp b/tcsd/tcs_server.cpp index 6139981e..96882f93 100644 --- a/tcsd/tcs_server.cpp +++ b/tcsd/tcs_server.cpp @@ -668,6 +668,13 @@ void doit(TcsIO &tcs_io, const std::string &client_cmd, bool is_slow_command) { } else + // publishstate + // + if ( cmd.compare( TCSD_PUBLISHSTATE ) == 0 ) { + ret = this->interface.publish_state( args, retstring ); + } + else + // llist // if ( cmd.compare( TCSD_LLIST ) == 0 ) { diff --git a/tcsd/tcsd.cpp b/tcsd/tcsd.cpp index e7d19000..4b9933e9 100644 --- a/tcsd/tcsd.cpp +++ b/tcsd/tcsd.cpp @@ -132,6 +132,9 @@ int main(int argc, char **argv) { // publish my snapshot so the world knows I'm online tcsd.interface.publish_snapshot(); + // thread to publish snapshot when connected + std::thread( std::ref(TCS::Interface::do_continuous_snapshot) ).detach(); + // This will pre-thread N_THREADS threads, a little differently from other // daemons. There will be N_THREADS-1 non-blocking threads as before then // loop forever on Accept to dynamically spawn a new thread for each blocking From 51b0a10672dd943e0e61cabf55011bea1ac9bcd0 Mon Sep 17 00:00:00 2001 From: David Hale Date: Sun, 26 Apr 2026 21:29:10 -0700 Subject: [PATCH 6/8] bug fix tcsd --- tcsd/tcs_interface.cpp | 7 +++---- tcsd/tcs_interface.h | 2 ++ tcsd/tcsd.cpp | 3 ++- 3 files changed, 7 insertions(+), 5 deletions(-) diff --git a/tcsd/tcs_interface.cpp b/tcsd/tcs_interface.cpp index 3fef6afc..31b7b083 100644 --- a/tcsd/tcs_interface.cpp +++ b/tcsd/tcs_interface.cpp @@ -94,7 +94,7 @@ namespace TCS { void Interface::do_continuous_snapshot() { auto next = std::chrono::steady_clock::now(); while (should_publish.load()) { - bool isopen = false + bool isopen = false; { std::lock_guard lock(tcs_info_mtx); isopen = this->tcs_info.isopen; @@ -433,7 +433,6 @@ namespace TCS { std::lock_guard lock(tcs_info_mtx); this->tcs_info.isopen = ( ! _name.empty() ? true : false ); this->tcs_info.tcsname = _name; - retstring = ( this->tcs_info.isopen ? "true" : "false" ); // return string is the state } @@ -1198,10 +1197,10 @@ namespace TCS { // parse the reply which stores it in the TcsInfo class // + std::ostringstream oss; + this->tcs_info.parse_pa(tcsreply); { std::lock_guard lock(tcs_info_mtx); - this->tcs_info.parse_pa(tcsreply); - std::ostringstream oss; oss << this->tcs_info.pa; } retstring = oss.str(); diff --git a/tcsd/tcs_interface.h b/tcsd/tcs_interface.h index 1c351acf..62776446 100644 --- a/tcsd/tcs_interface.h +++ b/tcsd/tcs_interface.h @@ -13,6 +13,7 @@ #include "common.h" #include "tcs_constants.h" #include "tcsd_commands.h" +#include "message_keys.h" #include #include #include @@ -427,6 +428,7 @@ logwrite(function,message.str()); private: zmqpp::context context; std::string default_tcs; ///< default TCS to use specified in .cfg + std::mutex tcs_info_mtx; ///< protects tcs_info public: inline void set_default_tcs(const std::string &which) { this->default_tcs=which; } diff --git a/tcsd/tcsd.cpp b/tcsd/tcsd.cpp index 4b9933e9..c21c20b5 100644 --- a/tcsd/tcsd.cpp +++ b/tcsd/tcsd.cpp @@ -133,7 +133,8 @@ int main(int argc, char **argv) { tcsd.interface.publish_snapshot(); // thread to publish snapshot when connected - std::thread( std::ref(TCS::Interface::do_continuous_snapshot) ).detach(); + std::thread( &TCS::Interface::do_continuous_snapshot, + std::ref(tcsd.interface) ).detach(); // This will pre-thread N_THREADS threads, a little differently from other // daemons. There will be N_THREADS-1 non-blocking threads as before then From a768cb005644d64d10e65a117062eb382b988edc Mon Sep 17 00:00:00 2001 From: David Hale Date: Sun, 26 Apr 2026 21:32:08 -0700 Subject: [PATCH 7/8] fixes issue with acamd tcs subscriber --- acamd/acam_interface.cpp | 108 ++++++++++++++++++--------------------- acamd/acam_interface.h | 16 ++++-- 2 files changed, 63 insertions(+), 61 deletions(-) diff --git a/acamd/acam_interface.cpp b/acamd/acam_interface.cpp index f19fbfcf..7e905256 100644 --- a/acamd/acam_interface.cpp +++ b/acamd/acam_interface.cpp @@ -1507,7 +1507,7 @@ namespace Acam { /***** Acam::Interface::request_snapshot ************************************/ /** - * @brief publises request for snapshot + * @brief [obsolete] publises request for snapshot * @details publishing Topic::SNAPSHOT induces subscribers to publish a * snapshot of their telemetry * @@ -1534,7 +1534,7 @@ namespace Acam { /***** Acam::Interface::wait_for_snapshots **********************************/ /** - * @brief wait for everyone to publish their snaphots + * @brief [obsolete] wait for everyone to publish their snaphots * @details When forcing subscribers to publish their telemetry, * this waits until they have done so. * @@ -1599,33 +1599,32 @@ namespace Acam { * */ void Interface::handletopic_tcsd( const nlohmann::json &jmessage ) { - { - std::lock_guard lock(snapshot_mtx); - snapshot_status[Topic::TCSD]=true; - } + + std::lockguard lock(tcsdata_mtx); + // extract and store values in the class // - Common::extract_telemetry_value( jmessage, "TCSNAME", telem.tcsname ); - Common::extract_telemetry_value( jmessage, "ISOPEN", telem.is_tcs_open ); - Common::extract_telemetry_value( jmessage, "CASANGLE", telem.angle_scope ); - Common::extract_telemetry_value( jmessage, "TELRA", telem.ra_scope_hms ); - Common::extract_telemetry_value( jmessage, "TELDEC", telem.dec_scope_dms ); - Common::extract_telemetry_value( jmessage, "RA", telem.ra_scope_h ); - Common::extract_telemetry_value( jmessage, "DEC", telem.dec_scope_d ); - Common::extract_telemetry_value( jmessage, "RAOFFSET", telem.offsetra ); - Common::extract_telemetry_value( jmessage, "DECLOFFS", telem.offsetdec ); - Common::extract_telemetry_value( jmessage, "AZ", telem.az ); - Common::extract_telemetry_value( jmessage, "TELFOCUS", telem.telfocus ); - Common::extract_telemetry_value( jmessage, "AIRMASS", telem.airmass ); + Common::extract_telemetry_value( jmessage, "TCSNAME", tcsdata.tcsname ); + Common::extract_telemetry_value( jmessage, "ISOPEN", tcsdata.is_tcs_open ); + Common::extract_telemetry_value( jmessage, "CASANGLE", tcsdata.angle_scope ); + Common::extract_telemetry_value( jmessage, "TELRA", tcsdata.ra_scope_hms ); + Common::extract_telemetry_value( jmessage, "TELDEC", tcsdata.dec_scope_dms ); + Common::extract_telemetry_value( jmessage, "RA", tcsdata.ra_scope_h ); + Common::extract_telemetry_value( jmessage, "DEC", tcsdata.dec_scope_d ); + Common::extract_telemetry_value( jmessage, "RAOFFSET", tcsdata.offsetra ); + Common::extract_telemetry_value( jmessage, "DECLOFFS", tcsdata.offsetdec ); + Common::extract_telemetry_value( jmessage, "AZ", tcsdata.az ); + Common::extract_telemetry_value( jmessage, "TELFOCUS", tcsdata.telfocus ); + Common::extract_telemetry_value( jmessage, "AIRMASS", tcsdata.airmass ); // save them to the database // - this->database.add_key_val( "CASANGLE", telem.angle_scope ); - this->database.add_key_val( "RAtel", telem.ra_scope_h ); - this->database.add_key_val( "DECLtel", telem.dec_scope_d ); - this->database.add_key_val( "AZ", telem.az ); - this->database.add_key_val( "focus", telem.telfocus ); - this->database.add_key_val( "AIRMASS", telem.airmass ); + this->database.add_key_val( "CASANGLE", tcsdata.angle_scope ); + this->database.add_key_val( "RAtel", tcsdata.ra_scope_h ); + this->database.add_key_val( "DECLtel", tcsdata.dec_scope_d ); + this->database.add_key_val( "AZ", tcsdata.az ); + this->database.add_key_val( "focus", tcsdata.telfocus ); + this->database.add_key_val( "AIRMASS", tcsdata.airmass ); } /***** Acam::Interface::handletopic_tcsd ************************************/ @@ -1638,10 +1637,6 @@ namespace Acam { * */ void Interface::handletopic_targetinfo( const nlohmann::json &jmessage ) { - { - std::lock_guard lock(snapshot_mtx); - snapshot_status[Topic::TARGETINFO]=true; - } this->database.add_from_json( jmessage, "OBS_ID" ); this->database.add_from_json( jmessage, "NAME" ); this->database.add_from_json( jmessage, "POINTMODE" ); @@ -1659,10 +1654,6 @@ namespace Acam { * */ void Interface::handletopic_slitd( const nlohmann::json &jmessage ) { - { - std::lock_guard lock(snapshot_mtx); - snapshot_status[Topic::SLITD]=true; - } this->telemkeys.add_json_key(jmessage, "SLITO", "SLITO", "slit offset in arcsec", "FLOAT", false); this->telemkeys.add_json_key(jmessage, "SLITW", "SLITW", "slit width in arcsec", "FLOAT", false); } @@ -2706,7 +2697,7 @@ namespace Acam { do { if ( iface.camera.andor.camera_info.exptime == 0 ) continue; // wait for non-zero exposure time - if ( iface.collect_header_info() == ERROR ) { // collect header information + if ( iface.assemble_header_info() == ERROR ) { // assemble header keyword information logwrite(function,"ERROR collecting header info"); continue; } @@ -4375,7 +4366,7 @@ logwrite( function, message.str() ); do { logwrite( function, std::to_string(nacquires) ); error |= this->camera.andor.acquire_one(); // acquire a single image - error |= this->collect_header_info(); // collect header information + error |= this->assemble_header_info(); // assemble header keyword information error |= this->camera.write_frame( "", this->imagename, this->tcs_online.load() ); // write to FITS file @@ -4420,7 +4411,7 @@ logwrite( function, message.str() ); do { logwrite( function, std::to_string(nacquires) ); error |= this->camera.andor.get_recent(3000); // - error |= this->collect_header_info(); // collect header information + error |= this->assemble_header_info(); // assemble header keyword information error |= this->camera.write_frame( "", this->imagename, this->tcs_online.load() ); // write to FITS file @@ -4454,7 +4445,7 @@ logwrite( function, message.str() ); // if ( tokens[1]=="getrecent" ) { error = this->camera.andor.get_recent(3000); - error |= this->collect_header_info(); // collect header information + error |= this->assemble_header_info(); // assemble header keyword information error |= this->camera.write_frame( "", this->imagename, this->tcs_online.load() ); // write to FITS file @@ -4583,7 +4574,7 @@ logwrite( function, message.str() ); retstring.append( " Gather information and add it to the internal keyword database.\n" ); return HELP; } - else error = this->collect_header_info(); // collect header information + else error = this->assemble_header_info(); // assemble header keyword information } else // -------------------------------- @@ -5005,7 +4996,7 @@ logwrite( function, message.str() ); /***** Acam::Interface::solve ***********************************************/ - /***** Acam::Interface::collect_header_info *********************************/ + /***** Acam::Interface::assemble_header_info ********************************/ /** * @brief gather information and add it to the internal keyword database * @details Some of the keys are fixed, some come from the Andor::Information @@ -5016,24 +5007,24 @@ logwrite( function, message.str() ); * @return ERROR or NO_ERROR * */ - long Interface::collect_header_info() { - // force subscribers to publish now, then wait - // esults in struct telem. - this->request_snapshot(); - this->wait_for_snapshots(); + long Interface::assemble_header_info() { - bool _tcs = telem.is_tcs_open; - std::string tcsname = ( _tcs ? telem.tcsname : "offline" ); + // ---------- scope lock tcsdata -------------- + { + std::lockguard lock(tcsdata_mtx); + + bool _tcs = tcsdata.is_tcs_open; + std::string tcsname = ( _tcs ? tcsdata.tcsname : "offline" ); double angle_acam=NAN, ra_acam=NAN, dec_acam=NAN; // outputs from fpoffsets - if ( _tcs ) this->target.save_casangle( telem.angle_scope ); // store in the Target class, required for acquisition + if ( _tcs ) this->target.save_casangle( tcsdata.angle_scope ); // store in the Target class, required for acquisition // Compute FP offsets from TCS coordinates (SCOPE) to ACAM coodinates. // compute_offset() always wants degrees and get_coords() returns RA hours. // Results in degrees. // - if ( _tcs ) this->fpoffsets.compute_offset( "SCOPE", "ACAM", (telem.ra_scope_h*TO_DEGREES), telem.dec_scope_d, telem.angle_scope, + if ( _tcs ) this->fpoffsets.compute_offset( "SCOPE", "ACAM", (tcsdata.ra_scope_h*TO_DEGREES), tcsdata.dec_scope_d, tcsdata.angle_scope, ra_acam, dec_acam, angle_acam ); // Get some info from the Andor::Information class, @@ -5054,11 +5045,21 @@ logwrite( function, message.str() ); this->camera.fitsinfo.fitskeys.addkey( "TCS", tcsname, "" ); + this->camera.fitsinfo.fitskeys.addkey( "TELFOCUS", tcsdata.telfocus, "telescope focus (mm)" ); + this->camera.fitsinfo.fitskeys.addkey( "AIRMASS", tcsdata.airmass, "" ); + this->camera.fitsinfo.fitskeys.addkey( "RA", tcsdata.ra_scope_hms, "Telecscope Right Ascension" ); + this->camera.fitsinfo.fitskeys.addkey( "DEC", tcsdata.dec_scope_dms, "Telescope Declination" ); + this->camera.fitsinfo.fitskeys.addkey( "TELRA", tcsdata.ra_scope_h, "Telecscope Right Ascension hours" ); + this->camera.fitsinfo.fitskeys.addkey( "TELDEC", tcsdata.dec_scope_d, "Telescope Declination degrees" ); + this->camera.fitsinfo.fitskeys.addkey( "RAOFFS", tcsdata.offsetra, "Telescope RA offset" ); + this->camera.fitsinfo.fitskeys.addkey( "DECLOFFS", tcsdata.offsetdec, "Telescope DEC offset" ); + this->camera.fitsinfo.fitskeys.addkey( "CASANGLE", tcsdata.angle_scope, "Cassegrain ring angle" ); + } + // ---------- end scope lock tcsdata ---------- + this->camera.fitsinfo.fitskeys.addkey( "CREATOR", "acamd", "file creator" ); this->camera.fitsinfo.fitskeys.addkey( "INSTRUME", "NGPS", "name of instrument" ); this->camera.fitsinfo.fitskeys.addkey( "TELESCOP", "P200", "name of telescope" ); - this->camera.fitsinfo.fitskeys.addkey( "TELFOCUS", telem.telfocus, "telescope focus (mm)" ); - this->camera.fitsinfo.fitskeys.addkey( "AIRMASS", telem.airmass, "" ); // get parameters from FPOffsets, results are stored in the class // @@ -5109,13 +5110,6 @@ logwrite( function, message.str() ); this->camera.fitsinfo.fitskeys.addkey( "POSANG", angle_acam, "" ); this->camera.fitsinfo.fitskeys.addkey( "TARGET", this->target.get_name(), "target name" ); - this->camera.fitsinfo.fitskeys.addkey( "RA", telem.ra_scope_hms, "Telecscope Right Ascension" ); - this->camera.fitsinfo.fitskeys.addkey( "DEC", telem.dec_scope_dms, "Telescope Declination" ); - this->camera.fitsinfo.fitskeys.addkey( "TELRA", telem.ra_scope_h, "Telecscope Right Ascension hours" ); - this->camera.fitsinfo.fitskeys.addkey( "TELDEC", telem.dec_scope_d, "Telescope Declination degrees" ); - this->camera.fitsinfo.fitskeys.addkey( "RAOFFS", telem.offsetra, "Telescope RA offset" ); - this->camera.fitsinfo.fitskeys.addkey( "DECLOFFS", telem.offsetdec, "Telescope DEC offset" ); - this->camera.fitsinfo.fitskeys.addkey( "CASANGLE", telem.angle_scope, "Cassegrain ring angle" ); this->camera.fitsinfo.fitskeys.addkey( "WCSAXES", 2, "" ); this->camera.fitsinfo.fitskeys.addkey( "RADESYSA", "ICRS", "" ); this->camera.fitsinfo.fitskeys.addkey( "CTYPE1", "RA---TAN", "" ); @@ -5137,7 +5131,7 @@ logwrite( function, message.str() ); return NO_ERROR; } - /***** Acam::Interface::collect_header_info *********************************/ + /***** Acam::Interface::assemble_header_info ********************************/ /***** Acam::Interface::target_coords ***************************************/ diff --git a/acamd/acam_interface.h b/acamd/acam_interface.h index 8c4b317e..04e29eb8 100644 --- a/acamd/acam_interface.h +++ b/acamd/acam_interface.h @@ -555,7 +555,9 @@ namespace Acam { double az; double telfocus; double airmass; - } telem; + } tcsdata; + + std::mutex tcsdata_mtx; std::mutex snapshot_mtx; std::unordered_map snapshot_status; @@ -613,8 +615,14 @@ namespace Acam { inline std::string get_imagename() { return this->imagename; } inline std::string get_wcsname() { return this->wcsname; } - inline void set_imagename( std::string name_in ) { this->imagename = ( name_in.empty() ? DEFAULT_IMAGENAME : name_in ); return; } - inline void set_wcsname( std::string name_in ) { this->wcsname = name_in; return; } + inline void set_imagename( std::string name_in ) { + this->imagename = ( name_in.empty() ? DEFAULT_IMAGENAME : std::move(name_in) ); + return; + } + inline void set_wcsname( std::string name_in ) { + this->wcsname = std::move(name_in); + return; + } GuideManager guide_manager; @@ -688,7 +696,7 @@ namespace Acam { long exptime( const std::string args, std::string &retstring ); long fan_mode( std::string args, std::string &retstring ); - long collect_header_info(); + long assemble_header_info(); inline void init_names() { imagename=""; wcsname=""; return; } // TODO still needed? From d968c271ac1e4ac40dab3f827a1cde3fd4d1162b Mon Sep 17 00:00:00 2001 From: David Hale Date: Sun, 26 Apr 2026 21:37:42 -0700 Subject: [PATCH 8/8] fixes bug in acam_interface.cpp --- acamd/acam_interface.cpp | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/acamd/acam_interface.cpp b/acamd/acam_interface.cpp index 7e905256..864292b8 100644 --- a/acamd/acam_interface.cpp +++ b/acamd/acam_interface.cpp @@ -1600,7 +1600,7 @@ namespace Acam { */ void Interface::handletopic_tcsd( const nlohmann::json &jmessage ) { - std::lockguard lock(tcsdata_mtx); + std::lock_guard lock(tcsdata_mtx); // extract and store values in the class // @@ -4074,6 +4074,10 @@ logwrite( function, message.str() ); // if ( this->motion.is_open() ) error |= this->motion.cover( "close", dontcare ); + // diable target acquisition + // + error |= this->acquire( "stop", dontcare); + // stop the framegrab thread // error |= this->framegrab( "stop", dontcare ); @@ -5009,15 +5013,15 @@ logwrite( function, message.str() ); */ long Interface::assemble_header_info() { + double angle_acam=NAN, ra_acam=NAN, dec_acam=NAN; // outputs from fpoffsets + // ---------- scope lock tcsdata -------------- { - std::lockguard lock(tcsdata_mtx); + std::lock_guard lock(tcsdata_mtx); bool _tcs = tcsdata.is_tcs_open; std::string tcsname = ( _tcs ? tcsdata.tcsname : "offline" ); - double angle_acam=NAN, ra_acam=NAN, dec_acam=NAN; // outputs from fpoffsets - if ( _tcs ) this->target.save_casangle( tcsdata.angle_scope ); // store in the Target class, required for acquisition // Compute FP offsets from TCS coordinates (SCOPE) to ACAM coodinates.