diff --git a/Andor/CMakeLists.txt b/Andor/CMakeLists.txt index 973fea2e..1af81c1a 100644 --- a/Andor/CMakeLists.txt +++ b/Andor/CMakeLists.txt @@ -10,7 +10,7 @@ set( ANDOR_DIR ${PROJECT_BASE_DIR}/Andor ) set( CMAKE_CXX_STANDARD 17 ) -add_definitions( -Wall -ansi -O0 -Wno-variadic-macros -ggdb ) +add_definitions( -Wall -O0 -Wno-variadic-macros -ggdb ) add_definitions(-D_LP64) include_directories( ${PROJECT_BASE_DIR}/utils ) @@ -30,12 +30,14 @@ find_library( CFITS_LIB cfitsio NAMES libcfitsio PATHS /usr/local/lib ) find_path( PYTHON_DEV "Python.h" PATHS /usr/include/python3.9 ) find_library( PYTHON_LIB python3.9 NAMES libpython3.9 PATHS /usr/lib64 ) include_directories( ${PYTHON_DEV} ) + add_link_options( -L/usr/lib64 -lpython3.9 -lcrypt -lpthread -ldl -lutil -lm -lm ) +include_directories( ${PYTHON_DEV} ) + add_library( andor STATIC ${ANDOR_DIR}/andor.cpp ${ANDOR_DIR}/andor_emulator.cpp - ${PYTHON_DEV} ) target_link_libraries( andor diff --git a/Andor/andor.h b/Andor/andor.h index 40616662..700dd4bb 100644 --- a/Andor/andor.h +++ b/Andor/andor.h @@ -517,7 +517,7 @@ namespace Andor { * @return "emulator" or "sdk" or "null" * */ - inline std::string_view get_andor_object() { + inline std::string_view get_andor_object() const { if ( this->andor == &emulator ) return ANDOR_OBJ_EMULATOR; else if ( this->andor == &sdk ) return ANDOR_OBJ_SDK; diff --git a/Andor/andor_emulator.cpp b/Andor/andor_emulator.cpp index df7a0e9d..c179278f 100644 --- a/Andor/andor_emulator.cpp +++ b/Andor/andor_emulator.cpp @@ -1144,8 +1144,8 @@ namespace Andor { * @return ERROR | NO_ERROR * */ - long SkySim::generate_image( const std::string_view &headerfile, - const std::string_view &outputfile, + long SkySim::generate_image( const std::string headerfile, + const std::string outputfile, const float exptime, const bool ismex, const int simsize ) { @@ -1218,7 +1218,7 @@ namespace Andor { } #ifdef LOGLEVEL_DEBUG - log_python_arguments(pFunction, pArgs, pKwArgs); +// log_python_arguments(pFunction, pArgs, pKwArgs); #endif // Call the Python function here diff --git a/Andor/andor_emulator.h b/Andor/andor_emulator.h index 09a6b594..8b8acb9a 100644 --- a/Andor/andor_emulator.h +++ b/Andor/andor_emulator.h @@ -41,7 +41,7 @@ namespace Andor { PyObject* pSkySimModule; - long generate_image( const std::string_view &headerfile, const std::string_view &outputfile, + long generate_image( const std::string headerfile, const std::string outputfile, const float exptime, const bool ismex, const int simsize ); void log_python_arguments(PyObject* pFunction, PyObject* pArgs, PyObject* pKwArgs); diff --git a/CMakeLists.txt b/CMakeLists.txt index 27ac4ab1..c2f900ec 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -4,23 +4,23 @@ # @author David Hale # ---------------------------------------------------------------------------- -add_definitions(-DLOGLEVEL_DEBUG) - cmake_minimum_required( VERSION 3.12 ) +project( NGPS ) + +add_definitions(-DLOGLEVEL_DEBUG) + set( CMAKE_CXX_STANDARD 17 ) set( CMAKE_CXX_STANDARD_REQUIRED ON ) set( CMAKE_CXX_EXTENSIONS OFF ) -project( NGPS ) - -set( PROJECT_BASE_DIR $ENV{PWD}/../ ) +set( PROJECT_BASE_DIR ${CMAKE_SOURCE_DIR}) set( PROJECT_SRC_DIR ${PROJECT_BASE_DIR}/src ) set( PROJECT_INC_DIR ${PROJECT_BASE_DIR}/include ) set( EXECUTABLE_OUTPUT_PATH ${PROJECT_BASE_DIR}/bin ) set( LIBRARY_OUTPUT_PATH ${PROJECT_BASE_DIR}/lib ) -set( CMAKE_C_COMPILER /usr/bin/g++ ) +set( CMAKE_C_COMPILER /usr/bin/gcc ) set( CMAKE_CXX_COMPILER /usr/bin/g++ ) find_package( Threads ) diff --git a/Config/sequencerd.cfg.in b/Config/sequencerd.cfg.in index 61d81707..636b86d1 100644 --- a/Config/sequencerd.cfg.in +++ b/Config/sequencerd.cfg.in @@ -190,16 +190,6 @@ CAL_TARGET=(SCIENCE close open on on on on off off off off off off # UPDATE_TIME_REMAINING=10 # block updates to active target with less than this exptime (sec) remaining -# ----------------------------------------------------------------------------- -# TELEM_PROVIDER=( ) -# -# This is a list of telemetry providers where is the daemon name, -# and is the port on which to send the telemetry request. -# Provide one per line. -# -TELEM_PROVIDER=(slitd @SLITD_NB_PORT@) -TELEM_PROVIDER=(tcsd @TCSD_NB_PORT@) -TELEM_PROVIDER=(camerad @CAMERAD_NB_PORT@) # SUBSCRIBE_TO=(slitd "tcp://127.0.0.1:@SLITD_PUB_PORT@") SUBSCRIBE_TO=(tcsd "tcp://127.0.0.1:@TCSD_PUB_PORT@") diff --git a/Config/slicecamd.cfg.in b/Config/slicecamd.cfg.in index e3abb098..46c35ca1 100644 --- a/Config/slicecamd.cfg.in +++ b/Config/slicecamd.cfg.in @@ -77,6 +77,40 @@ FINE_ACQUIRE_AIMPOINT=(L 150.0 115.5) # FINE_ACQUIRE_BACKGROUND=(80 165 30 210) +# FINE_ACQUIRE_MIN_SAMPLES= +# Minimum centroid samples before scatter-gated early exit is evaluated. +# Once this many samples are collected and the MAD scatter per axis is below +# the precision threshold, a correction is issued without waiting for MAX_SAMPLES. +# +FINE_ACQUIRE_MIN_SAMPLES=3 + +# FINE_ACQUIRE_SETTLE_FRAMES= +# frames discarded after each telescope move to allow settling before +# centroid measurements resume. +# +FINE_ACQUIRE_SETTLE_FRAMES=2 + +# FINE_ACQUIRE_GAIN= +# Proportional gain = {0..1} applied to the commanded offset when the residual +# is at or below FINE_ACQUIRE_GAIN_THRESHOLD arcsec. +# +FINE_ACQUIRE_GAIN=0.7 + +# FINE_ACQUIRE_GAIN_LARGE= +# Proportional gain applied when the residual exceeds FINE_ACQUIRE_GAIN_THRESHOLD. +# Set to 1.0 for a full-correction step on large offsets to reduce +# the number of correction cycles needed. Set equal to FINE_ACQUIRE_GAIN to +# disable gain scheduling. +# +FINE_ACQUIRE_GAIN_LARGE=1.0 + +# FINE_ACQUIRE_GAIN_THRESHOLD= +# Offset magnitude (arcsec) above which FINE_ACQUIRE_GAIN_LARGE is used instead +# of FINE_ACQUIRE_GAIN. Set to 0 to disable gain scheduling (GAIN is used for +# all corrections. +# +FINE_ACQUIRE_GAIN_THRESHOLD=2.0 + # SkySimulator options: # SKYSIM_IMAGE_SIZE= where is integer # Sets the keyword argument "IMAGE_SIZE=" diff --git a/PI/CMakeLists.txt b/PI/CMakeLists.txt index cb6e897d..abb08161 100644 --- a/PI/CMakeLists.txt +++ b/PI/CMakeLists.txt @@ -10,7 +10,7 @@ set( PI_DIR ${PROJECT_BASE_DIR}/PI ) set( CMAKE_CXX_STANDARD 17 ) -add_definitions( -Wall -ansi -O1 -Wno-variadic-macros -ggdb ) +add_definitions( -Wall -O1 -Wno-variadic-macros -ggdb ) include_directories( ${PROJECT_BASE_DIR}/utils ) include_directories( ${PROJECT_BASE_DIR}/common ) diff --git a/acamd/CMakeLists.txt b/acamd/CMakeLists.txt index 8c509f63..acea97ce 100644 --- a/acamd/CMakeLists.txt +++ b/acamd/CMakeLists.txt @@ -56,13 +56,15 @@ add_executable(acamd ${ACAMD_DIR}/acam_interface.cpp ${ACAMD_DIR}/acam_fits.cpp ${ACAMD_DIR}/motion_interface.cpp + ) +target_include_directories(acamd PRIVATE ${PYTHON_DEV} ${MYSQL_INCLUDES} ) target_link_libraries(acamd andor - python3.9 + ${PYTHON_LIB} physik_instrumente network common @@ -73,32 +75,31 @@ target_link_libraries(acamd database ${ZMQPP_LIB} ${ZMQ_LIB} - ${PYTHON_LIB} ${CCFITS_LIB} ${CFITS_LIB} ${CMAKE_THREAD_LIBS_INIT} ${MYSQL_LIB} ) -target_compile_options(acamd PRIVATE -g -Wall -O1 -Wno-variadic-macros -ggdb) +target_compile_options(acamd PRIVATE -g -Wall -O1 -Wno-deprecated-declarations -Wno-variadic-macros -ggdb) add_executable( newpytest ${ACAMD_DIR}/newpytest.cpp + ) +target_include_directories( newpytest PRIVATE ${PYTHON_DEV} ) - target_link_libraries( newpytest - python3.9 ${PYTHON_LIB} ) add_executable( pytest ${ACAMD_DIR}/pytest.cpp + ) +target_include_directories( pytest PRIVATE ${PYTHON_DEV} ) - target_link_libraries( pytest - python3.9 ${PYTHON_LIB} ) diff --git a/acamd/acam_interface.cpp b/acamd/acam_interface.cpp index f19fbfcf..796719e7 100644 --- a/acamd/acam_interface.cpp +++ b/acamd/acam_interface.cpp @@ -9,6 +9,7 @@ */ #include "acam_interface.h" +#include "fits_header_defs.h" namespace Acam { @@ -1492,6 +1493,7 @@ namespace Acam { jmessage_out[Key::Acamd::ATTEMPTS] = this->target.attempts; jmessage_out[Key::Acamd::SEEING] = this->astrometry.get_seeing(); jmessage_out[Key::Acamd::BACKGROUND] = this->astrometry.get_background(); + jmessage_out[Key::PUBTIME] = get_time_us(); try { this->publisher->publish( jmessage_out, Topic::ACAMD ); @@ -1507,7 +1509,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 +1536,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 +1601,32 @@ namespace Acam { * */ void Interface::handletopic_tcsd( const nlohmann::json &jmessage ) { - { - std::lock_guard lock(snapshot_mtx); - snapshot_status[Topic::TCSD]=true; - } + + std::lock_guard 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, Key::Tcsd::CASANGLE, tcsdata.angle_scope ); + Common::extract_telemetry_value( jmessage, Key::Tcsd::TELRA, tcsdata.ra_scope_hms ); + Common::extract_telemetry_value( jmessage, Key::Tcsd::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, Key::Tcsd::AZ, tcsdata.az ); + Common::extract_telemetry_value( jmessage, "TELFOCUS", tcsdata.telfocus ); + Common::extract_telemetry_value( jmessage, Key::Tcsd::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 +1639,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,12 +1656,10 @@ namespace Acam { * */ void Interface::handletopic_slitd( const nlohmann::json &jmessage ) { - { - std::lock_guard lock(snapshot_mtx); - snapshot_status[Topic::SLITD]=true; + for ( const auto &keyinfo : FitsHeaderKeys::SlitdTelemKeys ) { + telemkeys.add_json_key(jmessage, keyinfo.jkey, keyinfo.keyword, + keyinfo.comment, keyinfo.type, false); } - 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); } /***** Acam::Interface::handletopic_slitd ***********************************/ @@ -2706,7 +2701,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; } @@ -3377,7 +3372,11 @@ logwrite( function, message.str() ); this->acquire_mode = requested_mode; - iface->publish_status(); + // acam needs to publish status on every acquire command to initialize + // a freshness timer in the sequencer. The extra pub noise is a small + // price to pay for the simplicity of not adding another mechanism. + // + iface->publish_status(true); return NO_ERROR; } @@ -4083,6 +4082,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 ); @@ -4375,7 +4378,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 +4423,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 +4457,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 +4586,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 +5008,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 +5019,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(); - - bool _tcs = telem.is_tcs_open; - std::string tcsname = ( _tcs ? telem.tcsname : "offline" ); + long Interface::assemble_header_info() { 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 + // ---------- scope lock tcsdata -------------- + { + std::lock_guard lock(tcsdata_mtx); + + bool _tcs = tcsdata.is_tcs_open; + std::string tcsname = ( _tcs ? tcsdata.tcsname : "offline" ); + + 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 +5057,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 +5122,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 +5143,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? diff --git a/camerad/CMakeLists.txt b/camerad/CMakeLists.txt index f4fcc0a0..bb14f828 100644 --- a/camerad/CMakeLists.txt +++ b/camerad/CMakeLists.txt @@ -20,7 +20,7 @@ link_directories( ${PROJECT_BASE_DIR}/lib ) message( STATUS "compiling for AstroCam GenIII PCI/PCIe") set( INTERFACE_TARGET astrocam) set( ARCAPI_DIR "${PROJECT_BASE_DIR}/ARC") -add_definitions( -Wall -ansi -O1 -Wno-variadic-macros -ggdb ) +add_definitions( -Wall -O1 -Wno-variadic-macros -ggdb ) add_definitions( -DASTROCAM) add_definitions( -DARC66_PCIE) find_path( ARCAPI_BASE "CArcBase.h" PATHS ${ARCAPI_DIR}/CArcBase/inc) diff --git a/camerad/astrocam.cpp b/camerad/astrocam.cpp index 23d4db37..4e37df34 100644 --- a/camerad/astrocam.cpp +++ b/camerad/astrocam.cpp @@ -29,8 +29,9 @@ namespace AstroCam { nlohmann::json jmessage_out; // build JSON message with my telemetry - jmessage_out[Key::SOURCE] = "camerad"; + jmessage_out[Key::SOURCE] = Topic::CAMERAD; jmessage_out[Key::Camerad::READY] = this->can_expose.load(); + jmessage_out[Key::Camerad::SHUTTERTIME] = this->camera.shutter.get_duration(); // publish JSON message try { @@ -50,6 +51,21 @@ namespace AstroCam { /**** AstroCam::Interface::publish_snapshot *********************************/ + /***** AstroCam::Interface::handletopic_snapshot ****************************/ + /** + * @brief what to do when the topic is Topic::SNAPSHOT + * @details This publishes a JSON message containing a snapshot of my + * telemetry info when the subscriber receives the Topic::SNAPSHOT + * topic and the payload contains my name. + * @param[in] jmessage_in subscribed-received JSON message + * + */ + void Interface::handletopic_snapshot( const nlohmann::json &jmessage_in ) { + if ( jmessage_in.contains( Topic::CAMERAD ) ) this->publish_snapshot(); + } + /***** AstroCam::Interface::handletopic_snapshot ****************************/ + + long NewAstroCam::new_expose( std::string nseq_in ) { logwrite( "NewAstroCam::new_expose", nseq_in ); return( NO_ERROR ); @@ -5061,6 +5077,21 @@ logwrite(function, message.str()); // if (pcontroller->has_boi()) { skipspat = 0; } + // Capture the unbinned, unskipped detector dimensions before trimming + // so that detrows/detcols remain the full requested size across + // repeated bin commands. These are preserved even when the per-call + // geometry is shortened by the skip-modulo adjustment below. + // + int detrows_new, detcols_new; + pcontroller->logical_to_physical(spat, spec, detrows_new, detcols_new); + + // Capture the original requested overscan in physical (row/col) form + // before any modulo-binning trim below, so that osrows0/oscols0 retain + // the true requested overscan across repeated bin commands. + // + int osrows0_new, oscols0_new; + pcontroller->logical_to_physical(osspat, osspec, osrows0_new, oscols0_new); + // Remove those skipped pixels from the image size spat -= skipspat; spec -= skipspec; @@ -5084,10 +5115,10 @@ logwrite(function, message.str()); // unchanged by binning, so that when reverting to binning=1 from some // binnnig factor, this is the default image size to revert to. // - pcontroller->detrows = rows; - pcontroller->detcols = cols; - pcontroller->osrows0 = osrows; - pcontroller->oscols0 = oscols; + pcontroller->detrows = detrows_new; + pcontroller->detcols = detcols_new; + pcontroller->osrows0 = osrows0_new; + pcontroller->oscols0 = oscols0_new; pcontroller->skipcols = skipcols; pcontroller->skiprows = skiprows; @@ -5836,7 +5867,7 @@ logwrite(function, message.str()); int &spat, int &spec, int &osspat, int &osspec, int &binspat, int &binspec) { if (!pcontroller) return; pcontroller->physical_to_logical( pcontroller->detrows, pcontroller->detcols, spat, spec ); - pcontroller->physical_to_logical( pcontroller->osrows, pcontroller->oscols, osspat, osspec ); + pcontroller->physical_to_logical( pcontroller->osrows0, pcontroller->oscols0, osspat, osspec ); pcontroller->physical_to_logical( pcontroller->info.binning[_ROW_], pcontroller->info.binning[_COL_], binspat, binspec ); } diff --git a/camerad/astrocam.h b/camerad/astrocam.h index 4e60f415..78bde549 100644 --- a/camerad/astrocam.h +++ b/camerad/astrocam.h @@ -636,6 +636,11 @@ namespace AstroCam { can_expose(true), // am I ready for the next exposure? modeselected(false), useframes(true) { + topic_handlers = { + { Topic::SNAPSHOT, std::function( + [this](const nlohmann::json &msg) { handletopic_snapshot(msg); } ) } + }; + this->pFits.resize( NUM_EXPBUF ); // pre-allocate FITS_file object pointers for each exposure buffer this->fitsinfo.resize( NUM_EXPBUF ); // pre-allocate Camera Information object pointers for each exposure buffer this->writes_pending.resize( NUM_EXPBUF ); // pre-allocate writes_pending vector for each exposure buffer @@ -687,6 +692,7 @@ namespace AstroCam { void start_subscriber_thread() { Common::PubSubHandler::start_subscriber_thread(*this); } void stop_subscriber_thread() { Common::PubSubHandler::stop_subscriber_thread(*this); } void publish_snapshot(std::string* retstring=nullptr); + void handletopic_snapshot(const nlohmann::json &jmessage_in); // vector of pointers to Camera Information containers, one for each exposure number // 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/common/CMakeLists.txt b/common/CMakeLists.txt index 3fc96925..5cd16789 100644 --- a/common/CMakeLists.txt +++ b/common/CMakeLists.txt @@ -23,10 +23,14 @@ include_directories( ${PYTHON_DEV} ) add_library(common STATIC ${PROJECT_UTILS_DIR}/common.cpp + ) +target_link_libraries(common PRIVATE ${ZMQPP_LIB} ${ZMQ_LIB} ) add_library(skyinfo STATIC ${PROJECT_UTILS_DIR}/skyinfo.cpp + ) +target_link_libraries(skyinfo PRIVATE ${PYTHON_LIB} ) diff --git a/common/common.cpp b/common/common.cpp index 3fc176eb..bfd417e8 100644 --- a/common/common.cpp +++ b/common/common.cpp @@ -9,6 +9,38 @@ namespace Common { + /***** Common::Broadcaster::emit ********************************************/ + /** + * @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 Logs message via logwrite, then publishes a JSON payload + * on Topic::BROADCAST if the publisher has been initialized. + * + */ + void Broadcaster::emit( 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] = this->source; + 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()) ); + } + } + /***** Common::Broadcaster::emit ********************************************/ + + /***** Common::collect_telemetry ********************************************/ /** * @brief send the TELEMREQUEST command to daemon to get telemetry diff --git a/common/common.h b/common/common.h index d540dbd3..2e5d8f8e 100644 --- a/common/common.h +++ b/common/common.h @@ -22,6 +22,7 @@ #include "logentry.h" #include "network.h" +#include "message_keys.h" const long NOTHING = -1; const long NO_ERROR = 0; @@ -369,6 +370,56 @@ namespace Common { }; + /**************** Common::Broadcaster ***************************************/ + /** + * @class Broadcaster + * @brief logs a narrative message and publishes it on Topic::BROADCAST + * @details Captures a reference to a publisher and the source daemon name + * at construction time so that call sites need only supply the + * caller function name and message (and severity, for emit). + * The publisher reference is to the daemon's Common::PubSub, which + * may be null at the time this Broadcaster is constructed and gets + * populated later by init_pubsub. + * + */ + class Broadcaster { + private: + const std::unique_ptr &publisher; ///< reference to owner's publisher + std::string source; ///< source daemon name + + public: + Broadcaster( const std::unique_ptr &publisher, + std::string source ) + : publisher(publisher), source(std::move(source)) { } + + /** + * @brief publish a NOTICE severity broadcast + */ + inline void notice( const std::string &function, const std::string &message ) { + this->emit( function, Severity::NOTICE, message ); + } + + /** + * @brief publish a WARNING severity broadcast + */ + inline void warning( const std::string &function, const std::string &message ) { + this->emit( function, Severity::WARNING, message ); + } + + /** + * @brief publish an ERROR severity broadcast + */ + inline void error( const std::string &function, const std::string &message ) { + this->emit( function, Severity::ERROR, message ); + } + + void emit( const std::string &function, + const std::string &severity, + const std::string &message ); + }; + /**************** Common::Broadcaster ***************************************/ + + void collect_telemetry(const std::pair &provider, std::string &retstring); /***** Common::extract_telemetry_value **************************************/ @@ -424,7 +475,8 @@ namespace Common { } else if constexpr ( std::is_same::value || std::is_same::value || std::is_same::value ) { - if ( jvalue.type() == json::value_t::number_unsigned ) { + if ( jvalue.type() == json::value_t::number_integer || + jvalue.type() == json::value_t::number_unsigned ) { value = jvalue.template get(); } } @@ -917,9 +969,13 @@ namespace Common { * @brief template class adds key,value,comment to indicated keydb from json message * @details This extracts the value from a JSON message and uses add_key to add the * keyword to the indicated database map. - * @param[in] type reference to FitsKeys database object - * @param[in] jmessage JSON message is the source of the value - * @param[in] comment comment string for header keyword + * @param[in] jmessage JSON message is the source of the value + * @param[in] jkey key to index jmessage + * @param[in] keyword FITS header keyword + * @param[in] comment comment string for header keyword + * @param[in] type type of key can be optionally specified + * @param[in] use_extension extension or primary + * @param[in] chan channel selects keyword db * */ void add_json_key( const json &jmessage, diff --git a/common/fits_header_defs.h b/common/fits_header_defs.h new file mode 100644 index 00000000..a439ef3d --- /dev/null +++ b/common/fits_header_defs.h @@ -0,0 +1,129 @@ +/** + * @file fits_header_defs.h + * @brief shared FITS header keyword metadata for add_json_key consumers + * @author David Hale + * + */ +#pragma once + +#include "message_keys.h" + +namespace FitsHeaderKeys { + + /** + * @struct Primary + * @brief holds metadata for a primary-header keyword extracted from JSON + */ + struct Primary { + const char* jkey; ///< key to extract from JSON message + const char* keyword; ///< FITS header keyword (uses jkey if empty) + const char* comment; ///< FITS keyword comment + const char* type = ""; ///< optional explicit datatype + }; + + /** + * @struct Extension + * @brief holds metadata for an extension-header keyword extracted from JSON + */ + struct Extension { + const char* chan; ///< extension channel name + const char* jkey; ///< key to extract from JSON message + const char* keyword; ///< FITS header keyword (uses jkey if empty) + const char* comment; ///< FITS keyword comment + const char* type = ""; ///< optional explicit datatype + }; + + const Primary CalibInfoKeys[] = { + { Key::Calibd::MODFEAR.c_str(), "", "FeAr lamp modulator pow dut per" }, + { Key::Calibd::MODTHAR.c_str(), "", "ThAr lamp modulator pow dut per" }, + { Key::Calibd::MODBLCON.c_str(), "", "Blue continuum modulator pow dut per" }, + { Key::Calibd::MODBLBYP.c_str(), "", "Blue bypass modulator pow dut per" }, + { Key::Calibd::MODRDCON.c_str(), "", "Red continuum modulator pow dut per" }, + { Key::Calibd::MODRDBYP.c_str(), "", "Red bypass modulator pow dut per" }, + { Key::Calibd::CALCOVER.c_str(), "", "calib cover state" }, + { Key::Calibd::CALDOOR.c_str(), "", "calib door state" } + }; + + const Extension FlexureInfoKeys[] = { + { "I", Key::Flexured::FLXSPE_I.c_str(), "FLXSPE", "I flexure spectral axis 2 (X) in um", "" }, + { "I", Key::Flexured::FLXSPA_I.c_str(), "FLXSPA", "I flexure spatial axis 3 (Y) in um", "" }, + { "I", Key::Flexured::FLXPIS_I.c_str(), "FLXPIS", "I flexure piston axis 1 (Z) in um", "" }, + { "R", Key::Flexured::FLXSPE_R.c_str(), "FLXSPE", "R flexure spectral axis 2 (X) in um", "" }, + { "R", Key::Flexured::FLXSPA_R.c_str(), "FLXSPA", "R flexure spatial axis 3 (Y) in um", "" }, + { "R", Key::Flexured::FLXPIS_R.c_str(), "FLXPIS", "R flexure piston axis 1 (Z) in um", "" }, + { "G", Key::Flexured::FLXSPE_G.c_str(), "FLXSPE", "G flexure spectral axis 2 (X) in um", "" }, + { "G", Key::Flexured::FLXSPA_G.c_str(), "FLXSPA", "G flexure spatial axis 3 (Y) in um", "" }, + { "G", Key::Flexured::FLXPIS_G.c_str(), "FLXPIS", "G flexure piston axis 1 (Z) in um", "" }, + { "U", Key::Flexured::FLXSPE_U.c_str(), "FLXSPE", "U flexure spectral axis 2 (X) in um", "" }, + { "U", Key::Flexured::FLXSPA_U.c_str(), "FLXSPA", "U flexure spatial axis 3 (Y) in um", "" }, + { "U", Key::Flexured::FLXPIS_U.c_str(), "FLXPIS", "U flexure piston axis 1 (Z) in um", "" } + }; + + const Extension FocusInfoKeys[] = { + { "I", Key::Focusd::FOCUSI.c_str(), "FOCUS", "science camera I focus position in mm", "" }, + { "R", Key::Focusd::FOCUSR.c_str(), "FOCUS", "science camera R focus position in mm", "" }, + { "G", Key::Focusd::FOCUSG.c_str(), "FOCUS", "science camera G focus position in mm", "" }, + { "U", Key::Focusd::FOCUSU.c_str(), "FOCUS", "science camera U focus position in mm", "" } + }; + + const Primary PowerInfoKeys[] = { + { Key::Powerd::LAMPTHAR.c_str(), "", "is ThAr lamp on" }, + { Key::Powerd::LAMPFEAR.c_str(), "", "is FeAr lamp on" }, + { Key::Powerd::LAMPBLUC.c_str(), "", "is blue Xe continuum lamp on" }, + { Key::Powerd::LAMPREDC.c_str(), "", "is red continuum lamp on" }, + { Key::Powerd::LAMPXE.c_str(), "", "is Xe lamp on" }, + { Key::Powerd::LAMPINCA.c_str(), "", "is Incandescent lamp on" } + }; + + const Primary SlitInfoKeys[] = { + { Key::Slitd::SLITW.c_str(), "", "slit width in arcsec" }, + { Key::Slitd::SLITO.c_str(), "", "slit offset in arcsec" }, + { Key::Slitd::SLITPOSA.c_str(), "", "slit actuator A position in mm" }, + { Key::Slitd::SLITPOSB.c_str(), "", "slit actuator B position in mm" } + }; + + const Primary TargetInfoKeys[] = { + { "OBS_ID", "", "Observation ID", "INT" }, + { "NAME", "", "target name", "STRING" }, + { "SLITA", "", "slit angle in deg", "FLOAT" }, + { "POINTMDE", "", "pointing mode", "STRING" }, + { "RA", "", "requested Right Ascension in J2000", "STRING" }, + { "DECL", "", "requested Declination in J2000", "STRING" } + }; + + const Primary TcsInfoKeys[] = { + { Key::Tcsd::CASANGLE.c_str(), "", "TCS reported Cassegrain angle in deg", "FLOAT" }, + { Key::Tcsd::HA.c_str(), "", "hour angle" }, + { Key::Tcsd::RAOFFSET.c_str(), "", "offset Right Ascension" }, + { Key::Tcsd::DECLOFFS.c_str(), "", "offset Declination" }, + { Key::Tcsd::TELRA.c_str(), "", "TCS reported Right Ascension" }, + { Key::Tcsd::TELDEC.c_str(), "", "TCS reported Declination" }, + { Key::Tcsd::AZ.c_str(), "", "TCS reported azimuth" }, + { Key::Tcsd::ZENANGLE.c_str(), "", "TCS reported Zenith angle", "FLOAT" }, + { Key::Tcsd::DOMEAZ.c_str(), "", "TCS reported dome azimuth", "FLOAT" }, + { Key::Tcsd::DOMESHUT.c_str(), "", "dome shutters" }, + { Key::Tcsd::TELFOCUS.c_str(), "", "TCS reported telescope focus position in mm", "FLOAT" } + }; + + const Extension ThermalInfoKeys[] = { + { "I", Key::Thermald::TCCD_I.c_str(), "CCDTEMP", "I CCD temperature in Kelvin", "FLOAT" }, + { "R", Key::Thermald::TCCD_R.c_str(), "CCDTEMP", "R CCD temperature in Kelvin", "FLOAT" }, + { "G", Key::Thermald::TCCD_G.c_str(), "CCDTEMP", "G CCD temperature in Kelvin", "FLOAT" }, + { "U", Key::Thermald::TCCD_U.c_str(), "CCDTEMP", "U CCD temperature in Kelvin", "FLOAT" }, + + { "I", Key::Thermald::TCOLL_I.c_str(), "COLTEMP", "I collimator temp in deg C", "FLOAT" }, + { "R", Key::Thermald::TCOLL_R.c_str(), "COLTEMP", "R collimator temp in deg C", "FLOAT" }, + { "G", Key::Thermald::TCOLL_G.c_str(), "COLTEMP", "G collimator temp in deg C", "FLOAT" }, + + { "I", Key::Thermald::TFOCUS_I.c_str(), "FOCTEMP", "I focus temp in deg C", "FLOAT" }, + { "R", Key::Thermald::TFOCUS_R.c_str(), "FOCTEMP", "R focus temp in deg C", "FLOAT" }, + { "G", Key::Thermald::TFOCUS_G.c_str(), "FOCTEMP", "G focus temp in deg C", "FLOAT" }, + { "U", Key::Thermald::TFOCUS_U.c_str(), "FOCTEMP", "U focus temp in deg C", "FLOAT" } + }; + + const Primary SlitdTelemKeys[] = { + { Key::Slitd::SLITO.c_str(), "SLITO", "slit offset in arcsec", "FLOAT" }, + { Key::Slitd::SLITW.c_str(), "SLITW", "slit width in arcsec", "FLOAT" } + }; + +} diff --git a/common/message_keys.h b/common/message_keys.h index ab7d8a46..fa731040 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"; @@ -24,14 +50,27 @@ namespace Topic { namespace Key { - inline const std::string SOURCE = "source"; + inline const std::string SOURCE = "source"; + inline const std::string PUBTIME = "pubtime"; + + namespace Broadcast { + inline const std::string SEVERITY = "severity"; + inline const std::string MESSAGE = "message"; + } namespace Sequencer { - inline const std::string SEQSTATE = "seqstate"; + inline const std::string SEQSTATE = "seqstate"; + inline const std::string SHOULD_FINEACQUIRE = "should_fineacquire"; } 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 +89,87 @@ 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"; + inline const std::string MOTION = "MOTION"; + inline const std::string HA = "HA"; + inline const std::string RAOFFSET = "RAOFFSET"; + inline const std::string DECLOFFS = "DECLOFFS"; + inline const std::string ZENANGLE = "ZENANGLE"; + inline const std::string DOMEAZ = "DOMEAZ"; + inline const std::string DOMESHUT = "DOMESHUT"; + inline const std::string TELFOCUS = "TELFOCUS"; + } + + namespace Calibd { + inline const std::string MODFEAR = "MODFEAR"; + inline const std::string MODTHAR = "MODTHAR"; + inline const std::string MODBLCON = "MODBLCON"; + inline const std::string MODBLBYP = "MODBLBYP"; + inline const std::string MODRDCON = "MODRDCON"; + inline const std::string MODRDBYP = "MODRDBYP"; + inline const std::string CALCOVER = "CALCOVER"; + inline const std::string CALDOOR = "CALDOOR"; + } + + namespace Flexured { + inline const std::string FLXSPE_I = "FLXSPE_I"; + inline const std::string FLXSPA_I = "FLXSPA_I"; + inline const std::string FLXPIS_I = "FLXPIS_I"; + inline const std::string FLXSPE_R = "FLXSPE_R"; + inline const std::string FLXSPA_R = "FLXSPA_R"; + inline const std::string FLXPIS_R = "FLXPIS_R"; + inline const std::string FLXSPE_G = "FLXSPE_G"; + inline const std::string FLXSPA_G = "FLXSPA_G"; + inline const std::string FLXPIS_G = "FLXPIS_G"; + inline const std::string FLXSPE_U = "FLXSPE_U"; + inline const std::string FLXSPA_U = "FLXSPA_U"; + inline const std::string FLXPIS_U = "FLXPIS_U"; + } + + namespace Focusd { + inline const std::string FOCUSI = "FOCUSI"; + inline const std::string FOCUSR = "FOCUSR"; + inline const std::string FOCUSG = "FOCUSG"; + inline const std::string FOCUSU = "FOCUSU"; + } + + namespace Powerd { + inline const std::string LAMPTHAR = "LAMPTHAR"; + inline const std::string LAMPFEAR = "LAMPFEAR"; + inline const std::string LAMPBLUC = "LAMPBLUC"; + inline const std::string LAMPREDC = "LAMPREDC"; + inline const std::string LAMPXE = "LAMPXE"; + inline const std::string LAMPINCA = "LAMPINCA"; + } + + namespace Thermald { + inline const std::string TCCD_I = "TCCD_I"; + inline const std::string TCCD_R = "TCCD_R"; + inline const std::string TCCD_G = "TCCD_G"; + inline const std::string TCCD_U = "TCCD_U"; + inline const std::string TCOLL_I = "TCOLL_I"; + inline const std::string TCOLL_R = "TCOLL_R"; + inline const std::string TCOLL_G = "TCOLL_G"; + inline const std::string TFOCUS_I = "TFOCUS_I"; + inline const std::string TFOCUS_R = "TFOCUS_R"; + inline const std::string TFOCUS_G = "TFOCUS_G"; + inline const std::string TFOCUS_U = "TFOCUS_U"; + } + } diff --git a/common/powerd_commands.h b/common/powerd_commands.h index da8ef3fa..f10fc2c3 100644 --- a/common/powerd_commands.h +++ b/common/powerd_commands.h @@ -13,6 +13,8 @@ const std::string POWERD_ISOPEN = "isopen"; const std::string POWERD_LIST = "list"; const std::string POWERD_OPEN = "open"; const std::string POWERD_REOPEN = "reopen"; +const std::string POWERD_GET = "get"; +const std::string POWERD_SET = "set"; const std::string POWERD_STATUS = "status"; const std::vector POWERD_SYNTAX = { POWERD_CLOSE, @@ -20,9 +22,9 @@ const std::vector POWERD_SYNTAX = { POWERD_LIST+" [?]", POWERD_OPEN, POWERD_REOPEN+" [?]", + POWERD_GET+" | ", + POWERD_SET+" { ON | OFF | BOOT }", + POWERD_SET+" { ON | OFF | BOOT }", POWERD_STATUS+" [?]", - TELEMREQUEST+" [?]", - "", - " [ ON | OFF | BOOT ]", - " [ ON | OFF | BOOT ]" + TELEMREQUEST+" [?]" }; diff --git a/common/sequencerd_commands.h b/common/sequencerd_commands.h index cf38c57d..40fcdd14 100644 --- a/common/sequencerd_commands.h +++ b/common/sequencerd_commands.h @@ -10,15 +10,19 @@ const std::string SEQUENCERD_ABORT = "abort"; const std::string SEQUENCERD_CONFIG = "config"; const std::string SEQUENCERD_DOTYPE = "do"; +const std::string SEQUENCERD_ENGINEERING = "engineering"; const std::string SEQUENCERD_EXIT = "exit"; +const std::string SEQUENCERD_FINEACQUIRE= "fineacquire"; const std::string SEQUENCERD_GETONETARGET = "getone"; const std::string SEQUENCERD_GUIDE = "guide"; const std::string SEQUENCERD_MODEXPTIME = "modexptime"; const std::string SEQUENCERD_ONTARGET = "ontarget"; const std::string SEQUENCERD_USERCONTINUE = "usercontinue"; +const std::string SEQUENCERD_OP = "op"; const std::string SEQUENCERD_PAUSE = "pause"; const std::string SEQUENCERD_REPEAT = "repeat"; const std::string SEQUENCERD_RESUME = "resume"; +const std::string SEQUENCERD_SCRIPT = "script"; const std::string SEQUENCERD_SHUTDOWN = "shutdown"; const std::string SEQUENCERD_START = "start"; const std::string SEQUENCERD_STARTONE = "startone"; @@ -34,7 +38,10 @@ const std::string SEQUENCERD_ACAM = "acam"; const std::string SEQUENCERD_CALIB = "calib"; const std::string SEQUENCERD_CAMERA = "camera"; const std::string SEQUENCERD_FILTER = "filter"; +const std::string SEQUENCERD_FLEXURE = "flexure"; +const std::string SEQUENCERD_FOCUS = "focus"; const std::string SEQUENCERD_POWER = "power"; +const std::string SEQUENCERD_SLICECAM = "slicecam"; const std::string SEQUENCERD_SLIT = "slit"; const std::string SEQUENCERD_TCS = "tcs"; const std::vector SEQUENCERD_SYNTAX = { @@ -42,23 +49,30 @@ const std::vector SEQUENCERD_SYNTAX = { SEQUENCERD_CALIB+" ...", SEQUENCERD_CAMERA+" ...", SEQUENCERD_FILTER+" ...", + SEQUENCERD_FLEXURE+" ...", + SEQUENCERD_FOCUS+" ...", SEQUENCERD_POWER+" ...", + SEQUENCERD_SLICECAM+" ...", SEQUENCERD_SLIT+" ...", SEQUENCERD_TCS+" ...", "", SEQUENCERD_ABORT, SEQUENCERD_CONFIG, SEQUENCERD_DOTYPE+" [ one | all ]", + SEQUENCERD_ENGINEERING+" [ true | false ]", SEQUENCERD_EXIT, + SEQUENCERD_FINEACQUIRE+" [ enable | disable ]", SEQUENCERD_GETONETARGET, SEQUENCERD_GUIDE, SEQUENCERD_MODEXPTIME+" ", SEQUENCERD_ONTARGET, + SEQUENCERD_OP, SEQUENCERD_PAUSE, SEQUENCERD_REPEAT, SEQUENCERD_RESUME, TELEMREQUEST+" [?]", SEQUENCERD_USERCONTINUE, + SEQUENCERD_SCRIPT, SEQUENCERD_SHUTDOWN, SEQUENCERD_START, SEQUENCERD_STARTONE, diff --git a/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/emulator/CMakeLists.txt b/emulator/CMakeLists.txt index 91dd386e..1f1f5c9e 100644 --- a/emulator/CMakeLists.txt +++ b/emulator/CMakeLists.txt @@ -10,7 +10,7 @@ set( EMULATOR_DIR ${PROJECT_BASE_DIR}/emulator ) set( CMAKE_CXX_STANDARD 17 ) -add_definitions( -Wall -ansi -O0 -Wno-variadic-macros ) +add_definitions( -Wall -O0 -Wno-variadic-macros ) include_directories( ${EMULATOR_DIR} ) include_directories( ${PROJECT_BASE_DIR}/utils ) diff --git a/emulator/tcs.h b/emulator/tcs.h index be4d43ce..99e43820 100644 --- a/emulator/tcs.h +++ b/emulator/tcs.h @@ -136,7 +136,7 @@ namespace TcsEmulator { std::map map_returnval; std::map map_motionval; - long parse_command( const std::string cmd, std::string &retstring ); ///< parse commands for the TCS + long parse_command( std::string cmd, std::string &retstring ); ///< parse commands for the TCS }; /***** TcsEmulator::Interface ***********************************************/ diff --git a/flexured/CMakeLists.txt b/flexured/CMakeLists.txt index 8522b400..ae161f4a 100644 --- a/flexured/CMakeLists.txt +++ b/flexured/CMakeLists.txt @@ -16,6 +16,9 @@ include_directories( ${PROJECT_BASE_DIR}/common ) link_directories( ${PROJECT_BASE_DIR}/lib ) +find_library( ZMQPP_LIB zmqpp NAMES libzmqpp PATHS /usr/local/lib ) +find_library( ZMQ_LIB zmq NAMES libzmq PATHS /usr/local/lib ) + add_executable(flexured ${FLEXURED_DIR}/flexured.cpp ${FLEXURED_DIR}/flexure_server.cpp @@ -28,6 +31,8 @@ target_link_libraries(flexured common logentry utilities + ${ZMQPP_LIB} + ${ZMQ_LIB} ${CMAKE_THREAD_LIBS_INIT} ) diff --git a/flexured/flexure_interface.cpp b/flexured/flexure_interface.cpp index 55288f71..626eaa9c 100644 --- a/flexured/flexure_interface.cpp +++ b/flexured/flexure_interface.cpp @@ -520,8 +520,8 @@ namespace Flexure { else if ( messagetype == "tcsinfo" ) { double casangle=NAN, alt=NAN; - Common::extract_telemetry_value( message_in, "CASANGLE", casangle ); - Common::extract_telemetry_value( message_in, "ALT", alt ); + Common::extract_telemetry_value( message_in, Key::Tcsd::CASANGLE, casangle ); + Common::extract_telemetry_value( message_in, Key::Tcsd::ALT, alt ); message.str(""); message << "casangle=" << casangle << " alt=" << alt; logwrite( function, message.str() ); } diff --git a/powerd/power_server.cpp b/powerd/power_server.cpp index 3ebd3d29..f4f75c25 100644 --- a/powerd/power_server.cpp +++ b/powerd/power_server.cpp @@ -603,19 +603,30 @@ namespace Power { ret = JSON; } } + else - // all other commands go to the powerd interface for parsing + // set|get plug state -- strip command and forward the + // args to Interface::command(), which already parses + // [ON|OFF|BOOT] and [ON|OFF|BOOT]. // - else { + if ( cmd==POWERD_SET || cmd==POWERD_GET ) { try { - std::transform( buf.begin(), buf.end(), buf.begin(), ::toupper ); // make uppercase + std::transform( args.begin(), args.end(), args.begin(), ::toupper ); // make uppercase } catch( ...) { logwrite( function, "ERROR converting command to uppercase" ); ret = ERROR; } - ret = this->interface.command( buf, retstring ); // send the command + ret = this->interface.command( args, retstring ); // send the command } + else { + + // unknown commands generate an error + // + logwrite( function, "ERROR unknown command '"+cmd+"'"); + ret = ERROR; + } + // If retstring not empty then append "DONE" or "ERROR" depending on value of ret, // and log the reply along with the command number. Write the reply back to the socket. diff --git a/sequencerd/CMakeLists.txt b/sequencerd/CMakeLists.txt index dda2f133..cc9d8a41 100644 --- a/sequencerd/CMakeLists.txt +++ b/sequencerd/CMakeLists.txt @@ -10,7 +10,7 @@ set( SEQUENCER_DIR ${PROJECT_BASE_DIR}/sequencerd ) set( CMAKE_CXX_STANDARD 17 ) -add_definitions( -Wall -ansi -O1 -Wno-variadic-macros -ggdb ) +add_definitions( -Wall -O1 -Wno-variadic-macros -Wno-deprecated-declarations -ggdb ) include_directories( ${PROJECT_BASE_DIR}/utils ) include_directories( ${PROJECT_BASE_DIR}/common ) @@ -18,10 +18,14 @@ include_directories( ${PROJECT_BASE_DIR}/acamd ) link_directories( ${PROJECT_BASE_DIR}/lib ) +# C-Python API Development libraries and headers +# find_path( PYTHON_DEV "Python.h" PATHS /usr/include/python3.9 ) find_library( PYTHON_LIB python3.9 NAMES libpython3.9 PATHS /usr/lib64 ) include_directories( ${PYTHON_DEV} ) +# ZMQ +# find_library( ZMQPP_LIB zmqpp NAMES libzmqpp PATHS /usr/local/lib ) find_library( ZMQ_LIB zmq NAMES libzmq PATHS /usr/local/lib ) @@ -38,16 +42,21 @@ add_executable(sequencerd ${SEQUENCER_DIR}/sequencer_server.cpp ${SEQUENCER_DIR}/sequencer_interface.cpp ${SEQUENCER_DIR}/sequence_acquisition.cpp - ${SEQUENCER_DIR}/sequence.cpp - ${MYSQL_INCLUDES} + ${SEQUENCER_DIR}/sequence.cpp + ${SEQUENCER_DIR}/sequence_wait.cpp + ${SEQUENCER_DIR}/sequence_builder.cpp + ${SEQUENCER_DIR}/operation_builders.cpp + ${SEQUENCER_DIR}/sequence_operations.cpp + ) +target_include_directories(sequencerd PRIVATE ${PYTHON_DEV} ) - target_link_libraries(sequencerd network common logentry utilities + md5 skyinfo database ${CMAKE_THREAD_LIBS_INIT} diff --git a/sequencerd/command.h b/sequencerd/command.h new file mode 100644 index 00000000..d03dff3e --- /dev/null +++ b/sequencerd/command.h @@ -0,0 +1,176 @@ +/** + * @file command.h + * @brief header-only library for handling commands to daemons + * @details This provides a wrapper to form command and arg list strings. + * Also a wrapper that stores transition states, and validates that + * a command is allowed to be sent while in the current state. + * @author David Hale + * + */ + +#pragma once + +#include +#include +#include +#include + +/***** Sequencer **************************************************************/ +/** + * @brief namespace for the observation sequencer + * + */ +namespace Sequencer { + + /** + * @brief wrapper to form string from command and arglist + */ + struct Command { + std::string name; + std::vector arglist; + + std::string str() const { + std::string strung = name; + for (const auto &arg : arglist) { + strung += " " + arg; + } + return strung; + } + }; + + + /** + * @brief command specs right now just holds min/max number of allowed args + */ + struct CommandSpec { + int min_args; + int max_args; + }; + + using CommandSpecMap = std::unordered_map; + + + /** + * @brief structure contains command and states it can transition from->to + */ + template + struct Transition { + State from; + std::string command; + State to; + }; + + + /** + * @brief contains the functionality of the library + * @details This pairs specs and transitions with a command and holds + * the client object used to communiate with the daemon. + */ + template + class CommandClient { + private: + Common::DaemonClient &client; + const CommandSpecMap &specs; + State state; + const std::vector> &transitions; + const std::unordered_map &state_names; + + /** + * @brief rejects cmd if its name is unknown or its arg count is out of range + * @param[in] cmd command to check + * @throws std::runtime_error on unknown command name or arg count out of range + */ + void validate_args( const Command &cmd ) const { + auto it = specs.find( cmd.name ); + if (it == specs.end()) throw std::runtime_error("unknown command: "+cmd.name); + int nargs = cmd.arglist.size(); + if (nargs < it->second.min_args || nargs > it->second.max_args) { + throw std::runtime_error("invalid arg count for "+cmd.name); + } + } + + /** + * @brief rejects cmd if it is not permitted from the current state + * @param[in] cmd command to check + * @throws std::runtime_error + */ + void validate_order( const Command &cmd ) const { + for (const auto &transition : transitions) { + if (transition.from == state && transition.command == cmd.name) return; + } + // not valid from current state; collect all valid from-states for the error message + auto name_of = [&](State s) -> std::string { + auto it = state_names.find(s); + return (it != state_names.end()) ? it->second : "?"; + }; + std::string valid; + for (const auto &transition : transitions) { + if (transition.command == cmd.name) { + if (!valid.empty()) valid += ", "; + valid += name_of(transition.from); + } + } + if (valid.empty()) valid = "(none registered)"; + throw std::runtime_error( + "invalid command order: '"+cmd.name+"' not valid in state " + +name_of(state)+"; valid from: {"+valid+"}"); + } + + /** + * @brief advances internal state after a successful send + * @param[in] cmd command that was just forwarded to the daemon + * @details Searches the transition table for a matching (from==state, + * command==cmd.name) entry and sets state to transition.to. + * No-ops silently when no matching transition exists. + */ + void advance_state( const Command &cmd ) { + for (const auto &transition : transitions) { + if (transition.from == state && transition.command == cmd.name) { + state = transition.to; + return; + } + } + } + + public: + /** + * @brief constructs a CommandClient bound to a daemon connection + * @param[in] client DaemonClient used to forward wire commands + * @param[in] specs per-command arg-count constraints + * @param[in] initial_state starting state of the state machine + * @param[in] transitions valid (from, command, to) state transitions + * @param[in] state_names human-readable names for each State value, used in errors + */ + CommandClient( Common::DaemonClient &client, + const CommandSpecMap &specs, + State initial_state, + const std::vector> &transitions, + const std::unordered_map &state_names ) + : client(client), + specs(specs), + state(initial_state), + transitions(transitions), + state_names(state_names) { } + + /** + * @brief primary interface to sending commands + * @details This validates the number of args, validates the + * transition state, that this command is allowed to be used + * in the current state, and sends the command. + * @param[in] cmd Command struct contains command and arglist + * @return return value from the client + * + */ + long send( const Command &cmd ) { + validate_args( cmd ); + validate_order( cmd ); + long ret = client.command( cmd.str() ); + advance_state( cmd ); + return ret; + } + + /** @brief returns the current state-machine state */ + State get_state() const { return state; } + }; +} +/***** Sequencer **************************************************************/ diff --git a/sequencerd/command_rules.h b/sequencerd/command_rules.h new file mode 100644 index 00000000..f1f1d370 --- /dev/null +++ b/sequencerd/command_rules.h @@ -0,0 +1,668 @@ +/** + * @file command_rules.h + * @brief rules for validating command parameters and transitioning states + * @details Each daemon has an enum of states, min/max number of parameters + * and a list of from-to states for each command. The daemon must + * be in the "from" state to accept the command, which then takes it + * to the "to" state. + * @author David Hale + * + */ + +#pragma once +#include "command.h" +#include +#include + +namespace Sequencer { + + // ---------- CAMERAD -------------------------------------------------------- + + enum class CameraState { + IDLE, ///< not yet opened + READY, ///< open and ready to accept commands + EXPOSING, ///< exposure in progress + READING, ///< readout in progress + PAUSED, ///< exposure paused + INVALID ///< sentinel for unverified commands; no live client ever holds this value + }; + + const std::unordered_map camerad_state_names = { + { CameraState::IDLE, "IDLE" }, + { CameraState::READY, "READY" }, + { CameraState::EXPOSING, "EXPOSING" }, + { CameraState::READING, "READING" }, + { CameraState::PAUSED, "PAUSED" }, + { CameraState::INVALID, "INVALID" }, + }; + + const CommandSpecMap camerad_specs = { + { CAMERAD_ABORT, {0, 0} }, + { CAMERAD_ACTIVATE, {0, 4} }, ///< 0 or more of: [ { U G R I } ] + { CAMERAD_BIN, {1, 2} }, ///< [ ] + { CAMERAD_BOI, {1,21} }, ///< | [full| [ [...]]]] + { CAMERAD_CLOSE, {0, 0} }, + { CAMERAD_DEACTIVATE, {1, 4} }, ///< 1 or more of: { U G R I } + { CAMERAD_EXPTIME, {0, 1} }, ///< [ ] + { CAMERAD_EXPOSE, {0, 0} }, + { CAMERAD_FITSNAME, {0, 1} }, + { CAMERAD_FITSNAMING, {0, 1} }, ///< [ time | number] + { CAMERAD_FRAMETRANSFER, {1, 2} }, ///< | | all [ yes | no ] + { CAMERAD_GEOMETRY, {1, 3} }, ///< | [ | ] + { CAMERAD_IMDIR, {0, 1} }, ///< [ ] + { CAMERAD_IMNUM, {0, 1} }, ///< [ ] + { CAMERAD_IMSIZE, {1, 5} }, ///< | [ ] + { CAMERAD_KEY, {1, 2} }, ///< list | KEYWORD=VALUE//COMMENT + { CAMERAD_LOAD, {0, 2} }, ///< [ ] + { CAMERAD_MODEXPTIME, {1, 1} }, ///< + { CAMERAD_OPEN, {0, 4} }, ///< 0 or more of: [ { 0 1 2 3 } ] + { CAMERAD_PAUSE, {0, 0} }, + { CAMERAD_READOUT, {0, 2} }, + { CAMERAD_RESUME, {0, 0} }, + { CAMERAD_SHUTTER, {0, 1} }, ///< [ enable | 1 | disable | 0 ] + { CAMERAD_STOP, {0, 0} }, + // Commands added for structural completeness; arg ranges from camerad_commands.h syntax. + // Transitions for these are all INVALID->INVALID so the sequencer cannot invoke them + // until the from/to states are verified against camerad behavior. + { CAMERAD_AUTODIR, {0, 1} }, + { CAMERAD_BASENAME, {0, 1} }, + { CAMERAD_BIAS, {0, 4} }, + { CAMERAD_BUFFER, {1, 3} }, ///< ? | | [ | ] + { CAMERAD_CONFIG, {0, 1} }, + { CAMERAD_ECHO, {0, 4} }, + { CAMERAD_INTERFACE, {0, 0} }, + { CAMERAD_ISACTIVE, {0, 0} }, + { CAMERAD_ISOPEN, {0, 1} }, + { CAMERAD_LONGERROR, {0, 0} }, + { CAMERAD_MEX, {0, 4} }, + { CAMERAD_MEXAMPS, {0, 4} }, + { CAMERAD_NATIVE, {1, 6} }, ///< ? | [|] [..] + { CAMERAD_PREEXPOSURES, {0, 1} }, + { CAMERAD_TEST, {1, 8} }, ///< ? | ... + { CAMERAD_USEFRAMES, {0, 1} }, + { CAMERAD_WRITEKEYS, {0, 0} } + }; + + const std::vector> camerad_transitions = { + { CameraState::EXPOSING, CAMERAD_ABORT, CameraState::READY }, + { CameraState::READING, CAMERAD_ABORT, CameraState::READY }, + { CameraState::PAUSED, CAMERAD_ABORT, CameraState::READY }, + { CameraState::READY, CAMERAD_ACTIVATE, CameraState::READY }, + { CameraState::READY, CAMERAD_BIN, CameraState::READY }, + { CameraState::READY, CAMERAD_BOI, CameraState::READY }, + { CameraState::READY, CAMERAD_CLOSE, CameraState::IDLE }, + { CameraState::READY, CAMERAD_DEACTIVATE, CameraState::READY }, + { CameraState::READY, CAMERAD_EXPTIME, CameraState::READY }, + { CameraState::READY, CAMERAD_EXPOSE, CameraState::EXPOSING }, + { CameraState::READY, CAMERAD_FITSNAME, CameraState::READY }, + { CameraState::READY, CAMERAD_FITSNAMING, CameraState::READY }, + { CameraState::READY, CAMERAD_FRAMETRANSFER, CameraState::READY }, + { CameraState::READY, CAMERAD_GEOMETRY, CameraState::READY }, + { CameraState::READY, CAMERAD_IMDIR, CameraState::READY }, + { CameraState::READY, CAMERAD_IMNUM, CameraState::READY }, + { CameraState::READY, CAMERAD_IMSIZE, CameraState::READY }, + { CameraState::READY, CAMERAD_KEY, CameraState::READY }, + { CameraState::READY, CAMERAD_LOAD, CameraState::READY }, + { CameraState::EXPOSING, CAMERAD_MODEXPTIME, CameraState::EXPOSING }, + { CameraState::IDLE, CAMERAD_OPEN, CameraState::READY }, + { CameraState::EXPOSING, CAMERAD_PAUSE, CameraState::PAUSED }, + { CameraState::EXPOSING, CAMERAD_READOUT, CameraState::READING }, + { CameraState::READING, CAMERAD_READOUT, CameraState::READY }, + { CameraState::PAUSED, CAMERAD_RESUME, CameraState::EXPOSING }, + { CameraState::READY, CAMERAD_SHUTTER, CameraState::READY }, + { CameraState::EXPOSING, CAMERAD_STOP, CameraState::READY }, + // INVALID->INVALID transitions: these commands cannot be invoked by the + // sequencer until from/to states are verified. The unreachable-from-state + // condition rejects them at runtime via CommandClient::send(). + { CameraState::INVALID, CAMERAD_AUTODIR, CameraState::INVALID }, // TODO: verify and assign real states + { CameraState::INVALID, CAMERAD_BASENAME, CameraState::INVALID }, // TODO: verify and assign real states + { CameraState::INVALID, CAMERAD_BIAS, CameraState::INVALID }, // TODO: verify and assign real states + { CameraState::INVALID, CAMERAD_BUFFER, CameraState::INVALID }, // TODO: verify and assign real states + { CameraState::INVALID, CAMERAD_CONFIG, CameraState::INVALID }, // TODO: verify and assign real states + { CameraState::INVALID, CAMERAD_ECHO, CameraState::INVALID }, // TODO: verify and assign real states + { CameraState::INVALID, CAMERAD_INTERFACE, CameraState::INVALID }, // TODO: verify and assign real states + { CameraState::INVALID, CAMERAD_ISACTIVE, CameraState::INVALID }, // TODO: verify and assign real states + { CameraState::INVALID, CAMERAD_ISOPEN, CameraState::INVALID }, // TODO: verify and assign real states + { CameraState::INVALID, CAMERAD_LONGERROR, CameraState::INVALID }, // TODO: verify and assign real states + { CameraState::INVALID, CAMERAD_MEX, CameraState::INVALID }, // TODO: verify and assign real states + { CameraState::INVALID, CAMERAD_MEXAMPS, CameraState::INVALID }, // TODO: verify and assign real states + { CameraState::INVALID, CAMERAD_NATIVE, CameraState::INVALID }, // TODO: verify and assign real states + { CameraState::INVALID, CAMERAD_PREEXPOSURES, CameraState::INVALID }, // TODO: verify and assign real states + { CameraState::INVALID, CAMERAD_TEST, CameraState::INVALID }, // TODO: verify and assign real states + { CameraState::INVALID, CAMERAD_USEFRAMES, CameraState::INVALID }, // TODO: verify and assign real states + { CameraState::INVALID, CAMERAD_WRITEKEYS, CameraState::INVALID } // TODO: verify and assign real states + }; + + + // ---------- ACAMD ---------------------------------------------------------- + + enum class AcamState { + IDLE, ///< not yet opened + READY, ///< open and ready to accept commands + ACQUIRING, ///< target acquisition in progress + GUIDING, ///< guiding in progress + INVALID ///< sentinel for unverified commands; no live client ever holds this value + }; + + const std::unordered_map acamd_state_names = { + { AcamState::IDLE, "IDLE" }, + { AcamState::READY, "READY" }, + { AcamState::ACQUIRING, "ACQUIRING" }, + { AcamState::GUIDING, "GUIDING" }, + { AcamState::INVALID, "INVALID" }, + }; + + const CommandSpecMap acamd_specs = { + { ACAMD_OPEN, {0, 2} }, ///< [ motion ] [ camera [] ] + { ACAMD_CLOSE, {0, 0} }, + { ACAMD_INIT, {0, 0} }, + { ACAMD_ACQUIRE, {0, 4} }, ///< [ | target | guide | stop ] + { ACAMD_COORDS, {0, 3} }, ///< [ ] + { ACAMD_EXPTIME, {0, 1} }, ///< [ ] + { ACAMD_BIN, {0, 2} }, ///< [ ] + { ACAMD_GAIN, {0, 1} }, ///< [ ] + { ACAMD_TEMP, {0, 1} }, ///< [ ] + { ACAMD_FRAMEGRAB, {1, 3} }, ///< start | stop | one [ ] | status + { ACAMD_HOME, {0, 1} }, ///< [ ] + { ACAMD_FILTER, {0, 1} }, ///< [ | home | ishome ] + { ACAMD_COVER, {0, 1} }, ///< [ open | close | home | ishome ] + { ACAMD_ISOPEN, {0, 1} }, ///< [ motion | camera ] + { ACAMD_ISHOME, {0, 1} }, ///< [ ] + { ACAMD_ISACQUIRED, {0, 0} }, + { ACAMD_PUTONSLIT, {0, 2} }, ///< [ ] + { ACAMD_SOLVE, {0, 1} }, ///< [ ] [ = ... ] -- simplified + { ACAMD_SHUTDOWN, {0, 0} }, + { ACAMD_TCSINIT, {0, 1} }, ///< [ tcs | sim ] + { ACAMD_OFFSETGOAL, {0, 2} }, ///< [ ] + { ACAMD_OFFSETPERIOD,{0, 1} }, ///< [ ] + // Commands added for structural completeness; arg ranges from acamd_commands.h syntax. + // Transitions for these are all INVALID->INVALID so the sequencer cannot invoke them + // until from/to states are verified against acamd behavior. + { ACAMD_AVGFRAMES, {0, 1} }, + { ACAMD_CONFIG, {0, 1} }, + { ACAMD_ECHO, {0, 1} }, + { ACAMD_EXIT, {0, 0} }, + { ACAMD_FRAMEGRABFIX, {0, 1} }, + { ACAMD_IMFLIP, {0, 2} }, + { ACAMD_IMROT, {0, 1} }, + { ACAMD_SPEED, {0, 2} }, + { ACAMD_EMULATOR, {0, 1} }, + { ACAMD_FAN, {0, 1} }, + { ACAMD_GUIDESET, {0, 4} }, + { ACAMD_LOADCALIBRATION, {0, 1} }, + { ACAMD_MOTION, {0, 4} }, + { ACAMD_OFFSETCAL, {0, 1} }, + { ACAMD_QUALITY, {0, 1} }, + { ACAMD_SAVEFRAMES, {0, 1} }, + { ACAMD_SKIPFRAMES, {0, 1} }, + { ACAMD_TCSGET, {0, 1} }, + { ACAMD_TCSISCONNECTED, {0, 1} }, + { ACAMD_TCSISOPEN, {0, 1} }, + { ACAMD_TEST, {1, 8} } + }; + + const std::vector> acamd_transitions = { + { AcamState::IDLE, ACAMD_OPEN, AcamState::READY }, // TODO: verify + { AcamState::READY, ACAMD_CLOSE, AcamState::IDLE }, // TODO: verify + { AcamState::READY, ACAMD_INIT, AcamState::READY }, // TODO: verify + { AcamState::READY, ACAMD_ACQUIRE, AcamState::ACQUIRING }, // TODO: verify + { AcamState::READY, ACAMD_COORDS, AcamState::READY }, // TODO: verify + { AcamState::READY, ACAMD_EXPTIME, AcamState::READY }, // TODO: verify + { AcamState::READY, ACAMD_BIN, AcamState::READY }, // TODO: verify + { AcamState::READY, ACAMD_GAIN, AcamState::READY }, // TODO: verify + { AcamState::READY, ACAMD_TEMP, AcamState::READY }, // TODO: verify + { AcamState::READY, ACAMD_FRAMEGRAB, AcamState::READY }, // TODO: verify + { AcamState::READY, ACAMD_HOME, AcamState::READY }, // TODO: verify + { AcamState::READY, ACAMD_FILTER, AcamState::READY }, // TODO: verify + { AcamState::READY, ACAMD_COVER, AcamState::READY }, // TODO: verify + { AcamState::READY, ACAMD_PUTONSLIT, AcamState::READY }, // TODO: verify + { AcamState::READY, ACAMD_SOLVE, AcamState::READY }, // TODO: verify + { AcamState::READY, ACAMD_OFFSETGOAL, AcamState::READY }, // TODO: verify + { AcamState::READY, ACAMD_OFFSETPERIOD,AcamState::READY }, // TODO: verify + { AcamState::ACQUIRING, ACAMD_ACQUIRE, AcamState::READY }, // TODO: verify (stop) + { AcamState::ACQUIRING, ACAMD_ISACQUIRED, AcamState::ACQUIRING }, // TODO: verify + { AcamState::READY, ACAMD_TCSINIT, AcamState::READY }, // TODO: verify + { AcamState::READY, ACAMD_SHUTDOWN, AcamState::IDLE }, // TODO: verify + { AcamState::READY, ACAMD_ISOPEN, AcamState::READY }, // TODO: verify + { AcamState::READY, ACAMD_ISHOME, AcamState::READY }, // TODO: verify + { AcamState::READY, ACAMD_ISACQUIRED, AcamState::READY }, // TODO: verify + // INVALID->INVALID transitions: rejected at runtime until from/to verified. + { AcamState::INVALID, ACAMD_AVGFRAMES, AcamState::INVALID }, // TODO: verify and assign real states + { AcamState::INVALID, ACAMD_CONFIG, AcamState::INVALID }, // TODO: verify and assign real states + { AcamState::INVALID, ACAMD_ECHO, AcamState::INVALID }, // TODO: verify and assign real states + { AcamState::INVALID, ACAMD_EXIT, AcamState::INVALID }, // TODO: verify and assign real states + { AcamState::INVALID, ACAMD_FRAMEGRABFIX, AcamState::INVALID }, // TODO: verify and assign real states + { AcamState::INVALID, ACAMD_IMFLIP, AcamState::INVALID }, // TODO: verify and assign real states + { AcamState::INVALID, ACAMD_IMROT, AcamState::INVALID }, // TODO: verify and assign real states + { AcamState::INVALID, ACAMD_SPEED, AcamState::INVALID }, // TODO: verify and assign real states + { AcamState::INVALID, ACAMD_EMULATOR, AcamState::INVALID }, // TODO: verify and assign real states + { AcamState::INVALID, ACAMD_FAN, AcamState::INVALID }, // TODO: verify and assign real states + { AcamState::INVALID, ACAMD_GUIDESET, AcamState::INVALID }, // TODO: verify and assign real states + { AcamState::INVALID, ACAMD_LOADCALIBRATION, AcamState::INVALID }, // TODO: verify and assign real states + { AcamState::INVALID, ACAMD_MOTION, AcamState::INVALID }, // TODO: verify and assign real states + { AcamState::INVALID, ACAMD_OFFSETCAL, AcamState::INVALID }, // TODO: verify and assign real states + { AcamState::INVALID, ACAMD_QUALITY, AcamState::INVALID }, // TODO: verify and assign real states + { AcamState::INVALID, ACAMD_SAVEFRAMES, AcamState::INVALID }, // TODO: verify and assign real states + { AcamState::INVALID, ACAMD_SKIPFRAMES, AcamState::INVALID }, // TODO: verify and assign real states + { AcamState::INVALID, ACAMD_TCSGET, AcamState::INVALID }, // TODO: verify and assign real states + { AcamState::INVALID, ACAMD_TCSISCONNECTED, AcamState::INVALID }, // TODO: verify and assign real states + { AcamState::INVALID, ACAMD_TCSISOPEN, AcamState::INVALID }, // TODO: verify and assign real states + { AcamState::INVALID, ACAMD_TEST, AcamState::INVALID } // TODO: verify and assign real states + }; + + + // ---------- CALIBD --------------------------------------------------------- + + enum class CalibState { + IDLE, ///< not yet opened + READY, ///< open and ready to accept commands + MOVING, ///< actuator motion in progress + INVALID ///< sentinel for unverified commands; no live client ever holds this value + }; + + const std::unordered_map calibd_state_names = { + { CalibState::IDLE, "IDLE" }, + { CalibState::READY, "READY" }, + { CalibState::MOVING, "MOVING" }, + { CalibState::INVALID, "INVALID" }, + }; + + const CommandSpecMap calibd_specs = { + { CALIBD_OPEN, {0, 1} }, ///< [ motion | lampmod ] + { CALIBD_CLOSE, {0, 1} }, ///< [ motion | lampmod ] + { CALIBD_ISOPEN, {0, 1} }, ///< [ motion | lampmod ] + { CALIBD_GET, {0, 1} }, ///< [ ] + { CALIBD_HOME, {0, 0} }, + { CALIBD_ISHOME, {0, 0} }, + { CALIBD_SET, {1, 8} }, ///< =open|close ... (variable) + { CALIBD_LAMPMOD, {1, 4} }, ///< open | close | reconnect | default | ... + { CALIBD_NATIVE, {2, 2} }, ///< + // INVALID-transition commands: rejected at runtime until from/to verified. + { CALIBD_EXIT, {0, 0} } + }; + + const std::vector> calibd_transitions = { + { CalibState::IDLE, CALIBD_OPEN, CalibState::READY }, // TODO: verify + { CalibState::READY, CALIBD_CLOSE, CalibState::IDLE }, // TODO: verify + { CalibState::READY, CALIBD_ISOPEN, CalibState::READY }, // TODO: verify + { CalibState::READY, CALIBD_GET, CalibState::READY }, // TODO: verify + { CalibState::READY, CALIBD_HOME, CalibState::MOVING }, // TODO: verify + { CalibState::MOVING, CALIBD_ISHOME, CalibState::MOVING }, // TODO: verify + { CalibState::READY, CALIBD_ISHOME, CalibState::READY }, // TODO: verify + { CalibState::READY, CALIBD_SET, CalibState::MOVING }, // TODO: verify + { CalibState::MOVING, CALIBD_SET, CalibState::READY }, // TODO: verify (completion) + { CalibState::READY, CALIBD_LAMPMOD, CalibState::READY }, // TODO: verify + { CalibState::READY, CALIBD_NATIVE, CalibState::READY }, // TODO: verify + { CalibState::INVALID, CALIBD_EXIT, CalibState::INVALID } // TODO: verify and assign real states + }; + + + // ---------- SLITD ---------------------------------------------------------- + + enum class SlitState { + IDLE, ///< not yet opened + READY, ///< open and ready to accept commands + MOVING, ///< slit motion in progress + HOMED, ///< slit has been homed + INVALID ///< sentinel for unverified commands; no live client ever holds this value + }; + + const std::unordered_map slitd_state_names = { + { SlitState::IDLE, "IDLE" }, + { SlitState::READY, "READY" }, + { SlitState::MOVING, "MOVING" }, + { SlitState::HOMED, "HOMED" }, + { SlitState::INVALID, "INVALID" }, + }; + + const CommandSpecMap slitd_specs = { + { SLITD_OPEN, {0, 0} }, + { SLITD_CLOSE, {0, 0} }, + { SLITD_ISOPEN, {0, 0} }, + { SLITD_GET, {0, 1} }, ///< [ mm ] + { SLITD_HOME, {0, 0} }, + { SLITD_ISHOME, {0, 0} }, + { SLITD_OFFSET, {1, 1} }, ///< + { SLITD_SET, {1, 2} }, ///< [ ] + { SLITD_NATIVE, {2, 4} } ///< [ ] + }; + + const std::vector> slitd_transitions = { + { SlitState::IDLE, SLITD_OPEN, SlitState::READY }, // TODO: verify + { SlitState::READY, SLITD_CLOSE, SlitState::IDLE }, // TODO: verify + { SlitState::HOMED, SLITD_CLOSE, SlitState::IDLE }, // TODO: verify + { SlitState::READY, SLITD_ISOPEN, SlitState::READY }, // TODO: verify + { SlitState::HOMED, SLITD_ISOPEN, SlitState::HOMED }, // TODO: verify + { SlitState::READY, SLITD_GET, SlitState::READY }, // TODO: verify + { SlitState::HOMED, SLITD_GET, SlitState::HOMED }, // TODO: verify + { SlitState::READY, SLITD_HOME, SlitState::MOVING }, // TODO: verify + { SlitState::MOVING, SLITD_ISHOME, SlitState::MOVING }, // TODO: verify (poll) + { SlitState::MOVING, SLITD_HOME, SlitState::HOMED }, // TODO: verify (completion) + { SlitState::READY, SLITD_ISHOME, SlitState::READY }, // TODO: verify + { SlitState::HOMED, SLITD_ISHOME, SlitState::HOMED }, // TODO: verify + { SlitState::HOMED, SLITD_SET, SlitState::MOVING }, // TODO: verify + { SlitState::MOVING, SLITD_SET, SlitState::HOMED }, // TODO: verify (completion) + { SlitState::HOMED, SLITD_OFFSET, SlitState::MOVING }, // TODO: verify + { SlitState::MOVING, SLITD_OFFSET, SlitState::HOMED }, // TODO: verify (completion) + { SlitState::HOMED, SLITD_NATIVE, SlitState::HOMED }, // TODO: verify + { SlitState::READY, SLITD_NATIVE, SlitState::READY } // TODO: verify + }; + + + // ---------- TCSD ----------------------------------------------------------- + + enum class TcsState { + IDLE, ///< not yet opened + READY, ///< open and ready to accept commands + SLEWING, ///< telescope slew in progress + TRACKING, ///< telescope tracking on target + OFFSETTING, ///< telescope offset in progress + INVALID ///< sentinel for unverified commands; no live client ever holds this value + }; + + const std::unordered_map tcsd_state_names = { + { TcsState::IDLE, "IDLE" }, + { TcsState::READY, "READY" }, + { TcsState::SLEWING, "SLEWING" }, + { TcsState::TRACKING, "TRACKING" }, + { TcsState::OFFSETTING, "OFFSETTING" }, + { TcsState::INVALID, "INVALID" }, + }; + + const CommandSpecMap tcsd_specs = { + { TCSD_OPEN, {1, 1} }, ///< + { TCSD_CLOSE, {0, 0} }, + { TCSD_ISOPEN, {0, 0} }, + { TCSD_COORDS, {5, 7} }, ///< [] [""] + { TCSD_GET_COORDS, {0, 0} }, + { TCSD_GET_CASS, {0, 0} }, + { TCSD_GET_DOME, {0, 0} }, + { TCSD_GET_FOCUS, {0, 0} }, + { TCSD_GET_MOTION, {0, 0} }, + { TCSD_GET_OFFSETS, {0, 0} }, + { TCSD_GET_PA, {0, 0} }, + { TCSD_GET_NAME, {0, 0} }, + { TCSD_RINGGO, {1, 1} }, ///< + { TCSD_PTOFFSET, {2, 2} }, ///< + { TCSD_RETOFFSETS, {0, 0} }, + { TCSD_ZERO_OFFSETS, {0, 0} }, + { TCSD_OFFSETRATE, {0, 2} }, ///< [ ] + { TCSD_SET_FOCUS, {1, 1} }, ///< + { TCSD_NATIVE, {1, 1} }, ///< + { TCSD_LIST, {0, 0} }, + { TCSD_LLIST, {0, 0} }, + { TCSD_WEATHER_COORDS, {0, 0} }, + // INVALID-transition commands: rejected at runtime until from/to verified. + { TCSD_EXIT, {0, 0} }, + { TCSD_PUBLISHSTATE, {0, 1} } + }; + + const std::vector> tcsd_transitions = { + { TcsState::IDLE, TCSD_OPEN, TcsState::READY }, // TODO: verify + { TcsState::READY, TCSD_CLOSE, TcsState::IDLE }, // TODO: verify + { TcsState::TRACKING, TCSD_CLOSE, TcsState::IDLE }, // TODO: verify + { TcsState::READY, TCSD_ISOPEN, TcsState::READY }, // TODO: verify + { TcsState::TRACKING, TCSD_ISOPEN, TcsState::TRACKING }, // TODO: verify + { TcsState::READY, TCSD_COORDS, TcsState::SLEWING }, // TODO: verify + { TcsState::TRACKING, TCSD_COORDS, TcsState::SLEWING }, // TODO: verify + { TcsState::SLEWING, TCSD_GET_MOTION, TcsState::SLEWING }, // TODO: verify (poll) + { TcsState::SLEWING, TCSD_GET_COORDS, TcsState::TRACKING }, // TODO: verify (completion) + { TcsState::READY, TCSD_GET_COORDS, TcsState::READY }, // TODO: verify + { TcsState::TRACKING, TCSD_GET_COORDS, TcsState::TRACKING }, // TODO: verify + { TcsState::READY, TCSD_GET_CASS, TcsState::READY }, // TODO: verify + { TcsState::TRACKING, TCSD_GET_CASS, TcsState::TRACKING }, // TODO: verify + { TcsState::READY, TCSD_GET_DOME, TcsState::READY }, // TODO: verify + { TcsState::TRACKING, TCSD_GET_DOME, TcsState::TRACKING }, // TODO: verify + { TcsState::READY, TCSD_GET_FOCUS, TcsState::READY }, // TODO: verify + { TcsState::TRACKING, TCSD_GET_FOCUS, TcsState::TRACKING }, // TODO: verify + { TcsState::READY, TCSD_GET_MOTION, TcsState::READY }, // TODO: verify + { TcsState::READY, TCSD_GET_OFFSETS, TcsState::READY }, // TODO: verify + { TcsState::TRACKING, TCSD_GET_OFFSETS, TcsState::TRACKING }, // TODO: verify + { TcsState::READY, TCSD_GET_PA, TcsState::READY }, // TODO: verify + { TcsState::TRACKING, TCSD_GET_PA, TcsState::TRACKING }, // TODO: verify + { TcsState::READY, TCSD_GET_NAME, TcsState::READY }, // TODO: verify + { TcsState::TRACKING, TCSD_GET_NAME, TcsState::TRACKING }, // TODO: verify + { TcsState::TRACKING, TCSD_RINGGO, TcsState::TRACKING }, // TODO: verify + { TcsState::READY, TCSD_RINGGO, TcsState::READY }, // TODO: verify + { TcsState::TRACKING, TCSD_PTOFFSET, TcsState::OFFSETTING }, // TODO: verify + { TcsState::OFFSETTING, TCSD_GET_MOTION, TcsState::OFFSETTING }, // TODO: verify (poll) + { TcsState::OFFSETTING, TCSD_RETOFFSETS, TcsState::TRACKING }, // TODO: verify (completion) + { TcsState::TRACKING, TCSD_RETOFFSETS, TcsState::TRACKING }, // TODO: verify + { TcsState::READY, TCSD_ZERO_OFFSETS, TcsState::READY }, // TODO: verify + { TcsState::TRACKING, TCSD_ZERO_OFFSETS, TcsState::TRACKING }, // TODO: verify + { TcsState::READY, TCSD_OFFSETRATE, TcsState::READY }, // TODO: verify + { TcsState::TRACKING, TCSD_OFFSETRATE, TcsState::TRACKING }, // TODO: verify + { TcsState::READY, TCSD_SET_FOCUS, TcsState::READY }, // TODO: verify + { TcsState::TRACKING, TCSD_SET_FOCUS, TcsState::TRACKING }, // TODO: verify + { TcsState::READY, TCSD_NATIVE, TcsState::READY }, // TODO: verify + { TcsState::TRACKING, TCSD_NATIVE, TcsState::TRACKING }, // TODO: verify + { TcsState::READY, TCSD_LIST, TcsState::READY }, // TODO: verify + { TcsState::READY, TCSD_LLIST, TcsState::READY }, // TODO: verify + { TcsState::READY, TCSD_WEATHER_COORDS, TcsState::READY }, // TODO: verify + { TcsState::TRACKING, TCSD_WEATHER_COORDS, TcsState::TRACKING }, // TODO: verify + // INVALID->INVALID transitions: rejected at runtime until from/to verified. + { TcsState::INVALID, TCSD_EXIT, TcsState::INVALID }, // TODO: verify and assign real states + { TcsState::INVALID, TCSD_PUBLISHSTATE, TcsState::INVALID } // TODO: verify and assign real states + }; + + + // ---------- FOCUSD --------------------------------------------------------- + + enum class FocusState { + IDLE, ///< not yet opened + READY, ///< open and ready to accept commands + MOVING, ///< focus motion in progress + INVALID ///< sentinel for unverified commands; no live client ever holds this value + }; + + const std::unordered_map focusd_state_names = { + { FocusState::IDLE, "IDLE" }, + { FocusState::READY, "READY" }, + { FocusState::MOVING, "MOVING" }, + { FocusState::INVALID, "INVALID" }, + }; + + const CommandSpecMap focusd_specs = { + { FOCUSD_OPEN, {0, 0} }, + { FOCUSD_CLOSE, {0, 0} }, + { FOCUSD_ISOPEN, {0, 0} }, + { FOCUSD_GET, {1, 1} }, ///< + { FOCUSD_HOME, {0, 1} }, ///< [ ] + { FOCUSD_ISHOME, {0, 0} }, + { FOCUSD_SET, {2, 2} }, ///< { | nominal } + { FOCUSD_DEFAULTPOS, {0, 0} }, + { FOCUSD_NATIVE, {2, 2} }, ///< + { FOCUSD_TEST, {1, 4} } ///< ... + }; + + const std::vector> focusd_transitions = { + { FocusState::IDLE, FOCUSD_OPEN, FocusState::READY }, // TODO: verify + { FocusState::READY, FOCUSD_CLOSE, FocusState::IDLE }, // TODO: verify + { FocusState::READY, FOCUSD_ISOPEN, FocusState::READY }, // TODO: verify + { FocusState::READY, FOCUSD_GET, FocusState::READY }, // TODO: verify + { FocusState::READY, FOCUSD_HOME, FocusState::MOVING }, // TODO: verify + { FocusState::MOVING, FOCUSD_HOME, FocusState::READY }, // TODO: verify (completion) + { FocusState::READY, FOCUSD_ISHOME, FocusState::READY }, // TODO: verify + { FocusState::MOVING, FOCUSD_ISHOME, FocusState::MOVING }, // TODO: verify (poll) + { FocusState::READY, FOCUSD_SET, FocusState::MOVING }, // TODO: verify + { FocusState::MOVING, FOCUSD_SET, FocusState::READY }, // TODO: verify (completion) + { FocusState::READY, FOCUSD_DEFAULTPOS, FocusState::MOVING }, // TODO: verify + { FocusState::MOVING, FOCUSD_DEFAULTPOS, FocusState::READY }, // TODO: verify (completion) + { FocusState::READY, FOCUSD_NATIVE, FocusState::READY }, // TODO: verify + { FocusState::READY, FOCUSD_TEST, FocusState::READY } // TODO: verify + }; + + + // ---------- FLEXURED ------------------------------------------------------- + + enum class FlexureState { + IDLE, ///< not yet opened + READY, ///< open and ready to accept commands + MOVING, ///< flexure compensation motion in progress + INVALID ///< sentinel for unverified commands; no live client ever holds this value + }; + + const std::unordered_map flexured_state_names = { + { FlexureState::IDLE, "IDLE" }, + { FlexureState::READY, "READY" }, + { FlexureState::MOVING, "MOVING" }, + { FlexureState::INVALID, "INVALID" }, + }; + + const CommandSpecMap flexured_specs = { + { FLEXURED_OPEN, {0, 0} }, + { FLEXURED_CLOSE, {0, 0} }, + { FLEXURED_ISOPEN, {0, 0} }, + { FLEXURED_GET, {2, 2} }, ///< + { FLEXURED_SET, {3, 3} }, ///< + { FLEXURED_COMPENSATE, {0, 1} }, ///< [ dryrun ] + { FLEXURED_DEFAULTPOS, {0, 0} }, + { FLEXURED_NATIVE, {2, 2} }, ///< + { FLEXURED_TEST, {1, 4} } ///< ... + }; + + const std::vector> flexured_transitions = { + { FlexureState::IDLE, FLEXURED_OPEN, FlexureState::READY }, // TODO: verify + { FlexureState::READY, FLEXURED_CLOSE, FlexureState::IDLE }, // TODO: verify + { FlexureState::READY, FLEXURED_ISOPEN, FlexureState::READY }, // TODO: verify + { FlexureState::READY, FLEXURED_GET, FlexureState::READY }, // TODO: verify + { FlexureState::READY, FLEXURED_SET, FlexureState::MOVING }, // TODO: verify + { FlexureState::MOVING, FLEXURED_SET, FlexureState::READY }, // TODO: verify (completion) + { FlexureState::READY, FLEXURED_COMPENSATE, FlexureState::MOVING }, // TODO: verify + { FlexureState::MOVING, FLEXURED_COMPENSATE, FlexureState::READY }, // TODO: verify (completion) + { FlexureState::READY, FLEXURED_DEFAULTPOS, FlexureState::MOVING }, // TODO: verify + { FlexureState::MOVING, FLEXURED_DEFAULTPOS, FlexureState::READY }, // TODO: verify (completion) + { FlexureState::READY, FLEXURED_NATIVE, FlexureState::READY }, // TODO: verify + { FlexureState::READY, FLEXURED_TEST, FlexureState::READY } // TODO: verify + }; + + + // ---------- POWERD --------------------------------------------------------- + + enum class PowerdState { + IDLE, ///< not yet opened + READY, ///< open and ready to accept commands + INVALID ///< sentinel for unverified commands; no live client ever holds this value + }; + + const std::unordered_map powerd_state_names = { + { PowerdState::IDLE, "IDLE" }, + { PowerdState::READY, "READY" }, + { PowerdState::INVALID, "INVALID" }, + }; + + const CommandSpecMap powerd_specs = { + { POWERD_OPEN, {0, 0} }, + { POWERD_CLOSE, {0, 0} }, + { POWERD_ISOPEN, {0, 0} }, + { POWERD_STATUS, {0, 1} }, ///< [ ? ] + { POWERD_REOPEN, {0, 0} }, + { POWERD_LIST, {0, 0} }, + { POWERD_GET, {1, 2} }, + { POWERD_SET, {1, 3} } ///< [ ON | OFF | BOOT ] or [ ON | OFF | BOOT ] + }; + + const std::vector> powerd_transitions = { + { PowerdState::IDLE, POWERD_OPEN, PowerdState::READY }, // TODO: verify + { PowerdState::READY, POWERD_CLOSE, PowerdState::IDLE }, // TODO: verify + { PowerdState::READY, POWERD_ISOPEN, PowerdState::READY }, // TODO: verify + { PowerdState::READY, POWERD_STATUS, PowerdState::READY }, // TODO: verify + { PowerdState::READY, POWERD_REOPEN, PowerdState::READY }, // TODO: verify + { PowerdState::READY, POWERD_LIST, PowerdState::READY }, // TODO: verify + { PowerdState::READY, POWERD_GET, PowerdState::READY }, // TODO: verify + { PowerdState::READY, POWERD_SET, PowerdState::READY } // TODO: verify + }; + + + // ---------- SLICECAMD ------------------------------------------------------ + + enum class SlicecamState { + IDLE, ///< not yet opened + READY, ///< open and ready to accept commands + ACQUIRING, ///< fine acquisition in progress + GUIDING, ///< guiding in progress + INVALID ///< sentinel for unverified commands; no live client ever holds this value + }; + + const std::unordered_map slicecamd_state_names = { + { SlicecamState::IDLE, "IDLE" }, + { SlicecamState::READY, "READY" }, + { SlicecamState::ACQUIRING, "ACQUIRING" }, + { SlicecamState::GUIDING, "GUIDING" }, + { SlicecamState::INVALID, "INVALID" }, + }; + + const CommandSpecMap slicecamd_specs = { + { SLICECAMD_OPEN, {0, 2} }, ///< [ motion ] [ camera [] ] + { SLICECAMD_CLOSE, {0, 0} }, + { SLICECAMD_ISOPEN, {0, 1} }, ///< [ motion | camera ] + { SLICECAMD_INIT, {0, 0} }, + { SLICECAMD_EXPTIME, {0, 1} }, ///< [ ] + { SLICECAMD_BIN, {0, 3} }, ///< [ ] + { SLICECAMD_GAIN, {0, 1} }, ///< [ ] + { SLICECAMD_TEMP, {0, 1} }, ///< [ ] + { SLICECAMD_FRAMEGRAB, {1, 3} }, ///< start | stop | one [ ] | status + { SLICECAMD_FINEACQUIRE, {1, 2} }, ///< status | stop | start { L | R } + { SLICECAMD_PUTONSLIT, {0, 4} }, ///< [ ] + { SLICECAMD_SAVEFRAMES, {0, 2} }, ///< [ ] + { SLICECAMD_SHUTTER, {1, 1} }, ///< open | close | auto + { SLICECAMD_SHUTDOWN, {0, 0} }, + { SLICECAMD_TCSINIT, {0, 1} }, ///< [ tcs | sim ] + { SLICECAMD_IMFLIP, {0, 3} }, ///< [ ] + { SLICECAMD_IMROT, {0, 2} }, ///< [ ] + // Commands added for structural completeness; arg ranges from slicecamd_commands.h syntax. + // Transitions for these are all INVALID->INVALID so the sequencer cannot invoke them + // until from/to states are verified against slicecamd behavior. + { SLICECAMD_AVGFRAMES, {0, 1} }, + { SLICECAMD_CONFIG, {0, 1} }, + { SLICECAMD_ECHO, {0, 1} }, + { SLICECAMD_EXIT, {0, 0} }, + { SLICECAMD_FRAMEGRABFIX, {0, 1} }, + { SLICECAMD_SPEED, {0, 2} }, + { SLICECAMD_EMULATOR, {0, 1} }, + { SLICECAMD_FAN, {0, 2} }, + { SLICECAMD_GUISET, {0, 2} }, + { SLICECAMD_ISACQUIRED, {0, 0} }, + { SLICECAMD_TCSGET, {0, 1} }, + { SLICECAMD_TCSISCONNECTED, {0, 1} }, + { SLICECAMD_TCSISOPEN, {0, 1} }, + { SLICECAMD_TEST, {1, 8} } + }; + + const std::vector> slicecamd_transitions = { + { SlicecamState::IDLE, SLICECAMD_OPEN, SlicecamState::READY }, // TODO: verify + { SlicecamState::READY, SLICECAMD_CLOSE, SlicecamState::IDLE }, // TODO: verify + { SlicecamState::READY, SLICECAMD_ISOPEN, SlicecamState::READY }, // TODO: verify + { SlicecamState::READY, SLICECAMD_INIT, SlicecamState::READY }, // TODO: verify + { SlicecamState::READY, SLICECAMD_EXPTIME, SlicecamState::READY }, // TODO: verify + { SlicecamState::READY, SLICECAMD_BIN, SlicecamState::READY }, // TODO: verify + { SlicecamState::READY, SLICECAMD_GAIN, SlicecamState::READY }, // TODO: verify + { SlicecamState::READY, SLICECAMD_TEMP, SlicecamState::READY }, // TODO: verify + { SlicecamState::READY, SLICECAMD_FRAMEGRAB, SlicecamState::READY }, // TODO: verify + { SlicecamState::READY, SLICECAMD_FINEACQUIRE, SlicecamState::ACQUIRING }, // TODO: verify + { SlicecamState::ACQUIRING, SLICECAMD_FINEACQUIRE, SlicecamState::READY }, // TODO: verify (stop) + { SlicecamState::READY, SLICECAMD_PUTONSLIT, SlicecamState::READY }, // TODO: verify + { SlicecamState::READY, SLICECAMD_SAVEFRAMES, SlicecamState::READY }, // TODO: verify + { SlicecamState::READY, SLICECAMD_SHUTTER, SlicecamState::READY }, // TODO: verify + { SlicecamState::READY, SLICECAMD_SHUTDOWN, SlicecamState::IDLE }, // TODO: verify + { SlicecamState::READY, SLICECAMD_TCSINIT, SlicecamState::READY }, // TODO: verify + { SlicecamState::READY, SLICECAMD_IMFLIP, SlicecamState::READY }, // TODO: verify + { SlicecamState::READY, SLICECAMD_IMROT, SlicecamState::READY }, // TODO: verify + // INVALID->INVALID transitions: rejected at runtime until from/to verified. + { SlicecamState::INVALID, SLICECAMD_AVGFRAMES, SlicecamState::INVALID }, // TODO: verify and assign real states + { SlicecamState::INVALID, SLICECAMD_CONFIG, SlicecamState::INVALID }, // TODO: verify and assign real states + { SlicecamState::INVALID, SLICECAMD_ECHO, SlicecamState::INVALID }, // TODO: verify and assign real states + { SlicecamState::INVALID, SLICECAMD_EXIT, SlicecamState::INVALID }, // TODO: verify and assign real states + { SlicecamState::INVALID, SLICECAMD_FRAMEGRABFIX, SlicecamState::INVALID }, // TODO: verify and assign real states + { SlicecamState::INVALID, SLICECAMD_SPEED, SlicecamState::INVALID }, // TODO: verify and assign real states + { SlicecamState::INVALID, SLICECAMD_EMULATOR, SlicecamState::INVALID }, // TODO: verify and assign real states + { SlicecamState::INVALID, SLICECAMD_FAN, SlicecamState::INVALID }, // TODO: verify and assign real states + { SlicecamState::INVALID, SLICECAMD_GUISET, SlicecamState::INVALID }, // TODO: verify and assign real states + { SlicecamState::INVALID, SLICECAMD_ISACQUIRED, SlicecamState::INVALID }, // TODO: verify and assign real states + { SlicecamState::INVALID, SLICECAMD_TCSGET, SlicecamState::INVALID }, // TODO: verify and assign real states + { SlicecamState::INVALID, SLICECAMD_TCSISCONNECTED, SlicecamState::INVALID }, // TODO: verify and assign real states + { SlicecamState::INVALID, SLICECAMD_TCSISOPEN, SlicecamState::INVALID }, // TODO: verify and assign real states + { SlicecamState::INVALID, SLICECAMD_TEST, SlicecamState::INVALID } // TODO: verify and assign real states + }; + +} diff --git a/sequencerd/operation_builders.cpp b/sequencerd/operation_builders.cpp new file mode 100644 index 00000000..d387cd9f --- /dev/null +++ b/sequencerd/operation_builders.cpp @@ -0,0 +1,435 @@ +/** + * @file operation_builders.cpp + * @brief implementation for building operations from command names + * @author David Hale + * + * This file registers the mapping from a parsed sequencer command name to an + * OperationBuilder lambda. Builders fall into two categories: + * + * 1. Internal : sequencer-level orchestration (database-driven, may span + * multiple daemons). These delegate to Ops:: factory methods + * which wrap Sequence:: member functions. Parameters are + * carried as key=value pairs on cmd.params and are exposed to + * the member function via Sequence::current_op_params (set by + * Sequence::run() immediately before op.func() is invoked). + * + * 2. Passthrough : per-daemon translated passthrough. The sequencer DSL form + * "subsystem [do=verb] [key1=val1 ... keyN=valN]" is + * translated to a daemon-native Command{name, arglist} by + * translate_command(), which dispatches to either a + * daemon-specific builder (e.g. build_powerd_command) or + * build_generic_command(). The translated Command is then + * forwarded via CommandClient::send(), which performs + * per-command arg-count and state-machine validation before + * forwarding to the daemon. + * + */ + +#include +#include +#include +#include + +#include "sequence.h" +#include "sequencerd_commands.h" +#include "powerd_commands.h" + +namespace Sequencer { + + /***** Sequencer::Sequence::validate_keys **********************************/ + /** + * @brief reject any params key not in the allowed whitelist + * @param[in] cmd ParsedCommand whose params are being checked + * @param[in] allowed initializer list of permitted key names + * @throws std::runtime_error if any key in cmd.params is not in allowed + * @details Called from daemon-specific builders after the verb and + * required keys have been matched. Ensures that DSL lines like + * "power do=set name=A state=ON garbage=oops" are rejected + * rather than silently forwarded. + * + */ + void Sequence::validate_keys( const ParsedCommand &cmd, + std::initializer_list allowed ) { + for (const auto &[key, val] : cmd.params) { + if (std::find(allowed.begin(), allowed.end(), + std::string_view{key}) == allowed.end()) { + throw std::runtime_error( + "subsystem '"+cmd.subsystem+"': unexpected key '"+key+"'"); + } + } + } + /***** Sequencer::Sequence::validate_keys **********************************/ + + + /***** Sequencer::Sequence::build_generic_command **************************/ + /** + * @brief default ParsedCommand -> Command translation + * @param[in] cmd ParsedCommand to translate + * @return Command{ name, arglist } ready for CommandClient::send() + * @throws std::runtime_error if cmd does not match one of the two + * supported generic forms + * @details Two cases only: + * 1. verb non-empty, params empty + * -> Command{ verb, {} } + * e.g. "acam do=isopen" -> isopen + * 2. verb empty, params has exactly one key=val + * -> Command{ key, { val } } + * e.g. "camera exptime=30000" -> exptime 30000 + * Any other shape (verb + params, multiple params, etc.) is + * a translation error; add a daemon-specific builder. + * + */ + Command Sequence::build_generic_command( const ParsedCommand &cmd ) { + if (!cmd.verb.empty() && cmd.params.size() == 0) { + return Command{ cmd.verb, {} }; + } + if (cmd.verb.empty() && cmd.params.size() == 1) { + const auto &[key, val] = *cmd.params.begin(); + return Command{ key, { val } }; + } + throw std::runtime_error( + "subsystem '"+cmd.subsystem+"': cannot build command" + " (verb='"+cmd.verb+"', "+std::to_string(cmd.params.size())+" params)" + " -- add a daemon-specific translator"); + } + /***** Sequencer::Sequence::build_generic_command **************************/ + + + /***** Sequencer::Sequence::build_powerd_command ***************************/ + /** + * @brief powerd-specific ParsedCommand -> Command translation + * @param[in] cmd ParsedCommand to translate + * @return Command{ name, arglist } in powerd native positional syntax + * @throws std::runtime_error if required keys are missing or unknown + * keys are present + * @details Powerd accepts the following DSL forms: + * power do=set name= state= + * power do=set unit= plug=

state= + * power do=get name= + * power do=get unit= plug=

+ * All other powerd subcommands (open, close, list, status, + * isopen, reopen) fall through to build_generic_command(). + * + */ + Command Sequence::build_powerd_command( const ParsedCommand &cmd ) { + if (cmd.verb == POWERD_SET) { + if (cmd.params.contains("name") && cmd.params.contains("state")) { + validate_keys(cmd, {"name", "state"}); + return Command{ POWERD_SET, + { cmd.params.at("name"), cmd.params.at("state") } }; + } + if (cmd.params.contains("unit") && + cmd.params.contains("plug") && + cmd.params.contains("state")) { + validate_keys(cmd, {"unit", "plug", "state"}); + return Command{ POWERD_SET, + { cmd.params.at("unit"), + cmd.params.at("plug"), + cmd.params.at("state") } }; + } + throw std::runtime_error( + "powerd "+POWERD_SET+": requires name+state or unit+plug+state"); + } + if (cmd.verb == POWERD_GET) { + if (cmd.params.contains("name")) { + validate_keys(cmd, {"name"}); + return Command{ POWERD_GET, { cmd.params.at("name") } }; + } + if (cmd.params.contains("unit") && cmd.params.contains("plug")) { + validate_keys(cmd, {"unit", "plug"}); + return Command{ POWERD_GET, + { cmd.params.at("unit"), cmd.params.at("plug") } }; + } + throw std::runtime_error( + "powerd "+POWERD_GET+": requires name or unit+plug"); + } + return build_generic_command(cmd); + } + /***** Sequencer::Sequence::build_powerd_command ***************************/ + + + /***** Sequencer::Sequence::translate_command ******************************/ + /** + * @brief central ParsedCommand -> Command dispatcher + * @param[in] cmd ParsedCommand from the DSL parser + * @return Command in daemon native syntax + * @throws std::runtime_error from the selected builder + * @details Dispatches to a daemon-specific builder when one exists; + * otherwise falls through to build_generic_command(). Plain + * if/else dispatch -- no registry, no table. + * + */ + Command Sequence::translate_command( const ParsedCommand &cmd ) { + if (cmd.subsystem == SEQUENCERD_POWER) { + return build_powerd_command(cmd); + } + return build_generic_command(cmd); + } + /***** Sequencer::Sequence::translate_command ******************************/ + + + /***** Sequencer::Sequence::init_operation_builders ************************/ + /** + * @brief initializes registration between name and operation + * @details Maps command names to functions that populate an Operation + * from a ParsedCommand. + * + */ + void Sequence::init_operation_builders() { + + // ---------- INTERNAL ------------------------------------------------------ + + // sequencer-level orchestration (Ops factory methods) + // Command names come from the OP_* -> op_names map in sequence.h. + + op_builders[op_names.at(OP_ACAM_INIT)] = [this](Operation &op, const ParsedCommand &cmd) { + op = ops.acam_init(); + op.params = cmd.params; + }; + + op_builders[op_names.at(OP_CALIB_INIT)] = [this](Operation &op, const ParsedCommand &cmd) { + op = ops.calib_init(); + op.params = cmd.params; + }; + + op_builders[op_names.at(OP_CALIB_SETUP)] = [this](Operation &op, const ParsedCommand &cmd) { + op = ops.calib_setup(); + op.params = cmd.params; + }; + + op_builders[op_names.at(OP_CAMERA_INIT)] = [this](Operation &op, const ParsedCommand &cmd) { + op = ops.camera_init(); + op.params = cmd.params; + }; + + op_builders[op_names.at(OP_CAMERA_SETUP)] = [this](Operation &op, const ParsedCommand &cmd) { + op = ops.camera_setup(); + op.params = cmd.params; + }; + + op_builders[op_names.at(OP_EXPOSURE)] = [this](Operation &op, const ParsedCommand &cmd) { + op = ops.do_expose(); + op.params = cmd.params; + }; + + op_builders[op_names.at(OP_FLEXURE_INIT)] = [this](Operation &op, const ParsedCommand &cmd) { + op = ops.flexure_init(); + op.params = cmd.params; + }; + + op_builders[op_names.at(OP_FLEXURE_SETUP)] = [this](Operation &op, const ParsedCommand &cmd) { + op = ops.flexure_setup(); + op.params = cmd.params; + }; + + op_builders[op_names.at(OP_FOCUS_INIT)] = [this](Operation &op, const ParsedCommand &cmd) { + op = ops.focus_init(); + op.params = cmd.params; + }; + + op_builders[op_names.at(OP_FOCUS_SETUP)] = [this](Operation &op, const ParsedCommand &cmd) { + op = ops.focus_setup(); + op.params = cmd.params; + }; + + op_builders[op_names.at(OP_MOVE_TO_TARGET)] = [this](Operation &op, const ParsedCommand &cmd) { + op = ops.move_to_target(); + op.params = cmd.params; // may carry optional ra= / dec= overrides + }; + + op_builders[op_names.at(OP_POWER_INIT)] = [this](Operation &op, const ParsedCommand &cmd) { + op = ops.power_init(); + op.params = cmd.params; + }; + + op_builders[op_names.at(OP_REPEAT_EXPOSURE)] = [this](Operation &op, const ParsedCommand &cmd) { + op = ops.repeat_exposure(); + op.params = cmd.params; + }; + + op_builders[op_names.at(OP_SHUTDOWN)] = [this](Operation &op, const ParsedCommand &cmd) { + op = ops.do_shutdown(); + op.params = cmd.params; + }; + + op_builders[op_names.at(OP_SLICECAM_INIT)] = [this](Operation &op, const ParsedCommand &cmd) { + op = ops.slicecam_init(); + op.params = cmd.params; + }; + + op_builders[op_names.at(OP_SLIT_INIT)] = [this](Operation &op, const ParsedCommand &cmd) { + op = ops.slit_init(); + op.params = cmd.params; + }; + + op_builders[op_names.at(OP_SLIT_SETUP)] = [this](Operation &op, const ParsedCommand &cmd) { + op = ops.slit_setup(); + op.params = cmd.params; // may carry optional mode= override + }; + + op_builders[op_names.at(OP_STARTUP)] = [this](Operation &op, const ParsedCommand &cmd) { + op = ops.do_startup(); + op.params = cmd.params; + }; + + op_builders[op_names.at(OP_TCS_INIT)] = [this](Operation &op, const ParsedCommand &cmd) { + op = ops.tcs_init(); + op.params = cmd.params; + }; + + // target_offset shares OP_MOVE_TO_TARGET with move_to_target, so it needs a + // distinct CLI/DSL name. Use its Sequence:: member-function name "target_offset" + // (there is no separate entry in op_names for it). + op_builders["target_offset"] = [this](Operation &op, const ParsedCommand &cmd) { + op = ops.target_offset(); + op.params = cmd.params; + }; + + // ---------- PASSTHROUGH --------------------------------------------------- + + // per-daemon translated passthrough (inline, no Ops method). + // Command names come from SEQUENCERD_* constants in common/sequencerd_commands.h. + // translate_command() converts the DSL ParsedCommand into a daemon-native + // Command{name, arglist}, which is then forwarded via + // CommandClient::send(). The client performs per-command arg-count + // validation and state-machine validation before forwarding to the daemon. + + op_builders[SEQUENCERD_ACAM] = [this](Operation &op, const ParsedCommand &cmd) { + op.id = OP_ACAM_CMD; + op.params = cmd.params; + op.func = [this, cmd_copy=cmd]() { + const std::string function("Sequencer::Sequence::op_builders["+SEQUENCERD_ACAM+"]"); + try { + return this->acamd_cmd.send(translate_command(cmd_copy)); + } + catch (const std::runtime_error &e) { + logwrite(function, "ERROR "+std::string(e.what())); + return ERROR; + } + }; + }; + + op_builders[SEQUENCERD_CALIB] = [this](Operation &op, const ParsedCommand &cmd) { + op.id = OP_CALIB_CMD; + op.params = cmd.params; + op.func = [this, cmd_copy=cmd]() { + const std::string function("Sequencer::Sequence::op_builders["+SEQUENCERD_CALIB+"]"); + try { + return this->calibd_cmd.send(translate_command(cmd_copy)); + } + catch (const std::runtime_error &e) { + logwrite(function, "ERROR "+std::string(e.what())); + return ERROR; + } + }; + }; + + op_builders[SEQUENCERD_CAMERA] = [this](Operation &op, const ParsedCommand &cmd) { + op.id = OP_CAMERA_CMD; + op.params = cmd.params; + op.func = [this, cmd_copy=cmd]() { + const std::string function("Sequencer::Sequence::op_builders["+SEQUENCERD_CAMERA+"]"); + try { + return this->camerad_cmd.send(translate_command(cmd_copy)); + } + catch (const std::runtime_error &e) { + logwrite(function, "ERROR "+std::string(e.what())); + return ERROR; + } + }; + }; + + op_builders[SEQUENCERD_FLEXURE] = [this](Operation &op, const ParsedCommand &cmd) { + op.id = OP_FLEXURE_CMD; + op.params = cmd.params; + op.func = [this, cmd_copy=cmd]() { + const std::string function("Sequencer::Sequence::op_builders["+SEQUENCERD_FLEXURE+"]"); + try { + return this->flexured_cmd.send(translate_command(cmd_copy)); + } + catch (const std::runtime_error &e) { + logwrite(function, "ERROR "+std::string(e.what())); + return ERROR; + } + }; + }; + + op_builders[SEQUENCERD_FOCUS] = [this](Operation &op, const ParsedCommand &cmd) { + op.id = OP_FOCUS_CMD; + op.params = cmd.params; + op.func = [this, cmd_copy=cmd]() { + const std::string function("Sequencer::Sequence::op_builders["+SEQUENCERD_FOCUS+"]"); + try { + return this->focusd_cmd.send(translate_command(cmd_copy)); + } + catch (const std::runtime_error &e) { + logwrite(function, "ERROR "+std::string(e.what())); + return ERROR; + } + }; + }; + + op_builders[SEQUENCERD_POWER] = [this](Operation &op, const ParsedCommand &cmd) { + op.id = OP_POWER_CMD; + op.params = cmd.params; + op.func = [this, cmd_copy=cmd]() { + const std::string function("Sequencer::Sequence::op_builders["+SEQUENCERD_POWER+"]"); + try { + return this->powerd_cmd.send(translate_command(cmd_copy)); + } + catch (const std::runtime_error &e) { + logwrite(function, "ERROR "+std::string(e.what())); + return ERROR; + } + }; + }; + + op_builders[SEQUENCERD_SLICECAM] = [this](Operation &op, const ParsedCommand &cmd) { + op.id = OP_SLICECAM_CMD; + op.params = cmd.params; + op.func = [this, cmd_copy=cmd]() { + const std::string function("Sequencer::Sequence::op_builders["+SEQUENCERD_SLICECAM+"]"); + try { + return this->slicecamd_cmd.send(translate_command(cmd_copy)); + } + catch (const std::runtime_error &e) { + logwrite(function, "ERROR "+std::string(e.what())); + return ERROR; + } + }; + }; + + op_builders[SEQUENCERD_SLIT] = [this](Operation &op, const ParsedCommand &cmd) { + op.id = OP_SLIT_CMD; + op.params = cmd.params; + op.func = [this, cmd_copy=cmd]() { + const std::string function("Sequencer::Sequence::op_builders["+SEQUENCERD_SLIT+"]"); + try { + return this->slitd_cmd.send(translate_command(cmd_copy)); + } + catch (const std::runtime_error &e) { + logwrite(function, "ERROR "+std::string(e.what())); + return ERROR; + } + }; + }; + + op_builders[SEQUENCERD_TCS] = [this](Operation &op, const ParsedCommand &cmd) { + op.id = OP_TCS_CMD; + op.params = cmd.params; + op.func = [this, cmd_copy=cmd]() { + const std::string function("Sequencer::Sequence::op_builders["+SEQUENCERD_TCS+"]"); + try { + return this->tcsd_cmd.send(translate_command(cmd_copy)); + } + catch (const std::runtime_error &e) { + logwrite(function, "ERROR "+std::string(e.what())); + return ERROR; + } + }; + }; + + } + /***** Sequencer::Sequence::init_operation_builders ************************/ + +} diff --git a/sequencerd/sequence.cpp b/sequencerd/sequence.cpp index 5db8147d..1fc11393 100644 --- a/sequencerd/sequence.cpp +++ b/sequencerd/sequence.cpp @@ -18,6 +18,801 @@ namespace Sequencer { constexpr long CAMERA_PROLOG_TIMEOUT = 6000; ///< timeout msec to send camera prolog command + namespace { + // ------------------------------------------------------------------------ + // Maps each Operation::id to the DaemonBit it depends on. Operations + // that manage daemon state themselves (startup, shutdown, power_init, + // power_shutdown) are intentionally absent from this map. + // Used exclusively by validate_sequence(). + // ------------------------------------------------------------------------ + const std::unordered_map opid_to_daemon = { + { OP_EXPOSURE, DAEMON_CAMERA }, + { OP_CAMERA_SETUP, DAEMON_CAMERA }, + { OP_CAMERA_INIT, DAEMON_CAMERA }, + { OP_CAMERA_SHUTDOWN, DAEMON_CAMERA }, + { OP_SLIT_SETUP, DAEMON_SLIT }, + { OP_SLIT_INIT, DAEMON_SLIT }, + { OP_SLIT_SHUTDOWN, DAEMON_SLIT }, + { OP_MOVE_TO_TARGET, DAEMON_TCS }, + { OP_TCS_INIT, DAEMON_TCS }, + { OP_TCS_SHUTDOWN, DAEMON_TCS }, + { OP_FLEXURE_SETUP, DAEMON_FLEXURE }, + { OP_FLEXURE_INIT, DAEMON_FLEXURE }, + { OP_FLEXURE_SHUTDOWN, DAEMON_FLEXURE }, + { OP_FOCUS_SETUP, DAEMON_FOCUS }, + { OP_FOCUS_INIT, DAEMON_FOCUS }, + { OP_FOCUS_SHUTDOWN, DAEMON_FOCUS }, + { OP_CALIB_SETUP, DAEMON_CALIB }, + { OP_CALIB_INIT, DAEMON_CALIB }, + { OP_CALIB_SHUTDOWN, DAEMON_CALIB }, + { OP_ACAM_INIT, DAEMON_ACAM }, + { OP_ACAM_SHUTDOWN, DAEMON_ACAM }, + { OP_SLICECAM_INIT, DAEMON_SLICECAM }, + { OP_SLICECAM_SHUTDOWN, DAEMON_SLICECAM }, + { OP_ACAM_CMD, DAEMON_ACAM }, + { OP_CALIB_CMD, DAEMON_CALIB }, + { OP_CAMERA_CMD, DAEMON_CAMERA }, + { OP_FLEXURE_CMD, DAEMON_FLEXURE }, + { OP_FOCUS_CMD, DAEMON_FOCUS }, + { OP_POWER_CMD, DAEMON_POWER }, + { OP_SLICECAM_CMD, DAEMON_SLICECAM }, + { OP_SLIT_CMD, DAEMON_SLIT }, + { OP_TCS_CMD, DAEMON_TCS } + }; + + // ------------------------------------------------------------------------ + // Returns true if the two Operation::id values are unsafe to run in the + // same PARALLEL group. Serial ordering of any such pair is fine. + // ------------------------------------------------------------------------ + inline bool is_parallel_unsafe_pair(OperationStatusBits a, OperationStatusBits b) { + // normalize ordering so each pair is checked once + if (a > b) std::swap(a, b); + + // global sequence ops conflict with anything big + if (a == OP_STARTUP && b == OP_SHUTDOWN) return true; + if (a == OP_STARTUP && b == OP_EXPOSURE) return true; + + // startup runs all inits internally; parallelling any init with startup is a double-init + auto is_init = [](OperationStatusBits t) { + return t == OP_ACAM_INIT || t == OP_CALIB_INIT || + t == OP_CAMERA_INIT || t == OP_FLEXURE_INIT|| + t == OP_FOCUS_INIT || t == OP_POWER_INIT || + t == OP_SLICECAM_INIT || t == OP_SLIT_INIT || + t == OP_TCS_INIT; + }; + auto is_shutdown = [](OperationStatusBits t) { + return t == OP_ACAM_SHUTDOWN || t == OP_CALIB_SHUTDOWN || + t == OP_CAMERA_SHUTDOWN || t == OP_FLEXURE_SHUTDOWN|| + t == OP_FOCUS_SHUTDOWN || t == OP_POWER_SHUTDOWN || + t == OP_SLICECAM_SHUTDOWN || t == OP_SLIT_SHUTDOWN || + t == OP_TCS_SHUTDOWN; + }; + + if (a == OP_STARTUP && is_init(b)) return true; + if (a == OP_SHUTDOWN && is_init(b)) return true; + + // power_init vs power_shutdown + if (a == OP_POWER_INIT && b == OP_POWER_SHUTDOWN) return true; + + // matching init/shutdown pair for the same subsystem + if (is_init(a) && is_shutdown(b)) { + auto subsystem_of = [](OperationStatusBits t) -> int { + switch (t) { + case OP_ACAM_INIT: case OP_ACAM_SHUTDOWN: return 1; + case OP_CALIB_INIT: case OP_CALIB_SHUTDOWN: return 2; + case OP_CAMERA_INIT: case OP_CAMERA_SHUTDOWN: return 3; + case OP_FLEXURE_INIT: case OP_FLEXURE_SHUTDOWN: return 4; + case OP_FOCUS_INIT: case OP_FOCUS_SHUTDOWN: return 5; + case OP_POWER_INIT: case OP_POWER_SHUTDOWN: return 6; + case OP_SLICECAM_INIT: case OP_SLICECAM_SHUTDOWN: return 7; + case OP_SLIT_INIT: case OP_SLIT_SHUTDOWN: return 8; + case OP_TCS_INIT: case OP_TCS_SHUTDOWN: return 9; + default: return 0; + } + }; + if (subsystem_of(a) == subsystem_of(b)) return true; + } + + return false; + } + } // anonymous namespace + + /***** Sequencer::Sequence::operation_sleep ********************************/ + /** + * @brief interruptable sleep for operations + * @details Use this if an Operation needs to sleep. Can be cancelled. + * @param[in] delay_ms delay in milliseconds + * + */ + void Sequence::operation_sleep(int delay_ms) { + std::unique_lock lock(cv_mutex); + cv.wait_for(lock, + std::chrono::milliseconds(delay_ms), + [this]() { return is_cancelled(); }); + } + /***** Sequencer::Sequence::operation_sleep ********************************/ + + + /***** Sequencer::Sequence::handle_operation_exception *********************/ + /** + * @brief logs exceptions thrown by operations + * @param[in] eptr exception pointer + * @param[in] name name of operation + * @param[in] caller calling function + * + */ + void Sequence::handle_operation_exception( std::exception_ptr eptr, + std::string name, std::string caller ) { + try { + if (eptr) std::rethrow_exception(eptr); + } + catch (const std::exception &e) { + logwrite(caller, "ERROR in "+name+": "+std::string(e.what())); + } + } + /***** Sequencer::Sequence::handle_operation_exception *********************/ + + + /***** Sequencer::Sequence::run ********************************************/ + /** + * @brief executes a single operation + * @param[in] op Operation + * @param[in] caller name of calling function for logging + * @return ERROR|NO_ERROR|ABORT + * + */ + long Sequence::run( const Operation &op, + std::string caller ) { + long error=NO_ERROR; + int attempt=1; + + logwrite(caller, "starting "+op.name()); + + while (!is_cancelled()) { + + try { + this->current_op_params = op.params; // expose params to op member functions + error = op.func(); + if (error==NO_ERROR) return NO_ERROR; + if (error==ABORT || + is_cancelled()) return ABORT; + } + catch (...) { + error=ERROR; + handle_operation_exception( std::current_exception(), op.name(), caller ); + } + + if (attempt >= op.max_attempts) { + logwrite(caller, "ERROR "+op.name()+ + " failed after "+std::to_string(attempt)+ + " attempt(s)"); + return ERROR; + } + + ++attempt; + + if (op.on_retry) { + logwrite(caller, "retrying operation "+op.name()+ + " attempt "+std::to_string(attempt)); + if (!is_cancelled()) op.on_retry(); + if (!is_cancelled()) operation_sleep(op.retry_delay); + } + } + return ( is_cancelled() ? ABORT : error ); + } + /***** Sequencer::Sequence::run ********************************************/ + + + /***** Sequencer::Sequence::run_parallel ***********************************/ + /** + * @brief executes operations in parallel threads + * @details This will return only when all have completed. + * @param[in] op vector of Operations to execute + * @param[in] caller name of calling function for logging + * @return ERROR|NO_ERROR|ABORT + * + */ + long Sequence::run_parallel( const std::vector &ops, + std::string caller ) { + + std::vector> futures; + futures.reserve(ops.size()); + + // start each operation in a thread from the pool + // + for (const auto &op : ops) { + if (is_cancelled()) return ABORT; + futures.emplace_back( pool.enqueue( [this, op, caller]() { + if (is_cancelled()) return ABORT; + return run(op, caller); + } ) ); + } + + bool is_error=false; + bool is_abort=false; + + // wait for all threads to complete before returning, + // logging each status as they arrive + // + for (size_t i=0; i < futures.size(); ++i) { + std::ostringstream oss; + long ret; + try { + ret = futures[i].get(); + oss << ops[i].name(); + if (ret==ABORT) { is_abort=true; oss << " cancelled"; } + else { + if (ret != NO_ERROR) is_error=true; + oss << " completed" << ( (ret==NO_ERROR) ? "" : " with error"); + } + } + catch (const std::exception &e) { + oss << " received exception: " << e.what(); + is_error=true; + } + catch (...) { + oss << " received unknown exception"; + is_error=true; + } + logwrite(caller, oss.str()); + } + + if (is_abort) return ABORT; + if (is_error) return ERROR; + + return NO_ERROR; + } + /***** Sequencer::Sequence::run_parallel ***********************************/ + + + /***** Sequencer::Sequence::run_sequence ***********************************/ + /** + * @brief executes a sequence, a collection of operation groups + * @details An operation group contains a vector of operations paired + * with a type, SERIAL|PARALLEL which specifies how that group + * is to be executed. This executes a vector of Operation Groups. + * The optional continue_on_error allows a group to continue, or + * stop immediately when any operation within the group fails. + * @param[in] groups vector of OperationGroups + * @param[in] caller name of calling function for logging + * @param[in] continue_on_error continue or stop group execution on error + * @return ERROR|NO_ERROR|ABORT + * + */ + long Sequence::run_sequence( const std::vector &sequence, + std::string caller ) { + long error = NO_ERROR; + + logwrite(caller, "starting sequence"); + + for (const auto &group : sequence) { + if (is_cancelled()) return ABORT; + + // PARALLEL Groups are executed in parallel threads + // + if (group.type == OperationType::PARALLEL) { + long ret = run_parallel(group.operations, caller); + error |= ret; // accumulate any errors + + if (ret != NO_ERROR && + group.on_error == OnError::STOP) return ret; + } + // SERIAL Groups are executed one at a time + // + else { + for (const auto &op : group.operations) { + if (is_cancelled()) return ABORT; + + long ret = run(op, caller); + error |= ret; // accumulate any errors + + if (ret != NO_ERROR && + group.on_error == OnError::STOP) return ret; + } + } + } + + logwrite(caller, "sequence complete"); + + return (error==NO_ERROR) ? NO_ERROR : ERROR; + } + /***** Sequencer::Sequence::run_sequence ***********************************/ + + + /***** Sequencer::Sequence::run_default_sequence ***************************/ + /** + * @brief executes a default observation sequence + * @param[in] caller name of calling function for logging + * @return ERROR|NO_ERROR|ABORT + * + */ + long Sequence::run_default_sequence(std::string caller) { + + std::vector sequence; + + // ---------- RUN THESE IN PARALLEL -------------------- + + // If pointmode is ACAM then the user has chosen to put the star on ACAM, in + // which case the assumption is made that nothing else matters. This special + // mode of operation only points the telescope so this is the only operation + // added to the sequence. + // + if (this->target.pointmode == Acam::POINTMODE_ACAM) { + this->dotype("ONE"); + sequence.push_back( { OperationType::PARALLEL, OnError::STOP, + { { OP_MOVE_TO_TARGET, [this]{ return move_to_target(); } } } } ); + } + else { + this->target.pointmode = Acam::POINTMODE_SLIT; + + // these are the default operations prior to exposure, + // they can be done in parallel + sequence.push_back( { OperationType::PARALLEL, OnError::STOP, + { { OP_MOVE_TO_TARGET, [this]{ return move_to_target(); } }, + { OP_CAMERA_SETUP, [this]{ return camera_setup(); } }, + { OP_FOCUS_SETUP, [this]{ return focus_setup(); } }, + { OP_FLEXURE_SETUP, [this]{ return flexure_setup(); } }, + { OP_CALIB_SETUP, [this]{ return calib_setup(); } }, + { OP_SLIT_SETUP, [this]{ return slit_setup(this->target.iscal ? VSM_DATABASE + : VSM_ACQUIRE); } } + } } ); + } + + // ---------- RUN THESE IN SERIES ---------------------- + + if (this->target.pointmode != Acam::POINTMODE_ACAM) { + sequence.push_back( { OperationType::SERIAL, OnError::STOP, + { { OP_ACQUISITION, + [this,caller]() { return this->do_target_acquisition(caller); } }, + + { OP_MOVE_TO_TARGET, + [this,caller]() { return this->do_target_offset(caller); } }, + + { OP_SLIT_SETUP, + [this]() { return this->do_target_virtualslit(Sequencer::VSM_EXPOSE); } }, + + { OP_EXPOSURE, + [this,caller]() { return this->do_exposure(caller); } } + } + } ); + } + + // ---------- RUN THE SEQUENCE NOW --------------------- + + return run_sequence(sequence, caller); + } + /***** Sequencer::Sequence::run_default_sequence ***************************/ + + + /***** Sequencer::Sequence::run_script *************************************/ + /** + * @brief executes a user script + * @param[in] filename filename of script + * @return ERROR|NO_ERROR|ABORT + * + */ + long Sequence::run_script(const std::string &filename) { + const std::string function("Sequencer::Sequence::run_script"); + + // ---------- PARSE ------------------------------------------------------- + + std::vector commands; + if ( parse_script(filename, commands) != NO_ERROR ) { + logwrite(function, "ERROR parsing '"+filename+"'"); + return ERROR; + } + + // ---------- BUILD ------------------------------------------------------- + + std::vector sequence; + if ( build_sequence(commands, sequence) != NO_ERROR ) { + logwrite(function, "ERROR building sequence from '"+filename+"'"); + return ERROR; + } + + // ---------- VALIDATE ---------------------------------------------------- + + if ( validate_sequence(sequence) != NO_ERROR ) { + logwrite(function, "ERROR validating sequence from '"+filename+"'"); + return ERROR; + } + + // ---------- RUN --------------------------------------------------------- + + return run_sequence(sequence, function); + } + /***** Sequencer::Sequence::run_script *************************************/ + + + /***** Sequencer::Sequence::parse_script ***********************************/ + /** + * @brief parses a user script + * @details This parses a script, makes a ParsedCommand struct from each + * line, returning a vector of ParsedCommands. No validation is + * done here, only parsing. + * @param[in] filename filename of script + * @param[out] commands_out reference to vector of ParsedCommands + * @return ERROR|NO_ERROR + * + */ + long Sequence::parse_script(const std::string &filename, + std::vector &commands_out) { + const std::string function("Sequencer::Sequence::parse_script"); + + std::ifstream file(filename); + if (!file.is_open()) { + logwrite(function, "ERROR opening '"+filename+"'"); + return ERROR; + } + + commands_out.clear(); + std::string line; + int linenum=0; + + while (std::getline(file, line)) { + ++linenum; + + // trim off leading and trailing spaces + lrtrim(line); + + // skip empty lines + if (line.empty()) continue; + + // skip comment lines + if (line[0] == '#') continue; + + // strip inline comments, everything after '#' + auto pos = line.find('#'); + if (pos != std::string::npos) line = line.substr(0, pos); + + auto command = parse_command(line); + + if (!command) continue; + + command->linenum = linenum; + + commands_out.push_back(std::move(command.value())); + } + + if (commands_out.empty()) { + logwrite(function, "ERROR empty script"); + return ERROR; + } + + return NO_ERROR; + } + /***** Sequencer::Sequence::parse_script ***********************************/ + + + /***** Sequencer::Sequence::parse_command **********************************/ + /** + * @brief parses a single command line + * @details The DSL grammar accepted by this parser is strictly: + * command [key1=val1 key2=val2 ... keyN=valN] + * The first token is the subsystem (command). All remaining + * tokens must be key=value pairs — bare tokens, empty keys, + * empty values, duplicate keys, and duplicate do= are all + * hard parse errors and cause the line to be rejected. + * The reserved key DSL_KEY_DO ("do") is extracted to + * ParsedCommand::verb and not stored in params; all other + * pairs are stored in params. + * @param[in] args string containing one command line (consumed by reference) + * @return std::nullopt on empty line or parse error; ParsedCommand otherwise + * + */ + std::optional Sequence::parse_command(std::string &args) { + const std::string function("Sequencer::Sequence::parse_command"); + + std::istringstream iss(args); + std::string word; + + // first word is the subsystem (the command); empty line returns nullopt + if (!(iss >> word)) return std::nullopt; + + ParsedCommand command; + command.subsystem = word; + + while (iss >> word) { + auto eq = word.find('='); + + if (eq == std::string::npos) { + logwrite(function, "ERROR bare token '"+word+"' is invalid" + " (key=value required) in line: "+args); + return std::nullopt; + } + + std::string key = word.substr(0, eq); + std::string val = word.substr(eq+1); + + if (key.empty()) { + logwrite(function, "ERROR empty key in token '"+word+"' in line: "+args); + return std::nullopt; + } + if (val.empty()) { + logwrite(function, "ERROR empty value for key '"+key+"' in line: "+args); + return std::nullopt; + } + + if (key == std::string(DSL_KEY_DO)) { + if (!command.verb.empty()) { + logwrite(function, "ERROR duplicate "+std::string(DSL_KEY_DO) + +"= key in line: "+args); + return std::nullopt; + } + command.verb = val; + continue; + } + + if (command.params.contains(key)) { + logwrite(function, "ERROR duplicate key '"+key+"' in line: "+args); + return std::nullopt; + } + + command.params.set(key, val); + } + + return command; + } + /***** Sequencer::Sequence::parse_command **********************************/ + + + /***** Sequencer::Sequence::validate_sequence ******************************/ + /** + * @brief applies validation rules to sequence + * @param[in] sequence vector of OperationGroups + * @return ERROR|NO_ERROR + * + */ + long Sequence::validate_sequence(const std::vector &sequence) { + const std::string function("Sequencer::Sequence::validate_sequence"); + long error = NO_ERROR; + + // ---------- RULE ------------------------------------------------------- + // Sequencer must not already be running, stopping, starting, or paused. + // Only READY or NOTREADY states allow a new sequence. + + if ( seq_state_manager.is_set(Sequencer::SEQ_RUNNING) ) { + this->broadcast.error( function, "sequence rejected: sequencer already running" ); + return ERROR; + } + + if ( seq_state_manager.is_set(Sequencer::SEQ_STOPPING) ) { + this->broadcast.error( function, "sequence rejected: sequencer is stopping" ); + return ERROR; + } + + if ( seq_state_manager.is_set(Sequencer::SEQ_STARTING) ) { + this->broadcast.error( function, "sequence rejected: sequencer is starting" ); + return ERROR; + } + + if ( seq_state_manager.is_set(Sequencer::SEQ_PAUSED) ) { + this->broadcast.error( function, "sequence rejected: sequencer is paused" ); + return ERROR; + } + + // ---------- RULE ------------------------------------------------------- + // Sequence must not be empty. + + if ( sequence.empty() ) { + this->broadcast.error( function, "sequence is empty" ); + return ERROR; + } + + // ---------- RULE ------------------------------------------------------- + // Every group must contain at least one operation. + + for (const auto &group : sequence) { + if ( group.operations.empty() ) { + this->broadcast.error( function, "sequence contains an empty operation group" ); + return ERROR; + } + } + + // ---------- RULE ------------------------------------------------------- + // Collect presence flags + counts by Operation::id identifier. + // Used by subsequent rules. + + bool has_expose = false; + bool has_camera_setup = false; + bool has_slit_setup = false; + int expose_count = 0; + + for (const auto &group : sequence) { + for (const auto &op : group.operations) { + switch (op.id) { + case OP_EXPOSURE: has_expose = true; ++expose_count; break; + case OP_CAMERA_SETUP: has_camera_setup = true; break; + case OP_SLIT_SETUP: has_slit_setup = true; break; + default: break; + } + } + } + + // ---------- RULE ------------------------------------------------------- + // If expose is present, camera_setup must also be present. + + if ( has_expose && !has_camera_setup ) { + this->broadcast.error( function, "sequence contains 'expose' without 'camera_setup'" ); + error = ERROR; + } + + // ---------- RULE ------------------------------------------------------- + // Daemon readiness. + // Science mode (engineering_mode == false): + // If the sequence contains any op that depends on a daemon, SEQ_READY + // must be set (meaning all daemons are up, established by startup()). + // Engineering mode (engineering_mode == true): + // Only the specific daemons needed by the operations in the sequence + // are required. All failures are reported together. + + { + // collect the set of daemons this sequence depends on + std::bitset required; + for (const auto &group : sequence) { + for (const auto &op : group.operations) { + auto it = opid_to_daemon.find(op.id); + if ( it != opid_to_daemon.end() ) required.set(it->second); + } + } + + if ( required.any() ) { + // for engineering mode it is sufficient if only the required subsystem + // is ready. + if ( this->engineering_mode.load() ) { + for (std::size_t bit = 0; bit < NUM_DAEMONS; ++bit) { + if ( required.test(bit) && + !daemon_manager.is_set(static_cast(bit)) ) { + std::ostringstream oss; + oss << "sequence requires '" + << daemon_name.at(static_cast(bit)) + << "' but it is not ready"; + this->broadcast.error(function, oss.str()); + error = ERROR; + } + } + } + else + // for science mode (non-engineering) all subsystems must be ready + if ( !seq_state_manager.is_set(Sequencer::SEQ_READY) ) { + this->broadcast.error( function, "sequence requires SEQ_READY (all daemons ready) in science mode" ); + error = ERROR; + } + } + } + + // ---------- RULE ------------------------------------------------------- + // Parallel-unsafe pairs. + // Always (either mode): + // No PARALLEL group may contain a parallel-unsafe pair of operations. + // Science mode only: + // A parallel-unsafe pair is also rejected if it appears anywhere in + // the sequence (across groups), because scripted science ops should + // not mix startup/shutdown/expose even in serial form. + // Engineering mode: + // SERIAL placement of any parallel-unsafe pair is allowed; ordering + // is explicit and on_error STOP handles failures. + + { + // intra-group check: no PARALLEL group may contain a parallel-unsafe pair + for (const auto &group : sequence) { + if ( group.type != OperationType::PARALLEL ) continue; + for (std::size_t i = 0; i < group.operations.size(); ++i) { + for (std::size_t j = i+1; j < group.operations.size(); ++j) { + if ( is_parallel_unsafe_pair(group.operations[i].id, + group.operations[j].id) ) { + std::ostringstream oss; + oss << "parallel group contains unsafe pair: '" + << op_names.at(group.operations[i].id) << "' and '" + << op_names.at(group.operations[j].id) << "'"; + this->broadcast.error(function, oss.str()); + error = ERROR; + } + } + } + } + + // science-mode cross-group check: reject parallel-unsafe pairs anywhere + if ( !this->engineering_mode.load() ) { + std::vector all_ops; + all_ops.reserve(NUM_OP_STATES); + for (const auto &group : sequence) { + for (const auto &op : group.operations) all_ops.push_back(op.id); + } + for (std::size_t i = 0; i < all_ops.size(); ++i) { + for (std::size_t j = i+1; j < all_ops.size(); ++j) { + if ( is_parallel_unsafe_pair(all_ops[i], all_ops[j]) ) { + std::ostringstream oss; + oss << "sequence contains unsafe pair in science mode: '" + << op_names.at(all_ops[i]) << "' and '" + << op_names.at(all_ops[j]) + << "' (use 'engineering true' to allow in SERIAL groups)"; + this->broadcast.error(function, oss.str()); + error = ERROR; + } + } + } + } + } + + // ---------- RULE ------------------------------------------------------- + // TBD should I allow expose to not appear more than once across all groups? + + if ( expose_count > 1 ) { + this->broadcast.notice( function, "sequence contains multiple 'expose' operations -- placeholder check" ); + // not a hard error yet; pending clarification of multi-expose DSL usage + } + + // ---------- RULE ------------------------------------------------------- + // PLACEHOLDER: parallel contention check. + // Parallel groups must not contain operations that share a subsystem wait + // state, since they would contend on the same daemon. + // + // When Operation gains a wait_bit field (WaitStateBits), check here that + // no two operations in the same PARALLEL group share the same wait_bit. + // For now this is a no-op stub that compiles and runs cleanly. + + if (false) { + // stub -- replace with real contention check + this->broadcast.notice(function, + "PLACEHOLDER parallel contention check not yet implemented"); + } + + return error; + } + /***** Sequencer::Sequence::validate_sequence ******************************/ + + + /***** Sequencer::Sequence::handle_cli_operation ***************************/ + /** + * @brief handle incoming operation request + * @details This performs all the same steps for a single command as a + * sequence of one operation. This is inefficient for performing + * multiple steps. + * @param[in] args string containing command and any arguments + * @return ERROR|NO_ERROR|ABORT + * + */ + long Sequence::handle_cli_operation(std::string args) { + const std::string function("Sequencer::Sequence::handle_cli_operation"); + + if (args.empty()) return ERROR; + + // ----- build a mini-sequence of one command in order to validate it ----- + + auto parsed = parse_command(args); + if (!parsed) { + logwrite(function, "ERROR parsing '"+args+"'"); + return ERROR; + } + // Wrap the single command in an implicit serial group so build_sequence() + // accepts it without requiring an explicit serial:/end block in the input. + ParsedCommand group_open; + group_open.subsystem = "serial:"; + ParsedCommand group_close; + group_close.subsystem = "end"; + std::vector commands = { group_open, *parsed, group_close }; + + std::vector sequence; + + if ( build_sequence(commands, sequence) != NO_ERROR ) return ERROR; + + if ( validate_sequence(sequence) != NO_ERROR ) return ERROR; + + if ( sequence.empty() || sequence[0].operations.empty() ) { + logwrite(function, "ERROR invalid command '"+args+"'"); + return ERROR; + } + + Operation op = sequence[0].operations[0]; + + // ---------- RUN THE COMMAND --------------------------------------------- + + return run(op, function); + } + /***** Sequencer::Sequence::handle_cli_operation ***************************/ + + /***** Sequencer::Sequence::handletopic_snapshot ***************************/ /** * @brief publishes snapshot of my telemetry @@ -48,16 +843,34 @@ namespace Sequencer { * */ void Sequence::handletopic_camerad(const nlohmann::json &jmessage) { + this->target.column_from_json( DBCol::EXPTIME, Key::Camerad::SHUTTERTIME, jmessage ); if (jmessage.contains(Key::Camerad::READY)) { int isready = jmessage[Key::Camerad::READY].get(); this->can_expose.store(isready, std::memory_order_relaxed); - std::lock_guard lock(camerad_mtx); - this->camerad_cv.notify_all(); } + + std::lock_guard lock(camerad_mtx); + this->camerad_cv.notify_all(); } /***** Sequencer::Sequence::handletopic_camerad ****************************/ + /***** Sequencer::Sequence::handletopic_slitd ******************************/ + /** + * @brief handles Topic::SLITD telemetry + * @param[in] jmessage subscribed-received JSON message + * + */ + void Sequence::handletopic_slitd(const nlohmann::json &jmessage) { + this->target.column_from_json( DBCol::SLITWIDTH, Key::Slitd::SLITW, jmessage ); + this->target.column_from_json( DBCol::SLITOFFSET, Key::Slitd::SLITO, jmessage ); + + std::lock_guard lock(slitd_mtx); + this->slitd_cv.notify_all(); + } + /***** Sequencer::Sequence::handletopic_slitd ******************************/ + + /***** Sequencer::Sequence::handletopic_slicecamd **************************/ /** * @brief handles Topic::SLICECAMD telemetry @@ -65,16 +878,45 @@ namespace Sequencer { * */ void Sequence::handletopic_slicecamd(const nlohmann::json &jmessage) { - // set is_fineacquire_locked flag - bool fineacquirelocked; - Common::extract_telemetry_value( jmessage, Key::Slicecamd::FINEACQUIRE_LOCKED, fineacquirelocked ); - this->is_fineacquire_locked.store(fineacquirelocked, std::memory_order_relaxed); - std::lock_guard lock(this->fineacquire_mtx); - this->fineacquire_cv.notify_all(); + bool changed = false; + if ( jmessage.contains( Key::Slicecamd::FINEACQUIRE_RUNNING ) ) { + this->is_fineacquire_running.store( + jmessage[Key::Slicecamd::FINEACQUIRE_RUNNING].get(), std::memory_order_relaxed ); + changed = true; + } + if ( jmessage.contains( Key::Slicecamd::FINEACQUIRE_LOCKED ) ) { + this->is_fineacquire_locked.store( + jmessage[Key::Slicecamd::FINEACQUIRE_LOCKED].get(), std::memory_order_relaxed ); + changed = true; + } + if ( changed ) { + std::lock_guard lock(this->fineacquire_mtx); + this->fineacquire_cv.notify_all(); + } } /***** Sequencer::Sequence::handletopic_slicecamd **************************/ + /***** Sequencer::Sequence::handletopic_tcsd *******************************/ + /** + * @brief handles Topic::TCSD telemetry + * @param[in] jmessage subscribed-received JSON message + * + */ + void Sequence::handletopic_tcsd(const nlohmann::json &jmessage) { + this->target.column_from_json( DBCol::TELRA, Key::Tcsd::TELRA, jmessage ); + this->target.column_from_json( DBCol::TELDECL, Key::Tcsd::TELDEC, jmessage ); + this->target.column_from_json( DBCol::ALT, Key::Tcsd::ALT, jmessage ); + this->target.column_from_json( DBCol::AZ, Key::Tcsd::AZ, jmessage ); + this->target.column_from_json( DBCol::AIRMASS, Key::Tcsd::AIRMASS, jmessage ); + this->target.column_from_json( DBCol::CASANGLE, Key::Tcsd::CASANGLE, jmessage ); + + std::lock_guard lock(tcsd_mtx); + this->tcsd_cv.notify_all(); + } + /***** Sequencer::Sequence::handletopic_tcsd *******************************/ + + /***** Sequencer::Sequence::handletopic_acamd ******************************/ /** * @brief handles Topic::ACAMD telemetry @@ -82,10 +924,20 @@ namespace Sequencer { * */ void Sequence::handletopic_acamd(const nlohmann::json &jmessage) { - // set is_acam_guiding flag bool acquired; Common::extract_telemetry_value( jmessage, Key::Acamd::IS_ACQUIRED, acquired ); this->is_acam_guiding.store(acquired, std::memory_order_relaxed); + + // track whether acamd is actively trying to acquire (mode == "acquiring") + if ( jmessage.contains( Key::Acamd::ACQUIRE_MODE ) ) { + const std::string mode = jmessage[Key::Acamd::ACQUIRE_MODE].get(); + this->is_acam_acquiring.store( mode == "acquiring", std::memory_order_relaxed ); + } + + int64_t pubtime=0; + Common::extract_telemetry_value( jmessage, Key::PUBTIME, pubtime ); + this->acam_pubtime.store( pubtime, std::memory_order_relaxed ); + std::lock_guard lock(this->acam_mtx); this->acam_cv.notify_all(); } @@ -108,13 +960,37 @@ namespace Sequencer { this->publish_waitstate(); this->publish_daemonstate(); } - /***** Sequencer::Sequence::publish_snapshot *******************************/ + /***** Sequencer::Sequence::publish_snapshot *******************************/ + + + /***** Sequencer::Sequence::request_snapshot ********************************/ + /** + * @brief asks other daemons to publish their current status + * @details Publishes a Topic::SNAPSHOT message containing the names of all + * daemons this sequencer subscribes to. Each named daemon will + * respond by re-publishing its own snapshot, ensuring the sequencer + * receives current state (e.g. camera readiness) even if it + * started after those daemons published their initial status. + * + */ + void Sequence::request_snapshot() { + nlohmann::json jmessage; + jmessage[Daemon::CAMERAD] = true; + jmessage[Daemon::ACAMD] = true; + jmessage[Daemon::SLICECAMD] = true; + jmessage[Daemon::SLITD] = true; + jmessage[Daemon::TCSD] = true; + this->publisher->publish( jmessage, Topic::SNAPSHOT ); + } + /***** Sequencer::Sequence::request_snapshot ********************************/ /***** Sequencer::Sequence::publish_seqstate ********************************/ /** - * @brief publishes sequencer state with topic "seq_seqstate" - * @details seqstate is a single string + * @brief publishes sequencer state under Topic::SEQ_SEQSTATE + * @details The payload carries the sequencer state (Key::Sequencer::SEQSTATE) + * plus Key::Sequencer::SHOULD_FINEACQUIRE, which is a user-settable + * sequencer config and so rides along on this topic. * */ void Sequence::publish_seqstate() { @@ -126,6 +1002,9 @@ namespace Sequencer { rtrim( seqstate ); jmessage_out[Key::Sequencer::SEQSTATE] = seqstate; + // user-settable: should automatic fine acquisition run? + jmessage_out[Key::Sequencer::SHOULD_FINEACQUIRE] = this->should_fineacquire.load(); + try { this->publisher->publish( jmessage_out, Topic::SEQ_SEQSTATE ); } @@ -212,8 +1091,8 @@ namespace Sequencer { // iterate through map of thread state bits, add each as a key in the JSON message, // and set true|false if the bit is set or not - std::string active_states( this->thread_state_manager.get_set_states() ); - for ( const auto &[bit,state] : Sequencer::thread_names ) { + std::string active_states( this->op_state_manager.get_set_states() ); + for ( const auto &[bit,state] : Sequencer::op_names ) { jmessage_out[state] = ( active_states.find(state)!=std::string::npos ? true : false); } @@ -232,23 +1111,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} ); } } @@ -257,31 +1138,42 @@ 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() { + const std::string function("Sequencer::Sequence::broadcast_seqstate"); + + // publish the structured seqstate topic + // this->publish_seqstate(); - this->async.enqueue_and_log( "Sequencer::Sequence::broadcast_seqstate", - "SEQSTATE: "+seq_state_manager.get_set_states() ); 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.notice( function, "sequencer state: "+current ); + } } /***** Sequencer::Sequence::broadcast_seqstate ******************************/ /***** 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 *****************************/ @@ -307,7 +1199,7 @@ namespace Sequencer { void Sequence::dothread_sequencer_async_listener( Sequencer::Sequence &seq, Network::UdpSocket udp ) { const std::string function("Sequencer::Sequence::dothread_sequencer_async_listener"); - ScopedState thr_state( seq.thread_state_manager, Sequencer::THR_SEQUENCER_ASYNC_LISTENER ); + ScopedState thr_state( seq.op_state_manager, Sequencer::OP_SEQUENCER_ASYNC_LISTENER ); int retval = udp.Listener(); @@ -398,54 +1290,6 @@ namespace Sequencer { /***** Sequencer::Sequence::dothread_sequencer_async_listener ***************/ - void Sequence::dothread_test() { - logwrite( "Sequencer::Sequence::dothread_test", "here I am" ); - std::string targetstatus; - this->target.get_specified_target( "4430", targetstatus ); - logwrite( "Sequencer::Sequence::dothread_test", targetstatus ); - return; - } - - - /***** Sequencer::Sequence::wait_for_user ***********************************/ - /** - * @brief waits for the user to click a button, or cancel - * @details Use this when you just want to slow things down or get a - * cup of coffee instead of observing. - * @return NO_ERROR on continue | ABORT on cancel - * - */ - long Sequence::wait_for_user() { - const std::string function("Sequencer::Sequence::wait_for_user"); - { - ScopedState wait_state( wait_state_manager, Sequencer::SEQ_WAIT_USER ); - - this->async.enqueue_and_log( function, "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->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" ); - return ABORT; - } - - this->is_usercontinue.store(false); - - this->async.enqueue_and_log( function, "NOTICE: received USER continue signal!" ); - - return NO_ERROR; - } - /***** Sequencer::Sequence::wait_for_user ***********************************/ - - /***** Sequencer::Sequence::sequence_start **********************************/ /** * @brief main sequence start thread @@ -460,9 +1304,9 @@ namespace Sequencer { * @param[in] obsid_in optional obsid, specify for single-target observation * */ - void Sequence::sequence_start(std::string obsid_in="") { + void Sequence::sequence_start(std::string obsid_in) { const std::string function("Sequencer::Sequence::sequence_start"); - std::stringstream message; + std::ostringstream message; std::string reply; std::string targetstatus; TargetInfo::TargetState targetstate; @@ -470,19 +1314,21 @@ 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" ); + if ( op_state_manager.is_set( Sequencer::OP_SEQUENCE_START ) ) { + this->broadcast.error( function, "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.error( function, "cannot start: system not ready" ); return; } - ScopedState thr_state( thread_state_manager, Sequencer::THR_SEQUENCE_START ); // this thread is running + // ---------- SEQUENCER IS RUNNING --------------------- + // + ScopedState thr_state( op_state_manager, Sequencer::OP_SEQUENCE_START ); // this thread is running ScopedState seq_state( seq_state_manager, Sequencer::SEQ_RUNNING, true ); // state = RUNNING (only) seq_state.destruct_set( Sequencer::SEQ_READY ); // set state=READY on exit @@ -492,11 +1338,11 @@ namespace Sequencer { // clear the thread error state // - this->thread_error_manager.clear_all(); + this->op_error_manager.clear_all(); // clear stop flags // - this->cancel_flag.store(false); + clear_cancel_flag(); this->is_ontarget.store(false); this->is_usercontinue.store(false); @@ -526,8 +1372,7 @@ namespace Sequencer { this->dotype( "ONE" ); // single-target mode must set dotype=ONE } - message.str(""); message << "NOTICE: " << targetstatus; - this->async.enqueue( message.str() ); // broadcast target status + logwrite(function, "targetstatus: "+targetstatus); if ( targetstate == TargetInfo::TARGET_FOUND ) { // target found, get the threads going @@ -536,225 +1381,54 @@ 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->thread_error_manager.set( THR_SEQUENCE_START ); // report error + this->broadcast.error( function, message.str() ); + this->op_error_manager.set( OP_SEQUENCE_START ); // report error break; } } error = this->target.update_state( Sequencer::TARGET_ACTIVE ); // set ACTIVE state in database (this says we are using this target) if (error!=NO_ERROR) { - this->thread_error_manager.set( THR_SEQUENCE_START ); // report any error + this->op_error_manager.set( OP_SEQUENCE_START ); // report any error break; } - - // let the world know of the state change - // - message.str(""); message << "TARGETSTATE:" << this->target.state << " TARGET:" << this->target.name << " OBSID:" << this->target.obsid; - this->async.enqueue( message.str() ); -#ifdef LOGLEVEL_DEBUG - logwrite( function, "[DEBUG] target found, starting threads" ); -#endif } else // targetstate not TARGET_FOUND if ( targetstate == TargetInfo::TARGET_NOT_FOUND ) { // no target found is an automatic stop - logwrite( function, "NOTICE: no targets found. stopping" ); + this->broadcast.notice( function, "no targets found. stopping" ); break; } else if ( targetstate == TargetInfo::TARGET_ERROR ) { // request stop on error - this->async.enqueue_and_log( function, "ERROR getting next target. stopping" ); + this->broadcast.error( function, "getting next target. stopping" ); break; } - // get the threads going -- - // - // These things can all be done in parallel, just have to sync up at the end. - // - - // threads to start, pair their ThreadStatusBit with the function to call - std::vector>> worker_threads; - - // If pointmode is ACAM then the user has chosen to put the star on ACAM, in - // which case the assumption is made that nothing else matters. This special - // mode of operation only points the telescope. - // - if ( this->target.pointmode == Acam::POINTMODE_ACAM ) { - this->dotype( "ONE" ); - worker_threads = { { THR_MOVE_TO_TARGET, std::bind(&Sequence::move_to_target, this) } }; - - } - else { - - // For any other pointmode (SLIT, or empty, which assumes SLIT), all - // subsystems are readied. - // - // set pointmode explicitly, in case it's empty - this->target.pointmode = Acam::POINTMODE_SLIT; - - // threads to start, pair their ThreadStatusBit with the function to call - // - worker_threads = { { THR_MOVE_TO_TARGET, std::bind(&Sequence::move_to_target, this) }, - { THR_CAMERA_SET, std::bind(&Sequence::camera_set, this) }, - { THR_FOCUS_SET, std::bind(&Sequence::focus_set, this) }, - { THR_FLEXURE_SET, std::bind(&Sequence::flexure_set, this) }, - { THR_CALIB_SET, std::bind(&Sequence::calib_set, this) }, - // for CAL targets, slit comes from database, otherwise use VSM acquire position - { THR_SLIT_SET, std::bind(&Sequence::slit_set, this, - this->target.iscal ? Sequencer::VSM_DATABASE : Sequencer::VSM_ACQUIRE) } - }; - } - - // pair their ThreadStatusBit with their future - std::vector>> worker_futures; - - // start all of the threads - // - for ( const auto &[thr, func] : worker_threads ) { - worker_futures.emplace_back( thr, std::async(std::launch::async, func) ); - } - - // wait for the threads to complete. these can be cancelled. + // ---------- default observation sequence ----------- // - for ( auto &[thr, future] : worker_futures) { - try { - error |= future.get(); // wait for this worker to finish - logwrite( function, "NOTICE: worker "+Sequencer::thread_names.at(thr)+" completed"); - } - catch (const std::exception& e) { - logwrite( function, "ERROR: worker "+Sequencer::thread_names.at(thr)+" exception: "+std::string(e.what()) ); - return; - } - } - - logwrite(function, "DONE waiting on threads"); + error = run_default_sequence(function); - if ( this->cancel_flag.load() ) { - this->async.enqueue_and_log( function, "NOTICE: sequence cancelled" ); - return; - } - - // For pointmode ACAM, there is nothing to be done so get out - // - if ( this->target.pointmode == Acam::POINTMODE_ACAM ) { - this->async.enqueue_and_log( function, "NOTICE: target list processing has stopped" ); + if (error != NO_ERROR) { + this->op_error_manager.set(OP_SEQUENCE_START); break; } - // If not a calibration target then acquire, first acam then slicecam - // - if ( !this->target.iscal ) { - - // start ACAM acquisition. If it fails then wait for user to continue or cancel. - if ( this->do_acam_acquire() != NO_ERROR ) { - this->async.enqueue_and_log( function, "WARNING acam acquisition failed" ); - if (this->wait_for_user()==ABORT) { - this->async.enqueue_and_log( function, "NOTICE: cancelled" ); - return; - } - } - else - // start SLICECAM fine acquisition - if ( this->do_slicecam_fineacquire() != NO_ERROR ) { - this->async.enqueue_and_log( function, "WARNING slicecam fine acquisition failed" ); - } - } - - if ( !this->target.iscal ) { - // send offsets. wait for user if that fails to continue or cancel. - if ( this->target_offset() == ERROR ) { - if (this->wait_for_user()==ABORT) { - this->async.enqueue_and_log( function, "NOTICE: cancelled" ); - return; - } - } - // ensure slit offset is in "expose" position when needed - try { - error |= this->slit_set(Sequencer::VSM_EXPOSE); - } - catch (const std::exception& e) { - logwrite( function, "ERROR slit offset exception: "+std::string(e.what()) ); - return; - } - } - - logwrite( function, "starting exposure" ); ///< TODO @todo log to telemetry! - - // Start the exposure in a thread... - // set the EXPOSE bit here, outside of the trigger_exposure function, because that - // function only triggers the exposure -- it doesn't block waiting for the exposure. - // - this->wait_state_manager.set( Sequencer::SEQ_WAIT_EXPOSE ); // set EXPOSE bit - auto start_exposure = std::async(std::launch::async, &Sequence::trigger_exposure, this); - try { - error |= start_exposure.get(); - } - catch (const std::exception& e) { - logwrite( function, "ERROR repeat_exposure exception: "+std::string(e.what()) ); - return; - } - - // wait for the exposure to end (naturally or cancelled) - // - logwrite( function, "waiting for exposure" ); - while ( !this->cancel_flag.load() && wait_state_manager.is_set( Sequencer::SEQ_WAIT_EXPOSE ) ) { - std::unique_lock lock(cv_mutex); - this->cv.wait( lock, [this]() { return( !wait_state_manager.is_set(SEQ_WAIT_EXPOSE) || this->cancel_flag.load() ); } ); - } - - // When an exposure is aborted then it will be marked as UNASSIGNED - // - if ( this->cancel_flag.load() ) { - this->async.enqueue_and_log( function, "NOTICE: exposure cancelled" ); - error = this->target.update_state( Sequencer::TARGET_UNASSIGNED ); - message.str(""); message << ( error==NO_ERROR ? "" : "ERROR " ) << "marking target " << this->target.name - << " id " << this->target.obsid << " order " << this->target.obsorder - << " as " << Sequencer::TARGET_UNASSIGNED; - logwrite( function, message.str() ); - return; - } - - this->async.enqueue_and_log( function, "NOTICE: done waiting for expose" ); - message.str(""); message << "exposure complete for target " << this->target.name - << " id " << this->target.obsid << " order " << this->target.obsorder; - logwrite( function, message.str() ); - - // If not using frame transfer then wait for readout, too - // - if (!this->is_science_frame_transfer) { - logwrite( function, "waiting for readout" ); - while ( !this->cancel_flag.load() && wait_state_manager.is_set( Sequencer::SEQ_WAIT_READOUT ) ) { - std::unique_lock lock(cv_mutex); - this->cv.wait( lock, [this]() { return( !wait_state_manager.is_set(SEQ_WAIT_READOUT) || this->cancel_flag.load() ); } ); - } - } - - // Now that we're done waiting, check for errors or abort - // - if ( this->thread_error_manager.are_any_set() ) { - message.str(""); message << "ERROR stopping sequencer because the following thread(s) had an error: " - << this->thread_error_manager.get_set_states(); - logwrite( function, message.str() ); + if (is_cancelled()) { + this->broadcast.notice( function, "sequence cancelled" ); break; } - // before writing to the completed database table, get current - // telemetry from other daemons. - // - this->get_external_telemetry(); - // Update this target's state in the database // error = this->target.update_state( Sequencer::TARGET_COMPLETE ); // update the active target table if (error==NO_ERROR) error = this->target.insert_completed(); // insert into the completed table - if (error!=NO_ERROR) this->thread_error_manager.set( THR_SEQUENCE_START ); // report any error + if (error!=NO_ERROR) this->op_error_manager.set( OP_SEQUENCE_START ); // report any error - // let the world know of the state change + // abort sequence on error // - message.str(""); message << "TARGETSTATE:" << this->target.state << " TARGET:" << this->target.name << " OBSID:" << this->target.obsid; - this->async.enqueue( message.str() ); + if ( this->op_error_manager.are_any_set() ) break; // Check the "dotype" -- // If this was "do one" then do_once is set and get out now. @@ -766,22 +1440,32 @@ namespace Sequencer { } // end while true - if ( this->thread_error_manager.are_any_set() ) { - logwrite( function, "requesting stop because an error was detected" ); - if ( this->target.get_next( Sequencer::TARGET_ACTIVE, targetstatus ) == TargetInfo::TARGET_FOUND ) { // If this target was flagged as active, - this->target.update_state( Sequencer::TARGET_UNASSIGNED ); // then change it to unassigned on error. + if ( this->op_error_manager.are_any_set() ) { + this->broadcast.error( function, "sequence completed with errors in: "+ + this->op_error_manager.get_set_states() ); + // If this target was flagged as active, then change it to unassigned on error. + if ( this->target.get_next( Sequencer::TARGET_ACTIVE, targetstatus ) == TargetInfo::TARGET_FOUND ) { + this->target.update_state( Sequencer::TARGET_UNASSIGNED ); } - this->thread_error_manager.clear_all(); // clear the thread error state + this->op_error_manager.clear_all(); // clear the thread error state this->do_once.store(true); } + // Override default exit state (SEQ_READY) if any subsystem daemon is down. + // broadcast_daemonstate() suppresses NOTREADY transitions during SEQ_RUNNING, + // so we must check daemon_manager here on the way out. + // + if ( !this->daemon_manager.are_all_set() ) { + seq_state.destruct_set( Sequencer::SEQ_NOTREADY ); + } + logwrite( function, "target list processing has stopped" ); return; } /***** Sequencer::Sequence::sequence_start **********************************/ - /***** Sequencer::Sequence::camera_set **************************************/ + /***** Sequencer::Sequence::camera_setup **************************************/ /** * @brief sets the camera according to the parameters in the target entry * @return NO_ERROR @@ -790,35 +1474,20 @@ namespace Sequencer { * At the moment, this is only exposure time. * */ - long Sequence::camera_set() { - const std::string function("Sequencer::Sequence::camera_set"); + long Sequence::camera_setup() { + const std::string function("Sequencer::Sequence::camera_setup"); std::string reply; std::stringstream camcmd; long error=NO_ERROR; - // wait until camera is ready to expose - // - std::unique_lock lock(this->camerad_mtx); - if (!this->can_expose.load()) { - - this->async.enqueue_and_log(function, "NOTICE: waiting for camera to be ready to expose"); - - this->camerad_cv.wait( lock, [this]() { - return( this->can_expose.load() || this->cancel_flag.load() ); - } ); + ScopedState thr_state( op_state_manager, Sequencer::OP_CAMERA_SETUP ); + ScopedState wait_state( wait_state_manager, Sequencer::SEQ_WAIT_CAMERA ); - if (this->cancel_flag.load()) { - logwrite(function, "sequence cancelled"); - return NO_ERROR; - } - } + this->wait_for_canexpose(function); logwrite( function, "setting camera parameters"); - ScopedState thr_state( thread_state_manager, Sequencer::THR_CAMERA_SET ); - ScopedState wait_state( wait_state_manager, Sequencer::SEQ_WAIT_CAMERA ); - - this->thread_error_manager.set( THR_CAMERA_SET ); // assume the worse, clear on success + this->op_error_manager.set( OP_CAMERA_SETUP ); // assume the worse, clear on success // Controller activate states stored in Sequencer::CalibrationTarget::calinfo map, // indexed by name. Calibration targets use target.name for the index, or @@ -836,15 +1505,16 @@ namespace Sequencer { // send two commands, one for each if (!activechans.str().empty()) { std::string cmd = CAMERAD_ACTIVATE + activechans.str(); +/*** if ( camerad_cmd.send( { CAMERAD_ACTIVATE, { activechans.str() } } ) != NO_ERROR ) { WIP ***/ if (this->camerad.send(cmd, reply)!=NO_ERROR) { - this->async.enqueue_and_log(function, "ERROR sending \""+cmd+"\": "+reply); + logwrite( function, "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); + logwrite( function, "ERROR sending \""+cmd+"\": "+reply ); throw std::runtime_error("camera returned "+reply); } } @@ -857,7 +1527,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 ); + logwrite( function, "ERROR sending \""+camcmd.str()+"\": "+reply ); throw std::runtime_error( "camera returned "+reply ); } @@ -865,23 +1535,23 @@ 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 ); + logwrite( function, "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 ); + logwrite( function, "ERROR sending \""+camcmd.str()+"\": "+reply ); throw std::runtime_error( "camera returned "+reply ); } - this->thread_error_manager.clear( THR_CAMERA_SET ); // success + this->op_error_manager.clear( OP_CAMERA_SETUP ); // success return NO_ERROR; } - /***** Sequencer::Sequence::camera_set **************************************/ + /***** Sequencer::Sequence::camera_setup **************************************/ - /***** Sequencer::Sequence::slit_set ****************************************/ + /***** Sequencer::Sequence::slit_setup ****************************************/ /** * @brief sets the slit width and offset * @details Slit width is always set according to the value in the target @@ -891,12 +1561,12 @@ namespace Sequencer { * @throws std::runtime_error * */ - long Sequence::slit_set(VirtualSlitMode mode) { - const std::string function("Sequencer::Sequence::slit_set"); + long Sequence::slit_setup(VirtualSlitMode mode) { + const std::string function("Sequencer::Sequence::slit_setup"); std::string reply, modestr; std::stringstream slitcmd, message; - ScopedState thr_state( thread_state_manager, Sequencer::THR_SLIT_SET ); + ScopedState thr_state( op_state_manager, Sequencer::OP_SLIT_SETUP ); ScopedState wait_state( wait_state_manager, Sequencer::SEQ_WAIT_SLIT ); // send the SET command to slitd @@ -926,19 +1596,17 @@ namespace Sequencer { break; } - this->async.enqueue( "NOTICE: moving slit to "+modestr+" position" ); - - logwrite( function, "moving slit to "+slitcmd.str()+" for "+modestr+"position" ); + this->broadcast.notice(function, "moving slit to "+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->thread_error_manager.set( THR_SLIT_SET ); + logwrite( function, "ERROR setting slit" ); + this->op_error_manager.set( OP_SLIT_SETUP ); throw std::runtime_error("slit returned: "+reply); } return NO_ERROR; } - /***** Sequencer::Sequence::slit_set ****************************************/ + /***** Sequencer::Sequence::slit_setup ****************************************/ /***** Sequencer::Sequence::power_init **************************************/ @@ -951,13 +1619,13 @@ namespace Sequencer { long Sequence::power_init() { const std::string function("Sequencer::Sequence::power_init"); - ScopedState thr_state( thread_state_manager, Sequencer::THR_POWER_INIT ); + ScopedState thr_state( op_state_manager, Sequencer::OP_POWER_INIT ); ScopedState wait_state( wait_state_manager, Sequencer::SEQ_WAIT_POWER ); 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" ); + logwrite( function, "ERROR initializing power control" ); throw std::runtime_error("could not initialize power control"); } @@ -978,7 +1646,7 @@ namespace Sequencer { long Sequence::power_shutdown() { const std::string function("Sequencer::Sequence::power_shutdown"); - ScopedState thr_state( this->thread_state_manager, Sequencer::THR_POWER_SHUTDOWN ); + ScopedState thr_state( this->op_state_manager, Sequencer::OP_POWER_SHUTDOWN ); ScopedState wait_state( this->wait_state_manager, Sequencer::SEQ_WAIT_POWER ); ScopedState daemon_state( this->daemon_manager, Sequencer::DAEMON_POWER ); @@ -1001,21 +1669,21 @@ namespace Sequencer { long Sequence::slit_init() { const std::string function("Sequencer::Sequence::slit_init"); - ScopedState thr_state( thread_state_manager, Sequencer::THR_SLIT_INIT ); + ScopedState thr_state( op_state_manager, Sequencer::OP_SLIT_INIT ); ScopedState wait_state( wait_state_manager, Sequencer::SEQ_WAIT_SLIT ); this->daemon_manager.clear( Sequencer::DAEMON_SLIT ); // slitd not ready - this->thread_error_manager.set( THR_SLIT_INIT ); // assume the worst, clear on success + this->op_error_manager.set( OP_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" ); + logwrite( function, "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" ); + logwrite( function, "ERROR connecting to slit" ); throw std::runtime_error("could not open connection to slit hardware"); } @@ -1024,7 +1692,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" ); + logwrite( function, "ERROR communicating with slit hardware" ); throw std::runtime_error("could not communicate with slit hardware: "+reply); } this->parse_state( function, reply, ishomed ); @@ -1034,7 +1702,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" ); + logwrite( function, "ERROR communicating with slit hardware" ); throw std::runtime_error("could not home slit hardware: "+reply); } } @@ -1044,12 +1712,12 @@ 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" ); + logwrite( function, "ERROR sending \""+cmd+"\" to slit" ); throw std::runtime_error("slit "+cmd+" returned: "+reply); } } - this->thread_error_manager.clear( THR_SLIT_INIT ); // success + this->op_error_manager.clear( OP_SLIT_INIT ); // success this->daemon_manager.set( Sequencer::DAEMON_SLIT ); // slitd ready return NO_ERROR; @@ -1070,11 +1738,11 @@ namespace Sequencer { std::string reply; long error=NO_ERROR; - ScopedState thr_state( this->thread_state_manager, Sequencer::THR_SLIT_SHUTDOWN ); + ScopedState thr_state( this->op_state_manager, Sequencer::OP_SLIT_SHUTDOWN ); ScopedState wait_state( this->wait_state_manager, Sequencer::SEQ_WAIT_SLIT ); ScopedState daemon_state( this->daemon_manager, Sequencer::DAEMON_SLIT ); - this->thread_error_manager.set( THR_SLIT_SHUTDOWN ); // assume the worst, clear on success + this->op_error_manager.set( OP_SLIT_SHUTDOWN ); // assume the worst, clear on success // already off? // @@ -1098,7 +1766,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" ); + logwrite( function, "ERROR sending \""+cmd+"\" to slit" ); throw std::runtime_error(cmd+" returned: "+reply); } } @@ -1108,7 +1776,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" ); + logwrite( function, "ERROR closing connection to slit hardware" ); throw std::runtime_error("closing slit connection returned: "+reply); } @@ -1117,7 +1785,7 @@ namespace Sequencer { logwrite( function, "disconnecting from slitd" ); this->slitd.disconnect(); - this->thread_error_manager.clear( THR_SLIT_SHUTDOWN ); // success + this->op_error_manager.clear( OP_SLIT_SHUTDOWN ); // success return NO_ERROR; } @@ -1137,27 +1805,27 @@ namespace Sequencer { this->daemon_manager.clear( Sequencer::DAEMON_SLICECAM ); // slicecamd not ready - ScopedState thr_state( thread_state_manager, Sequencer::THR_SLICECAM_INIT ); + ScopedState thr_state( op_state_manager, Sequencer::OP_SLICECAM_INIT ); ScopedState wait_state( wait_state_manager, Sequencer::SEQ_WAIT_SLICECAM ); - this->thread_error_manager.set( THR_SLICECAM_INIT ); // assume the worst, clear on success + this->op_error_manager.set( OP_SLICECAM_INIT ); // assume the worst, clear on success // 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" ); + logwrite( function, "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" ); + logwrite( function, "ERROR starting slicecam" ); throw SlicecamException("could not start slicecam"); } this->daemon_manager.set( Sequencer::DAEMON_SLICECAM ); // slicecamd ready - this->thread_error_manager.clear( THR_SLICECAM_INIT ); // success + this->op_error_manager.clear( OP_SLICECAM_INIT ); // success return NO_ERROR; } @@ -1177,15 +1845,15 @@ namespace Sequencer { this->daemon_manager.clear( Sequencer::DAEMON_ACAM ); // acamd not ready - ScopedState thr_state( thread_state_manager, Sequencer::THR_ACAM_INIT ); + ScopedState thr_state( op_state_manager, Sequencer::OP_ACAM_INIT ); ScopedState wait_state( wait_state_manager, Sequencer::SEQ_WAIT_ACAM ); - this->thread_error_manager.set( THR_ACAM_INIT ); // assume the worst, clear on success + this->op_error_manager.set( OP_ACAM_INIT ); // assume the worst, clear on success // 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" ); + logwrite( function, "ERROR powering acam hardware" ); throw std::runtime_error("could not power acam hardware"); } @@ -1193,7 +1861,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" ); + logwrite( function, "ERROR opening acam camera" ); throw AcamException(ErrorCode::ERROR_ACAM_CAMERA, "could not open acam camera"); } @@ -1204,20 +1872,20 @@ 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 ); + logwrite( function, "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 ); + logwrite( function, "ERROR sending \""+cmd+"\" to acamd: "+reply ); throw std::runtime_error("acam "+cmd+" returned: "+reply); } } } - this->thread_error_manager.clear( THR_ACAM_INIT ); // success + this->op_error_manager.clear( OP_ACAM_INIT ); // success this->daemon_manager.set( Sequencer::DAEMON_ACAM ); // acamd ready return NO_ERROR; @@ -1238,11 +1906,11 @@ namespace Sequencer { std::string reply; long error=NO_ERROR; - ScopedState thr_state( this->thread_state_manager, Sequencer::THR_SLICECAM_SHUTDOWN ); + ScopedState thr_state( this->op_state_manager, Sequencer::OP_SLICECAM_SHUTDOWN ); ScopedState wait_state( this->wait_state_manager, Sequencer::SEQ_WAIT_SLICECAM ); ScopedState daemon_state( this->daemon_manager, Sequencer::DAEMON_SLICECAM ); - this->thread_error_manager.set( THR_SLICECAM_SHUTDOWN ); // assume the worst, clear on success + this->op_error_manager.set( OP_SLICECAM_SHUTDOWN ); // assume the worst, clear on success // already off? // @@ -1260,14 +1928,14 @@ namespace Sequencer { } if ( (error=this->connect_to_daemon(this->slicecamd)) != NO_ERROR ) { - this->async.enqueue_and_log(function, "ERROR connecting to slicecamd"); + logwrite( function, "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" ); + logwrite( function, "ERROR closing connection to slicecam hardware" ); } // disconnect me from slicecamd, irrespective of any previous error @@ -1278,11 +1946,11 @@ 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" ); + logwrite( function, "ERROR switching off slicecam" ); throw std::runtime_error("could not power off slicecam hardware"); } - this->thread_error_manager.clear( THR_SLICECAM_SHUTDOWN ); // success + this->op_error_manager.clear( OP_SLICECAM_SHUTDOWN ); // success return NO_ERROR; } /***** Sequencer::Sequence::slicecam_shutdown *******************************/ @@ -1301,11 +1969,11 @@ namespace Sequencer { std::string reply; long error=NO_ERROR; - ScopedState thr_state( this->thread_state_manager, Sequencer::THR_ACAM_SHUTDOWN ); + ScopedState thr_state( this->op_state_manager, Sequencer::OP_ACAM_SHUTDOWN ); ScopedState wait_state( this->wait_state_manager, Sequencer::SEQ_WAIT_ACAM ); ScopedState daemon_state( this->daemon_manager, Sequencer::DAEMON_ACAM ); - this->thread_error_manager.set( THR_ACAM_SHUTDOWN ); // assume the worst, clear on success + this->op_error_manager.set( OP_ACAM_SHUTDOWN ); // assume the worst, clear on success // ensure a connection to the daemon // @@ -1318,14 +1986,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 ); + logwrite( function, "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 ); + logwrite( function, "ERROR sending \""+cmd+"\" to acamd: "+reply ); throw std::runtime_error("acam "+cmd+" returned: "+reply); } } @@ -1338,7 +2006,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 ) logwrite( function, "ERROR shutting down acam" ); } // disconnect me from acamd, irrespective of any previous error @@ -1349,11 +2017,11 @@ 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" ); + logwrite( function, "ERROR switching off acam" ); throw std::runtime_error("could not switch off acam"); } - this->thread_error_manager.clear( THR_ACAM_SHUTDOWN ); // success + this->op_error_manager.clear( OP_ACAM_SHUTDOWN ); // success return NO_ERROR; } @@ -1372,21 +2040,21 @@ namespace Sequencer { this->daemon_manager.clear( Sequencer::DAEMON_CALIB ); - ScopedState thr_state( thread_state_manager, Sequencer::THR_CALIB_INIT ); + ScopedState thr_state( op_state_manager, Sequencer::OP_CALIB_INIT ); ScopedState wait_state( wait_state_manager, Sequencer::SEQ_WAIT_CALIB ); - this->thread_error_manager.set( THR_CALIB_INIT ); // assume the worst, clear on success + this->op_error_manager.set( OP_CALIB_INIT ); // assume the worst, clear on success // 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" ); + logwrite( function, "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" ); + logwrite( function, "ERROR initializing calib control" ); throw std::runtime_error("could not power calib control"); } @@ -1398,14 +2066,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" ); + logwrite( function, "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" ); + logwrite( function, "ERROR communicating with calib hardware" ); throw std::runtime_error("could not communicate with calib hardware: "+reply); } } @@ -1418,7 +2086,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" ); + logwrite( function, "ERROR moving calib door and/or cover" ); throw std::runtime_error("could not move calib door and/or cover"); } } @@ -1427,7 +2095,7 @@ namespace Sequencer { // calibd is ready this->daemon_manager.set( Sequencer::DAEMON_CALIB ); - this->thread_error_manager.clear( THR_CALIB_INIT ); + this->op_error_manager.clear( OP_CALIB_INIT ); return NO_ERROR; } @@ -1445,17 +2113,17 @@ namespace Sequencer { const std::string function("Sequencer::Sequence::calib_shutdown"); long error=NO_ERROR; - ScopedState thr_state( this->thread_state_manager, Sequencer::THR_CALIB_SHUTDOWN ); + ScopedState thr_state( this->op_state_manager, Sequencer::OP_CALIB_SHUTDOWN ); ScopedState wait_state( this->wait_state_manager, Sequencer::SEQ_WAIT_CALIB ); ScopedState daemon_state( this->daemon_manager, Sequencer::DAEMON_CALIB ); - this->thread_error_manager.set( THR_CALIB_SHUTDOWN ); // assume the worst, clear on success + this->op_error_manager.set( OP_CALIB_SHUTDOWN ); // assume the worst, clear on success // is calib hardware powered? // bool poweron=false; if ( check_power_switch(ON, POWER_CALIB, poweron ) != NO_ERROR ) { - this->async.enqueue_and_log( function, "ERROR checking calib power switch" ); + logwrite( function, "ERROR checking calib power switch" ); throw std::runtime_error("checking calib power switch"); } @@ -1473,7 +2141,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" ); + logwrite( function, "ERROR moving calib door and/or cover" ); throw std::runtime_error("moving calib door and/or cover"); } } @@ -1486,7 +2154,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 ) logwrite( function, "ERROR closing connection to calib hardware" ); } // disconnect me from calibd, irrespective of any previous error @@ -1497,25 +2165,25 @@ 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" ); + logwrite( function, "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" ); + logwrite( function, "ERROR powering off lamps" ); error=ERROR; } // set this thread's error status // if (error!=NO_ERROR) { - this->thread_error_manager.set( THR_CALIB_SHUTDOWN ); + this->op_error_manager.set( OP_CALIB_SHUTDOWN ); throw std::runtime_error("shutting down calib control"); } - this->thread_error_manager.clear( THR_CALIB_SHUTDOWN ); // success + this->op_error_manager.clear( OP_CALIB_SHUTDOWN ); // success return NO_ERROR; } /***** Sequencer::Sequence::calib_shutdown **********************************/ @@ -1532,14 +2200,14 @@ namespace Sequencer { * */ long Sequence::tcs_init() { - ScopedState thr_state( thread_state_manager, Sequencer::THR_TCS_INIT ); + ScopedState thr_state( op_state_manager, Sequencer::OP_TCS_INIT ); ScopedState wait_state( wait_state_manager, Sequencer::SEQ_WAIT_TCS ); 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->thread_error_manager.set( THR_TCS_INIT ); + logwrite( "Sequencer::Sequence::tcs_init", "ERROR initializing TCS" ); + this->op_error_manager.set( OP_TCS_INIT ); throw std::runtime_error("could not initialize TCS"); } @@ -1567,7 +2235,7 @@ namespace Sequencer { const std::string function("Sequencer::Sequence::tcs_shutdown"); std::stringstream message; - ScopedState thr_state( this->thread_state_manager, Sequencer::THR_TCS_SHUTDOWN ); + ScopedState thr_state( this->op_state_manager, Sequencer::OP_TCS_SHUTDOWN ); ScopedState wait_state( this->wait_state_manager, Sequencer::SEQ_WAIT_TCS ); ScopedState daemon_state( this->daemon_manager, Sequencer::DAEMON_TCS ); @@ -1582,7 +2250,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" ); + logwrite( function, "ERROR closing connection to TCS" ); throw std::runtime_error("closing TCS connection: "+reply); } } @@ -1608,7 +2276,7 @@ namespace Sequencer { long Sequence::flexure_init() { const std::string function("Sequencer::Sequence::flexure_init"); - ScopedState thr_state( thread_state_manager, Sequencer::THR_FLEXURE_INIT ); + ScopedState thr_state( op_state_manager, Sequencer::OP_FLEXURE_INIT ); ScopedState wait_state( wait_state_manager, Sequencer::SEQ_WAIT_FLEXURE ); this->daemon_manager.clear( Sequencer::DAEMON_FLEXURE ); // flexured not ready @@ -1616,14 +2284,14 @@ 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->thread_error_manager.set( THR_FLEXURE_INIT ); + logwrite( function, "ERROR powering flexure control" ); + this->op_error_manager.set( OP_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->thread_error_manager.set( THR_FLEXURE_INIT ); + logwrite( function, "ERROR initializing flexure control" ); + this->op_error_manager.set( OP_FLEXURE_INIT ); throw std::runtime_error("could not initialize flexure control"); } @@ -1649,7 +2317,7 @@ namespace Sequencer { std::string reply; long error=NO_ERROR; - ScopedState thr_state( this->thread_state_manager, Sequencer::THR_FLEXURE_SHUTDOWN ); + ScopedState thr_state( this->op_state_manager, Sequencer::OP_FLEXURE_SHUTDOWN ); ScopedState wait_state( this->wait_state_manager, Sequencer::SEQ_WAIT_FLEXURE ); ScopedState daemon_state( this->daemon_manager, Sequencer::DAEMON_FLEXURE ); @@ -1669,7 +2337,7 @@ namespace Sequencer { } if ( this->connect_to_daemon(this->flexured) != NO_ERROR ) { - this->async.enqueue_and_log( function, "ERROR connecting to flexure hardware" ); + logwrite( function, "ERROR connecting to flexure hardware" ); error=ERROR; } @@ -1678,7 +2346,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" ); + logwrite( function, "ERROR closing connection to flexure hardware" ); } // disconnect me from flexured, irrespective of any previous error @@ -1689,7 +2357,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" ); + logwrite( function, "ERROR switching off flexure" ); throw std::runtime_error("switching off flexure hardware"); } @@ -1708,22 +2376,22 @@ namespace Sequencer { long Sequence::focus_init() { const std::string function("Sequencer::Sequence::focus_init"); - ScopedState thr_state( thread_state_manager, Sequencer::THR_FOCUS_INIT ); + ScopedState thr_state( op_state_manager, Sequencer::OP_FOCUS_INIT ); ScopedState wait_state( wait_state_manager, Sequencer::SEQ_WAIT_FOCUS ); this->daemon_manager.clear( Sequencer::DAEMON_FOCUS ); // focusd not ready - this->thread_error_manager.set( THR_FOCUS_INIT ); // assume failure, clear on success + this->op_error_manager.set( OP_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" ); + logwrite( function, "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" ); + logwrite( function, "ERROR initializing focus control" ); throw std::runtime_error("could not open focus hardware"); } @@ -1735,14 +2403,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" ); + logwrite( function, "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" ); + logwrite( function, "ERROR communicating with focus hardware" ); throw std::runtime_error("focus "+FOCUSD_HOME+" returned: "+reply); } } @@ -1752,13 +2420,13 @@ 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 ); + logwrite( function, "ERROR setting focus "+chan ); throw std::runtime_error("focus "+command+" returned: "+reply); } } } - this->thread_error_manager.clear( THR_FOCUS_INIT ); // success + this->op_error_manager.clear( OP_FOCUS_INIT ); // success this->daemon_manager.set( Sequencer::DAEMON_FOCUS ); // focusd is ready return NO_ERROR; @@ -1778,7 +2446,7 @@ namespace Sequencer { std::string reply; long error=NO_ERROR; - ScopedState thr_state( this->thread_state_manager, Sequencer::THR_FOCUS_SHUTDOWN ); + ScopedState thr_state( this->op_state_manager, Sequencer::OP_FOCUS_SHUTDOWN ); ScopedState wait_state( this->wait_state_manager, Sequencer::SEQ_WAIT_FOCUS ); ScopedState daemon_state( this->daemon_manager, Sequencer::DAEMON_FOCUS ); @@ -1798,7 +2466,7 @@ namespace Sequencer { } if ( this->connect_to_daemon(this->focusd) != NO_ERROR ) { - this->async.enqueue_and_log( function, "ERROR connecting to focus hardware" ); + logwrite( function, "ERROR connecting to focus hardware" ); error=ERROR; } @@ -1807,7 +2475,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" ); + logwrite( function, "ERROR closing connection to focus hardware" ); } // disconnect me from focusd, irrespective of any previous error @@ -1818,7 +2486,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" ); + logwrite( function, "ERROR switching off focus" ); throw std::runtime_error("switching off focus hardware"); } @@ -1837,22 +2505,22 @@ namespace Sequencer { long Sequence::camera_init() { const std::string function("Sequencer::Sequence::camera_init"); - ScopedState thr_state( thread_state_manager, Sequencer::THR_CAMERA_INIT ); + ScopedState thr_state( op_state_manager, Sequencer::OP_CAMERA_INIT ); ScopedState wait_state( wait_state_manager, Sequencer::SEQ_WAIT_CAMERA ); this->daemon_manager.clear( Sequencer::DAEMON_CAMERA ); // camerad not ready - this->thread_error_manager.set( THR_CAMERA_INIT ); // assume failure, clear on success + this->op_error_manager.set( OP_CAMERA_INIT ); // assume failure, clear on success // 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" ); + logwrite( function, "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" ); + logwrite( function, "ERROR initializing camera" ); throw std::runtime_error("initializing camera"); } @@ -1862,7 +2530,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" ); + logwrite( function, "ERROR sending \""+cmd+"\" to camera" ); throw std::runtime_error("sending \""+cmd+"\" to camera"); } } @@ -1873,7 +2541,7 @@ namespace Sequencer { this->camerad.send( CAMERAD_FRAMETRANSFER+" all", reply ); this->is_science_frame_transfer = ( reply.find("yes") != std::string::npos ); - this->thread_error_manager.clear( THR_CAMERA_INIT ); // success + this->op_error_manager.clear( OP_CAMERA_INIT ); // success this->daemon_manager.set( Sequencer::DAEMON_CAMERA ); // camerad ready return NO_ERROR; @@ -1891,11 +2559,11 @@ namespace Sequencer { long Sequence::camera_shutdown() { const std::string function("Sequencer::Sequence::camera_shutdown"); - ScopedState thr_state( this->thread_state_manager, Sequencer::THR_CAMERA_SHUTDOWN ); + ScopedState thr_state( this->op_state_manager, Sequencer::OP_CAMERA_SHUTDOWN ); ScopedState wait_state( this->wait_state_manager, Sequencer::SEQ_WAIT_CAMERA ); ScopedState daemon_state( this->daemon_manager, Sequencer::DAEMON_CAMERA ); - this->thread_error_manager.set( THR_CAMERA_SHUTDOWN ); // assume failure, clear on success + this->op_error_manager.set( OP_CAMERA_SHUTDOWN ); // assume failure, clear on success // Are any cameras on? // @@ -1933,11 +2601,11 @@ 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" ); + logwrite( function, "ERROR powering off camera" ); throw std::runtime_error("switching off camera"); } - this->thread_error_manager.clear( THR_CAMERA_SHUTDOWN ); // success + this->op_error_manager.clear( OP_CAMERA_SHUTDOWN ); // success return NO_ERROR; } /***** Sequencer::Sequence::camera_shutdown *********************************/ @@ -1958,8 +2626,45 @@ namespace Sequencer { std::stringstream message; long error=NO_ERROR; - ScopedState thr_state( thread_state_manager, Sequencer::THR_MOVE_TO_TARGET ); + // no telescope moves for calibration targets + // + if ( this->target.iscal ) return NO_ERROR; + + ScopedState thr_state( op_state_manager, Sequencer::OP_MOVE_TO_TARGET ); ScopedState wait_state( wait_state_manager, Sequencer::SEQ_WAIT_TCS ); + ScopedState wait_moveto( wait_state_manager, Sequencer::SEQ_WAIT_MOVETO ); + + // Apply optional CLI/DSL coordinate overrides carried in current_op_params + // (e.g. "move_to_target ra=01:23:45 dec=+45:00:00"). Overrides are written + // into this->target for the duration of this call and restored on exit so + // the database-backed target fields are not permanently mutated. + // + struct TargetCoordGuard { + TargetInfo &t; + mysqlx::string saved_ra; + mysqlx::string saved_dec; + bool ra_overridden; + bool dec_overridden; + TargetCoordGuard(TargetInfo &t_, bool ra_ov, bool dec_ov) + : t(t_), + saved_ra(t_.ra_hms), + saved_dec(t_.dec_dms), + ra_overridden(ra_ov), + dec_overridden(dec_ov) {} + ~TargetCoordGuard() { + if (ra_overridden) t.ra_hms = saved_ra; + if (dec_overridden) t.dec_dms = saved_dec; + } + }; + const bool ra_override = this->current_op_params.has("ra"); + const bool dec_override = this->current_op_params.has("dec"); + TargetCoordGuard coord_guard( this->target, ra_override, dec_override ); + if (ra_override) { + this->target.ra_hms = this->current_op_params.get("ra", std::string(this->target.ra_hms)); + } + if (dec_override) { + this->target.dec_dms = this->current_op_params.get("dec", std::string(this->target.dec_dms)); + } // If RA and DEC fields are both empty then no telescope move // @@ -1972,7 +2677,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.notice( function, "no move required for repeat target" ); return NO_ERROR; } @@ -1996,8 +2701,8 @@ 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->thread_error_manager.set( THR_MOVE_TO_TARGET ); + logwrite( function, "ERROR "+message.str() ); + this->op_error_manager.set( OP_MOVE_TO_TARGET ); throw std::runtime_error(message.str()); } @@ -2014,9 +2719,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.notice( function, message.str() ); } // Send coordinates using TCS-native COORDS command. @@ -2046,9 +2751,9 @@ 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() ); - this->thread_error_manager.set( THR_MOVE_TO_TARGET ); + message.str(""); message << "sending COORDS command. TCS reply: " << coords_reply; + logwrite( function, "ERROR "+message.str() ); + this->op_error_manager.set( OP_MOVE_TO_TARGET ); throw std::runtime_error("sending COORDS to TCS: "+coords_reply); } } @@ -2060,7 +2765,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.notice( function, "sending "+ringgo_cmd.str()+" to TCS" ); error = this->tcsd.send( ringgo_cmd.str(), noreply ); } @@ -2068,21 +2773,21 @@ 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.notice( function, "waiting for TCS operator to send \"ontarget\" signal" ); - while ( !this->cancel_flag.load() && !this->is_ontarget.load() ) { + while ( !is_cancelled() && !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->cv.wait( lock, [this]() { return( this->is_ontarget.load() || is_cancelled() ); } ); } - this->async.enqueue_and_log( function, "NOTICE: received " - +(this->cancel_flag.load() ? std::string("cancel") : std::string("ontarget")) + this->broadcast.notice( function, "received " + +(is_cancelled() ? std::string("cancel") : std::string("ontarget")) +" signal!" ); } // If waiting for TCS operator was cancelled then don't continue // - if ( this->cancel_flag.load() ) return NO_ERROR; + if ( is_cancelled() ) return NO_ERROR; this->is_ontarget.store(false); @@ -2111,7 +2816,7 @@ namespace Sequencer { const std::string function("Sequencer::Sequence::dothread_notify_tcs"); std::stringstream message; - ScopedState thr_state( seq.thread_state_manager, Sequencer::THR_NOTIFY_TCS ); + ScopedState thr_state( seq.op_state_manager, Sequencer::OP_NOTIFY_TCS ); // If this target is the same as the last then this section gets skipped; // no need to repoint the telescope and wait for a slew if it's already @@ -2147,8 +2852,8 @@ namespace Sequencer { // if ( std::isnan( radec_to_decimal( seq.target.ra_hms, ra_hms ) ) || std::isnan( radec_to_decimal( seq.target.dec_dms, dec_dms ) ) ) { - seq.async.enqueue_and_log( function, "ERROR: can't handle NaN value for RA, DEC" ); - seq.thread_error_manager.set( THR_NOTIFY_TCS ); + seq.broadcast.error( function, "can't handle NaN value for RA, DEC" ); + seq.op_error_manager.set( OP_NOTIFY_TCS ); return; } @@ -2172,59 +2877,59 @@ namespace Sequencer { /***** Sequencer::Sequence::dothread_notify_tcs *****************************/ - /***** Sequencer::Sequence::focus_set ***************************************/ + /***** Sequencer::Sequence::focus_setup ***************************************/ /** * @brief set the focus * @return NO_ERROR * @todo focus not yet implemented * */ - long Sequence::focus_set() { - const std::string function("Sequencer::Sequence::focus_set"); + long Sequence::focus_setup() { + const std::string function("Sequencer::Sequence::focus_setup"); - ScopedState thr_state( thread_state_manager, Sequencer::THR_FOCUS_SET ); + ScopedState thr_state( op_state_manager, Sequencer::OP_FOCUS_SETUP ); logwrite( function, "focus not yet implemented." ); return NO_ERROR; } - /***** Sequencer::Sequence::focus_set ***************************************/ + /***** Sequencer::Sequence::focus_setup ***************************************/ - /***** Sequencer::Sequence::flexure_set *************************************/ + /***** Sequencer::Sequence::flexure_setup *************************************/ /** * @brief set the flexure * @return NO_ERROR * @todo flexure not yet implemented * */ - long Sequence::flexure_set() { - const std::string function("Sequencer::Sequence::flexure_set"); + long Sequence::flexure_setup() { + const std::string function("Sequencer::Sequence::flexure_setup"); - ScopedState thr_state( thread_state_manager, Sequencer::THR_FLEXURE_SET ); + ScopedState thr_state( op_state_manager, Sequencer::OP_FLEXURE_SETUP ); logwrite( function, "flexure not yet implemented." ); return NO_ERROR; } - /***** Sequencer::Sequence::flexure_set *************************************/ + /***** Sequencer::Sequence::flexure_setup *************************************/ - /***** Sequencer::Sequence::calib_set ***************************************/ + /***** Sequencer::Sequence::calib_setup ***************************************/ /** * @brief set the calibrator * @return NO_ERROR * @throws std::runtime_error * */ - long Sequence::calib_set() { - const std::string function("Sequencer::Sequence::calib_set"); + long Sequence::calib_setup() { + const std::string function("Sequencer::Sequence::calib_setup"); std::stringstream message; - ScopedState thr_state( thread_state_manager, Sequencer::THR_CALIBRATOR_SET ); + ScopedState thr_state( op_state_manager, Sequencer::OP_CALIB_SETUP ); ScopedState wait_state( wait_state_manager, Sequencer::SEQ_WAIT_CALIB ); - this->thread_error_manager.set( THR_CALIBRATOR_SET ); // assume the worse, clear on success + this->op_error_manager.set( OP_CALIB_SETUP ); // assume the worse, clear on success const std::string calname = std::string(this->target.iscal ? this->target.name : "SCIENCE"); @@ -2233,7 +2938,7 @@ namespace Sequencer { // const auto &calinfo = this->caltarget.get_info(calname); - this->async.enqueue_and_log(function, "NOTICE: configuring calibrator for "+calname); + this->broadcast.notice( function, "configuring calibrator for "+calname ); // set the calib door and cover // @@ -2243,22 +2948,21 @@ namespace Sequencer { << " cover=" << ( calinfo.calcover ? "open" : "close" ); logwrite( function, "calib: "+cmd.str() ); - if ( !this->cancel_flag.load() && + if ( !is_cancelled() && this->calibd.command_timeout( cmd.str(), CALIBD_SET_TIMEOUT ) != NO_ERROR ) { - this->async.enqueue_and_log( function, "ERROR moving calib door and/or cover" ); + logwrite( function, "ERROR moving calib door and/or cover" ); throw std::runtime_error("moving calib door and/or cover"); } // set the internal calibration lamps // for ( const auto &[lamp,state] : calinfo.lamp ) { - if ( this->cancel_flag.load() ) break; - cmd.str(""); cmd << lamp << " " << (state?"on":"off"); - message.str(""); message << "power " << cmd.str(); - logwrite( function, message.str() ); + if ( is_cancelled() ) break; + std::string setpower = POWERD_SET+" "+lamp+" "+(state?"on":"off"); + logwrite( function, "power "+setpower); std::string reply; - if ( this->powerd.send( cmd.str(), reply ) != NO_ERROR ) { - this->async.enqueue_and_log( function, "ERROR "+message.str() ); + if ( this->powerd.send( setpower, reply ) != NO_ERROR ) { + logwrite( function, "ERROR "+message.str() ); throw std::runtime_error("setting lamp "+message.str()); } } @@ -2268,10 +2972,10 @@ namespace Sequencer { // // set the dome lamps // // // for ( const auto &[lamp,state] : calinfo.domelamp ) { -// if ( this->cancel_flag.load() ) break; +// if ( is_cancelled() ) break; // cmd.str(""); cmd << TCSD_NATIVE << " NPS " << lamp << " " << (state?1:0); // if ( this->tcsd.command( cmd.str() ) != NO_ERROR ) { -// this->async.enqueue_and_log( function, "ERROR "+cmd.str() ); +// logwrite( function, "ERROR "+cmd.str() ); // throw std::runtime_error("setting dome lamp: "+cmd.str()); // } // } @@ -2279,22 +2983,22 @@ namespace Sequencer { // set the lamp modulators // for ( const auto &[mod,state] : calinfo.lampmod ) { - if ( this->cancel_flag.load() ) break; + if ( is_cancelled() ) 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() ); + logwrite( function, "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" ); + if ( is_cancelled() ) { + this->broadcast.notice( function, "abort may have left calib system partially set" ); } - this->thread_error_manager.clear( THR_CALIBRATOR_SET ); // success + this->op_error_manager.clear( OP_CALIB_SETUP ); // success return NO_ERROR; } - /***** Sequencer::Sequence::calib_set ***************************************/ + /***** Sequencer::Sequence::calib_setup ***************************************/ /***** Sequencer::Sequence::abort_process *********************************/ @@ -2305,8 +3009,24 @@ namespace Sequencer { void Sequence::abort_process() { const std::string function("Sequencer::Sequence::abort_process"); - ScopedState thr_state( this->thread_state_manager, Sequencer::THR_ABORT_PROCESS ); + ScopedState thr_state( this->op_state_manager, Sequencer::OP_ABORT_PROCESS ); + + // 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, + Sequencer::SEQ_PAUSED ); + const bool abort_during_lifecycle = this->seq_state_manager.are_any_set( Sequencer::SEQ_STARTING, + Sequencer::SEQ_STOPPING ); + + // Enter SEQ_ABORTING as a strict one-hot state. + // + this->seq_state_manager.set_only( {Sequencer::SEQ_ABORTING} ); + // Clear any prior cancel state so stop_exposure and acquisition-stop calls + // below run to completion rather than bailing early on a stale flag. + // this->cancel_flag.store(false); // stop any exposure that may be in progress @@ -2319,16 +3039,62 @@ namespace Sequencer { logwrite( function, "ERROR stop_exposure exception: "+std::string(e.what()) ); } + // aborts incomplete acquisitions in progress + // + if (this->wait_state_manager.is_set(Sequencer::SEQ_WAIT_FINEACQUIRE) && + this->do_slicecam_stop() != NO_ERROR ) { + this->broadcast.warning(function, "stopping fine acquisition"); + } + if (this->wait_state_manager.is_set(Sequencer::SEQ_WAIT_ACAM_ACQUIRE) && + this->do_acam_stop() != NO_ERROR ) { + this->broadcast.warning(function, "stopping guiding"); + } + // set the cancel flag to stop any cancel-able tasks // this->cancel_flag.store(true); this->cv.notify_all(); + // Wake threads blocked on subsystem CVs so they can check cancel_flag. + { std::lock_guard lock(this->acam_mtx); this->acam_cv.notify_all(); } + { std::lock_guard lock(this->fineacquire_mtx); this->fineacquire_cv.notify_all(); } + { std::lock_guard lock(this->camerad_mtx); this->camerad_cv.notify_all(); } // drop into do-one to prevent auto increment to next target // this->do_once.store(true); - this->async.enqueue_and_log( function, "NOTICE: cancel signal sent" ); + this->broadcast.notice( function, "cancel signal sent" ); + + // Wait for sequence_start to fully exit before switching to SEQ_READY. + // Without this, we could have SEQ_READY set while OP_SEQUENCE_START is + // still set. Workers just received cancel_flag + CV notifications above, + // so this loop exits after at most a few iterations. + // + if ( abort_during_run ) { + const auto drain_timeout = std::chrono::steady_clock::now() + std::chrono::seconds(5); + while ( this->op_state_manager.is_set( Sequencer::OP_SEQUENCE_START ) && + std::chrono::steady_clock::now() < drain_timeout ) { + std::this_thread::sleep_for( std::chrono::milliseconds(20) ); + } + if ( this->op_state_manager.is_set( Sequencer::OP_SEQUENCE_START ) ) { + logwrite( function, "WARNING: sequence_start did not exit within drain timeout" ); + } + } + + // 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::stop_exposure *********************************/ @@ -2340,16 +3106,16 @@ namespace Sequencer { void Sequence::stop_exposure() { const std::string function("Sequencer::Sequence::stop_exposure"); - ScopedState thr_state( this->thread_state_manager, Sequencer::THR_STOP_EXPOSURE ); + ScopedState thr_state( this->op_state_manager, Sequencer::OP_STOP_EXPOSURE ); // This function is only used while exposing // if ( ! this->wait_state_manager.is_set( Sequencer::SEQ_WAIT_EXPOSE ) ) { - this->async.enqueue_and_log( function, "NOTICE: not currently exposing" ); + this->broadcast.notice( function, "not currently exposing" ); return; } - this->cancel_flag.store(false); + clear_cancel_flag(); // Send command to the camera to stop the exposure. // @@ -2362,12 +3128,12 @@ namespace Sequencer { else if ( error == NOTHING ) { // if not exposing, this is a way to ensure WAIT_EXPOSE bit can be cleared - this->async.enqueue_and_log( function, "NOTICE: not exposing" ); + this->broadcast.notice( function, "not exposing" ); this->wait_state_manager.clear( Sequencer::SEQ_WAIT_EXPOSE ); } else if ( error == BUSY ) { - this->async.enqueue_and_log( function, "NOTICE: too late to stop exposure" ); + this->broadcast.notice( function, "too late to stop exposure" ); // can't stop in the last 5 sec so wait that long and it should stop on its own std::this_thread::sleep_for(std::chrono::seconds(5)); } @@ -2388,22 +3154,21 @@ namespace Sequencer { long Sequence::repeat_exposure() { const std::string function("Sequencer::Sequence::repeat_exposure"); std::stringstream message; - long error = NO_ERROR; // can only repeat when state is READY // if ( ! seq_state_manager.is_set( Sequencer::SEQ_READY ) ) { - this->async.enqueue_and_log( function, "ERROR cannot repeat: system not ready" ); + this->broadcast.error( function, "cannot repeat: system not ready" ); return ERROR; } - ScopedState thr_state( thread_state_manager, Sequencer::THR_REPEAT_EXPOSURE ); + ScopedState thr_state( op_state_manager, Sequencer::OP_REPEAT_EXPOSURE ); ScopedState seq_state( seq_state_manager, Sequencer::SEQ_RUNNING, true ); // set state=RUNNING (only) seq_state.destruct_set( Sequencer::SEQ_READY ); // set state=READY on exit // clear stop flags // - this->cancel_flag.store(false); + clear_cancel_flag(); this->is_ontarget.store(false); this->is_usercontinue.store(false); @@ -2412,49 +3177,16 @@ namespace Sequencer { logwrite( function, targetstatus ); - // threads to start, pair their ThreadStatusBit with the function to call - std::vector>> worker_threads; - - worker_threads = { { THR_CAMERA_SET, std::bind(&Sequence::camera_set, this) }, -// { THR_SLIT_SET, std::bind(&Sequence::slit_set, this) } - }; - - // pair their ThreadStatusBit with their future - std::vector>> worker_futures; - - // start the threads - for ( const auto &[thr, func] : worker_threads ) { - worker_futures.emplace_back( thr, std::async(std::launch::async, func) ); - } - - // wait for the threads to complete. these can be cancelled. - for ( auto &[thr, future] : worker_futures) { - try { - error |= future.get(); // wait for this worker to finish - logwrite( function, "NOTICE: worker "+Sequencer::thread_names.at(thr)+" completed"); - } - catch (const std::exception& e) { - logwrite( function, "ERROR: worker "+Sequencer::thread_names.at(thr)+" exception: "+std::string(e.what()) ); - return ERROR; + std::vector sequence = { + { OperationType::SERIAL, OnError::STOP, { + { OP_SLIT_SETUP, + [this]() { return this->do_target_virtualslit(Sequencer::VSM_EXPOSE); } }, + { OP_EXPOSURE, + [this,function]() { return this->do_exposure(function); } } } } - } - - if ( this->cancel_flag.load() ) { - this->async.enqueue_and_log( function, "NOTICE: cancelled repeat exposure" ); - return NO_ERROR; - } + }; - // Start the exposure in a thread... - // - auto start_exposure = std::async(std::launch::async, &Sequence::trigger_exposure, this); - try { - error |= start_exposure.get(); - } - catch (const std::exception& e) { - logwrite( function, "ERROR repeat_exposure exception: "+std::string(e.what()) ); - return ERROR; - } - return NO_ERROR; + return run_sequence(sequence, function); } /***** Sequencer::Sequence::repeat_exposure *********************************/ @@ -2481,7 +3213,9 @@ namespace Sequencer { std::string reply; long error=NO_ERROR; - ScopedState thr_state( thread_state_manager, Sequencer::THR_TRIGGER_EXPOSURE ); + this->wait_for_canexpose(function); + + ScopedState thr_state( op_state_manager, Sequencer::OP_TRIGGER_EXPOSURE ); // Check tcs_preauth_time and set notify_tcs_next_target -- // When the preauth_time is non-zero, set this flag to true in order @@ -2503,8 +3237,8 @@ namespace Sequencer { // if ( this->camerad.async( message.str() ) != NO_ERROR ) { // if ( this->camerad.send( message.str(), reply ) != NO_ERROR ) { if ( this->camerad.command_timeout( message.str(), reply, 30000 ) != NO_ERROR ) { - this->async.enqueue_and_log( function, "ERROR sending camera "+message.str() ); - this->thread_error_manager.set( THR_TRIGGER_EXPOSURE ); // tell the world this thread had an error + logwrite( function, "ERROR sending camera "+message.str() ); + this->op_error_manager.set( OP_TRIGGER_EXPOSURE ); // tell the world this thread had an error this->target.update_state( Sequencer::TARGET_PENDING ); // return the target state to pending this->wait_state_manager.clear( Sequencer::SEQ_WAIT_EXPOSE ); // clear EXPOSE bit this->arm_readout_flag = false; // disarm async_listener from looking for readout @@ -2519,6 +3253,30 @@ namespace Sequencer { /***** Sequencer::Sequence::trigger_exposure ********************************/ + /***** Sequencer::Sequence::do_exposure *************************************/ + /** + * @brief wrapper for performing science exposure + * @details Triggers an exposure and waits for the exposure and readout. + * This blocks until + * @param[in] caller name of calling function + * @return ERROR|NO_ERROR + * + */ + long Sequence::do_exposure(std::string caller) { + + this->wait_state_manager.set( Sequencer::SEQ_WAIT_EXPOSE ); + + if ( this->trigger_exposure() != NO_ERROR ) return ERROR; + + if ( this->wait_for_exposure(caller) != NO_ERROR ) return ERROR; + + if ( this->wait_for_readout(caller) != NO_ERROR ) return ERROR; + + return NO_ERROR; + } + /***** Sequencer::Sequence::do_exposure *************************************/ + + /***** Sequencer::Sequence::modify_exptime **********************************/ /** * @brief modify the exposure time while an exposure is running @@ -2536,12 +3294,12 @@ namespace Sequencer { long error = NO_ERROR; double updated_exptime=0; - ScopedState thr_state( this->thread_state_manager, Sequencer::THR_MODIFY_EXPTIME ); + ScopedState thr_state( this->op_state_manager, Sequencer::OP_MODIFY_EXPTIME ); // This function is only used while exposing // if ( ! this->wait_state_manager.is_set( Sequencer::SEQ_WAIT_EXPOSE ) ) { - this->async.enqueue_and_log( function, "ERROR cannot update exposure time when not currently exposing" ); + this->broadcast.error( function, "cannot update exposure time when not currently exposing" ); error = ERROR; } @@ -2563,15 +3321,10 @@ namespace Sequencer { if ( error==NO_ERROR ) { this->target.exptime_req = updated_exptime; - message.str(""); message << "NOTICE: updated exptime to " << updated_exptime << " sec"; - this->async.enqueue_and_log( function, message.str() ); + message.str(""); message << "updated exptime to " << updated_exptime << " sec"; + this->broadcast.notice( function, message.str() ); } - // announce the success or failure in an asynchronous broadcast message - // - message.str(""); message << "MODIFY_EXPTIME: " << this->target.exptime_req << ( error==NO_ERROR ? " DONE" : " ERROR" ); - this->async.enqueue( message.str() ); - return; } /***** Sequencer::Sequence::modify_exptime **********************************/ @@ -2584,199 +3337,66 @@ namespace Sequencer { * */ long Sequence::startup() { - const std::string function("Sequencer::Sequence::startup"); - 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 " - << seq_state_manager.get_set_states(); - this->async.enqueue_and_log( function, message.str() ); - return ERROR; - } - - ScopedState thread_state( thread_state_manager, Sequencer::THR_STARTUP ); // this thread is running - - // set only STARTING (and clear everything else) - ScopedState seq_state( seq_state_manager, Sequencer::SEQ_STARTING, true ); // state=STARTING (only) - - this->thread_error_manager.clear_all(); // clear the thread error state - - // clear stop flags - // - this->cancel_flag.store(false); - this->is_ontarget.store(false); - this->is_usercontinue.store(false); - - // Everything (except TCS) needs the power control to be running - // so initialize the power control first. - // - auto start_power = std::async(std::launch::async, &Sequence::power_init, this); - 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)" ); - } - - // threads to start, pair their ThreadStatusBit with the function to call - // - std::vector>> worker_threads = { - { THR_CALIB_INIT, std::bind(&Sequence::calib_init, this) }, - { THR_CAMERA_INIT, std::bind(&Sequence::camera_init, this) }, - { THR_FLEXURE_INIT, std::bind(&Sequence::flexure_init, this) }, - { THR_FOCUS_INIT, std::bind(&Sequence::focus_init, this) }, - { THR_SLIT_INIT, std::bind(&Sequence::slit_init, this) }, - { THR_TCS_INIT, std::bind(&Sequence::tcs_init, this) } - }; - - std::vector>> worker_futures; - - // launch all of the worker threads listed in the vector - // - for ( const auto &[thr, func] : worker_threads ) { - worker_futures.emplace_back( thr, std::async(std::launch::async, func) ); - } - - // get() will block, waiting for the threads to complete - // - for ( auto &[thr, future] : worker_futures) { - try { - // wait for this worker to finish - if ( future.get() != NO_ERROR ) { - logwrite( function, "ERROR from "+Sequencer::thread_names.at(thr)); - error = ERROR; - } - else logwrite(function, Sequencer::thread_names.at(thr)+" success"); - } - catch (const std::exception& e) { - logwrite( function, "ERROR worker "+Sequencer::thread_names.at(thr)+" exception: "+std::string(e.what()) ); - error = ERROR; - break; - } - } - - // Now the Andor cameras must be done individually, first slicecam, - // then the acam. - // Sometimes the Andors lose connection with the driver and the only - // recovery seems to be power-cycling the Andor and restarting the - // daemon. Try up to maxattempts times if necessary. - // - const int maxattempts=3; - - // slicecam_init - { - long __error=NO_ERROR; // keep track of the error just for this scope - int attempt=1; - while (attempt <= maxattempts) { - try { - // launch slicecam_init async task and wait for result - std::async(std::launch::async, &Sequence::slicecam_init, this).get(); - logwrite(function, Sequencer::thread_names.at(THR_SLICECAM_INIT)+" success"); - break; - } - catch (const SlicecamException &e) { - logwrite( function, "ERROR slicecam_init exception: "+std::string(e.what()) ); - - // If there was an error with the SLICECAM cameras, turn them off, - // 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" ); - __error=ERROR; - break; - } - logwrite(function, "slicecams powered off"); - - // restart slicecamd - __error=this->daemon_restart(this->slicecamd); - - logwrite(function, "retrying slicecam_init"); - ++attempt; - continue; - } - else { - async.enqueue_and_log( function, "ERROR exceeded max attempts starting slicecam" ); - __error=ERROR; - } - } - catch (const std::exception &e) { - logwrite( function, "ERROR slicecam_init exception: "+std::string(e.what()) ); - __error=ERROR; - break; - } - catch (...) { - logwrite(function, "ERROR unknown slicecam_init exception"); - __error=ERROR; - break; - } - } // end while - if (__error == ERROR) { - async.enqueue_and_log( function, "ERROR slicecam not initialized" ); - error=ERROR; - } + const std::string function("Sequencer::Sequence::startup"); + std::stringstream message; + + 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->broadcast.error( function, message.str() ); + return ERROR; } - // acam_init - { - long __error=NO_ERROR; // keep track of the error just for this scope - int attempt=1; - while (attempt <= maxattempts) { - try { - // launch acam_init async task and wait for result - std::async(std::launch::async, &Sequence::acam_init, this).get(); - logwrite(function, Sequencer::thread_names.at(THR_ACAM_INIT)+" success"); - break; - } - catch (const AcamException &e) { - logwrite( function, "ERROR acam_init exception: "+std::string(e.what()) ); - - // If there was an error with the ACAM camera, turn it off, - // restart the acam daemon, then loop to try again. - 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" ); - __error=ERROR; - } - logwrite(function, "acam camera powered off"); - - // restart acamd - __error=this->daemon_restart(this->acamd); - - logwrite(function, "retrying acam_init"); - attempt++; - continue; - } - else { - async.enqueue_and_log( function, "ERROR exceeded max attempts starting acam" ); - __error=ERROR; - } - } - break; - } - catch (const std::exception &e) { - logwrite( function, "ERROR acam_init exception: "+std::string(e.what()) ); - __error=ERROR; - break; - } - catch (...) { - logwrite(function, "ERROR unknown acam_init exception"); - __error=ERROR; - break; + ScopedState thread_state( op_state_manager, Sequencer::OP_STARTUP ); // this thread is running + + // set only STARTING (and clear everything else) + ScopedState seq_state( seq_state_manager, Sequencer::SEQ_STARTING, true ); // state=STARTING (only) + + this->op_error_manager.clear_all(); // clear the thread error state + + // clear stop flags + // + clear_cancel_flag(); + this->is_ontarget.store(false); + this->is_usercontinue.store(false); + + Ops ops(this); + + // ---------- DEFINE STARTUP SEQUENCE ------------------------------------- + + std::vector sequence = { + // stop on error in power_init because everything else needs that + { OperationType::SERIAL, OnError::STOP, + { ops.power_init() } + }, + // everything else can continue on error + { OperationType::PARALLEL, OnError::CONTINUE, + { ops.calib_init(), + ops.camera_init(), + ops.flexure_init(), + ops.focus_init(), + ops.slit_init(), + ops.tcs_init(), + ops.acam_init() } // ACAM camera-open overlaps with parallel group + }, + // SLICECAM can't be in parallel with ACAM. separate group enforces this + { OperationType::SERIAL, OnError::CONTINUE, + { ops.slicecam_init() } } - } // end while - if (__error == ERROR) { - async.enqueue_and_log( function, "ERROR acam not initialized" ); - error=ERROR; - } - } + }; + + // ---------- RUN THE SEQUENCE -------------------------------------------- - // change state to READY if all daemons ready w/o error + long error = run_sequence(sequence, function); + + // change state to READY if all daemons ready w/o error; otherwise FAILED if ( error==NO_ERROR && daemon_manager.are_all_set() ) { 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; @@ -2796,79 +3416,69 @@ 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 + ScopedState thr_state( this->op_state_manager, Sequencer::OP_SHUTDOWN ); // this thread is running - // set only STOPPING (and clear everything else) - ScopedState seq_state( seq_state_manager, Sequencer::SEQ_STOPPING, true ); // state=STOPPING (only) - - seq_state.destruct_set( Sequencer::SEQ_NOTREADY ); // set state=NOTREADY on exit + // Enter STOPPING as a strict one-hot state. Explicit set_only is used + // instead of ScopedState because final state is set explicitly before return. + // + this->seq_state_manager.set_only( {Sequencer::SEQ_STOPPING} ); - // stop everything + // Wake any threads waiting on subsystem CVs so they can exit cleanly. + // Inline here rather than calling abort_process(), which transitions + // seqstate through ABORTING and FAILED — confusing during a normal shutdown. // - this->abort_process(); + this->cancel_flag.store(true); + this->cv.notify_all(); + { std::lock_guard lock(this->acam_mtx); this->acam_cv.notify_all(); } + { std::lock_guard lock(this->fineacquire_mtx); this->fineacquire_cv.notify_all(); } + { std::lock_guard lock(this->camerad_mtx); this->camerad_cv.notify_all(); } // clear stop flags // - this->cancel_flag.store(false); + clear_cancel_flag(); this->is_ontarget.store(false); this->is_usercontinue.store(false); // clear the thread error state // - this->thread_error_manager.clear_all(); + this->op_error_manager.clear_all(); // Everything (except TCS) needs the power control to be running // so make sure power control is initialized before continuing. // - 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)" ); + error = run( { OP_POWER_INIT, [this]{ return power_init(); }, { } }, function ); + + if ( error != NO_ERROR ) { + this->broadcast.error( function, "from power control. Will try to continue (but don't hold your breath)" ); } // container of shutdown threads to launch, // pair their ThreadStatusBit with the function to call // - std::vector>> worker_threads = { - { THR_ACAM_SHUTDOWN, std::bind(&Sequence::acam_shutdown, this) }, - { THR_CALIB_SHUTDOWN, std::bind(&Sequence::calib_shutdown, this) }, - { THR_CAMERA_SHUTDOWN, std::bind(&Sequence::camera_shutdown, this) }, - { THR_FLEXURE_SHUTDOWN, std::bind(&Sequence::flexure_shutdown, this) }, - { THR_FOCUS_SHUTDOWN, std::bind(&Sequence::focus_shutdown, this) }, - { THR_SLICECAM_SHUTDOWN, std::bind(&Sequence::slit_shutdown, this) }, - { THR_SLIT_SHUTDOWN, std::bind(&Sequence::slicecam_shutdown, this) }, - { THR_TCS_SHUTDOWN, std::bind(&Sequence::tcs_shutdown, this) } - }; - - std::vector>> worker_futures; - - // launch the shutdown threads - // - for ( const auto &[thr, func] : worker_threads ) { - worker_futures.emplace_back( thr, std::async(std::launch::async, func) ); - } - - // wait for the threads to complete - // - for ( auto &[thr, future] : worker_futures) { - try { - error=future.get(); // wait for this worker to finish - logwrite( function, "NOTICE: worker "+Sequencer::thread_names.at(thr)+" completed"); - } - catch (const std::exception& e) { - logwrite( function, "ERROR: worker "+Sequencer::thread_names.at(thr)+" exception: "+std::string(e.what()) ); - error=ERROR; - } - } + error = run_parallel( { + { OP_ACAM_SHUTDOWN, [this]{ return acam_shutdown(); }, { } }, + { OP_CALIB_SHUTDOWN, [this]{ return calib_shutdown(); }, { } }, + { OP_CAMERA_SHUTDOWN, [this]{ return camera_shutdown(); }, { } }, + { OP_FLEXURE_SHUTDOWN, [this]{ return flexure_shutdown(); }, { } }, + { OP_FOCUS_SHUTDOWN, [this]{ return focus_shutdown(); }, { } }, + { OP_SLIT_SHUTDOWN, [this]{ return slit_shutdown(); }, { } }, + { OP_SLICECAM_SHUTDOWN, [this]{ return slicecam_shutdown(); }, { } }, + { OP_TCS_SHUTDOWN, [this]{ return tcs_shutdown(); }, { } } + }, function ); - std::stringstream message; if (error==NO_ERROR) { - message << "NOTICE: instrument is shut down"; + this->broadcast.notice(function, "instrument is shut down"); } else { - message << "ERROR occurred during shutdown and may not have completed"; + this->broadcast.error(function, "shut down may not be complete"); } - this->async.enqueue_and_log( function, message.str() ); + // Always end in NOTREADY regardless of worker errors. SEQ_FAILED is + // reserved for startup failures and externally-aborted lifecycle transitions. + // Worker errors during shutdown are logged above but do not prevent + // the instrument from being considered shut down (not ready). + // + this->seq_state_manager.set_only( {Sequencer::SEQ_NOTREADY} ); return error; } @@ -3125,16 +3735,102 @@ namespace Sequencer { retstring = ( this->do_once.load() ? "ONE" : "ALL" ); - // send an async message with the current type - // - message.str(""); message << "DOTYPE: " << retstring; - this->async.enqueue( message.str() ); + // @TODO publish dotype return( error ); } /***** Sequencer::Sequence::dotype ******************************************/ + /***** Sequencer::Sequence::engineering *************************************/ + /** + * @brief set or get the engineering-mode flag + * @param[in] args empty (query) or { "true"|"false"|"1"|"0"|"enable"|"disable" } + * @param[out] retstring returns "true" or "false" + * @return NO_ERROR | ERROR + * + * When engineering_mode is true, validate_sequence() applies per-daemon + * readiness checks instead of requiring SEQ_READY, and it allows + * parallel-unsafe operation pairs when they are in SERIAL groups. + * + * Intended for single-subsystem / bench / engineering operation. Should not + * be used for science operations. + * + */ + long Sequence::engineering( std::string args, std::string &retstring ) { + const std::string function("Sequencer::Sequence::engineering"); + std::stringstream message; + long error = NO_ERROR; + + if ( not args.empty() ) { + std::transform( args.begin(), args.end(), args.begin(), ::tolower ); + + if ( args == "true" || args == "1" || args == "enable" ) { + this->engineering_mode.store( true ); + logwrite( function, "WARNING engineering mode ENABLED: validate_sequence will bypass full-instrument checks" ); + } + else + if ( args == "false" || args == "0" || args == "disable" ) { + this->engineering_mode.store( false ); + logwrite( function, "engineering mode DISABLED: validate_sequence will require SEQ_READY for full-instrument operations" ); + } + else { + message.str(""); message << "ERROR unrecognized argument " << args << ": expected {true|false|1|0|enable|disable}"; + logwrite( function, message.str() ); + error = ERROR; + } + } + + retstring = ( this->engineering_mode.load() ? "true" : "false" ); + + logwrite(function, retstring); + + return( error ); + } + /***** Sequencer::Sequence::engineering *************************************/ + + + /***** Sequencer::Sequence::fine_acquire ************************************/ + /** + * @brief enable or disable automatic fine acquisition step + * @param[in] args enable|disable + * @param[out] retstring state {enabled|disabled} + * @return ERROR|NO_ERROR|HELP + * + */ + long Sequence::fine_acquire(std::string args, std::string &retstring) { + if (args=="help"||args=="?") { + retstring = SLICECAMD_FINEACQUIRE; + retstring.append( " [ enable | disable ]\n" ); + retstring.append( " enables or disables the automatic fine acquisition step\n" ); + retstring.append( " no arg returns state only\n" ); + return HELP; + } + + const bool prev = this->should_fineacquire.load(); + + if (args=="enable") this->should_fineacquire.store(true); + else + if (args=="disable") this->should_fineacquire.store(false); + else + if (!args.empty()) { + logwrite("Sequencer::Sequence::fineacquire", + "ERROR invalid '"+args+"' expected enable|disable"); + return ERROR; + } + + retstring = this->should_fineacquire.load() ? "enabled" : "disabled"; + + // publish on change + if ( this->should_fineacquire.load() != prev ) { + this->publish_seqstate(); + } + + return NO_ERROR; + } + /***** Sequencer::Sequence::fine_acquire ************************************/ + + /***** Sequencer::Sequence::get_dome_position *******************************/ /** * @brief read the dome and telescope positions @@ -3360,9 +4056,12 @@ namespace Sequencer { long Sequence::target_offset() { const std::string function("Sequencer::Sequence::target_offset"); - // nothing to do if both ra and dec offsets are zero - if (this->target.offset_ra == 0.0 && - this->target.offset_dec == 0.0) return NO_ERROR; + bool is_ra_zero = std::abs(this->target.offset_ra) < std::numeric_limits::epsilon(); + bool is_dec_zero = std::abs(this->target.offset_dec) < std::numeric_limits::epsilon(); + + // nothing to do for calibrator or if both ra and dec offsets are zero + if ( this->target.iscal || + (is_ra_zero && is_dec_zero) ) return NO_ERROR; // zero TCS offsets before applying target offset long error = this->tcsd.command( TCSD_ZERO_OFFSETS ); @@ -3433,120 +4132,6 @@ namespace Sequencer { /***** Sequencer::Sequence::make_telemetry_message **************************/ - /***** Sequencer::Sequence::get_external_telemetry **************************/ - /** - * @brief collect telemetry from other daemon(s) - * @details This is used for any telemetry that I need to collect from - * another daemon. Common::collect_telemetry() sends a command - * to the daemon, which will respond with a JSON message. The - * daemon(s) to contact are configured with the TELEM_PROVIDER - * key in the config file. - * - */ - void Sequence::get_external_telemetry() { - // Loop through each configured telemetry provider. This requests - // their telemetry which is returned as a serialized json string - // held in retstring. - // - // handle_json_message() will parse the serialized json string. - // - std::string retstring; - for ( const auto &provider : this->telemetry_providers ) { - Common::collect_telemetry( provider, retstring ); - handle_json_message(retstring); - } - return; - } - /***** Sequencer::Sequence::get_external_telemetry **************************/ - - - /***** Sequencer::Sequence::handle_json_message *****************************/ - /** - * @brief parses incoming telemetry messages - * @details Requesting telemetry from another daemon returns a serialized - * JSON message which needs to be passed in here to parse it. - * @param[in] message_in incoming serialized JSON message (as a string) - * @return ERROR | NO_ERROR - * - */ - long Sequence::handle_json_message( const std::string message_in ) { - const std::string function("Sequencer::Sequence::handle_json_message"); - std::stringstream message; - - if ( message_in.empty() ) { - logwrite( function, "ERROR empty JSON message" ); - return ERROR; - } - - try { - nlohmann::json jmessage = nlohmann::json::parse( message_in ); - std::string messagetype; - - // jmessage must not contain key "error" and must contain key "messagetype" - // - if ( !jmessage.contains("error") ) { - if ( jmessage.contains("messagetype") && jmessage["messagetype"].is_string() ) { - messagetype = jmessage["messagetype"]; - } - else { - logwrite( function, "ERROR received JSON message with missing or invalid messagetype" ); - return ERROR; - } - } - else { - logwrite( function, "ERROR in JSON message" ); - return ERROR; - } - - // No errors, so disseminate the message contents based on the message type. - // - // column_from_json( colname, jkey, jmessage ) will extract the value of - // expected type with key jkey from json string jmessage, and assign it - // to this->target.external_telemetry[colname] map. It is expected that - // "colname" is the column name in the database. - // - if ( messagetype == "camerainfo" ) { - this->target.column_from_json( "EXPTIME", "SHUTTIME_SEC", jmessage ); - } - else - if ( messagetype == "slitinfo" ) { - this->target.column_from_json( "SLITWIDTH", "SLITW", jmessage ); - this->target.column_from_json( "SLITOFFSET", "SLITO", jmessage ); - } - else - if ( messagetype == "tcsinfo" ) { - this->target.column_from_json( "TELRA", "TELRA", jmessage ); - this->target.column_from_json( "TELDECL", "TELDEC", jmessage ); - this->target.column_from_json( "ALT", "ALT", jmessage ); - this->target.column_from_json( "AZ", "AZ", jmessage ); - this->target.column_from_json( "AIRMASS", "AIRMASS", jmessage ); - this->target.column_from_json( "CASANGLE", "CASANGLE", jmessage ); - } - else - if ( messagetype == "test" ) { - } - else { - message.str(""); message << "ERROR received unhandled JSON message type \"" << messagetype << "\""; - logwrite( function, message.str() ); - return ERROR; - } - } - catch ( const nlohmann::json::parse_error &e ) { - message.str(""); message << "ERROR json exception parsing message: " << e.what(); - logwrite( function, message.str() ); - return ERROR; - } - catch ( const std::exception &e ) { - message.str(""); message << "ERROR parsing message: " << e.what(); - logwrite( function, message.str() ); - return ERROR; - } - - return NO_ERROR; - } - /***** Sequencer::Sequence::handle_json_message *****************************/ - - /***** Sequencer::Sequence::dothread_test_fpoffset **************************/ /** * @brief for testing, calls a Python function from a thread @@ -3576,7 +4161,8 @@ namespace Sequencer { for ( const auto &plug : this->power_switch[which].plugname ) { std::string reply; - if ( this->powerd.send( plug, reply ) == NO_ERROR ) { + std::string cmd = POWERD_GET+" "+plug; + if ( this->powerd.send( cmd, reply ) == NO_ERROR ) { if ( reply.find(reqstatestr) != std::string::npos ) { is_set=true; break; @@ -3596,9 +4182,8 @@ namespace Sequencer { // for ( const auto &plug : this->power_switch[which].plugname ) { std::string reply; - std::stringstream cmd; - cmd << plug; - error = this->powerd.send( cmd.str(), reply ); + std::string cmd = POWERD_GET+" "+plug; + error = this->powerd.send( cmd, reply ); if ( error != NO_ERROR || reply.find(" DONE") == std::string::npos ) { logwrite( function, "ERROR checking plug: "+plug ); continue; @@ -3617,9 +4202,9 @@ namespace Sequencer { std::string reqstatestr = ( reqstate==ON ? "ON" : "OFF" ); if ( state != reqstate ) { - cmd << " " << reqstatestr; + cmd = POWERD_SET+" "+plug+" "+reqstatestr; logwrite( function, "switching plug "+plug+" "+reqstatestr ); - error = this->powerd.send( cmd.str(), reply ); + error = this->powerd.send( cmd, reply ); if ( error != NO_ERROR || reply.find(" DONE") != std::string::npos ) { logwrite( function, "ERROR switching plug: "+plug+" "+reqstatestr ); continue; @@ -3713,7 +4298,7 @@ namespace Sequencer { } // connection failed too many times if (attempt > maxattempts) { - async.enqueue_and_log(function, "ERROR exceeded max attempts connecting to " + daemon.name); + logwrite( function, "ERROR exceeded max attempts connecting to " + daemon.name ); return ERROR; } @@ -3722,7 +4307,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" ); + logwrite( function, "ERROR opening "+daemon.name+" hardware" ); return ERROR; } @@ -3732,7 +4317,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" ); + logwrite( function, "ERROR opening connection to "+daemon.name+" hardware" ); return ERROR; } was_opened=true; @@ -3761,7 +4346,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 ); + logwrite( function, "ERROR connecting to "+daemon.name ); return ERROR; } } @@ -3863,8 +4448,6 @@ namespace Sequencer { retstring.append( " addrow ? | \n" ); retstring.append( " async [ ? | ]\n" ); retstring.append( " acquire [ ? ]\n" ); - retstring.append( " calibset [ ? ]\n" ); - retstring.append( " cameraset [ ? ]\n" ); retstring.append( " cancel [ ? ]\n" ); retstring.append( " clearlasttarget\n" ); retstring.append( " completed [ ? ]\n" ); @@ -3872,7 +4455,6 @@ namespace Sequencer { retstring.append( " fpoffset ? | \n" ); retstring.append( " getnext [ ? ]\n" ); retstring.append( " getobsid [ ? ]\n" ); - retstring.append( " gettelem [ ? ]\n" ); retstring.append( " isready [ ? ]\n" ); retstring.append( " moveto [ ? | ]\n" ); retstring.append( " notify [ ? ]\n" ); @@ -3890,38 +4472,11 @@ namespace Sequencer { retstring.append( " tablenames [ ? ]\n" ); retstring.append( " threadoffset [ ? ]\n" ); retstring.append( " update ? | { pending | complete | unassigned }\n" ); + retstring.append( " threadpool [ ? ]\n" ); return HELP; } else - // ---------------------------------------------------- - // async -- queue an asynchronous message - // ---------------------------------------------------- - // - if ( testname == "async" ) { - if ( tokens.size() > 1 && tokens[1] == "?" ) { - retstring = "test async [ ]\n"; - retstring.append( " Queue and broadcast optional . If not supplied\n" ); - retstring.append( " then broadcast \"test\".\n" ); - return HELP; - } - if ( tokens.size() > 1 ) { - bool first=true; - message.str(""); - for ( const auto &word : tokens ) { - if ( first ) { first=false; continue; } // skip the testname - message << word << " "; - } - logwrite( function, message.str() ); - this->async.enqueue( message.str() ); - } - else { - logwrite( function, "test" ); - this->async.enqueue( "test" ); - } - } - else - // ---------------------------------------------------- // prologue -- show the camera prologue commands // ---------------------------------------------------- @@ -3955,68 +4510,22 @@ namespace Sequencer { // write to the log (textually) which bits are set // retstring.clear(); - message.str(""); message << "NOTICE: daemons ready: " << this->daemon_manager.get_set_states(); - this->async.enqueue_and_log( function, message.str() ); + message.str(""); message << "daemons ready: " << this->daemon_manager.get_set_states(); + this->broadcast.notice( function, message.str() ); retstring.append( message.str() ); retstring.append( "\n" ); - message.str(""); message << "NOTICE: daemons not ready: " << this->daemon_manager.get_cleared_states(); - this->async.enqueue_and_log( function, message.str() ); + message.str(""); message << "daemons not ready: " << this->daemon_manager.get_cleared_states(); + this->broadcast.notice( function, message.str() ); retstring.append( message.str() ); retstring.append( "\n" ); - message.str(""); message << "NOTICE: camera ready to expose: " << (this->can_expose.load() ? "yes" : "no"); - this->async.enqueue_and_log( function, message.str() ); + message.str(""); message << "camera ready to expose: " << (this->can_expose.load() ? "yes" : "no"); + this->broadcast.notice( function, message.str() ); retstring.append( message.str() ); error = NO_ERROR; } else - // ---------------------------------------------------- - // calibset -- sets the calibrator according to the parameters in the target entry - // ---------------------------------------------------- - // - if ( testname == "calibset" ) { - if ( tokens.size() > 1 && tokens[1] == "?" ) { - retstring = "test calibset\n"; - retstring.append( " Set only the calib according to the parameters in the target row.\n" ); - return HELP; - } - // launch thread and wait for it to return - auto calibset = std::async(std::launch::async, &Sequence::calib_set, this); - try { - error = calibset.get(); - } - catch (const std::exception& e) { - logwrite( function, "ERROR calib_set exception: "+std::string(e.what()) ); - return ERROR; - } - return error; - } - else - - // ---------------------------------------------------- - // cameraset -- sets the camera according to the parameters in the target entry - // ---------------------------------------------------- - // - if ( testname == "cameraset" ) { - if ( tokens.size() > 1 && tokens[1] == "?" ) { - retstring = "test cameraset\n"; - retstring.append( " Set only the camera according to the parameters in the target row.\n" ); - return HELP; - } - // launch thread and wait for it to return - auto cameraset = std::async(std::launch::async, &Sequence::camera_set, this); - try { - error = cameraset.get(); - } - catch (const std::exception& e) { - logwrite( function, "ERROR camera_set exception: "+std::string(e.what()) ); - return ERROR; - } - return error; - } - else - // ---------------------------------------------------- // expose -- trigger exposure // ---------------------------------------------------- @@ -4044,10 +4553,10 @@ namespace Sequencer { } message.str(""); message << "STATES: " << this->seq_state_manager.get_set_states(); - this->async.enqueue( message.str() ); + this->broadcast.notice( function, message.str() ); logwrite( function, message.str() ); - message.str(""); message << "THREADS: " << this->thread_state_manager.get_set_states(); + message.str(""); message << "THREADS: " << this->op_state_manager.get_set_states(); logwrite( function, message.str() ); message.str(""); message << "DAEMONS NOT READY: " << this->daemon_manager.get_cleared_states(); @@ -4151,8 +4660,7 @@ namespace Sequencer { } error = NO_ERROR; - message.str(""); message << "NOTICE: " << targetstatus; - this->async.enqueue( message.str() ); // broadcast target status + this->broadcast.notice(function, targetstatus); if ( ret == TargetInfo::TargetState::TARGET_FOUND ) { rts << "name obsid order ra dec casangle slitangle airmasslim\n"; @@ -4255,24 +4763,6 @@ namespace Sequencer { retstring = rts.str(); } else - // ---------------------------------------------------- - // gettelem -- get external telemetry - // ---------------------------------------------------- - // - if ( testname == "gettelem" ) { - if ( tokens.size() > 1 && tokens[1] == "?" ) { - retstring = "test gettelem\n"; - retstring.append( " Get external telemetry from other daemons.\n" ); - return HELP; - } - this->get_external_telemetry(); - message.str(""); - for ( const auto &[name,data] : this->target.external_telemetry ) { - message << "name=" << name << " valid=" << (data.valid?"T":"F") << " value=" << data.value << "\n"; - } - retstring = message.str(); - } - else // ---------------------------------------------------- // addrow -- insert a (fixed, hard-coded) row into the database @@ -4328,10 +4818,10 @@ namespace Sequencer { // let the world know of the state change // - message.str(""); message << "TARGETSTATE:" << this->target.state - << " TARGET:" << this->target.name - << " OBSID:" << this->target.obsid; - this->async.enqueue( message.str() ); + message.str(""); message << this->target.state + << " TARGET:" << this->target.name + << " (" << this->target.obsid << ")"; + this->broadcast.notice( function, message.str() ); } else @@ -4359,10 +4849,10 @@ namespace Sequencer { // let the world know of the state change // - message.str(""); message << "TARGETSTATE:" << this->target.state - << " TARGET:" << this->target.name - << " OBSID:" << this->target.obsid; - this->async.enqueue( message.str() ); + message.str(""); message << this->target.state + << " TARGET:" << this->target.name + << " (" << this->target.obsid << ")"; + this->broadcast.notice( function, message.str() ); } } else @@ -4469,13 +4959,13 @@ namespace Sequencer { else this->test_solver_args.clear(); // clear previous solver args if not specified if ( !this->test_solver_args.empty() ) { - message.str(""); message << "NOTICE: test solver args: " << this->test_solver_args; + message.str(""); message << "test solver args: " << this->test_solver_args; } - this->async.enqueue_and_log( function, message.str() ); + this->broadcast.notice( function, message.str() ); // clear stop flags // - this->cancel_flag.store(false); + clear_cancel_flag(); this->is_ontarget.store(false); this->is_usercontinue.store(false); @@ -4525,12 +5015,12 @@ namespace Sequencer { bool cas_isnan = std::isnan( angle_in ); if ( ra_isnan || dec_isnan || cas_isnan ) { - message.str(""); message << "ERROR: converting"; + message.str(""); message << "converting"; if ( ra_isnan ) { message << " RA=\"" << this->target.ra_hms << "\""; } if ( dec_isnan ) { message << " DEC=\"" << this->target.dec_dms << "\""; } if ( cas_isnan ) { message << " CASS=\"" << cass_now << "\""; } message << " to decimal"; - this->async.enqueue_and_log( function, message.str() ); + this->broadcast.error( function, message.str() ); return ERROR; } @@ -4616,49 +5106,6 @@ namespace Sequencer { } else - // --------------------------------------------------------- - // slitset -- spawn slit_set with the selected mode - // --------------------------------------------------------- - // - if ( testname == "slitset" ) { - - if ( tokens.size() > 1 && ( tokens[1] == "?" || tokens[1] == "help" || tokens[1] == "-h" ) ) { - retstring = "test slitset \n"; - retstring.append( " Spawn slit_set for = { expose, acquire, database }\n" ); - return HELP; - } - - if ( tokens.size() < 2 ) { - logwrite( function, "ERROR expected slitset " ); - retstring="invalid_argument"; - return ERROR; - } - - Sequencer::VirtualSlitMode mode; - - if ( tokens[1]=="expose" ) { mode = Sequencer::VSM_EXPOSE; } - else - if ( tokens[1]=="acquire" ) { mode = Sequencer::VSM_ACQUIRE; } - else - if ( tokens[1]=="database" ) { mode = Sequencer::VSM_DATABASE; } - else { - logwrite( function, "ERROR invalid mode "+tokens[1]+": expected { expose acquire database }" ); - retstring="invalid_argument"; - return ERROR; - } - - auto slitset = std::async(std::launch::async, &Sequence::slit_set, this, mode); - try { - error = slitset.get(); - } - catch (const std::exception& e) { - retstring="slit_set_exception"; - logwrite( function, "ERROR slit set exception: "+std::string(e.what()) ); - return ERROR; - } - } - else - // --------------------------------------------------------- // startup -- startup a single specified module // --------------------------------------------------------- @@ -4683,7 +5130,6 @@ namespace Sequencer { return( ERROR ); } - bool ispower = false; std::string reply; // power module must be initialized before any others. If this is not @@ -4766,6 +5212,47 @@ namespace Sequencer { logwrite( function, message.str() ); } } + else + + // --------------------------------------------------------- + // threadpool -- unit test for thread pool + // --------------------------------------------------------- + // + if ( testname == "threadpool" ) { + if ( tokens.size() > 1 && tokens[1] == "?" ) { + retstring = SEQUENCERD_TEST; + retstring.append( " threadpool\n" ); + retstring.append( " unit test ThreadPool object\n" ); + return HELP; + } + std::vector> futures; + futures.reserve(10); + + // add 15 2+ sec timers to the pool + for (int i=0; i<15; i++) { + std::ostringstream oss; + oss << "pooling timer " << i << ". tasks=" << pool.get_active() << " backlog=" << pool.get_backlog(); + logwrite(function, oss.str()); + futures.emplace_back( pool.enqueue( [this,i]() { + PreciseTimer timer; + timer.delay( 2000+(i*200) ); + } ) ); + std::this_thread::sleep_for(std::chrono::microseconds(100)); + } + + // wait for timer completion + for (size_t i=0; i < futures.size(); ++i) { + try { + futures[i].get(); + std::ostringstream oss; + oss << "timer " << i << " complete. tasks=" << pool.get_active() << " backlog=" << pool.get_backlog(); + logwrite(function, oss.str()); + } + catch (const std::exception &e) { + logwrite(function, "received exception: "+std::string(e.what())); + } + } + } else { // ---------------------------------------------------- diff --git a/sequencerd/sequence.h b/sequencerd/sequence.h index ab86cc45..427b5d2f 100644 --- a/sequencerd/sequence.h +++ b/sequencerd/sequence.h @@ -34,6 +34,9 @@ #include "tcsd_commands.h" #include "sequencerd_commands.h" #include "message_keys.h" +#include "thread_pool.h" +#include "command.h" +#include "command_rules.h" #include "tcs_constants.h" #include "acam_interface_shared.h" @@ -46,6 +49,19 @@ */ namespace Sequencer { + constexpr size_t NTHREADS = 10; ///< number of simultaneous operation threads + + /** + * @brief DSL reserved keys + * @details DSL_KEY_DO is globally reserved at parse level. It is extracted + * to ParsedCommand::verb and never stored in params. No daemon + * parameter may use this name. + * DSL_KEY_ACTION is semantic-level only — it stays in params and + * is consumed by build_sequence() in the on_error context. + */ + inline constexpr std::string_view DSL_KEY_DO { "do" }; + inline constexpr std::string_view DSL_KEY_ACTION { "action" }; + /** * @enum ErrorCodes * @brief @@ -125,6 +141,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 +152,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 +173,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,113 +185,135 @@ 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"} }; /** - * @enum ThreadStatusBits - * @brief assigns each thread a bit in a threadstate word - * @details bit is set while thread is running + * @enum OperationStatusBits + * @brief assigns each operation a bit in an operation-state word + * @details bit is set while the operation is running */ - enum ThreadStatusBits : size_t { - THR_SEQUENCER_ASYNC_LISTENER=0, - THR_TRIGGER_EXPOSURE, - THR_REPEAT_EXPOSURE, - THR_STOP_EXPOSURE, - THR_ABORT_PROCESS, - THR_SEQUENCE_START, - THR_MONITOR_READY_STATE, - THR_CALIB_SET, - THR_CAMERA_SET, - THR_SLIT_SET, - THR_MOVE_TO_TARGET, - THR_NOTIFY_TCS, - THR_FOCUS_SET, - THR_FLEXURE_SET, - THR_CALIBRATOR_SET, - THR_ACAM_INIT, - THR_ANDOR_INIT, - THR_SLICECAM_INIT, - THR_CALIB_INIT, - THR_TCS_INIT, - THR_SLIT_INIT, - THR_CAMERA_INIT, - THR_FLEXURE_INIT, - THR_FOCUS_INIT, - THR_POWER_INIT, - THR_ACAM_SHUTDOWN, - THR_ANDOR_SHUTDOWN, - THR_SLICECAM_SHUTDOWN, - THR_CALIB_SHUTDOWN, - THR_CAMERA_SHUTDOWN, - THR_FLEXURE_SHUTDOWN, - THR_FOCUS_SHUTDOWN, - THR_SLIT_SHUTDOWN, - THR_TCS_SHUTDOWN, - THR_POWER_SHUTDOWN, - THR_MODIFY_EXPTIME, - THR_ACQUISITION, - THR_GUIDING, - THR_STARTUP, - THR_SHUTDOWN, - NUM_THREAD_STATES + enum OperationStatusBits : size_t { + OP_SEQUENCER_ASYNC_LISTENER=0, + OP_TRIGGER_EXPOSURE, + OP_EXPOSURE, + OP_REPEAT_EXPOSURE, + OP_STOP_EXPOSURE, + OP_ABORT_PROCESS, + OP_SEQUENCE_START, + OP_RUN_SCRIPT, + OP_MONITOR_READY_STATE, + OP_CALIB_SETUP, + OP_CAMERA_SETUP, + OP_SLIT_SETUP, + OP_MOVE_TO_TARGET, + OP_NOTIFY_TCS, + OP_FOCUS_SETUP, + OP_FLEXURE_SETUP, + OP_ACAM_INIT, + OP_ANDOR_INIT, + OP_SLICECAM_INIT, + OP_CALIB_INIT, + OP_TCS_INIT, + OP_SLIT_INIT, + OP_CAMERA_INIT, + OP_FLEXURE_INIT, + OP_FOCUS_INIT, + OP_POWER_INIT, + OP_ACAM_SHUTDOWN, + OP_ANDOR_SHUTDOWN, + OP_SLICECAM_SHUTDOWN, + OP_CALIB_SHUTDOWN, + OP_CAMERA_SHUTDOWN, + OP_FLEXURE_SHUTDOWN, + OP_FOCUS_SHUTDOWN, + OP_SLIT_SHUTDOWN, + OP_TCS_SHUTDOWN, + OP_POWER_SHUTDOWN, + OP_MODIFY_EXPTIME, + OP_ACQUISITION, + OP_GUIDING, + OP_STARTUP, + OP_SHUTDOWN, + OP_ACAM_CMD, + OP_CALIB_CMD, + OP_CAMERA_CMD, + OP_FLEXURE_CMD, + OP_FOCUS_CMD, + OP_POWER_CMD, + OP_SLICECAM_CMD, + OP_SLIT_CMD, + OP_TCS_CMD, + NUM_OP_STATES }; - const std::map thread_names = { - {THR_SEQUENCER_ASYNC_LISTENER, "async_listener"}, - {THR_TRIGGER_EXPOSURE, "trigger_exposure"}, - {THR_REPEAT_EXPOSURE, "repeat_exposure"}, - {THR_STOP_EXPOSURE, "stop_exposure"}, - {THR_ABORT_PROCESS, "abort_process"}, - {THR_SEQUENCE_START, "sequence_start"}, - {THR_MONITOR_READY_STATE, "monitor_ready_state"}, - {THR_CALIB_SET, "calib_set"}, - {THR_CAMERA_SET, "camera_set"}, - {THR_SLIT_SET, "slit_set"}, - {THR_MOVE_TO_TARGET, "move_to_target"}, - {THR_NOTIFY_TCS, "notify_tcs"}, - {THR_FOCUS_SET, "focus_set"}, - {THR_FLEXURE_SET, "flexure_set"}, - {THR_CALIBRATOR_SET, "calibrator_set"}, - {THR_ACAM_INIT, "acam_init"}, - {THR_ANDOR_INIT, "andor_init"}, - {THR_SLICECAM_INIT, "slicecam_init"}, - {THR_CALIB_INIT, "calib_init"}, - {THR_TCS_INIT, "tcs_init"}, - {THR_SLIT_INIT, "slit_init"}, - {THR_CAMERA_INIT, "camera_init"}, - {THR_FLEXURE_INIT, "flexure_init"}, - {THR_FOCUS_INIT, "focus_init"}, - {THR_POWER_INIT, "power_init"}, - {THR_ACAM_SHUTDOWN, "acam_shutdown"}, - {THR_ANDOR_SHUTDOWN, "andor_shutdown"}, - {THR_SLICECAM_SHUTDOWN, "slicecam_shutdown"}, - {THR_CALIB_SHUTDOWN, "calib_shutdown"}, - {THR_CAMERA_SHUTDOWN, "camera_shutdown"}, - {THR_FLEXURE_SHUTDOWN, "flexure_shutdown"}, - {THR_FOCUS_SHUTDOWN, "focus_shutdown"}, - {THR_SLIT_SHUTDOWN, "slit_shutdown"}, - {THR_TCS_SHUTDOWN, "tcs_shutdown"}, - {THR_POWER_SHUTDOWN, "power_shutdown"}, - {THR_MODIFY_EXPTIME, "modify_exptime"}, - {THR_ACQUISITION, "acquisition"}, - {THR_GUIDING, "guiding"}, - {THR_STARTUP, "startup"}, - {THR_SHUTDOWN, "shutdown"} + const std::map op_names = { + {OP_SEQUENCER_ASYNC_LISTENER, "async_listener"}, + {OP_TRIGGER_EXPOSURE, "trigger_exposure"}, + {OP_EXPOSURE, "exposure"}, + {OP_REPEAT_EXPOSURE, "repeat_exposure"}, + {OP_STOP_EXPOSURE, "stop_exposure"}, + {OP_ABORT_PROCESS, "abort_process"}, + {OP_SEQUENCE_START, "sequence_start"}, + {OP_RUN_SCRIPT, "run_script"}, + {OP_MONITOR_READY_STATE, "monitor_ready_state"}, + {OP_CALIB_SETUP, "calib_setup"}, + {OP_CAMERA_SETUP, "camera_setup"}, + {OP_SLIT_SETUP, "slit_setup"}, + {OP_MOVE_TO_TARGET, "move_to_target"}, + {OP_NOTIFY_TCS, "notify_tcs"}, + {OP_FOCUS_SETUP, "focus_setup"}, + {OP_FLEXURE_SETUP, "flexure_setup"}, + {OP_ACAM_INIT, "acam_init"}, + {OP_ANDOR_INIT, "andor_init"}, + {OP_SLICECAM_INIT, "slicecam_init"}, + {OP_CALIB_INIT, "calib_init"}, + {OP_TCS_INIT, "tcs_init"}, + {OP_SLIT_INIT, "slit_init"}, + {OP_CAMERA_INIT, "camera_init"}, + {OP_FLEXURE_INIT, "flexure_init"}, + {OP_FOCUS_INIT, "focus_init"}, + {OP_POWER_INIT, "power_init"}, + {OP_ACAM_SHUTDOWN, "acam_shutdown"}, + {OP_ANDOR_SHUTDOWN, "andor_shutdown"}, + {OP_SLICECAM_SHUTDOWN, "slicecam_shutdown"}, + {OP_CALIB_SHUTDOWN, "calib_shutdown"}, + {OP_CAMERA_SHUTDOWN, "camera_shutdown"}, + {OP_FLEXURE_SHUTDOWN, "flexure_shutdown"}, + {OP_FOCUS_SHUTDOWN, "focus_shutdown"}, + {OP_SLIT_SHUTDOWN, "slit_shutdown"}, + {OP_TCS_SHUTDOWN, "tcs_shutdown"}, + {OP_POWER_SHUTDOWN, "power_shutdown"}, + {OP_MODIFY_EXPTIME, "modify_exptime"}, + {OP_ACQUISITION, "acquisition"}, + {OP_GUIDING, "guiding"}, + {OP_STARTUP, "startup"}, + {OP_SHUTDOWN, "shutdown"}, + {OP_ACAM_CMD, "acam_cmd"}, + {OP_CALIB_CMD, "calib_cmd"}, + {OP_CAMERA_CMD, "camera_cmd"}, + {OP_FLEXURE_CMD, "flexure_cmd"}, + {OP_FOCUS_CMD, "focus_cmd"}, + {OP_POWER_CMD, "power_cmd"}, + {OP_SLICECAM_CMD, "slicecam_cmd"}, + {OP_SLIT_CMD, "slit_cmd"}, + {OP_TCS_CMD, "tcs_cmd"} }; /***** Sequencer::Sequence **************************************************/ @@ -278,6 +322,7 @@ namespace Sequencer { * @brief the Sequence class defines the "sequences", i.e. functions of the sequencer */ class Sequence { + friend struct SequenceTestAccess; ///< test-only friend (see sequencerd/test_sequence.cpp) private: zmqpp::context context; bool ready_to_start; ///< set on nightly startup success, used to return seqstate to READY after an abort @@ -288,12 +333,189 @@ namespace Sequencer { std::atomic cancel_flag{false}; std::atomic is_ontarget{false}; ///< remotely set by the TCS operator to indicate that the target is ready std::atomic is_usercontinue{false}; ///< remotely set by the user to continue + std::atomic should_fineacquire{true}; ///< should I use fineacquire? (user-switchable) std::atomic is_fineacquire_locked{false}; ///< is slicecam fine acquisition locked? - std::atomic is_acam_guiding{false}; ///< is acam guiding? + std::atomic is_fineacquire_running{false}; ///< is slicecam fine acquisition running? + std::atomic is_acam_guiding{false}; ///< is acam guiding (IS_ACQUIRED)? + std::atomic is_acam_acquiring{false}; ///< is acam in an acquire mode? + std::atomic acam_pubtime{0}; ///< publish time (us) of latest received acamd status + + /** @brief guard-band (us) subtracted from the acquire-command send time + * when computing the freshness boundary in do_acam_acquire. Tolerates + * jitter and the race between the command send and ACAM's forced publish. + * Adjust here to tune. + */ + static constexpr int64_t ACAM_FRESHNESS_GUARD_US = 500'000; + + std::atomic engineering_mode{false}; ///< when true, validate_sequence applies per-daemon checks instead of SEQ_READY + + ThreadPool pool; + + /** @brief operation type can be SERIAL or PARALLEL + */ + enum class OperationType { + PARALLEL, + SERIAL + }; + + /** @brief map of parameter key=value pairs associated with operation + */ + struct OperationParams { + std::unordered_map map; + + bool has(const std::string &key) const { + return map.find(key) != map.end(); + } + + /** @brief alias for has() — provided for STL-style readability */ + bool contains(const std::string &key) const { + return has(key); + } + + /** @brief throwing accessor — caller must guarantee key presence */ + const std::string &at(const std::string &key) const { + return map.at(key); + } + + /** @brief number of stored key=value pairs */ + size_t size() const { + return map.size(); + } + + /** @brief iteration — ordering unspecified (unordered_map). + * Use only for single-entry access or explicit key checks. */ + auto begin() const { return map.begin(); } + auto end() const { return map.end(); } + + /** @brief insert or overwrite a key=value pair */ + void set(const std::string &key, const std::string &val) { + map[key] = val; + } + + template + T get(const std::string &key, const T &default_val) const { + auto it = map.find(key); + if (it == map.end()) return default_val; + + if constexpr (std::is_same_v) { + return it->second; + } + else { + std::istringstream iss(it->second); + T val; + iss >> val; + return iss.fail() ? default_val : val; + } + } + }; + + /** @brief sequencer operation contains status bit, function and params + */ + struct Operation { + OperationStatusBits id; + std::function func; + OperationParams params; + const std::string &name() const { + static const std::string empty; + auto it = op_names.find(id); + return(it==op_names.end() ? empty : it->second); + } + int max_attempts=1; + int retry_delay=0; + std::function on_retry; + }; + + /** @brief what to do with an OperationGroup on error + */ + enum class OnError { + CONTINUE, + STOP + }; + + /** @brief a group of operations stored in a vector with the operation type + */ + struct OperationGroup { + OperationType type; + OnError on_error; + std::vector operations; + }; + + /** @brief associates a sequencer command with its parameters + * @details A DSL line of the form + * command [key1=val1 key2=val2 ... keyN=valN] + * parses to: + * subsystem = command (first token) + * verb = value of the reserved do= key, or "" if absent + * params = the remaining key=value pairs (do= removed) + * Block keywords (parallel:, serial:, end, on_error) are + * intercepted by build_sequence() before op_builder dispatch, + * so subsystem here always names a real subsystem after that + * filter has run. + */ + struct ParsedCommand { + std::string subsystem; + std::string verb; + OperationParams params; + int linenum{0}; + }; + + /** @brief DSL -> daemon Command translation helpers + * @details Implemented in operation_builders.cpp. Static because they + * do not depend on Sequence state; declared as members so + * they have access to the private ParsedCommand type. + */ + static void validate_keys( const ParsedCommand &cmd, + std::initializer_list allowed ); + static Command build_generic_command( const ParsedCommand &cmd ); + static Command build_powerd_command( const ParsedCommand &cmd ); + static Command translate_command( const ParsedCommand &cmd ); + + /** @brief key=value params of the currently executing operation + * @details set by run() just before op.func() is invoked and consumed + * by Sequence:: member functions that accept optional overrides + * (e.g. move_to_target, slit_setup) + */ + OperationParams current_op_params; + + class Ops { + private: + Sequence* seq; + public: + explicit Ops(Sequence* seq); + + Operation acam_init(); + Operation calib_init(); + Operation calib_setup(); + Operation camera_init(); + Operation camera_setup(); + Operation do_expose(); + Operation do_shutdown(); + Operation do_startup(); + Operation flexure_init(); + Operation flexure_setup(); + Operation focus_init(); + Operation focus_setup(); + Operation move_to_target(); + Operation power_init(); + Operation repeat_exposure(); + Operation slicecam_init(); + Operation slit_init(); + Operation slit_setup(); + Operation target_offset(); + Operation tcs_init(); + }; + + using OperationBuilder = std::function; + + std::unordered_map op_builders; + + Ops ops{ this }; ///< factory used by op_builders to build Internal Operations + + void init_operation_builders(); /** @brief safely runs function in a detached thread using lambda to catch exceptions */ - void safe_thread(long (Sequence::*method)(), const std::string &function) { + void safe_thread(long (Sequence::*method)(), std::string function) { std::thread([this, method, function]() { try { (this->*method)(); @@ -315,6 +537,7 @@ namespace Sequencer { is_science_frame_transfer(false), notify_tcs_next_target(false), arm_readout_flag(false), + pool(NTHREADS), acquisition_timeout(0), acquisition_max_retrys(-1), tcs_offsetrate_ra(45), @@ -333,9 +556,10 @@ namespace Sequencer { is_subscriber_thread_running(false), should_subscriber_thread_run(false) { + init_operation_builders(); seq_state_manager.set_callback([this](const std::bitset& states) { broadcast_seqstate(); }); wait_state_manager.set_callback([this](const std::bitset& states) { broadcast_waitstate(); }); - thread_state_manager.set_callback([this](const std::bitset& states) { publish_threadstate(); }); + op_state_manager.set_callback([this](const std::bitset& states) { publish_threadstate(); }); daemon_manager.set_callback([this](const std::bitset& states) { broadcast_daemonstate(); }); topic_handlers = { @@ -345,6 +569,10 @@ namespace Sequencer { [this](const nlohmann::json &msg) { handletopic_acamd(msg); } ) }, { Topic::SLICECAMD, std::function( [this](const nlohmann::json &msg) { handletopic_slicecamd(msg); } ) }, + { Topic::SLITD, std::function( + [this](const nlohmann::json &msg) { handletopic_slitd(msg); } ) }, + { Topic::TCSD, std::function( + [this](const nlohmann::json &msg) { handletopic_tcsd(msg); } ) }, { Topic::CAMERAD, std::function( [this](const nlohmann::json &msg) { handletopic_camerad(msg); } ) } }; @@ -374,9 +602,17 @@ namespace Sequencer { this->is_usercontinue.store(false); } - inline void reset_cancel_flag() { this->cancel_flag.store(false); } + inline bool is_cancelled() const { return this->cancel_flag.load(std::memory_order_acquire); } - std::map telemetry_providers; ///< map of port[daemon_name] for external telemetry providers + inline void clear_cancel_flag() { this->cancel_flag.store(false, std::memory_order_release); } + inline void set_cancel_flag() { + this->cancel_flag.store(true, std::memory_order_release); + this->cv.notify_all(); + // Wake threads blocked on subsystem CVs so they can observe the cancel_flag. + { std::lock_guard lock(this->camerad_mtx); this->camerad_cv.notify_all(); } + { std::lock_guard lock(this->acam_mtx); this->acam_cv.notify_all(); } + { std::lock_guard lock(this->fineacquire_mtx); this->fineacquire_cv.notify_all(); } + } double acquisition_timeout; ///< timeout for target acquisition (in sec) set by configuration parameter ACAM_ACQUIRE_TIMEOUT int acquisition_max_retrys; ///< max number of acquisition loop attempts @@ -396,6 +632,10 @@ namespace Sequencer { std::condition_variable acam_cv; std::mutex camerad_mtx; std::condition_variable camerad_cv; + std::mutex slitd_mtx; + std::condition_variable slitd_cv; + std::mutex tcsd_mtx; + std::condition_variable tcsd_cv; std::mutex wait_mtx; std::condition_variable cv; std::mutex cv_mutex; @@ -409,10 +649,10 @@ namespace Sequencer { std::mutex seqstate_mtx; std::condition_variable seqstate_cv; - ImprovedStateManager(Sequencer::NUM_THREAD_STATES)> thread_error_manager{ Sequencer::thread_names }; + ImprovedStateManager(Sequencer::NUM_OP_STATES)> op_error_manager{ Sequencer::op_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}; - ImprovedStateManager(Sequencer::NUM_THREAD_STATES)> thread_state_manager{ Sequencer::thread_names }; + ImprovedStateManager(Sequencer::NUM_OP_STATES)> op_state_manager{ Sequencer::op_names }; ImprovedStateManager(Sequencer::NUM_DAEMONS)> daemon_manager{ Sequencer::daemon_name }; TargetInfo target; ///< TargetInfo object contains info for a target row and how to read it @@ -435,8 +675,6 @@ namespace Sequencer { std::string daemon_control; ///< daemon control script - Common::Queue async; ///< asynchronous message queue - // Here are all the daemon client objects that the Sequencer connects to. // Common::DaemonClient acamd { "acamd" }; @@ -450,12 +688,73 @@ namespace Sequencer { Common::DaemonClient slitd { "slitd" }; Common::DaemonClient tcsd { "tcsd" }; + // CommandClient wrappers add per-daemon arg-count checks and + // state-machine validation in front of the DaemonClient TCP send. + // Initial state is IDLE for every daemon -- they transition to READY + // after the first successful "open" (or equivalent) command. + // + CommandClient acamd_cmd { acamd, acamd_specs, + AcamState::IDLE, + acamd_transitions, + acamd_state_names }; + CommandClient calibd_cmd { calibd, calibd_specs, + CalibState::IDLE, + calibd_transitions, + calibd_state_names }; + CommandClient camerad_cmd { camerad, camerad_specs, + CameraState::IDLE, + camerad_transitions, + camerad_state_names }; + CommandClient flexured_cmd { flexured, flexured_specs, + FlexureState::IDLE, + flexured_transitions, + flexured_state_names }; + CommandClient focusd_cmd { focusd, focusd_specs, + FocusState::IDLE, + focusd_transitions, + focusd_state_names }; + CommandClient powerd_cmd { powerd, powerd_specs, + PowerdState::IDLE, + powerd_transitions, + powerd_state_names }; + CommandClient slicecamd_cmd { slicecamd, slicecamd_specs, + SlicecamState::IDLE, + slicecamd_transitions, + slicecamd_state_names }; + CommandClient slitd_cmd { slitd, slitd_specs, + SlitState::IDLE, + slitd_transitions, + slitd_state_names }; + CommandClient tcsd_cmd { tcsd, tcsd_specs, + TcsState::IDLE, + tcsd_transitions, + tcsd_state_names }; + std::map power_switch; ///< STL map of PowerSwitch objects maps all plugnames to each subsystem float slitoffsetexpose; ///< "virtual slit mode" offset for expose float slitoffsetacquire; ///< "virtual slit mode" offset for acquire float slitwidthacquire; ///< "virtual slit mode" width for acquire + // ---------- sequencer scripting and execution tools -------------------- + // + void operation_sleep(int delay_ms); + void handle_operation_exception(std::exception_ptr eptr, std::string name, std::string caller); + long run(const Operation &op, std::string caller); + long run_parallel(const std::vector &ops, std::string function); + long run_default_sequence(std::string caller); + long run_sequence( const std::vector &groups, + std::string caller ); + + long run_script(const std::string &filename); ///< run user script + long parse_script(const std::string &filename, + std::vector &commands_out); ///< parse script into commands/args + std::optional parse_command(std::string &args); + long build_sequence(const std::vector &commands, + std::vector &sequence_out); ///< build sequence from parsed commands + long validate_sequence(const std::vector &sequence); ///< validate sequence + long handle_cli_operation(std::string command); ///< handle incoming operation request + // publish/subscribe functions // long init_pubsub(const std::initializer_list &topics={}) { @@ -468,14 +767,18 @@ namespace Sequencer { void handletopic_camerad( const nlohmann::json &jmessage ); void handletopic_acamd( const nlohmann::json &jmessage ); void handletopic_slicecamd( const nlohmann::json &jmessage ); + void handletopic_slitd( const nlohmann::json &jmessage ); + void handletopic_tcsd( const nlohmann::json &jmessage ); void publish_snapshot(); void publish_snapshot(std::string &retstring); + void request_snapshot(); void publish_seqstate(); void publish_waitstate(); void publish_daemonstate(); void publish_threadstate(); std::unique_ptr publisher; ///< publisher object + Common::Broadcaster broadcast { this->publisher, Sequencer::DAEMON_NAME }; ///< logs and publishes a narrative message on Topic::BROADCAST std::string publisher_address; ///< publish socket endpoint std::string publisher_topic; ///< my default topic for publishing std::unique_ptr subscriber; ///< subscriber object @@ -499,8 +802,10 @@ 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 + + std::string last_seqstate_str; ///< last seqstate string announced via broadcast_seqstate() (for change detection) uint32_t get_reqstate(); ///< get the reqstate word @@ -517,12 +822,14 @@ namespace Sequencer { long parse_calibration_target(); long parse_state( std::string whoami, std::string reply, bool &state ); ///< parse true|false state from reply string void dothread_test_fpoffset(); ///< for testing, calls Python function from thread + long fine_acquire( std::string args, std::string &retstring ); ///< enable|disable fineacquisition step long test( std::string args, std::string &retstring ); ///< handles test commands long extract_tcs_value( std::string reply, int &value ); ///< extract value returned by the TCS via tcsd long parse_tcs_generic( int value ); ///< parse generic TCS reply std::string seqstate_string( uint32_t state ); ///< returns string form of states set in state word long dotype( std::string args ); ///< set do type (one/all) long dotype( std::string args, std::string &retstring ); ///< set or get do type (one/all) + long engineering( std::string args, std::string &retstring ); ///< set or get engineering mode (true/false) long poll_dome_position( double &domeazi, double &telazi ); long get_dome_position( double &domeazi, double &telazi ); long get_dome_position( bool poll, double &domeazi, double &telazi ); @@ -538,8 +845,6 @@ namespace Sequencer { long target_offset(); void make_telemetry_message( std::string &retstring ); ///< assembles my telemetry message - void get_external_telemetry(); ///< collect telemetry from another daemon - long handle_json_message( const std::string message_in ); ///< parses incoming telemetry messages long set_power_switch( PowerState state, const std::string which, std::chrono::seconds delay ); long check_power_switch( PowerState checkstate, const std::string which, bool &is_set ); @@ -554,28 +859,38 @@ namespace Sequencer { // These are various jobs that are done in their own threads // long trigger_exposure(); ///< trigger and wait for exposure + long do_exposure(std::string caller); ///< wrapper performs and waits for science exposure void abort_process(); ///< tries to abort everything void stop_exposure(); ///< stop exposure timer in progress long repeat_exposure(); ///< repeat the last exposure void modify_exptime( double exptime_in ); ///< modify exptime while exposure running void dothread_test(); - long wait_for_user(); ///< wait for the user or cancel - void sequence_start(std::string obsid_in); ///< main sequence start thread. optional obsid_in for single target obs - long calib_set(); ///< sets calib according to target entry params - long camera_set(); ///< sets camera according to target entry params - long slit_set(VirtualSlitMode mode=VSM_DATABASE); ///< sets slit according to target entry params and mode + long wait_for_ontarget(std::string caller); ///< wait for TCS Operator + long wait_for_user(std::string caller); ///< wait for the user or cancel + long wait_for_exposure(std::string caller); ///< wait for exposure completion or cancel + long wait_for_readout(std::string caller); ///< wait for readout completion or cancel + long wait_for_canexpose(std::string caller); ///< wait for camera can_expose + + void sequence_start(std::string obsid_in=""); ///< main sequence start thread. optional obsid_in for single target obs + long calib_setup(); ///< sets calib according to target entry params + long camera_setup(); ///< sets camera according to target entry params + long slit_setup(VirtualSlitMode mode=VSM_DATABASE); ///< sets slit according to target entry params and mode long move_to_target(); ///< sends request to TCS to move to target coords static void dothread_notify_tcs( Sequencer::Sequence &seq ); ///< like move_to_target but for preauth only - long focus_set(); - long flexure_set(); + long focus_setup(); + long flexure_setup(); /** * these are in sequence_acquisition.cpp */ long do_acam_acquire(); + long do_acam_stop(); long do_slicecam_fineacquire(); - + long do_slicecam_stop(); + long do_target_acquisition(std::string caller); + long do_target_offset(std::string caller); + long do_target_virtualslit(VirtualSlitMode mode); long acam_init(); ///< initializes connection to acamd long calib_init(); ///< initializes connection to calibd diff --git a/sequencerd/sequence_acquisition.cpp b/sequencerd/sequence_acquisition.cpp index 7beab093..1b613340 100644 --- a/sequencerd/sequence_acquisition.cpp +++ b/sequencerd/sequence_acquisition.cpp @@ -19,8 +19,8 @@ namespace Sequencer { const std::string function("Sequencer::Sequence::do_acam_acquire"); std::string reply; - ScopedState thr_state( thread_state_manager, Sequencer::THR_ACQUISITION ); - ScopedState wait_state( wait_state_manager, Sequencer::SEQ_WAIT_ACQUIRE ); + ScopedState thr_state( op_state_manager, Sequencer::OP_ACQUISITION ); + ScopedState wait_state( wait_state_manager, Sequencer::SEQ_WAIT_ACAM_ACQUIRE ); // form and send the ACQUIRE command to ACAM // @@ -29,17 +29,24 @@ 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" ); + logwrite( function, "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.notice( function, "starting ACAM acquisition" ); + + // Freshness boundary: any ACAMD status publish strictly newer than this + // timestamp is considered fresh for this acquire cycle. The guard-band + // absorbs jitter and the timing race between this command send and + // ACAM's forced publish triggered by the command. + // + const int64_t freshness_boundary_us = get_time_us() - ACAM_FRESHNESS_GUARD_US; if ( this->acamd.command( cmd.str(), reply ) != NO_ERROR ) { - this->async.enqueue_and_log( function, "ERROR sending acquire command to acamd" ); + logwrite( function, "ERROR sending acquire command to acamd" ); return ERROR; } @@ -47,27 +54,102 @@ namespace Sequencer { const auto timeout_time = std::chrono::steady_clock::now() + std::chrono::duration( this->acquisition_timeout ); - // wait for is_acam_guiding (I subscribe to this) - // or cancel, or timeout + // wait for is_acam_guiding (I subscribe to this) with a fresh ACAMD publish + // since the acquire command was sent, or acamd stopped without acquiring + // (failure), or cancel, or timeout. + // was_acquiring latches true once acamd enters "acquiring" mode, so that + // !is_acam_acquiring only signals failure after the attempt began — not + // before acamd's first telemetry publish arrives. // + bool was_acquiring = false; std::unique_lock lock(this->acam_mtx); this->acam_cv.wait(lock, [&]() { - return this->is_acam_guiding.load() || this->cancel_flag.load() || - (use_timeout && std::chrono::steady_clock::now() > timeout_time); + if ( this->is_acam_acquiring.load() ) was_acquiring = true; + const bool fresh = this->acam_pubtime.load() > freshness_boundary_us; + return ( fresh && this->is_acam_guiding.load() ) || this->cancel_flag.load() || + ( was_acquiring && !this->is_acam_acquiring.load() ) || + ( use_timeout && std::chrono::steady_clock::now() > timeout_time ); }); 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!"); - return TIMEOUT; + + // Determine outcome by reading state. If not guiding, then this is a + // failure of some kind; distinguish a true timeout from acamd stopping + // without acquiring by consulting the clock. This ordering ensures that + // a failure publish arriving at or near the timeout boundary is still + // reported as a failure (not mis-labeled as a timeout). + // + if (!this->is_acam_guiding.load()) { + if (use_timeout && std::chrono::steady_clock::now() >= timeout_time) { + logwrite( function, "ERROR ACAM acquisition timed out!" ); + return TIMEOUT; + } + logwrite( function, "ERROR ACAM acquisition stopped without acquiring target" ); + return ERROR; } - this->async.enqueue_and_log(function, "ACAM target acquired"); + this->broadcast.notice( function, "ACAM target acquired" ); return NO_ERROR; } /***** Sequencer::Sequence::do_acam_acquire **********************************/ + /***** Sequencer::Sequence::do_acam_stop *************************************/ + /** + * @brief stops ACAM guiding + * @return NO_ERROR | ERROR | TIMEOUT + * + */ + long Sequence::do_acam_stop() { + const std::string function("Sequencer::Sequence::do_acam_stop"); + + ScopedState wait_state( wait_state_manager, Sequencer::SEQ_WAIT_ACAM ); + + // any acamd status newer than this timestamp is considered fresh + const int64_t freshness_boundary_us = get_time_us() - ACAM_FRESHNESS_GUARD_US; + + // send ACQUIRE STOP command to ACAM + std::string reply; + if ( this->acamd.command( ACAMD_ACQUIRE+" stop", reply ) != NO_ERROR ) { + logwrite( function, "ERROR stopping guiding" ); + return ERROR; + } + + if ( reply.find("ERROR") != std::string::npos ) { + logwrite( function, "ERROR acam: "+reply ); + return ERROR; + } + + const bool use_timeout = ( this->acquisition_timeout > 0 ); + const auto timeout_time = std::chrono::steady_clock::now() + + std::chrono::duration( this->acquisition_timeout ); + + // wait for a new is_acam_guiding, or cancel, or timeout + std::unique_lock lock(this->acam_mtx); + this->acam_cv.wait(lock, [&]() { + const bool fresh = this->acam_pubtime.load() > freshness_boundary_us; + return ( fresh && !this->is_acam_guiding.load() ) || this->cancel_flag.load() || + ( use_timeout && std::chrono::steady_clock::now() > timeout_time ); + }); + + // success + if (!this->is_acam_guiding.load()) { + this->broadcast.notice( function, "guiding stopped" ); + return NO_ERROR; + } + + // failure + if (this->cancel_flag.load()) return ABORT; + if (use_timeout) { + logwrite( function, "ERROR timeout stopping guiding" ); + return TIMEOUT; + } + logwrite( function, "ERROR stopping guiding" ); + return ERROR; + } + /***** Sequencer::Sequence::do_acam_stop *************************************/ + + /***** Sequencer::Sequence::do_slicecam_fineacquire **************************/ /** * @brief trigger SLICECAM fine acquisition and wait until locked @@ -77,44 +159,191 @@ 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"); + if (this->slicecamd.command( SLICECAMD_FINEACQUIRE+" start", reply ) != NO_ERROR) { + logwrite( function, "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); + logwrite( function, "ERROR slicecam fine acquisition mode: "+reply ); return ERROR; } - this->async.enqueue_and_log(function, "NOTICE: slicecam fine acquisition started"); + this->broadcast.notice( function, "slicecam fine acquisition started" ); const bool use_timeout = ( this->acquisition_timeout > 0 ); const auto timeout_time = std::chrono::steady_clock::now() + std::chrono::duration( this->acquisition_timeout ); // wait for is_fineacquire_locked (I subscribe to this) - // or cancel, or timeout + // or slicecam stopped without locking (failure), or cancel, or timeout. + // was_running latches true once slicecamd confirms it started, so that + // !is_fineacquire_running only signals failure after the run began — not + // before slicecamd's first telemetry publish arrives. // + bool was_running = false; std::unique_lock lock(this->fineacquire_mtx); this->fineacquire_cv.wait(lock, [&]() { + if ( this->is_fineacquire_running.load() ) was_running = true; return this->is_fineacquire_locked.load() || this->cancel_flag.load() || + ( was_running && !this->is_fineacquire_running.load() ) || (use_timeout && std::chrono::steady_clock::now() > timeout_time); }); 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!"); - return TIMEOUT; + + // Determine outcome by reading state. If not locked, then this is a + // failure of some kind; distinguish a true timeout from slicecamd + // stopping without locking by consulting the clock. This ordering + // ensures that a failure publish arriving at or near the timeout + // boundary is still reported as a failure (not mis-labeled as a + // timeout). + // + if (!this->is_fineacquire_locked.load()) { + if (use_timeout && std::chrono::steady_clock::now() >= timeout_time) { + logwrite( function, "ERROR slicecam fine acquisition timed out!" ); + return TIMEOUT; + } + logwrite( function, "ERROR slicecam fine acquisition stopped without locking" ); + return ERROR; } - this->async.enqueue_and_log(function, "slicecam fine acquisition target acquired"); + this->broadcast.notice( function, "slicecam fine acquisition target acquired" ); return NO_ERROR; } /***** Sequencer::Sequence::do_slicecam_fineacquire **************************/ + + /***** Sequencer::Sequence::do_slicecam_stop **********************************/ + /** + * @brief stops slicecam fineacquire + * @return NO_ERROR | ERROR | TIMEOUT + * + */ + long Sequence::do_slicecam_stop() { + const std::string function("Sequencer::Sequence::do_slicecam_stop"); + + ScopedState wait_state( wait_state_manager, Sequencer::SEQ_WAIT_SLICECAM ); + + // send STOP command to SLICECAM + std::string reply; + if (this->slicecamd.command( SLICECAMD_FINEACQUIRE+" stop", reply ) != NO_ERROR) { + logwrite( function, "ERROR stopping fine acquisition" ); + return ERROR; + } + + if ( reply.find("ERROR") != std::string::npos ) { + logwrite( function, "ERROR slicecam fine acquisition mode: "+reply ); + return ERROR; + } + + const bool use_timeout = ( this->acquisition_timeout > 0 ); + const auto timeout_time = std::chrono::steady_clock::now() + + std::chrono::duration( this->acquisition_timeout ); + + // wait for !is_fineacquire_locked or cancel, or timeout + std::unique_lock lock(this->fineacquire_mtx); + this->fineacquire_cv.wait(lock, [&]() { + return !this->is_fineacquire_locked.load() || this->cancel_flag.load() || + (use_timeout && std::chrono::steady_clock::now() > timeout_time); + }); + + // success + if (!this->is_fineacquire_locked.load()) { + this->broadcast.notice( function, "slicecam fine acquisition stopped" ); + return NO_ERROR; + } + + // failure + if (this->cancel_flag.load()) return ABORT; + if (use_timeout) { + logwrite( function, "ERROR slicecam fine acquisition timed out!" ); + return TIMEOUT; + } + logwrite( function, "ERROR stopping fine acquisition" ); + return ERROR; + } + /***** Sequencer::Sequence::do_slicecam_stop *********************************/ + + + /***** Sequencer::Sequence::do_target_acquisition ****************************/ + /** + * @brief performs target acquisition + * @details First acquire on ACAM, then run slicecam fineacquire if enabled. + * Waits for user confirmation when fineacquire is disabled or fails. + * @param[in] caller name of calling function for logging + * @return NO_ERROR | ABORT + * + */ + long Sequence::do_target_acquisition(std::string caller) { + + if (this->target.iscal) return NO_ERROR; + + // ---------- ACAM acquire ----------------------------- + // + if ( this->do_acam_acquire() != NO_ERROR ) { + this->broadcast.warning( caller, "acam acquisition failed" ); + if ( this->wait_for_user(caller) == ABORT ) return ABORT; + return NO_ERROR; // user chose to continue + } + + // ---------- SLICECAM fineacquire --------------------- + // + bool dofine = this->should_fineacquire.load(); + long ret = NO_ERROR; + if ( dofine ) ret = this->do_slicecam_fineacquire(); + if ( ret != NO_ERROR ) this->broadcast.warning( caller, "slicecam fine acquisition failed" ); + + // wait for user if fineacquire was disabled or failed + if ( (!dofine || ret != NO_ERROR) && + this->wait_for_user(caller) == ABORT ) return ABORT; + + return NO_ERROR; + } + /***** Sequencer::Sequence::do_target_acquisition ****************************/ + + + /***** Sequencer::Sequence::do_target_virtualslit ****************************/ + /** + * @brief move to virtual slit position + * @param[in] mode VirtualSlitMode + * @return NO_ERROR + * + */ + long Sequence::do_target_virtualslit(VirtualSlitMode mode) { + if (this->target.iscal) return NO_ERROR; + return this->slit_setup(mode); + } + /***** Sequencer::Sequence::do_target_virtualslit ****************************/ + + + /***** Sequencer::Sequence::do_target_offset *********************************/ + /** + * @brief send guide offsets to the TCS after successful fine acquisition + * @details Offsets are only sent when fineacquire achieved a lock. + * If the offset fails, the operator is given the opportunity to + * continue or cancel. + * @param[in] caller name of calling function for logging + * @return NO_ERROR | ABORT + * + */ + long Sequence::do_target_offset(std::string caller) { + + if (this->target.iscal) return NO_ERROR; + + // offsets are only meaningful when fineacquire achieved a lock + if ( !this->is_fineacquire_locked.load() ) return NO_ERROR; + + if ( this->target_offset() == ERROR ) { + this->broadcast.warning( caller, "target offset failed" ); + if ( this->wait_for_user(caller) == ABORT ) return ABORT; + } + + return NO_ERROR; + } + /***** Sequencer::Sequence::do_target_offset *********************************/ + } diff --git a/sequencerd/sequence_builder.cpp b/sequencerd/sequence_builder.cpp new file mode 100644 index 00000000..8191b1ff --- /dev/null +++ b/sequencerd/sequence_builder.cpp @@ -0,0 +1,127 @@ +/** + * @file sequence_builder.cpp + * @brief implementation for building sequences from operations + * @author David Hale + * + */ + +#include "sequence.h" + +namespace Sequencer { + + /***** Sequencer::Sequence::build_sequence *********************************/ + /** + * @brief build a sequence from parsed commands + * @details This accepts a vector of ParsedCommands and returns by reference + * a sequence, which is one or more OperationGroups. Accepts special + * ".dsl" commands for the Sequencer Domain Specific Language. + * Start a group with "parallel:" or "serial:", finish group + * with "end". Include "on_error stop|continue" to set behavior + * of that group when an error occurs in an operation in that + * group. + * @param[in] commands vector of ParsedCommands + * @param[out] sequence_out vector of Operation Groups (sequence) to execute + * @return ERROR|NO_ERROR|ABORT + * + */ + long Sequence::build_sequence(const std::vector &commands, + std::vector &sequence_out) { + const std::string function("Sequencer::Sequence::build_sequence"); + + // local failure-reporting wrapper + auto fail = [&](const ParsedCommand &cmd, const std::string &msg) { + logwrite(function, "ERROR line "+std::to_string(cmd.linenum)+": "+msg); + return ERROR; + }; + + // create temporay group of operations currently being processed + std::optional current_group; + + // empty the sequence + sequence_out.clear(); + + // loop through vector of commands + // + for (const auto &command : commands) { + + const std::string &name = command.subsystem; + + // ---------- SPECIAL COMMAND { parallel: | serial: } ------------------ + + // start a group + // + if (name == "parallel:" || name == "serial:") { + + if (current_group) return fail(command, "group already open"); + + // create a new group + OperationGroup new_group; + new_group.on_error = OnError::STOP; + new_group.type = (name=="parallel:") ? OperationType::PARALLEL + : OperationType::SERIAL; + current_group = std::move(new_group); + continue; + } + + // ---------- SPECIAL COMMAND { end } ---------------------------------- + + // finish a group + // + if (name == "end") { + if (!current_group) return fail(command, "unexpected 'end' outside group"); + + // add it to the sequence now + sequence_out.push_back(std::move(*current_group)); + + current_group.reset(); + continue; + } + + // ---------- SPECIAL COMMAND { on_error } ----------------------------- + + // set how does the group behave on error + // + if (name == "on_error") { + // must be in a group for on_error to mean anything + if (!current_group) return fail(command, "'on_error' out of a group"); + // on_error must precede any operations in the group so it cannot + // silently mutate the policy of an already-populated group + if (!current_group->operations.empty()) { + return fail(command, "'on_error' must appear before any operations in the group"); + } + if (!command.params.has("action")) return fail(command, "on_error requires action=stop|continue"); + + const std::string val = command.params.get("action",""); + + if (val == "stop") current_group->on_error = OnError::STOP; + else + if (val == "continue") current_group->on_error = OnError::CONTINUE; + else { + return fail(command, "on_error requires action=stop|continue"); + } + continue; + } + + // ---------- MAKE OPERATION FOR GIVEN COMMAND ------------------------- + + // anything other than { parallel | serial | end | on_error } is a + // command to parse, so a group must have been started first. + // + if (!current_group) return fail(command, "command outside group"); + + // find the command in the operation builders map + // + auto it = op_builders.find(name); + + if (it == op_builders.end()) return fail(command, "unrecognized command"); + + Operation &op = current_group->operations.emplace_back(); + + it->second(op, command); + } + + return NO_ERROR; + } + /***** Sequencer::Sequence::build_sequence *********************************/ + +} diff --git a/sequencerd/sequence_operations.cpp b/sequencerd/sequence_operations.cpp new file mode 100644 index 00000000..beb2451c --- /dev/null +++ b/sequencerd/sequence_operations.cpp @@ -0,0 +1,354 @@ +/** + * @file sequence_operations.cpp + * @brief implementation of operations + * @author David Hale + * + * Each Ops factory method here returns a populated Sequence::Operation that + * wraps a Sequence:: member function and identifies itself via its OP_ bit. + * These are the "Internal" sequencer-level operations (database-driven, + * may coordinate multiple daemons). Per-daemon "Passthrough" operations + * (e.g. "camera exptime 30000") are built inline by init_operation_builders() + * and therefore do not appear here. + * + */ + +#include "sequence.h" + +namespace Sequencer { + + Sequence::Ops::Ops(Sequence* seq) : seq(seq) { } + + /***** Sequencer::Sequence::Ops::acam_init *********************************/ + /** + * @brief defines the acam_init operation + * @return Operation + * + */ + Sequence::Operation Sequence::Ops::acam_init() { + Sequence::Operation op; + op.id = OP_ACAM_INIT; + op.func = [this]() { return seq->acam_init(); }; + op.params = { }; + op.max_attempts = 3; + op.retry_delay = 2000; + op.on_retry = [this]() { + long error=NO_ERROR; + error |= seq->set_power_switch(OFF, POWER_ACAM_CAM, std::chrono::seconds(5)); + error |= seq->daemon_restart(seq->acamd); + return error; + }; + return op; + } + /***** Sequencer::Sequence::Ops::acam_init *********************************/ + + + /***** Sequencer::Sequence::Ops::calib_init ********************************/ + /** + * @brief defines the calib_init operation + * @return Operation + * + */ + Sequence::Operation Sequence::Ops::calib_init() { + Sequence::Operation op; + op.id = OP_CALIB_INIT; + op.func = [this]() { return seq->calib_init(); }; + return op; + } + /***** Sequencer::Sequence::Ops::calib_init ********************************/ + + + /***** Sequencer::Sequence::Ops::calib_setup *********************************/ + /** + * @brief defines the calib_setup operation + * @return Operation + * + */ + Sequence::Operation Sequence::Ops::calib_setup() { + Sequence::Operation op; + op.id = OP_CALIB_SETUP; + op.func = [this]() { return seq->calib_setup(); }; + return op; + } + /***** Sequencer::Sequence::Ops::calib_setup *********************************/ + + + /***** Sequencer::Sequence::Ops::camera_init *******************************/ + /** + * @brief defines the camera_init operation + * @return Operation + * + */ + Sequence::Operation Sequence::Ops::camera_init() { + Sequence::Operation op; + op.id = OP_CAMERA_INIT; + op.func = [this]() { return seq->camera_init(); }; + return op; + } + /***** Sequencer::Sequence::Ops::camera_init *******************************/ + + + /***** Sequencer::Sequence::Ops::camera_setup ********************************/ + /** + * @brief defines the camera_setup operation + * @return Operation + * + */ + Sequence::Operation Sequence::Ops::camera_setup() { + Sequence::Operation op; + op.id = OP_CAMERA_SETUP; + op.func = [this]() { return seq->camera_setup(); }; + return op; + } + /***** Sequencer::Sequence::Ops::camera_setup ********************************/ + + + /***** Sequencer::Sequence::Ops::do_expose *********************************/ + /** + * @brief defines the expose operation + * @return Operation + * + */ + Sequence::Operation Sequence::Ops::do_expose() { + Sequence::Operation op; + op.id = OP_EXPOSURE; + op.func = [this]() { + const std::string caller("Sequencer::Sequence::Ops::do_expose"); + return seq->do_exposure(caller); + }; + return op; + } + /***** Sequencer::Sequence::Ops::do_expose *********************************/ + + + /***** Sequencer::Sequence::Ops::do_shutdown *******************************/ + /** + * @brief defines the shutdown operation + * @return Operation + * + */ + Sequence::Operation Sequence::Ops::do_shutdown() { + Sequence::Operation op; + op.id = OP_SHUTDOWN; + op.func = [this]() { return seq->shutdown(); }; + return op; + } + /***** Sequencer::Sequence::Ops::do_shutdown *******************************/ + + + /***** Sequencer::Sequence::Ops::do_startup ********************************/ + /** + * @brief defines the startup operation + * @return Operation + * + */ + Sequence::Operation Sequence::Ops::do_startup() { + Sequence::Operation op; + op.id = OP_STARTUP; + op.func = [this]() { return seq->startup(); }; + return op; + } + /***** Sequencer::Sequence::Ops::do_startup ********************************/ + + + /***** Sequencer::Sequence::Ops::flexure_init ******************************/ + /** + * @brief defines the flexure_init operation + * @return Operation + * + */ + Sequence::Operation Sequence::Ops::flexure_init() { + Sequence::Operation op; + op.id = OP_FLEXURE_INIT; + op.func = [this]() { return seq->flexure_init(); }; + return op; + } + /***** Sequencer::Sequence::Ops::flexure_init ******************************/ + + + /***** Sequencer::Sequence::Ops::flexure_setup *******************************/ + /** + * @brief defines the flexure_setup operation + * @return Operation + * + */ + Sequence::Operation Sequence::Ops::flexure_setup() { + Sequence::Operation op; + op.id = OP_FLEXURE_SETUP; + op.func = [this]() { return seq->flexure_setup(); }; + return op; + } + /***** Sequencer::Sequence::Ops::flexure_setup *******************************/ + + + /***** Sequencer::Sequence::Ops::focus_init ********************************/ + /** + * @brief defines the focus_init operation + * @return Operation + * + */ + Sequence::Operation Sequence::Ops::focus_init() { + Sequence::Operation op; + op.id = OP_FOCUS_INIT; + op.func = [this]() { return seq->focus_init(); }; + return op; + } + /***** Sequencer::Sequence::Ops::focus_init ********************************/ + + + /***** Sequencer::Sequence::Ops::focus_setup *********************************/ + /** + * @brief defines the focus_setup operation + * @return Operation + * + */ + Sequence::Operation Sequence::Ops::focus_setup() { + Sequence::Operation op; + op.id = OP_FOCUS_SETUP; + op.func = [this]() { return seq->focus_setup(); }; + return op; + } + /***** Sequencer::Sequence::Ops::focus_setup *********************************/ + + + /***** Sequencer::Sequence::Ops::move_to_target ****************************/ + /** + * @brief defines the move_to_target operation + * @details Optional ra= and dec= params on the ParsedCommand override the + * target coordinates for this invocation only; the overrides are + * read from seq->current_op_params inside Sequence::move_to_target() + * (populated by Sequence::run() just before op.func() is called) + * and restored on return so the database-backed target is not + * permanently mutated. + * @return Operation + * + */ + Sequence::Operation Sequence::Ops::move_to_target() { + Sequence::Operation op; + op.id = OP_MOVE_TO_TARGET; + op.func = [this]() { return seq->move_to_target(); }; + return op; + } + /***** Sequencer::Sequence::Ops::move_to_target ****************************/ + + + /***** Sequencer::Sequence::Ops::power_init ********************************/ + /** + * @brief defines the power_init operation + * @return Operation + * + */ + Sequence::Operation Sequence::Ops::power_init() { + Sequence::Operation op; + op.id = OP_POWER_INIT; + op.func = [this]() { return seq->power_init(); }; + return op; + } + /***** Sequencer::Sequence::Ops::power_init ********************************/ + + + /***** Sequencer::Sequence::Ops::repeat_exposure ***************************/ + /** + * @brief defines the repeat_exposure operation + * @return Operation + * + */ + Sequence::Operation Sequence::Ops::repeat_exposure() { + Sequence::Operation op; + op.id = OP_REPEAT_EXPOSURE; + op.func = [this]() { return seq->repeat_exposure(); }; + return op; + } + /***** Sequencer::Sequence::Ops::repeat_exposure ***************************/ + + + /***** Sequencer::Sequence::Ops::slicecam_init *****************************/ + /** + * @brief defines the slicecam_init operation + * @return Operation + * + */ + Sequence::Operation Sequence::Ops::slicecam_init() { + Sequence::Operation op; + op.id = OP_SLICECAM_INIT; + op.func = [this]() { return seq->slicecam_init(); }; + op.params = { }; + op.max_attempts = 3; + op.retry_delay = 2000; + op.on_retry = [this]() { + long error=NO_ERROR; + error |= seq->set_power_switch(OFF, POWER_SLICECAM, std::chrono::seconds(5)); + error |= seq->daemon_restart(seq->slicecamd); + return error; + }; + return op; + } + /***** Sequencer::Sequence::Ops::slicecam_init *****************************/ + + + /***** Sequencer::Sequence::Ops::slit_init *********************************/ + /** + * @brief defines the slit_init operation + * @return Operation + * + */ + Sequence::Operation Sequence::Ops::slit_init() { + Sequence::Operation op; + op.id = OP_SLIT_INIT; + op.func = [this]() { return seq->slit_init(); }; + return op; + } + /***** Sequencer::Sequence::Ops::slit_init *********************************/ + + + /***** Sequencer::Sequence::Ops::slit_setup **********************************/ + /** + * @brief defines the slit_setup operation + * @details Reads optional mode= param from seq->current_op_params. When + * absent the default VSM_DATABASE mode is used (drives slit from + * database target entry). + * @return Operation + * + */ + Sequence::Operation Sequence::Ops::slit_setup() { + Sequence::Operation op; + op.id = OP_SLIT_SETUP; + op.func = [this]() { + const size_t mode = seq->current_op_params.get("mode", + static_cast(VSM_DATABASE)); + return seq->slit_setup(static_cast(mode)); + }; + return op; + } + /***** Sequencer::Sequence::Ops::slit_setup **********************************/ + + + /***** Sequencer::Sequence::Ops::target_offset *****************************/ + /** + * @brief defines the target_offset operation + * @return Operation + * + */ + Sequence::Operation Sequence::Ops::target_offset() { + Sequence::Operation op; + op.id = OP_MOVE_TO_TARGET; // no dedicated OP_ for target_offset; reuse move_to_target identity + op.func = [this]() { return seq->target_offset(); }; + return op; + } + /***** Sequencer::Sequence::Ops::target_offset *****************************/ + + + /***** Sequencer::Sequence::Ops::tcs_init **********************************/ + /** + * @brief defines the tcs_init operation + * @return Operation + * + */ + Sequence::Operation Sequence::Ops::tcs_init() { + Sequence::Operation op; + op.id = OP_TCS_INIT; + op.func = [this]() { return seq->tcs_init(); }; + return op; + } + /***** Sequencer::Sequence::Ops::tcs_init **********************************/ + +} diff --git a/sequencerd/sequence_wait.cpp b/sequencerd/sequence_wait.cpp new file mode 100644 index 00000000..feba92fc --- /dev/null +++ b/sequencerd/sequence_wait.cpp @@ -0,0 +1,168 @@ +/** + * @file sequence_wait.cpp + * @brief wait wrappers used in the Sequence class + * @author David Hale + * + */ + +#include "sequence.h" + +namespace Sequencer { + + /***** Sequencer::Sequence::wait_for_ontarget *******************************/ + /** + * @brief waits for the TCS Operator to click 'ontarget' + * @param[in] caller reference to caller's name for logging + * @return NO_ERROR on continue | ABORT on cancel + * + */ + long Sequence::wait_for_ontarget(std::string caller) { + // waiting for TCS Operator input (or cancel) + { + ScopedState wait_state( wait_state_manager, Sequencer::SEQ_WAIT_TCSOP ); + + this->broadcast.notice( caller, "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->broadcast.notice( caller, "received " + +(this->cancel_flag.load() ? std::string("cancel") + : std::string("ontarget")) + +" signal!" ); + } + this->is_ontarget.store(false); + + return (this->cancel_flag.load() ? ABORT : NO_ERROR); + } + /***** Sequencer::Sequence::wait_for_ontarget *******************************/ + + + /***** Sequencer::Sequence::wait_for_user ***********************************/ + /** + * @brief waits for the user to click a button, or cancel + * @details Use this when you just want to slow things down or get a + * cup of coffee instead of observing. + * @param[in] caller reference to caller's name for logging + * @return NO_ERROR on continue | ABORT on cancel + * + */ + long Sequence::wait_for_user(std::string caller) { + { + ScopedState wait_state( wait_state_manager, Sequencer::SEQ_WAIT_USER ); + + this->broadcast.notice( caller, "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->broadcast.notice( caller, "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->broadcast.notice( caller, "sequence cancelled" ); + return ABORT; + } + + this->is_usercontinue.store(false); + + return NO_ERROR; + } + /***** Sequencer::Sequence::wait_for_user ***********************************/ + + + /***** Sequencer::Sequence::wait_for_exposure *******************************/ + /** + * @brief waits for exposure completion, or cancel + * @param[in] caller reference to caller's name for logging + * @return NO_ERROR on continue | ABORT on cancel + * + */ + long Sequence::wait_for_exposure(std::string caller) { + logwrite(caller, "waiting for exposure"); + while (!this->cancel_flag.load() && + wait_state_manager.is_set(Sequencer::SEQ_WAIT_EXPOSE)) { + std::unique_lock lock(cv_mutex); + this->cv.wait( lock, [this]() { return(!wait_state_manager.is_set(SEQ_WAIT_EXPOSE) || + this->cancel_flag.load()); } ); + } + + if (this->cancel_flag.load()) { + this->broadcast.notice( caller, "exposure cancelled" ); + return ABORT; + } + + return NO_ERROR; + } + /***** Sequencer::Sequence::wait_for_exposure *******************************/ + + + /***** Sequencer::Sequence::wait_for_readout ********************************/ + /** + * @brief waits for readout completion, or cancel + * @param[in] caller reference to caller's name for logging + * @return NO_ERROR on continue | ABORT on cancel + * + */ + long Sequence::wait_for_readout(std::string caller) { + + // don't have to wait for readout when using frame transfer + if ( this->is_science_frame_transfer ) return NO_ERROR; + + logwrite(caller, "waiting for readout"); + + while (!this->cancel_flag.load() && + wait_state_manager.is_set(Sequencer::SEQ_WAIT_READOUT)) { + std::unique_lock lock(cv_mutex); + this->cv.wait( lock, [this]() { return(!wait_state_manager.is_set(SEQ_WAIT_READOUT) || + this->cancel_flag.load()); } ); + } + + if (this->cancel_flag.load()) { + this->broadcast.notice( caller, "wait for readout cancelled" ); + return ABORT; + } + + return NO_ERROR; + } + /***** Sequencer::Sequence::wait_for_readout ********************************/ + + + /***** Sequencer::Sequence::wait_for_canexpose ******************************/ + /** + * @brief waits for camera to be ready to expose, or cancel + * @param[in] caller reference to caller's name for logging + * @return NO_ERROR on continue | ABORT on cancel + * + */ + long Sequence::wait_for_canexpose(std::string caller) { + + this->broadcast.notice( caller, "waiting for camera to be ready to expose" ); + + while ( !this->cancel_flag.load() && + !this->can_expose.load() ) { + + std::unique_lock lock(this->camerad_mtx); + this->camerad_cv.wait( lock, [this]() { return( this->can_expose.load() || + this->cancel_flag.load() ); } ); + } + + if (this->cancel_flag.load()) { + this->broadcast.notice( caller, "wait for can_expose cancelled" ); + return ABORT; + } + + return NO_ERROR; + } + /***** Sequencer::Sequence::wait_for_canexpose ******************************/ + +} diff --git a/sequencerd/sequencer_interface.h b/sequencerd/sequencer_interface.h index f4569e7c..fd59948b 100644 --- a/sequencerd/sequencer_interface.h +++ b/sequencerd/sequencer_interface.h @@ -26,6 +26,19 @@ #define ERROR_TARGETLIST_BAD_HEADER 1001 ///< TODO change this +namespace DBCol { + inline const std::string EXPTIME = "EXPTIME"; + inline const std::string SLITWIDTH = "SLITWIDTH"; + inline const std::string SLITOFFSET = "SLITOFFSET"; + + inline const std::string TELRA = "TELRA"; + inline const std::string TELDECL = "TELDECL"; + inline const std::string ALT = "ALT"; + inline const std::string AZ = "AZ"; + inline const std::string AIRMASS = "AIRMASS"; + inline const std::string CASANGLE = "CASANGLE"; +} + /***** Sequencer **************************************************************/ /** * @namespace Sequencer diff --git a/sequencerd/sequencer_server.cpp b/sequencerd/sequencer_server.cpp index 06514cef..f56da104 100644 --- a/sequencerd/sequencer_server.cpp +++ b/sequencerd/sequencer_server.cpp @@ -27,13 +27,10 @@ namespace Sequencer { case SIGTERM: case SIGINT: logwrite(function, "received termination signal"); - message << "NOTICE:" << Sequencer::DAEMON_NAME << " exit"; - Server::instance->sequence.async.enqueue( message.str() ); Server::instance->exit_cleanly(); // shutdown the daemon break; case SIGHUP: // TODO reconfigure? - Server::instance->sequence.async.enqueue_and_log( function, - "ERROR: caught unhandled HUP signal" ); + logwrite( function, "ERROR: caught unhandled HUP signal" ); break; case SIGPIPE: logwrite(function, "ignored SIGPIPE"); @@ -41,8 +38,6 @@ namespace Sequencer { default: message << "received unknown signal " << strsignal(signo); logwrite( function, message.str() ); - message.str(""); message << "NOTICE:" << Sequencer::DAEMON_NAME << " exit"; - Server::instance->sequence.async.enqueue( message.str() ); break; } return; @@ -58,6 +53,7 @@ namespace Sequencer { void Server::exit_cleanly(void) { std::string function = "Sequencer::Server::exit_cleanly"; logwrite( function, "exiting" ); + Server::instance->sequence.broadcast.notice(function, "sequencerd terminated"); exit(EXIT_SUCCESS); } @@ -90,7 +86,7 @@ namespace Sequencer { catch (const std::exception &e) { // should be impossible message.str(""); message << "ERROR parsing entry " << entry << " of " << this->config.n_entries << ": " << e.what(); - this->sequence.async.enqueue_and_log( function, message.str() ); + logwrite( function, message.str() ); return ERROR; } @@ -106,11 +102,11 @@ namespace Sequencer { } catch (const std::exception &e) { message.str(""); message << "ERROR parsing NBPORT: " << e.what(); - this->sequence.async.enqueue_and_log( function, message.str() ); + logwrite( function, message.str() ); return ERROR; } message.str(""); message << "SEQUENCERD:config:" << config.param[entry] << "=" << config.arg[entry]; - this->sequence.async.enqueue_and_log( function, message.str() ); + logwrite( function, message.str() ); applied++; } @@ -121,11 +117,11 @@ namespace Sequencer { } catch (const std::exception &e) { message.str(""); message << "ERROR parsing BLKPORT: " << e.what(); - this->sequence.async.enqueue_and_log( function, message.str() ); + logwrite( function, message.str() ); return ERROR; } message.str(""); message << "SEQUENCERD:config:" << config.param[entry] << "=" << config.arg[entry]; - this->sequence.async.enqueue_and_log( function, message.str() ); + logwrite( function, message.str() ); applied++; } @@ -136,11 +132,11 @@ namespace Sequencer { } catch (const std::exception &e) { message.str(""); message << "ERROR parsing ASYNCPORT: " << e.what(); - this->sequence.async.enqueue_and_log( function, message.str() ); + logwrite( function, message.str() ); return ERROR; } message.str(""); message << "SEQUENCERD:config:" << config.param[entry] << "=" << config.arg[entry]; - this->sequence.async.enqueue_and_log( function, message.str() ); + logwrite( function, message.str() ); applied++; } @@ -151,11 +147,11 @@ namespace Sequencer { } catch (const std::exception &e) { message.str(""); message << "ERROR parsing MESSAGEPORT: " << e.what(); - this->sequence.async.enqueue_and_log( function, message.str() ); + logwrite( function, message.str() ); return ERROR; } message.str(""); message << "SEQUENCERD:config:" << config.param[entry] << "=" << config.arg[entry]; - this->sequence.async.enqueue_and_log( function, message.str() ); + logwrite( function, message.str() ); applied++; } @@ -163,7 +159,7 @@ namespace Sequencer { if (config.param[entry] == "MESSAGEGROUP") { this->messagegroup = config.arg[entry]; message.str(""); message << "SEQUENCERD:config:" << config.param[entry] << "=" << config.arg[entry]; - this->sequence.async.enqueue_and_log( function, message.str() ); + logwrite( function, message.str() ); applied++; } @@ -172,7 +168,7 @@ namespace Sequencer { if ( config.param[entry] == "PUB_ENDPOINT" ) { this->sequence.publisher_address = config.arg[entry]; this->sequence.publisher_topic = DAEMON_NAME; - this->sequence.async.enqueue_and_log(function, "SEQUENCERD:config:"+config.param[entry]+"="+config.arg[entry]); + logwrite(function, "SEQUENCERD:config:"+config.param[entry]+"="+config.arg[entry]); applied++; } @@ -180,14 +176,14 @@ namespace Sequencer { // if ( config.param[entry] == "SUB_ENDPOINT" ) { this->sequence.subscriber_address = config.arg[entry]; - this->sequence.async.enqueue_and_log(function, "SEQUENCERD:config:"+config.param[entry]+"="+config.arg[entry]); + logwrite(function, "SEQUENCERD:config:"+config.param[entry]+"="+config.arg[entry]); applied++; } // DAEMON_CONTROL_SCRIPT if (config.param[entry] == "DAEMON_CONTROL_SCRIPT") { this->sequence.daemon_control = config.arg[entry]; - this->sequence.async.enqueue_and_log(function, "SEQUENCERD:config:"+config.param[entry]+"="+config.arg[entry]); + logwrite(function, "SEQUENCERD:config:"+config.param[entry]+"="+config.arg[entry]); applied++; } @@ -198,11 +194,11 @@ namespace Sequencer { } catch (const std::exception &e) { message.str(""); message << "ERROR parsing ACAMD_PORT: " << e.what(); - this->sequence.async.enqueue_and_log( function, message.str() ); + logwrite( function, message.str() ); return ERROR; } message.str(""); message << "SEQUENCERD:config:" << config.param[entry] << "=" << config.arg[entry]; - this->sequence.async.enqueue_and_log( function, message.str() ); + logwrite( function, message.str() ); applied++; } @@ -213,11 +209,11 @@ namespace Sequencer { } catch (const std::invalid_argument &e) { message.str(""); message << "ERROR parsing CAMERAD_PORT: " << e.what(); - this->sequence.async.enqueue_and_log( function, message.str() ); + logwrite( function, message.str() ); return ERROR; } message.str(""); message << "SEQUENCERD:config:" << config.param[entry] << "=" << config.arg[entry]; - this->sequence.async.enqueue_and_log( function, message.str() ); + logwrite( function, message.str() ); applied++; } @@ -228,11 +224,11 @@ namespace Sequencer { } catch (const std::exception &e) { message.str(""); message << "ERROR parsing CAMERAD_NBPORT: " << e.what(); - this->sequence.async.enqueue_and_log( function, message.str() ); + logwrite( function, message.str() ); return ERROR; } message.str(""); message << "SEQUENCERD:config:" << config.param[entry] << "=" << config.arg[entry]; - this->sequence.async.enqueue_and_log( function, message.str() ); + logwrite( function, message.str() ); applied++; } @@ -243,11 +239,11 @@ namespace Sequencer { } catch (const std::exception &e) { message.str(""); message << "ERROR parsing FLEXURED_PORT: " << e.what(); - this->sequence.async.enqueue_and_log( function, message.str() ); + logwrite( function, message.str() ); return ERROR; } message.str(""); message << "SEQUENCERD:config:" << config.param[entry] << "=" << config.arg[entry]; - this->sequence.async.enqueue_and_log( function, message.str() ); + logwrite( function, message.str() ); applied++; } @@ -258,11 +254,11 @@ namespace Sequencer { } catch (const std::exception &e) { message.str(""); message << "ERROR parsing POWERD_PORT: " << e.what(); - this->sequence.async.enqueue_and_log( function, message.str() ); + logwrite( function, message.str() ); return ERROR; } message.str(""); message << "SEQUENCERD:config:" << config.param[entry] << "=" << config.arg[entry]; - this->sequence.async.enqueue_and_log( function, message.str() ); + logwrite( function, message.str() ); applied++; } @@ -273,11 +269,11 @@ namespace Sequencer { } catch (const std::exception &e) { message.str(""); message << "ERROR parsing SLICECAMD_PORT: " << e.what(); - this->sequence.async.enqueue_and_log( function, message.str() ); + logwrite( function, message.str() ); return ERROR; } message.str(""); message << "SEQUENCERD:config:" << config.param[entry] << "=" << config.arg[entry]; - this->sequence.async.enqueue_and_log( function, message.str() ); + logwrite( function, message.str() ); applied++; } @@ -288,11 +284,11 @@ namespace Sequencer { } catch (const std::exception &e) { message.str(""); message << "ERROR parsing SLITD_PORT: " << e.what(); - this->sequence.async.enqueue_and_log( function, message.str() ); + logwrite( function, message.str() ); return ERROR; } message.str(""); message << "SEQUENCERD:config:" << config.param[entry] << "=" << config.arg[entry]; - this->sequence.async.enqueue_and_log( function, message.str() ); + logwrite( function, message.str() ); applied++; } @@ -303,11 +299,11 @@ namespace Sequencer { } catch (const std::exception &e) { message.str(""); message << "ERROR parsing TCSD_PORT: " << e.what(); - this->sequence.async.enqueue_and_log( function, message.str() ); + logwrite( function, message.str() ); return ERROR; } message.str(""); message << "SEQUENCERD:config:" << config.param[entry] << "=" << config.arg[entry]; - this->sequence.async.enqueue_and_log( function, message.str() ); + logwrite( function, message.str() ); applied++; } @@ -318,11 +314,11 @@ namespace Sequencer { } catch (const std::exception &e) { message.str(""); message << "ERROR parsing CALIBD_PORT: " << e.what(); - this->sequence.async.enqueue_and_log( function, message.str() ); + logwrite( function, message.str() ); return ERROR; } message.str(""); message << "SEQUENCERD:config:" << config.param[entry] << "=" << config.arg[entry]; - this->sequence.async.enqueue_and_log( function, message.str() ); + logwrite( function, message.str() ); applied++; } @@ -333,11 +329,11 @@ namespace Sequencer { } catch (const std::exception &e) { message.str(""); message << "ERROR parsing FILTERD_PORT: " << e.what(); - this->sequence.async.enqueue_and_log( function, message.str() ); + logwrite( function, message.str() ); return ERROR; } message.str(""); message << "SEQUENCERD:config:" << config.param[entry] << "=" << config.arg[entry]; - this->sequence.async.enqueue_and_log( function, message.str() ); + logwrite( function, message.str() ); applied++; } @@ -348,11 +344,11 @@ namespace Sequencer { } catch (const std::exception &e) { message.str(""); message << "ERROR parsing FOCUSD_PORT: " << e.what(); - this->sequence.async.enqueue_and_log( function, message.str() ); + logwrite( function, message.str() ); return ERROR; } message.str(""); message << "SEQUENCERD:config:" << config.param[entry] << "=" << config.arg[entry]; - this->sequence.async.enqueue_and_log( function, message.str() ); + logwrite( function, message.str() ); applied++; } @@ -363,11 +359,11 @@ namespace Sequencer { } catch (const std::exception &e) { message.str(""); message << "ERROR parsing ACQUIRE_TIMEOUT: " << e.what(); - this->sequence.async.enqueue_and_log( function, message.str() ); + logwrite( function, message.str() ); return ERROR; } message.str(""); message << "SEQUENCERD:config:" << config.param[entry] << "=" << config.arg[entry]; - this->sequence.async.enqueue_and_log( function, message.str() ); + logwrite( function, message.str() ); applied++; } @@ -379,27 +375,27 @@ namespace Sequencer { } catch (std::invalid_argument &) { message.str(""); message << "ACQUIRE_RETRYS: unable to convert " << config.arg[entry] << " to integer. retry limit disabled."; - this->sequence.async.enqueue_and_log( function, message.str() ); + logwrite( function, message.str() ); this->sequence.acquisition_max_retrys = -1; } catch (std::out_of_range &) { - this->sequence.async.enqueue_and_log( function, "ACQUIRE_RETRYS number out of integer range. retry limit disabled." ); + logwrite( function, "ACQUIRE_RETRYS number out of integer range. retry limit disabled." ); this->sequence.acquisition_max_retrys = -1; } this->sequence.acquisition_max_retrys = rt; message.str(""); message << "SEQUENCERD:config:" << config.param[entry] << "=" << config.arg[entry]; - this->sequence.async.enqueue_and_log( function, message.str() ); + logwrite( function, message.str() ); applied++; } // TCS_WHICH -- which TCS to connect to, defults to real if not specified if ( config.param[entry] == "TCS_WHICH" ) { if ( config.arg[entry] != "sim" && config.arg[entry] != "real" ) { - this->sequence.async.enqueue_and_log( function, "ERROR TCS_WHICH expected { sim real }" ); + logwrite( function, "ERROR TCS_WHICH expected { sim real }" ); return ERROR; } this->sequence.tcs_which = config.arg[entry]; - this->sequence.async.enqueue_and_log( function, "SEQUENCERD:config:"+config.param[entry]+"="+config.arg[entry] ); + logwrite( function, "SEQUENCERD:config:"+config.param[entry]+"="+config.arg[entry] ); applied++; } @@ -410,22 +406,22 @@ namespace Sequencer { mrate = std::stod( config.arg[entry] ); if ( mrate < 0 || mrate > 60 ) { message.str(""); message << "ERROR: TCS_OFFSET_RATE_RA " << mrate << " out of range {0:60}"; - this->sequence.async.enqueue_and_log( function, message.str() ); + logwrite( function, message.str() ); return( ERROR ); } } catch (std::invalid_argument &) { message.str(""); message << "ERROR: bad TCS_OFFSET_RATE_RA: unable to convert " << config.arg[entry] << " to double"; - this->sequence.async.enqueue_and_log( function, message.str() ); + logwrite( function, message.str() ); return(ERROR); } catch (std::out_of_range &) { - this->sequence.async.enqueue_and_log( function, "ERROR: TCS_OFFSET_RATE_RA number out of double range" ); + logwrite( function, "ERROR: TCS_OFFSET_RATE_RA number out of double range" ); return(ERROR); } this->sequence.tcs_offsetrate_ra = mrate; message.str(""); message << "SEQUENCERD:config:" << config.param[entry] << "=" << config.arg[entry]; - this->sequence.async.enqueue_and_log( function, message.str() ); + logwrite( function, message.str() ); applied++; } @@ -436,22 +432,22 @@ namespace Sequencer { mrate = std::stod( config.arg[entry] ); if ( mrate < 0 || mrate > 60 ) { message.str(""); message << "ERROR: TCS_OFFSET_RATE_DEC " << mrate << " out of range {0:60}"; - this->sequence.async.enqueue_and_log( function, message.str() ); + logwrite( function, message.str() ); return( ERROR ); } } catch (std::invalid_argument &) { message.str(""); message << "ERROR: bad TCS_OFFSET_RATE_DEC: unable to convert " << config.arg[entry] << " to double"; - this->sequence.async.enqueue_and_log( function, message.str() ); + logwrite( function, message.str() ); return(ERROR); } catch (std::out_of_range &) { - this->sequence.async.enqueue_and_log( function, "ERROR: TCS_OFFSET_RATE_DEC number out of double range" ); + logwrite( function, "ERROR: TCS_OFFSET_RATE_DEC number out of double range" ); return(ERROR); } this->sequence.tcs_offsetrate_dec = mrate; message.str(""); message << "SEQUENCERD:config:" << config.param[entry] << "=" << config.arg[entry]; - this->sequence.async.enqueue_and_log( function, message.str() ); + logwrite( function, message.str() ); applied++; } @@ -463,16 +459,16 @@ namespace Sequencer { } catch (std::invalid_argument &) { message.str(""); message << "ERROR: bad TCS_SETTLE_TIMEOUT: unable to convert " << config.arg[entry] << " to double"; - this->sequence.async.enqueue_and_log( function, message.str() ); + logwrite( function, message.str() ); return(ERROR); } catch (std::out_of_range &) { - this->sequence.async.enqueue_and_log( function, "ERROR: TCS_SETTLE_TIMEOUT number out of double range" ); + logwrite( function, "ERROR: TCS_SETTLE_TIMEOUT number out of double range" ); return(ERROR); } this->sequence.tcs_settle_timeout = to; message.str(""); message << "SEQUENCERD:config:" << config.param[entry] << "=" << config.arg[entry]; - this->sequence.async.enqueue_and_log( function, message.str() ); + logwrite( function, message.str() ); applied++; } @@ -484,16 +480,16 @@ namespace Sequencer { } catch (std::invalid_argument &) { message.str(""); message << "ERROR: bad TCS_SETTLE_STABLE: unable to convert " << config.arg[entry] << " to double"; - this->sequence.async.enqueue_and_log( function, message.str() ); + logwrite( function, message.str() ); return(ERROR); } catch (std::out_of_range &) { - this->sequence.async.enqueue_and_log( function, "ERROR: TCS_SETTLE_STABLE number out of double range" ); + logwrite( function, "ERROR: TCS_SETTLE_STABLE number out of double range" ); return(ERROR); } this->sequence.tcs_settle_stable = stablet; message.str(""); message << "SEQUENCERD:config:" << config.param[entry] << "=" << config.arg[entry]; - this->sequence.async.enqueue_and_log( function, message.str() ); + logwrite( function, message.str() ); applied++; } @@ -505,16 +501,16 @@ namespace Sequencer { } catch (std::invalid_argument &) { message.str(""); message << "ERROR: bad TCS_DOMEAZI_READY: unable to convert " << config.arg[entry] << " to double"; - this->sequence.async.enqueue_and_log( function, message.str() ); + logwrite( function, message.str() ); return(ERROR); } catch (std::out_of_range &) { - this->sequence.async.enqueue_and_log( function, "ERROR: TCS_DOMEAZI_READY number out of double range" ); + logwrite( function, "ERROR: TCS_DOMEAZI_READY number out of double range" ); return(ERROR); } this->sequence.tcs_domeazi_ready = domeazi; message.str(""); message << "SEQUENCERD:config:" << config.param[entry] << "=" << config.arg[entry]; - this->sequence.async.enqueue_and_log( function, message.str() ); + logwrite( function, message.str() ); applied++; } @@ -525,16 +521,16 @@ namespace Sequencer { to = std::stod( config.arg[entry] ); } catch (std::invalid_argument &) { - this->sequence.async.enqueue_and_log( function, "ERROR: bad TCS_PREAUTH_TIME: unable to convert to double" ); + logwrite( function, "ERROR: bad TCS_PREAUTH_TIME: unable to convert to double" ); return(ERROR); } catch (std::out_of_range &) { - this->sequence.async.enqueue_and_log( function, "ERROR: TCS_PREAUTH_TIME number out of double range" ); + logwrite( function, "ERROR: TCS_PREAUTH_TIME number out of double range" ); return(ERROR); } this->sequence.tcs_preauth_time = to; message.str(""); message << "SEQUENCERD:config:" << config.param[entry] << "=" << config.arg[entry]; - this->sequence.async.enqueue_and_log( function, message.str() ); + logwrite( function, message.str() ); applied++; } @@ -545,16 +541,16 @@ namespace Sequencer { offset = std::stod( config.arg[entry] ); } catch (std::invalid_argument &) { - this->sequence.async.enqueue_and_log( function, "ERROR: bad ACQUIRE_OFFSET_THRESHOLD: unable to convert to double" ); + logwrite( function, "ERROR: bad ACQUIRE_OFFSET_THRESHOLD: unable to convert to double" ); return(ERROR); } catch (std::out_of_range &) { - this->sequence.async.enqueue_and_log( function, "ERROR: ACQUIRE_OFFSET_THRESHOLD number out of double range" ); + logwrite( function, "ERROR: ACQUIRE_OFFSET_THRESHOLD number out of double range" ); return(ERROR); } this->sequence.target.offset_threshold = offset; message.str(""); message << "SEQUENCERD:config:" << config.param[entry] << "=" << config.arg[entry]; - this->sequence.async.enqueue_and_log( function, message.str() ); + logwrite( function, message.str() ); applied++; } @@ -565,16 +561,16 @@ namespace Sequencer { repeat = std::stoi( config.arg[entry] ); } catch (std::invalid_argument &) { - this->sequence.async.enqueue_and_log( function, "ERROR: bad ACQUIRE_MIN_REPEAT: unable to convert to int" ); + logwrite( function, "ERROR: bad ACQUIRE_MIN_REPEAT: unable to convert to int" ); return(ERROR); } catch (std::out_of_range &) { - this->sequence.async.enqueue_and_log( function, "ERROR: ACQUIRE_MIN_REPEAT number out of int range" ); + logwrite( function, "ERROR: ACQUIRE_MIN_REPEAT number out of int range" ); return(ERROR); } this->sequence.target.min_repeat = repeat; message.str(""); message << "SEQUENCERD:config:" << config.param[entry] << "=" << config.arg[entry]; - this->sequence.async.enqueue_and_log( function, message.str() ); + logwrite( function, message.str() ); applied++; } @@ -585,16 +581,16 @@ namespace Sequencer { offset = std::stod( config.arg[entry] ); } catch (std::invalid_argument &) { - this->sequence.async.enqueue_and_log( function, "ERROR: bad ACQUIRE_TCS_MAX_OFFSET: unable to convert to double" ); + logwrite( function, "ERROR: bad ACQUIRE_TCS_MAX_OFFSET: unable to convert to double" ); return(ERROR); } catch (std::out_of_range &) { - this->sequence.async.enqueue_and_log( function, "ERROR: ACQUIRE_TCS_MAX_OFFSET number out of double range" ); + logwrite( function, "ERROR: ACQUIRE_TCS_MAX_OFFSET number out of double range" ); return(ERROR); } this->sequence.target.max_tcs_offset = offset; message.str(""); message << "SEQUENCERD:config:" << config.param[entry] << "=" << config.arg[entry]; - this->sequence.async.enqueue_and_log( function, message.str() ); + logwrite( function, message.str() ); applied++; } @@ -609,7 +605,7 @@ namespace Sequencer { if ( this->sequence.target.configure_db( DB_HOST, config.arg[entry] ) == NO_ERROR ) { applied++; message.str(""); message << "SEQUENCERD:config:" << config.param[entry] << "=" << config.arg[entry]; - this->sequence.async.enqueue_and_log( function, message.str() ); + logwrite( function, message.str() ); } } @@ -618,7 +614,7 @@ namespace Sequencer { if ( this->sequence.target.configure_db( DB_PORT, config.arg[entry] ) == NO_ERROR ) { applied++; message.str(""); message << "SEQUENCERD:config:" << config.param[entry] << "=" << config.arg[entry]; - this->sequence.async.enqueue_and_log( function, message.str() ); + logwrite( function, message.str() ); } } @@ -627,7 +623,7 @@ namespace Sequencer { if ( this->sequence.target.configure_db( DB_USER, config.arg[entry] ) == NO_ERROR ) { applied++; message.str(""); message << "SEQUENCERD:config:" << config.param[entry] << "=" << config.arg[entry]; - this->sequence.async.enqueue_and_log( function, message.str() ); + logwrite( function, message.str() ); } } @@ -636,7 +632,7 @@ namespace Sequencer { if ( this->sequence.target.configure_db( DB_PASS, config.arg[entry] ) == NO_ERROR ) { applied++; message.str(""); message << "SEQUENCERD:config:" << config.param[entry] << "=" << config.arg[entry]; - this->sequence.async.enqueue_and_log( function, message.str() ); + logwrite( function, message.str() ); } } @@ -645,7 +641,7 @@ namespace Sequencer { if ( this->sequence.target.configure_db( DB_SCHEMA, config.arg[entry] ) == NO_ERROR ) { applied++; message.str(""); message << "SEQUENCERD:config:" << config.param[entry] << "=" << config.arg[entry]; - this->sequence.async.enqueue_and_log( function, message.str() ); + logwrite( function, message.str() ); } } @@ -654,7 +650,7 @@ namespace Sequencer { if ( this->sequence.target.configure_db( DB_ACTIVE, config.arg[entry] ) == NO_ERROR ) { applied++; message.str(""); message << "SEQUENCERD:config:" << config.param[entry] << "=" << config.arg[entry]; - this->sequence.async.enqueue_and_log( function, message.str() ); + logwrite( function, message.str() ); } } @@ -663,7 +659,7 @@ namespace Sequencer { if ( this->sequence.target.configure_db( DB_COMPLETED, config.arg[entry] ) == NO_ERROR ) { applied++; message.str(""); message << "SEQUENCERD:config:" << config.param[entry] << "=" << config.arg[entry]; - this->sequence.async.enqueue_and_log( function, message.str() ); + logwrite( function, message.str() ); } } @@ -672,7 +668,7 @@ namespace Sequencer { if ( this->sequence.target.configure_db( DB_SETS, config.arg[entry] ) == NO_ERROR ) { applied++; message.str(""); message << "SEQUENCERD:config:" << config.param[entry] << "=" << config.arg[entry]; - this->sequence.async.enqueue_and_log( function, message.str() ); + logwrite( function, message.str() ); } } @@ -681,7 +677,7 @@ namespace Sequencer { this->sequence.camera_prologue.push_back( this->config.arg[entry] ); applied++; message.str(""); message << "SEQUENCERD:config:" << config.param[entry] << "=" << config.arg[entry]; - this->sequence.async.enqueue_and_log( function, message.str() ); + logwrite( function, message.str() ); } // CAMERA_EPILOGUE @@ -689,7 +685,7 @@ namespace Sequencer { this->sequence.camera_epilogue.push_back( this->config.arg[entry] ); applied++; message.str(""); message << "SEQUENCERD:config:" << config.param[entry] << "=" << config.arg[entry]; - this->sequence.async.enqueue_and_log( function, message.str() ); + logwrite( function, message.str() ); } // *__INIT @@ -701,7 +697,7 @@ namespace Sequencer { this->sequence.config_init[key] = this->config.arg[entry]; applied++; message.str(""); message << "SEQUENCERD:config:" << config.param[entry] << "=" << config.arg[entry]; - this->sequence.async.enqueue_and_log( function, message.str() ); + logwrite( function, message.str() ); } // *__SHUTDOWN @@ -713,7 +709,7 @@ namespace Sequencer { this->sequence.config_shutdown[key] = this->config.arg[entry]; applied++; message.str(""); message << "SEQUENCERD:config:" << config.param[entry] << "=" << config.arg[entry]; - this->sequence.async.enqueue_and_log( function, message.str() ); + logwrite( function, message.str() ); } // VIRTUAL_SLITW_ACQUIRE @@ -755,7 +751,7 @@ namespace Sequencer { if ( this->sequence.power_switch[POWER_LAMP].configure( this->config.arg[entry] ) == NO_ERROR ) { applied++; message.str(""); message << "SEQUENCERD:config:" << config.param[entry] << "=" << config.arg[entry]; - this->sequence.async.enqueue_and_log( function, message.str() ); + logwrite( function, message.str() ); } } @@ -764,7 +760,7 @@ namespace Sequencer { if ( this->sequence.power_switch[POWER_SLIT].configure( this->config.arg[entry] ) == NO_ERROR ) { applied++; message.str(""); message << "SEQUENCERD:config:" << config.param[entry] << "=" << config.arg[entry]; - this->sequence.async.enqueue_and_log( function, message.str() ); + logwrite( function, message.str() ); } } @@ -773,7 +769,7 @@ namespace Sequencer { if ( this->sequence.power_switch[POWER_CAMERA].configure( this->config.arg[entry] ) == NO_ERROR ) { applied++; message.str(""); message << "SEQUENCERD:config:" << config.param[entry] << "=" << config.arg[entry]; - this->sequence.async.enqueue_and_log( function, message.str() ); + logwrite( function, message.str() ); } } @@ -782,7 +778,7 @@ namespace Sequencer { if ( this->sequence.power_switch[POWER_CALIB].configure( this->config.arg[entry] ) == NO_ERROR ) { applied++; message.str(""); message << "SEQUENCERD:config:" << config.param[entry] << "=" << config.arg[entry]; - this->sequence.async.enqueue_and_log( function, message.str() ); + logwrite( function, message.str() ); } } @@ -791,7 +787,7 @@ namespace Sequencer { if ( this->sequence.power_switch[POWER_FLEXURE].configure( this->config.arg[entry] ) == NO_ERROR ) { applied++; message.str(""); message << "SEQUENCERD:config:" << config.param[entry] << "=" << config.arg[entry]; - this->sequence.async.enqueue_and_log( function, message.str() ); + logwrite( function, message.str() ); } } @@ -800,7 +796,7 @@ namespace Sequencer { if ( this->sequence.power_switch[POWER_FILTER].configure( this->config.arg[entry] ) == NO_ERROR ) { applied++; message.str(""); message << "SEQUENCERD:config:" << config.param[entry] << "=" << config.arg[entry]; - this->sequence.async.enqueue_and_log( function, message.str() ); + logwrite( function, message.str() ); } } @@ -809,7 +805,7 @@ namespace Sequencer { if ( this->sequence.power_switch[POWER_FOCUS].configure( this->config.arg[entry] ) == NO_ERROR ) { applied++; message.str(""); message << "SEQUENCERD:config:" << config.param[entry] << "=" << config.arg[entry]; - this->sequence.async.enqueue_and_log( function, message.str() ); + logwrite( function, message.str() ); } } @@ -818,7 +814,7 @@ namespace Sequencer { if ( this->sequence.power_switch[POWER_TELEM].configure( this->config.arg[entry] ) == NO_ERROR ) { applied++; message.str(""); message << "SEQUENCERD:config:" << config.param[entry] << "=" << config.arg[entry]; - this->sequence.async.enqueue_and_log( function, message.str() ); + logwrite( function, message.str() ); } } @@ -827,7 +823,7 @@ namespace Sequencer { if ( this->sequence.power_switch[POWER_THERMAL].configure( this->config.arg[entry] ) == NO_ERROR ) { applied++; message.str(""); message << "SEQUENCERD:config:" << config.param[entry] << "=" << config.arg[entry]; - this->sequence.async.enqueue_and_log( function, message.str() ); + logwrite( function, message.str() ); } } @@ -836,7 +832,7 @@ namespace Sequencer { if ( this->sequence.power_switch[POWER_ACAM].configure( this->config.arg[entry] ) == NO_ERROR ) { applied++; message.str(""); message << "SEQUENCERD:config:" << config.param[entry] << "=" << config.arg[entry]; - this->sequence.async.enqueue_and_log( function, message.str() ); + logwrite( function, message.str() ); } } @@ -845,7 +841,7 @@ namespace Sequencer { if ( this->sequence.power_switch[POWER_ACAM_CAM].configure( this->config.arg[entry] ) == NO_ERROR ) { applied++; message.str(""); message << "SEQUENCERD:config:" << config.param[entry] << "=" << config.arg[entry]; - this->sequence.async.enqueue_and_log( function, message.str() ); + logwrite( function, message.str() ); } } @@ -854,7 +850,7 @@ namespace Sequencer { if ( this->sequence.power_switch[POWER_SLICECAM].configure( this->config.arg[entry] ) == NO_ERROR ) { applied++; message.str(""); message << "SEQUENCERD:config:" << config.param[entry] << "=" << config.arg[entry]; - this->sequence.async.enqueue_and_log( function, message.str() ); + logwrite( function, message.str() ); } } @@ -863,33 +859,8 @@ namespace Sequencer { if ( this->sequence.caltarget.configure( config.arg[entry] ) == NO_ERROR ) { applied++; message.str(""); message << "SEQUENCERD:config:" << config.param[entry] << "=" << config.arg[entry]; - this->sequence.async.enqueue_and_log( function, message.str() ); - } - } - - // TELEM_PROVIDER : contains daemon name and port to contact for header telemetry info - // - if ( config.param[entry] == "TELEM_PROVIDER" ) { - std::vector tokens; - Tokenize( config.arg[entry], tokens, " " ); - try { - if ( tokens.size() == 2 ) { - this->sequence.telemetry_providers[tokens.at(0)] = std::stod(tokens.at(1)); - } - else { - message.str(""); message << "ERROR bad format TELEM_PROVIDER=\"" << config.arg[entry] << "\": expected "; - logwrite( function, message.str() ); - return ERROR; - } - } - catch ( const std::exception &e ) { - message.str(""); message << "ERROR parsing TELEM_PROVIDER from " << config.arg[entry] << ": " << e.what(); logwrite( function, message.str() ); - return ERROR; } - message.str(""); message << "config:" << config.param[entry] << "=" << config.arg[entry]; - this->sequence.async.enqueue_and_log( "SEQUENCERD", function, message.str() ); - applied++; } } // end loop through the entries in the configuration file @@ -1018,49 +989,6 @@ namespace Sequencer { /**** Server::gui_main ******************************************************/ - /***** Server::async_main ***************************************************/ - /** - * @brief asynchronous message sending thread - * @param[in] seq reference to Sequencer::Server object - * @param[in] sock Network::udpSocket socket object - * - * Loops forever, when a message arrives in the status message queue it is - * sent out via multi-cast UDP datagram. - * - */ - void Server::async_main( Sequencer::Server &seq, Network::UdpSocket sock ) { - std::string function = "Sequencer::Server::async_main"; - std::stringstream message; - int retval; - - retval = sock.Create(); // create the UDP socket - if (retval < 0) { - logwrite(function, "error creating UDP multicast socket for asynchronous messages"); - seq.exit_cleanly(); // do not continue on error - } - if (retval==1) { // exit this thread but continue with daemon - logwrite(function, "asyncrhonous message port disabled by request"); - } - - while (1) { - std::string message = seq.sequence.async.dequeue(); // get the latest message from the queue (blocks) - retval = sock.Send(message); // transmit the message - if (retval < 0) { - std::stringstream errstm; - errstm << "error sending UDP message: " << message; - logwrite(function, errstm.str()); - } - if (message=="exit") { // terminate this thread - sock.Close(); - return; - } - } - - return; - } - /***** Server::async_main ***************************************************/ - - /***** Server::doit *********************************************************/ /** * @brief the workhorse of each thread connetion @@ -1096,11 +1024,11 @@ namespace Sequencer { if ( ( pollret=sock.Poll() ) <= 0 ) { if (pollret==0) { message.str(""); message << "ERROR: Poll timeout on fd " << sock.getfd() << " thread " << sock.id; - this->sequence.async.enqueue_and_log( function, message.str() ); + logwrite( function, message.str() ); } if ( pollret <0 && errno ) { message.str(""); message << "ERROR: Poll error on fd " << sock.getfd() << " thread " << sock.id << ": " << strerror(errno); - this->sequence.async.enqueue_and_log( function, message.str() ); + logwrite( function, message.str() ); } break; // this will close the connection } @@ -1116,11 +1044,11 @@ namespace Sequencer { #endif if ( ret<0 && errno ) { // could be an actual read error message.str(""); message << "ERROR: Read error on fd " << sock.getfd() << ": " << strerror(errno); - this->sequence.async.enqueue_and_log( function, message.str() ); + logwrite( function, message.str() ); } if (ret==-2) { // or a timeout message.str(""); message << "ERROR: timeout reading from fd " << sock.getfd(); - this->sequence.async.enqueue_and_log( function, message.str() ); + logwrite( function, message.str() ); } break; // Breaking out of the while loop will close the connection. // This probably means that the client has terminated abruptly, @@ -1159,12 +1087,12 @@ namespace Sequencer { catch ( std::runtime_error &e ) { std::stringstream errstream; errstream << e.what(); message.str(""); message << "ERROR: parsing arguments: " << errstream.str(); - this->sequence.async.enqueue_and_log( function, message.str() ); + logwrite( function, message.str() ); ret = -1; } catch ( ... ) { message.str(""); message << "ERROR: unknown error parsing arguments: " << args; - this->sequence.async.enqueue_and_log( function, message.str() ); + logwrite( function, message.str() ); ret = -1; } @@ -1186,6 +1114,10 @@ namespace Sequencer { this->exit_cleanly(); // shutdown the sequencer } else + if ( cmd == SEQUENCERD_FINEACQUIRE ) { + this->sequence.fine_acquire(args, retstring); + } + else // These commands go to acamd // @@ -1376,6 +1308,22 @@ namespace Sequencer { } else + // handle incoming CLI operation request + // + if ( cmd == SEQUENCERD_OP ) { + std::thread( &Sequencer::Sequence::handle_cli_operation, std::ref(this->sequence), args ).detach(); + ret = NO_ERROR; + } + else + + // run sequencer script + // + if ( cmd == SEQUENCERD_SCRIPT ) { + std::thread( &Sequencer::Sequence::run_script, std::ref(this->sequence), args ).detach(); + ret = NO_ERROR; + } + else + // Sequence "start" // if ( cmd == SEQUENCERD_START ) { @@ -1397,7 +1345,7 @@ namespace Sequencer { // if ( cmd == SEQUENCERD_REPEAT ) { if ( !this->sequence.seq_state_manager.is_set( Sequencer::SEQ_READY ) ) { - this->sequence.async.enqueue_and_log( function, "ERROR cannot start exposure: not ready" ); + this->sequence.broadcast.error( function, "cannot start exposure: not ready" ); ret = ERROR; } else { @@ -1468,6 +1416,17 @@ namespace Sequencer { } else + // Set/Get Engineering mode. + // When enabled, validate_sequence allows single-subsystem operation + // (per-daemon checks instead of SEQ_READY) and permits parallel-unsafe + // operation pairs in SERIAL groups. + // + if ( cmd == SEQUENCERD_ENGINEERING ) { + ret = this->sequence.engineering( args, retstring ); + retstring.append( " " ); + } + else + // Report the Sequencer State, // which will be returned, logged, and written to the async message port. // @@ -1493,7 +1452,7 @@ namespace Sequencer { if ( cmd == SEQUENCERD_TARGETSET ) { ret= this->sequence.target.targetset( args, retstring ); message.str(""); message << "TARGETSET: " << retstring; - this->sequence.async.enqueue( message.str() ); + logwrite(function, message.str()); retstring.append( " " ); } else @@ -1503,15 +1462,15 @@ namespace Sequencer { if ( cmd == SEQUENCERD_PAUSE ) { // Can only pause during an exposure // - if ( ! this->sequence.seq_state_manager.is_set( Sequencer::SEQ_WAIT_EXPOSE ) ) { - this->sequence.async.enqueue_and_log( function, "ERROR: can only pause during an active exposure" ); + if ( ! this->sequence.wait_state_manager.is_set( Sequencer::SEQ_WAIT_EXPOSE ) ) { + this->sequence.broadcast.error( function, "can only pause during an active exposure" ); ret = ERROR; } else // Can't already be paused // if ( this->sequence.seq_state_manager.is_set( Sequencer::SEQ_PAUSED ) ) { - this->sequence.async.enqueue_and_log( function, "ERROR: already paused" ); + this->sequence.broadcast.error( function, "already paused" ); ret = ERROR; } else { @@ -1530,7 +1489,7 @@ namespace Sequencer { // Can only resume when paused // if ( ! this->sequence.seq_state_manager.is_set( Sequencer::SEQ_PAUSED ) ) { - this->sequence.async.enqueue_and_log( function, "ERROR: can only resume when paused" ); + this->sequence.broadcast.error( function, "can only resume when paused" ); ret = ERROR; } else { @@ -1557,7 +1516,7 @@ namespace Sequencer { if ( cmd.compare( SEQUENCERD_MODEXPTIME ) == 0 ) { Tokenize( args, tokens, " " ); if ( tokens.size() != 1 ) { - this->sequence.async.enqueue_and_log( function, "ERROR: expected MODEXPTIME " ); + logwrite( function, "ERROR expected MODEXPTIME " ); ret = ERROR; } else { @@ -1566,13 +1525,13 @@ namespace Sequencer { double exptime_req=0; try { exptime_req = std::stod( tokens.at(0) ); } catch( std::out_of_range &e ) { - message.str(""); message << "ERROR: out of range parsing args " << args << ": " << e.what(); - this->sequence.async.enqueue_and_log( function, message.str() ); + message.str(""); message << "ERROR out of range parsing args " << args << ": " << e.what(); + logwrite( function, message.str() ); ret = ERROR; } catch( std::invalid_argument &e ) { - message.str(""); message << "ERROR: invalid argument parsing args " << args << ": " << e.what(); - this->sequence.async.enqueue_and_log( function, message.str() ); + message.str(""); message << "ERROR invalid argument parsing args " << args << ": " << e.what(); + logwrite( function, message.str() ); ret = ERROR; } @@ -1612,8 +1571,8 @@ namespace Sequencer { // Unknown commands generate an error // else { - message.str(""); message << "ERROR: unknown command: " << cmd; - this->sequence.async.enqueue_and_log( function, message.str() ); + message.str(""); message << "ERROR unknown command: " << cmd; + logwrite( function, message.str() ); ret = ERROR; } diff --git a/sequencerd/sequencer_server.h b/sequencerd/sequencer_server.h index f785d42d..f24bdc6f 100644 --- a/sequencerd/sequencer_server.h +++ b/sequencerd/sequencer_server.h @@ -82,7 +82,7 @@ namespace Sequencer { // are initialized here. The names are useful just for logging. // this->sequence.calibd.name = "calibd"; - this->sequence.camerad.name = "camerad"; +// this->sequence.camerad.name = "camerad"; this->sequence.filterd.name = "filterd"; this->sequence.flexured.name = "flexured"; this->sequence.focusd.name = "focusd"; @@ -145,7 +145,6 @@ namespace Sequencer { static void block_main( Sequencer::Server &server, std::shared_ptr ); ///< main function for blocking connection thread static void thread_main( Sequencer::Server &server, std::shared_ptr sock); ///< main function for all non-blocked threads static void gui_main( Sequencer::Server &server, std::shared_ptr sock ); ///< main function for gui threads - static void async_main( Sequencer::Server &server, Network::UdpSocket sock ); ///< asynchronous message sending thread void handle_signal( int signo ); diff --git a/sequencerd/sequencerd.cpp b/sequencerd/sequencerd.cpp index 3b9ba63f..4e13530e 100644 --- a/sequencerd/sequencerd.cpp +++ b/sequencerd/sequencerd.cpp @@ -131,14 +131,18 @@ int main(int argc, char **argv) { // if ( sequencerd.sequence.init_pubsub( { Topic::CAMERAD, Topic::ACAMD, - Topic::SLICECAMD } ) == ERROR ) { + Topic::SLICECAMD, + Topic::SLITD, + Topic::TCSD } ) == ERROR ) { 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) ); + std::this_thread::sleep_for( std::chrono::milliseconds(250) ); sequencerd.sequence.publish_snapshot(); + std::this_thread::sleep_for( std::chrono::milliseconds(250) ); + sequencerd.sequence.request_snapshot(); // 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. @@ -220,16 +224,10 @@ int main(int argc, char **argv) { std::ref(socklist[thrid]) ).detach(); // spawn a thread to handle requests on this socket *****/ - // Instantiate a multicast UDP object and spawn a thread to send asynchronous messages - // - Network::UdpSocket msg(sequencerd.messageport, sequencerd.messagegroup); - std::thread( std::ref(Sequencer::Server::async_main), - std::ref(sequencerd), - msg ).detach(); - // Create my own asynchronous listener thread. // This thread allows the sequencer to listen for asynchronous messages. // + Network::UdpSocket msg(sequencerd.messageport, sequencerd.messagegroup); std::thread( std::ref( Sequencer::Sequence::dothread_sequencer_async_listener ), std::ref( sequencerd.sequence), msg diff --git a/slicecamd/CMakeLists.txt b/slicecamd/CMakeLists.txt index 42969138..7a6ec2c8 100644 --- a/slicecamd/CMakeLists.txt +++ b/slicecamd/CMakeLists.txt @@ -40,21 +40,20 @@ add_executable(slicecamd ${SLICECAMD_DIR}/slicecam_camera.cpp ${SLICECAMD_DIR}/slicecam_fits.cpp ${SLICECAMD_DIR}/slicecam_math.cpp - ${PYTHON_DEV} ) target_link_libraries(slicecamd andor - python3.9 + ${PYTHON_LIB} network common tcsd_client logentry utilities + md5 skyinfo ${ZMQPP_LIB} ${ZMQ_LIB} - ${PYTHON_LIB} ${CCFITS_LIB} ${CFITS_LIB} ${CMAKE_THREAD_LIBS_INIT} diff --git a/slicecamd/guimanager.h b/slicecamd/guimanager.h index 2758fe41..d3856bd3 100644 --- a/slicecamd/guimanager.h +++ b/slicecamd/guimanager.h @@ -105,7 +105,7 @@ namespace Slicecam { * @details the script pushes the indicated file to the Guider GUI display * @param[in] filename fits file to send */ - void push_gui_image( std::string_view filename ) { + void push_gui_image( std::string filename ) { const std::string function("Slicecam::GUIManager::push_gui_image"); std::ostringstream cmd; cmd << push_image << " " diff --git a/slicecamd/slicecam_interface.cpp b/slicecamd/slicecam_interface.cpp index fca6d53f..1c57175c 100644 --- a/slicecamd/slicecam_interface.cpp +++ b/slicecamd/slicecam_interface.cpp @@ -10,6 +10,7 @@ #include "slicecam_interface.h" #include "slicecam_math.h" +#include "fits_header_defs.h" namespace Slicecam { @@ -54,21 +55,18 @@ namespace Slicecam { // stop fine acquisition if (action=="stop") { - if (!this->is_fineacquire_running.load(std::memory_order_acquire)) { - logwrite(function, "stopped"); - } - else { - this->is_fineacquire_running.store(false); - this->publish_status(); - logwrite(function, "stop requested"); - } + const bool was_running = this->is_fineacquire_running.load(std::memory_order_acquire); + this->is_fineacquire_locked.store(false, std::memory_order_release); + this->is_fineacquire_running.store(false, std::memory_order_release); + this->publish_status(); + logwrite(function, was_running ? "stop requested" : "stopped"); retstring=this->is_fineacquire_running.load(std::memory_order_acquire)?"running":"stopped"; return NO_ERROR; } else // not empty, stop or start is an error - if (action != "start" || tokens.size() < 2) { + if (action != "start") { logwrite(function, "ERROR expected stop | start [ { L | R } ]"); retstring="invalid_argument"; return ERROR; @@ -90,28 +88,57 @@ namespace Slicecam { return ERROR; } - // ACAM must be guiding - if (!this->is_acam_guiding.load(std::memory_order_acquire)) { - logwrite(function, "ERROR ACAM is not guiding"); - retstring="stopped"; + // Wait briefly for ACAM status to be fresh and guiding. This absorbs the + // pub/sub propagation lag where sequencerd has seen the ACAM acquire + // publish but slicecamd's subscriber thread has not yet delivered it. + { + const auto wait_duration = std::chrono::microseconds( ACAM_WAIT_TIMEOUT_US ); + std::unique_lock lock(this->acam_mtx); + const bool ready = this->acam_cv.wait_for( lock, wait_duration, [this]() { + return this->is_acam_status_fresh() && + this->is_acam_guiding.load(std::memory_order_relaxed); + }); + + if ( !ready ) { + if ( !this->is_acam_status_fresh() ) { + const int64_t age_ms = ( get_time_us() - this->last_acam_pubtime.load() ) / 1000; + message.str(""); message << "ERROR timed out waiting for fresh ACAM status (age=" + << age_ms << " ms)"; + } + else { + message.str(""); message << "ERROR timed out waiting for ACAM to guide"; + } + logwrite( function, message.str() ); + retstring = "stopped"; return ERROR; } + } // are optional but if specified then require all three - if (tokens.size() > 1 && tokens.size() != 4) { - logwrite(function, "ERROR ACAM is not guiding"); - retstring="stopped"; + if ( tokens.size() != 1 && tokens.size() != 4 ) { + logwrite(function, "ERROR expected stop | start [ { L | R } ]"); + retstring="invalid_argument"; return ERROR; } - else - // override the default camera and aimpoint if provided - if (tokens.size() > 1 && tokens.size() == 4) { + + if ( tokens.size() == 1 ) { + // no which/aimpoint args so use defaults from config file + if ( this->default_which.empty() || !this->default_aimpoint.is_valid() ) { + logwrite(function, "ERROR fineacquire defaults not configured"); + retstring="not_configured"; + return ERROR; + } + this->fineacquire_state.which = this->default_which; + this->fineacquire_state.aimpoint = this->default_aimpoint; + } + else { + // use the passed-in values try { const std::string which = tokens.at(1); - double x = std::stod(tokens.at(2)); - double y = std::stod(tokens.at(3)); + const double x = std::stod(tokens.at(2)); + const double y = std::stod(tokens.at(3)); if ( (which != "L" && which != "R") || - std::isnan(x) || std::isnan(y) || x<0 || y<0) { + std::isnan(x) || std::isnan(y) || x<0 || y<0 ) { logwrite(function, "ERROR expected stop | start [ { L | R } ]"); retstring="invalid_argument"; return ERROR; @@ -126,6 +153,12 @@ namespace Slicecam { } } + // log the effective camera and aimpoint for this invocation + message.str(""); message << "using camera=" << this->fineacquire_state.which + << " aimpoint=(" << this->fineacquire_state.aimpoint.x + << ", " << this->fineacquire_state.aimpoint.y << ")"; + logwrite(function, message.str()); + // start the state machine this->fineacquire_state.reset(); this->is_fineacquire_locked.store(false, std::memory_order_release); @@ -134,10 +167,10 @@ namespace Slicecam { // publishes status on change only this->publish_status(); - logwrite(function, "fine target acquisition enabled"); - retstring=is_fineacquire_running.load(std::memory_order_acquire)?"running":"stopped"; + logwrite(function, "fine target acquisition "+retstring); + return NO_ERROR; } /***** Slicecam::Interface::fineacquire *************************************/ @@ -158,8 +191,8 @@ namespace Slicecam { // skip frames if we are waiting for the telescope to settle after a move // - if (this->fineacquire_state.skip_frames > 0) { - this->fineacquire_state.skip_frames--; + if (this->fineacquire_state.settle_frames > 0) { + this->fineacquire_state.settle_frames--; return; } @@ -197,9 +230,17 @@ namespace Slicecam { this->fineacquire_state.bg_region, this->fineacquire_state.aimpoint, centroid) != NO_ERROR ) { - logwrite(function, "WARNING: failed to find centroid, skipping frame"); + const int max_failures = 3 * this->fineacquire_state.max_samples; + if ( ++this->fineacquire_state.consecutive_centroid_failures >= max_failures ) { + logwrite(function, "ERROR: too many consecutive centroid failures, stopping fine acquisition"); + this->is_fineacquire_running.store( false, std::memory_order_release ); + this->publish_status(); + } else { + logwrite(function, "WARNING: failed to find centroid, skipping frame"); + } return; } + this->fineacquire_state.consecutive_centroid_failures = 0; // convert centroid pixel -> sky using WCS from FITS header // @@ -236,23 +277,44 @@ namespace Slicecam { this->fineacquire_state.dra_samp.push_back( offsets.first ); this->fineacquire_state.ddec_samp.push_back( offsets.second ); - // wait until there are enough samples to evaluate a move - // + const int n = static_cast( this->fineacquire_state.dra_samp.size() ); const int max_samples = this->fineacquire_state.max_samples; - if ( static_cast(this->fineacquire_state.dra_samp.size()) < max_samples ) { - return; - } + const int min_samples = this->fineacquire_state.min_samples; + + // wait for the minimum number of samples before evaluating anything + // + if ( n < min_samples ) return; - // calculate median from accumulated samples + // compute running median // - std::vector dra_sorted = this->fineacquire_state.dra_samp; + std::vector dra_sorted = this->fineacquire_state.dra_samp; std::vector ddec_sorted = this->fineacquire_state.ddec_samp; std::sort( dra_sorted.begin(), dra_sorted.end() ); std::sort( ddec_sorted.begin(), ddec_sorted.end() ); - const double med_dra = dra_sorted[ max_samples / 2 ]; - const double med_ddec = ddec_sorted[ max_samples / 2 ]; + const double med_dra = dra_sorted[ n / 2 ]; + const double med_ddec = ddec_sorted[ n / 2 ]; + + // compute MAD-derived scatter per axis to gate early exit + // + std::vector abs_dra( n ), abs_ddec( n ); + for ( int i = 0; i < n; i++ ) { + abs_dra[i] = std::abs( this->fineacquire_state.dra_samp[i] - med_dra ); + abs_ddec[i] = std::abs( this->fineacquire_state.ddec_samp[i] - med_ddec ); + } + std::sort( abs_dra.begin(), abs_dra.end() ); + std::sort( abs_ddec.begin(), abs_ddec.end() ); + + const double sig_dra = 1.4826 * abs_dra[ n / 2 ] * 3600.0; // arcsec + const double sig_ddec = 1.4826 * abs_ddec[ n / 2 ] * 3600.0; // arcsec + + const double prec = this->fineacquire_state.prec_arcsec; + const bool scatter_ok = ( sig_dra <= prec && sig_ddec <= prec ); + + // keep accumulating if scatter is still too high and max not reached + // + if ( !scatter_ok && n < max_samples ) return; // convert to arcsec only for the threshold comparison and logging // @@ -269,17 +331,26 @@ namespace Slicecam { return; } + // select gain: use gain_large when offset is well above the goal threshold + // + const double effective_gain = ( offset_arcsec > this->fineacquire_state.gain_threshold_arcsec ) + ? this->fineacquire_state.gain_large + : this->fineacquire_state.gain; + // send gain-weighted offsets to acam // std::ostringstream oss; oss << "offset dRA=" << med_dra * 3600.0 << " dDEC=" << med_ddec * 3600.0 << " arcsec (r=" << offset_arcsec - << " arcsec) -- applying correction"; + << " arcsec, n=" << n + << " scatter=(" << sig_dra << "," << sig_ddec << ") arcsec)" + << " gain=" << effective_gain + << " -- applying correction"; logwrite( function, oss.str() ); - const double cmd_dra = this->fineacquire_state.gain * med_dra; - const double cmd_ddec = this->fineacquire_state.gain * med_ddec; + const double cmd_dra = effective_gain * med_dra; + const double cmd_ddec = effective_gain * med_ddec; if ( this->offset_acam_goal( { cmd_dra, cmd_ddec }, true ) != NO_ERROR ) { logwrite( function, "ERROR failed to send offset to ACAM" ); @@ -288,9 +359,9 @@ namespace Slicecam { return; } - // reset samples and skip a couple of frames for telescope settling + // reset samples and discard settle_count frames for telescope settling this->fineacquire_state.reset(); - this->fineacquire_state.skip_frames = 2; + this->fineacquire_state.settle_frames = this->fineacquire_state.settle_count; } /***** Slicecam::Interface::do_fineacquire **********************************/ @@ -421,10 +492,34 @@ namespace Slicecam { bool acquired; Common::extract_telemetry_value( jmessage, Key::Acamd::IS_ACQUIRED, acquired ); this->is_acam_guiding.store(acquired, std::memory_order_relaxed); + + // acam's publish time + int64_t pubtime=0; + Common::extract_telemetry_value( jmessage, Key::PUBTIME, pubtime ); + this->last_acam_pubtime.store( pubtime, std::memory_order_relaxed ); + + // wake any thread waiting on ACAM state (e.g. fineacquire) + std::lock_guard lock(this->acam_mtx); + this->acam_cv.notify_all(); } /***** Slicecam::Interface::handletopic_acamd *******************************/ + /***** Slicecam::Interface::is_acam_status_fresh ****************************/ + /** + * @brief check whether cached ACAM status is fresh enough to trust + * @return true if cached status has been received at least once and is + * within ACAM_STATUS_MAX_AGE_US of the current time + * + */ + bool Interface::is_acam_status_fresh() const { + const int64_t last = this->last_acam_pubtime.load(); + if ( last == 0 ) return false; + return ( get_time_us() - last ) <= ACAM_STATUS_MAX_AGE_US; + } + /***** Slicecam::Interface::is_acam_status_fresh ****************************/ + + /***** Slicecam::Interface::handletopic_slitd *******************************/ /** * @brief what to do when the topic is Topic::SLITD @@ -437,11 +532,13 @@ namespace Slicecam { std::lock_guard lock(snapshot_mtx); snapshot_status[Topic::SLITD]=true; } - Common::extract_telemetry_value( jmessage, "SLITO", telem.slitoffset ); - Common::extract_telemetry_value( jmessage, "SLITW", telem.slitwidth ); + Common::extract_telemetry_value( jmessage, Key::Slitd::SLITO, telem.slitoffset ); + Common::extract_telemetry_value( jmessage, Key::Slitd::SLITW, telem.slitwidth ); - 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); + for ( const auto &keyinfo : FitsHeaderKeys::SlitdTelemKeys ) { + this->telemkeys.add_json_key(jmessage, keyinfo.jkey, keyinfo.keyword, + keyinfo.comment, keyinfo.type, false); + } } /***** Slicecam::Interface::handletopic_slitd *******************************/ @@ -462,16 +559,16 @@ namespace Slicecam { // 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, Key::Tcsd::CASANGLE, telem.angle_scope ); + Common::extract_telemetry_value( jmessage, Key::Tcsd::TELRA, telem.ra_scope_hms ); + Common::extract_telemetry_value( jmessage, Key::Tcsd::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, Key::Tcsd::AZ, telem.az ); Common::extract_telemetry_value( jmessage, "TELFOCUS", telem.telfocus ); - Common::extract_telemetry_value( jmessage, "AIRMASS", telem.airmass ); + Common::extract_telemetry_value( jmessage, Key::Tcsd::AIRMASS, telem.airmass ); } /***** Slicecam::Interface::handletopic_tcsd ********************************/ @@ -754,7 +851,7 @@ namespace Slicecam { applied++; } else - if ( config.param[entry] == "FINE_ACQUIRE_AIMPOINT=" ) { + if ( config.param[entry] == "FINE_ACQUIRE_AIMPOINT" ) { std::string which; double x,y; std::istringstream iss(config.arg[entry]); @@ -762,11 +859,18 @@ namespace Slicecam { logwrite(function, "ERROR invalid FINE_ACQUIRE_AIMPOINT='"+config.arg[entry]+"' expected "); return ERROR; } + if ( which != "L" && which != "R" ) { + message.str(""); message << "ERROR invalid FINE_ACQUIRE_AIMPOINT which=\"" << which + << "\" : expected { L R }"; + logwrite( function, message.str() ); + return ERROR; + } + // store in the class for validation this->fineacquire_state.which = which; this->fineacquire_state.aimpoint = { x, y }; } else - if ( config.param[entry] == "FINE_ACQUIRE_BACKGROUND=" ) { + if ( config.param[entry] == "FINE_ACQUIRE_BACKGROUND" ) { long x1, x2, y1, y2; std::istringstream iss(config.arg[entry]); if (!(iss >> x1 >> x2 >> y1 >> y2)) { @@ -775,12 +879,82 @@ namespace Slicecam { } this->fineacquire_state.bg_region = { x1, x2, y1, y2 }; } + else + if ( config.param[entry] == "FINE_ACQUIRE_MIN_SAMPLES" ) { + try { this->fineacquire_state.min_samples = std::stoi( config.arg[entry] ); } + catch ( const std::exception &e ) { + message.str(""); message << "ERROR invalid FINE_ACQUIRE_MIN_SAMPLES " + << config.arg[entry] << ": " << e.what(); + logwrite( function, message.str() ); + return ERROR; + } + message.str(""); message << "SLICECAMD:config:" << config.param[entry] << "=" << config.arg[entry]; + logwrite( function, message.str() ); + applied++; + } + else + if ( config.param[entry] == "FINE_ACQUIRE_SETTLE_FRAMES" ) { + try { this->fineacquire_state.settle_count = std::stoi( config.arg[entry] ); } + catch ( const std::exception &e ) { + message.str(""); message << "ERROR invalid FINE_ACQUIRE_SETTLE_FRAMES " + << config.arg[entry] << ": " << e.what(); + logwrite( function, message.str() ); + return ERROR; + } + message.str(""); message << "SLICECAMD:config:" << config.param[entry] << "=" << config.arg[entry]; + logwrite( function, message.str() ); + applied++; + } + else + if ( config.param[entry] == "FINE_ACQUIRE_GAIN" ) { + try { this->fineacquire_state.gain = std::stod( config.arg[entry] ); } + catch ( const std::exception &e ) { + message.str(""); message << "ERROR invalid FINE_ACQUIRE_GAIN " + << config.arg[entry] << ": " << e.what(); + logwrite( function, message.str() ); + return ERROR; + } + message.str(""); message << "SLICECAMD:config:" << config.param[entry] << "=" << config.arg[entry]; + logwrite( function, message.str() ); + applied++; + } + else + if ( config.param[entry] == "FINE_ACQUIRE_GAIN_LARGE" ) { + try { this->fineacquire_state.gain_large = std::stod( config.arg[entry] ); } + catch ( const std::exception &e ) { + message.str(""); message << "ERROR invalid FINE_ACQUIRE_GAIN_LARGE " + << config.arg[entry] << ": " << e.what(); + logwrite( function, message.str() ); + return ERROR; + } + message.str(""); message << "SLICECAMD:config:" << config.param[entry] << "=" << config.arg[entry]; + logwrite( function, message.str() ); + applied++; + } + else + if ( config.param[entry] == "FINE_ACQUIRE_GAIN_THRESHOLD" ) { + try { this->fineacquire_state.gain_threshold_arcsec = std::stod( config.arg[entry] ); } + catch ( const std::exception &e ) { + message.str(""); message << "ERROR invalid FINE_ACQUIRE_GAIN_THRESHOLD " + << config.arg[entry] << ": " << e.what(); + logwrite( function, message.str() ); + return ERROR; + } + message.str(""); message << "SLICECAMD:config:" << config.param[entry] << "=" << config.arg[entry]; + logwrite( function, message.str() ); + applied++; + } } // FINE_ACQUIRE parameters must have been configured properly // - if (!this->fineacquire_state.is_valid()) { + if (this->fineacquire_state.is_valid()) { + // if so then save these as the defaults + this->default_which = this->fineacquire_state.which; + this->default_aimpoint = this->fineacquire_state.aimpoint; + } + else { logwrite(function, "ERROR bad or missing FINE_ACQUIRE configuration"); return ERROR; } diff --git a/slicecamd/slicecam_interface.h b/slicecamd/slicecam_interface.h index 0854d1a2..0785aa84 100644 --- a/slicecamd/slicecam_interface.h +++ b/slicecamd/slicecam_interface.h @@ -76,21 +76,28 @@ namespace Slicecam { * goal_arcsec - convergence threshold; loop stops when median offset * magnitude falls below this value * gain - fraction of the median offset commanded each cycle (0..1) - * skip_frames - counts down frames to discard while telescope settles - * after a commanded move + * settle_count - configured frames to discard after each commanded move + * settle_frames - runtime countdown; decremented to zero before resuming */ struct FineAcqState { std::string which; - Point aimpoint; ///< 1-based pixel aim point + Point aimpoint { NAN, NAN }; ///< 1-based pixel aim point; NAN until configured Rect bg_region; ///< background ROI (1-based) std::vector dra_samp; ///< dRA*cos(dec) samples, degrees std::vector ddec_samp; ///< dDEC samples, degrees int max_samples = 10; ///< samples before evaluating a move + int min_samples = 3; ///< minimum samples before scatter-gated early exit + double prec_arcsec = 0.1; ///< MAD scatter threshold per axis for early exit (arcsec) double goal_arcsec = 0.3; ///< convergence threshold, arcsec - double gain = 0.7; ///< gain applied to commanded offset - int skip_frames = 0; ///< frames to skip after a telescope move - - void reset() { dra_samp.clear(); ddec_samp.clear(); skip_frames = 0; } + double gain = 0.7; ///< gain applied when offset <= gain_threshold_arcsec + double gain_large = 1.0; ///< gain applied when offset > gain_threshold_arcsec + double gain_threshold_arcsec = 2.0; ///< offset above which gain_large is used + int settle_frames = 0; ///< countdown of frames to discard while telescope settles + int settle_count = 2; ///< configured: frames to discard after each move + int consecutive_centroid_failures = 0; ///< counts consecutive centroid failures + + void reset() { dra_samp.clear(); ddec_samp.clear(); settle_frames = 0; + consecutive_centroid_failures = 0; } bool is_valid() const noexcept { return !which.empty() && aimpoint.is_valid() && bg_region.is_valid(); } @@ -119,8 +126,14 @@ namespace Slicecam { std::mutex framegrab_mtx; std::condition_variable cv; + std::mutex acam_mtx; ///< guards waiters on acam_cv + std::condition_variable acam_cv; ///< notified when cached ACAM state updates + FineAcqState fineacquire_state; + std::string default_which; ///< configured default camera for fineacquire + Point default_aimpoint { NAN, NAN }; ///< configured default aimpoint for fineacquire + public: std::unique_ptr publisher; ///< publisher object std::string publisher_address; ///< publish socket endpoint @@ -140,6 +153,16 @@ namespace Slicecam { std::atomic is_fineacquire_locked; ///< set when fine acquire target acquired std::atomic is_acam_guiding; ///< is acam guiding? + std::atomic last_acam_pubtime{0}; ///< pubtime (us) of latest received acamd status + + /// Max acceptable age (us) for cached ACAM status used by fineacquire. + static constexpr int64_t ACAM_STATUS_MAX_AGE_US = 10'000'000; + + /// Max time (us) fineacquire() will wait for a fresh, guiding ACAM status before failing. + static constexpr int64_t ACAM_WAIT_TIMEOUT_US = 2'000'000; + + bool is_acam_status_fresh() const; + /** these are set by Interface::saveframes() */ std::atomic nsave_preserve_frames; ///< number of frames to preserve (normally overwritten) diff --git a/slicecamd/slicecam_math.cpp b/slicecamd/slicecam_math.cpp index a87d9e61..6d73637e 100644 --- a/slicecamd/slicecam_math.cpp +++ b/slicecamd/slicecam_math.cpp @@ -309,12 +309,12 @@ namespace Slicecam { } } - { - std::ostringstream oss; - oss << "[DEBUG] bkg=" << bkg << " sigma=" << sigma - << " best_val=" << best_val << " best_x=" << best_x << " best_y=" << best_y; - logwrite("Slicecam::Math::calculate_centroid", oss.str()); - } +// { +// std::ostringstream oss; +// oss << "[DEBUG] bkg=" << bkg << " sigma=" << sigma +// << " best_val=" << best_val << " best_x=" << best_x << " best_y=" << best_y; +// logwrite("Slicecam::Math::calculate_centroid", oss.str()); +// } if ( best_x < 0 ) return ERROR; // no source found 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/CMakeLists.txt b/tcsd/CMakeLists.txt index 5412d5cb..d1008d74 100644 --- a/tcsd/CMakeLists.txt +++ b/tcsd/CMakeLists.txt @@ -10,8 +10,8 @@ set( TCSD_DIR ${PROJECT_BASE_DIR}/tcsd ) set( CMAKE_CXX_STANDARD 17 ) -add_definitions( -Wall -ansi -O1 -Wno-variadic-macros -ggdb ) -#add_definitions( -Wall -Wextra -Wconversion -Wshadow -ansi -O1 -Wno-variadic-macros -ggdb ) +add_definitions( -Wall -O1 -Wno-variadic-macros -ggdb ) +#add_definitions( -Wall -Wextra -Wconversion -Wshadow -O1 -Wno-variadic-macros -ggdb ) include_directories( ${PROJECT_BASE_DIR}/utils ) include_directories( ${PROJECT_BASE_DIR}/common ) diff --git a/tcsd/tcs_interface.cpp b/tcsd/tcs_interface.cpp index ed59ae1f..deee79b0 100644 --- a/tcsd/tcs_interface.cpp +++ b/tcsd/tcs_interface.cpp @@ -42,28 +42,41 @@ namespace TCS { this->get_tcs_info(); nlohmann::json jmessage_out; - jmessage_out["source"] = "tcsd"; - - jmessage_out["ISOPEN"] = this->tcs_info.isopen; - jmessage_out["TCSNAME"] = this->tcs_info.tcsname; - - jmessage_out["PA"] = this->tcs_info.pa; // double - jmessage_out["CASANGLE"] = this->tcs_info.cassangle; // double - jmessage_out["HA"] = this->tcs_info.ha; // string - jmessage_out["RAOFFSET"] = this->tcs_info.offsetra; // double - jmessage_out["DECLOFFS"] = this->tcs_info.offsetdec; // double - jmessage_out["TELRA"] = this->tcs_info.ra_hms; // string "hh:mm:ss.s" - jmessage_out["TELDEC"] = this->tcs_info.dec_dms; // string "dd:mm:ss.s" - jmessage_out["RA"] = radec_to_decimal( this->tcs_info.ra_hms ); - jmessage_out["DEC"] = radec_to_decimal( this->tcs_info.dec_dms ); - jmessage_out["AZ"] = this->tcs_info.azimuth; - jmessage_out["ALT"] = 90. - this->tcs_info.zenithangle; - jmessage_out["ZENANGLE"] = this->tcs_info.zenithangle; - jmessage_out["DOMEAZ"] = this->tcs_info.domeazimuth; - jmessage_out["DOMESHUT"] = this->tcs_info.domeshutters==1?"open":"closed"; - jmessage_out["TELFOCUS"] = this->tcs_info.focus; - jmessage_out["AIRMASS"] = this->tcs_info.airmass; - jmessage_out["MOTION"] = this->tcs_info.motion; + jmessage_out[Key::SOURCE] = Daemon::TCSD; + + std::string motion; + { + std::lock_guard lock(tcs_info_mtx); + + motion = this->tcs_info.motion; + jmessage_out[Key::Tcsd::MOTION] = motion; + jmessage_out["ISOPEN"] = this->tcs_info.isopen; + jmessage_out["TCSNAME"] = this->tcs_info.tcsname; + + jmessage_out["PA"] = this->tcs_info.pa; // double + jmessage_out[Key::Tcsd::CASANGLE] = this->tcs_info.cassangle; // double + jmessage_out["HA"] = this->tcs_info.ha; // string + jmessage_out["RAOFFSET"] = this->tcs_info.offsetra; // double + jmessage_out["DECLOFFS"] = this->tcs_info.offsetdec; // double + jmessage_out[Key::Tcsd::TELRA] = this->tcs_info.ra_hms; // string "hh:mm:ss.s" + jmessage_out[Key::Tcsd::TELDEC] = this->tcs_info.dec_dms; // string "dd:mm:ss.s" + jmessage_out["RA"] = radec_to_decimal( this->tcs_info.ra_hms ); + jmessage_out["DEC"] = radec_to_decimal( this->tcs_info.dec_dms ); + jmessage_out[Key::Tcsd::AZ] = this->tcs_info.azimuth; + jmessage_out[Key::Tcsd::ALT] = 90. - this->tcs_info.zenithangle; + jmessage_out["ZENANGLE"] = this->tcs_info.zenithangle; + jmessage_out["DOMEAZ"] = this->tcs_info.domeazimuth; + jmessage_out["DOMESHUT"] = this->tcs_info.domeshutters==1?"open":"closed"; + jmessage_out["TELFOCUS"] = this->tcs_info.focus; + jmessage_out[Key::Tcsd::AIRMASS] = this->tcs_info.airmass; + } + + // broadcast motion status if it changed + if (!motion.empty() && + motion != this->last_published_motion) { + this->broadcast.notice("TCS::Interface::publish_snapshot", "telescope "+motion); + this->last_published_motion = motion; + } // for backwards compatibility jmessage_out["messagetype"] = "tcsinfo"; @@ -82,6 +95,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 +157,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 @@ -176,7 +243,7 @@ namespace TCS { */ long Interface::llist( const std::string &arg, std::string &retstring ) { std::string function = "TCS::Interface::llist"; - std::stringstream message, asyncmsg; + std::stringstream message; // Help // @@ -199,9 +266,6 @@ namespace TCS { retstring = message.str(); - asyncmsg << "TCSD:llist:" << retstring; - this->async.enqueue( asyncmsg.str() ); - return NO_ERROR; } /***** TCS::Interface::llist ************************************************/ @@ -216,7 +280,7 @@ namespace TCS { */ long Interface::open( std::string arg, std::string &retstring ) { const std::string function("TCS::Interface::open"); - std::stringstream message, asyncmsg; + std::stringstream message; long error = NO_ERROR; // Help @@ -351,7 +415,7 @@ namespace TCS { */ long Interface::isopen( const std::string &arg, std::string &retstring ) { std::string function = "TCS::Interface::isopen"; - std::stringstream message, asyncmsg; + std::stringstream message; // Help // @@ -371,16 +435,12 @@ 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 - - asyncmsg.str(""); asyncmsg << "TCSD:name:" << ( ! _name.empty() ? _name : "offline" ); - this->async.enqueue( asyncmsg.str() ); // broadcast the name + } return NO_ERROR; } @@ -395,7 +455,7 @@ namespace TCS { */ long Interface::close( ) { std::string function = "TCS::Interface::close"; - std::stringstream message, asyncmsg; + std::stringstream message; long error = NO_ERROR; for ( const auto &[key,tcs] : this->tcsmap ) { @@ -445,14 +505,11 @@ namespace TCS { logwrite( function, "connection open to "+tcs->getname() ); } retstring = tcs->getname(); // Found the connected TCS - this->async.enqueue( "TCSD:name:"+retstring ); return NO_ERROR; } } retstring="offline"; - this->async.enqueue( "TCSD:name:offline" ); - return NO_ERROR; } /***** TCS::Interface::get_name *********************************************/ @@ -469,7 +526,7 @@ namespace TCS { */ long Interface::get_weather_coords( const std::string &arg, std::string &retstring ) { std::string function = "TCS::Interface::get_weather_coords"; - std::stringstream message, asyncmsg; + std::stringstream message; std::string weather; long error = NO_ERROR; @@ -526,9 +583,6 @@ namespace TCS { retstring = message.str(); - asyncmsg << "TCSD:weathercoords:" << ( error==NO_ERROR ? message.str() : "ERROR" ); - this->async.enqueue( asyncmsg.str() ); - return error; } /***** TCS::Interface::get_weather_coords ***********************************/ @@ -546,7 +600,7 @@ namespace TCS { */ long Interface::get_coords( const std::string &arg, std::string &retstring ) { std::string function = "TCS::Interface::get_coords"; - std::stringstream message, asyncmsg; + std::stringstream message; long error = NO_ERROR; // Help @@ -621,9 +675,6 @@ namespace TCS { if ( !retstring.empty() && !silent ) logwrite( function, retstring ); - asyncmsg << "TCSD:coords:" << ( !retstring.empty() ? retstring : "ERROR" ); - this->async.enqueue( asyncmsg.str() ); - return error; } /***** TCS::Interface::get_coords *******************************************/ @@ -641,7 +692,7 @@ namespace TCS { */ long Interface::get_cass( const std::string &arg, std::string &retstring ) { std::string function = "TCS::Interface::get_cass"; - std::stringstream message, asyncmsg; + std::stringstream message; std::stringstream reply; long error = NO_ERROR; @@ -718,9 +769,6 @@ namespace TCS { if ( !retstring.empty() && !silent ) logwrite( function, retstring ); - asyncmsg << "TCSD:cassangle:" << ( !retstring.empty() ? retstring : "ERROR" ); - this->async.enqueue( asyncmsg.str() ); - return error; } /***** TCS::Interface::get_cass *********************************************/ @@ -738,7 +786,7 @@ namespace TCS { */ long Interface::get_dome( const std::string &arg, std::string &retstring ) { std::string function = "TCS::Interface::get_dome"; - std::stringstream message, asyncmsg; + std::stringstream message; std::string weather; long error = NO_ERROR; @@ -790,9 +838,6 @@ namespace TCS { if ( !retstring.empty() ) logwrite( function, retstring ); - asyncmsg << "TCSD:dome:" << ( !retstring.empty() ? retstring : "ERROR" ); - this->async.enqueue( asyncmsg.str() ); - return error; } /***** TCS::Interface::get_dome *********************************************/ @@ -809,7 +854,7 @@ namespace TCS { */ long Interface::set_focus( const std::string &arg, std::string &retstring ) { std::string function = "TCS::Interface::set_focus"; - std::stringstream message, asyncmsg; + std::stringstream message; long error = NO_ERROR; // Help @@ -823,7 +868,7 @@ namespace TCS { if ( arg.empty() ) { retstring="missing_argument"; return ERROR; } - double value; + double value=NAN; try { value = std::stod( arg ); @@ -889,7 +934,7 @@ namespace TCS { */ long Interface::get_focus( const std::string &arg, std::string &retstring ) { std::string function = "TCS::Interface::get_focus"; - std::stringstream message, asyncmsg; + std::stringstream message; std::stringstream reply; long error = NO_ERROR; @@ -974,9 +1019,6 @@ namespace TCS { if ( !retstring.empty() && !silent ) logwrite( function, retstring ); - asyncmsg << "TCSD:focus:" << ( !retstring.empty() ? retstring : "ERROR" ); - this->async.enqueue( asyncmsg.str() ); - return error; } /***** TCS::Interface::get_focus ********************************************/ @@ -1014,9 +1056,6 @@ namespace TCS { logwrite( function, retstring ); } - message.str(""); message << "TCSD:offsets:" << ( !retstring.empty() ? retstring : "ERROR" ); - this->async.enqueue( message.str() ); - return NO_ERROR; } /***** TCS::Interface::get_offsets ******************************************/ @@ -1137,18 +1176,16 @@ namespace TCS { // parse the reply which stores it in the TcsInfo class // - this->tcs_info.parse_pa(tcsreply); - std::ostringstream oss; + this->tcs_info.parse_pa(tcsreply); + { + std::lock_guard lock(tcs_info_mtx); oss << this->tcs_info.pa; + } retstring = oss.str(); if ( !retstring.empty() && !silent ) logwrite( function, retstring ); - std::stringstream asyncmsg; - asyncmsg << "TCSD:parallactic:" << ( !retstring.empty() ? retstring : "ERROR" ); - this->async.enqueue( asyncmsg.str() ); - return NO_ERROR; } /***** TCS::Interface::get_pa ***********************************************/ @@ -1165,7 +1202,7 @@ namespace TCS { */ long Interface::pt_offsetrate( const std::string &arg, std::string &retstring ) { std::string function = "TCS::Interface::pt_offsetrate"; - std::stringstream message, asyncmsg; + std::stringstream message; long error = NO_ERROR; // Help @@ -1233,7 +1270,7 @@ namespace TCS { */ long Interface::get_motion( const std::string &arg, std::string &retstring ) { std::string function = "TCS::Interface::get_motion"; - std::stringstream message, asyncmsg; + std::stringstream message; long error = NO_ERROR; // Help @@ -1252,9 +1289,6 @@ namespace TCS { error = ERROR; } - asyncmsg << "TCSD:motion:" << ( !retstring.empty() ? retstring : "ERROR" ); - this->async.enqueue( asyncmsg.str() ); - return error; } /***** TCS::Interface::get_motion *******************************************/ @@ -1271,7 +1305,7 @@ namespace TCS { */ long Interface::ringgo( const std::string &arg, std::string &retstring ) { std::string function = "TCS::Interface::ringgo"; - std::stringstream message, asyncmsg; + std::stringstream message; long error = ERROR; double angle = NAN; @@ -1354,7 +1388,7 @@ namespace TCS { long Interface::coords( std::string args, std::string &retstring ) { std::string function = "TCS::Interface::coords"; std::string retcode; - std::stringstream message, asyncmsg; + std::stringstream message; // Help // @@ -1383,9 +1417,6 @@ namespace TCS { long error = this->send_command( cmd.str(), retstring, TCS::FAST_RESPONSE ); - asyncmsg << "TCSD:coords:" << ( error == ERROR ? "ERROR" : retstring ); - this->async.enqueue( asyncmsg.str() ); - return error; } /***** TCS::Interface::coords ***********************************************/ @@ -1493,7 +1524,7 @@ namespace TCS { long Interface::zero_offsets( const std::string args, std::string &retstring ) { std::string function = "TCS::Interface::zero_offsets"; std::string retcode; - std::stringstream message, asyncmsg; + std::stringstream message; // Help // diff --git a/tcsd/tcs_interface.h b/tcsd/tcs_interface.h index 9c76b03f..6e73e6bd 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,8 @@ 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 + std::string last_published_motion; ///< last published motion (publish on change) public: inline void set_default_tcs(const std::string &which) { this->default_tcs=which; } @@ -445,13 +448,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 +490,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 +503,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 ); @@ -527,6 +532,7 @@ logwrite(function,message.str()); long get_tcs_info(); ///< fills the tcs_info class Common::Queue async; ///< asynchronous message queue object + Common::Broadcaster broadcast { this->publisher, Daemon::TCSD }; }; /***** TCS::Interface *******************************************************/ 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..c21c20b5 100644 --- a/tcsd/tcsd.cpp +++ b/tcsd/tcsd.cpp @@ -132,6 +132,10 @@ 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( &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 // loop forever on Accept to dynamically spawn a new thread for each blocking diff --git a/tcsd/tcsd_client.cpp b/tcsd/tcsd_client.cpp index 4ea8b42b..c0598402 100644 --- a/tcsd/tcsd_client.cpp +++ b/tcsd/tcsd_client.cpp @@ -25,7 +25,7 @@ * @return ERROR | NO_ERROR | HELP * */ - long TcsDaemonClient::init( std::string_view which, std::string &retstring ) { + long TcsDaemonClient::init( std::string which, std::string &retstring ) { std::string function = "TcsDaemonClient::init"; std::stringstream message; std::string reply, tcsname; diff --git a/tcsd/tcsd_client.h b/tcsd/tcsd_client.h index 29d30ed6..679bce15 100644 --- a/tcsd/tcsd_client.h +++ b/tcsd/tcsd_client.h @@ -23,7 +23,7 @@ public: Common::DaemonClient client { "tcsd", '\n', '\n' }; - long init( std::string_view which, std::string &retstring ); + long init( std::string which, std::string &retstring ); long get_name( std::string &name, bool poll ); long get_name( std::string &name ); long poll_name( std::string &name ); diff --git a/telemd/CMakeLists.txt b/telemd/CMakeLists.txt index fbb93bc7..0d5844a7 100644 --- a/telemd/CMakeLists.txt +++ b/telemd/CMakeLists.txt @@ -10,7 +10,7 @@ set( TELEMD_DIR ${PROJECT_BASE_DIR}/telemd ) set( CMAKE_CXX_STANDARD 17 ) -add_definitions( -Wall -ansi -O1 -Wno-variadic-macros -ggdb ) +add_definitions( -Wall -O1 -Wno-variadic-macros -ggdb ) include_directories( ${PROJECT_BASE_DIR}/utils ) include_directories( ${PROJECT_BASE_DIR}/common ) diff --git a/thermald/CMakeLists.txt b/thermald/CMakeLists.txt index f21492b1..c4a0144d 100644 --- a/thermald/CMakeLists.txt +++ b/thermald/CMakeLists.txt @@ -10,7 +10,7 @@ set( THERMALD_DIR ${PROJECT_BASE_DIR}/thermald ) set( CMAKE_CXX_STANDARD 17 ) -add_definitions( -Wall -ansi -O1 -Wno-variadic-macros -ggdb ) +add_definitions( -Wall -O1 -Wno-deprecated-declarations -Wno-variadic-macros -ggdb ) include_directories( ${PROJECT_BASE_DIR}/utils ) include_directories( ${PROJECT_BASE_DIR}/LKS ) diff --git a/utils/CMakeLists.txt b/utils/CMakeLists.txt index baa453ba..fb6c0401 100644 --- a/utils/CMakeLists.txt +++ b/utils/CMakeLists.txt @@ -10,13 +10,12 @@ set(PROJECT_UTILS_DIR ${PROJECT_BASE_DIR}/utils) set( CMAKE_CXX_STANDARD 17 ) -add_definitions( -Wall -Wextra -Wconversion -Wshadow -ansi -Og -Wno-variadic-macros -ggdb ) +add_definitions( -Wall -Wextra -Wconversion -Wshadow -Og -Wno-variadic-macros -ggdb ) add_library(utilities STATIC ${PROJECT_UTILS_DIR}/utilities.cpp - md5 ) -target_link_libraries(utilities PRIVATE stdc++fs) + target_link_libraries(utilities PUBLIC md5 stdc++fs) add_library(logentry STATIC ${PROJECT_UTILS_DIR}/logentry.cpp @@ -49,11 +48,11 @@ include_directories( ${MYSQL_INCLUDES} ) add_library(database STATIC ${PROJECT_UTILS_DIR}/database.cpp - ${MYSQL_LIB} ) +target_link_libraries(database PRIVATE ${MYSQL_LIB}) add_executable(socksend - ${PROJECT_UTILS_DIR}/sendcmd.c + ${PROJECT_UTILS_DIR}/sendcmd.cpp ) add_executable(listener @@ -66,3 +65,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/logentry.cpp b/utils/logentry.cpp index 0b95650e..a384ee9e 100644 --- a/utils/logentry.cpp +++ b/utils/logentry.cpp @@ -176,12 +176,11 @@ void close_log() { * */ void logwrite( const std::string &function, std::string message ) { - std::stringstream logmsg; - std::string timestamp = get_timestamp(); // get the current time (defined in utilities.h) + std::ostringstream logmsg; std::lock_guard lock(loglock); // lock mutex to protect from multiple access - logmsg << timestamp << " (" << function << ") " << message << "\n"; + logmsg << get_timestamp() << " (" << function << ") " << message << "\n"; if (filestream.is_open()) { filestream << logmsg.str(); // send to the file stream (if open) diff --git a/utils/make_directories.cpp b/utils/make_directories.cpp index 99c39aaf..b04fdb83 100644 --- a/utils/make_directories.cpp +++ b/utils/make_directories.cpp @@ -17,23 +17,23 @@ namespace fs = std::filesystem; std::string get_date() { std::stringstream current_date; // String to contain the return value std::time_t t=std::time(nullptr); // Container for system time - struct timespec timenow;; // Time of day container struct tm mytime; // time container - // Get the system time, return a bad datestamp on error - if ( clock_gettime( CLOCK_REALTIME, &timenow ) != 0 ) return( "" ); + // UTC now + if ( gmtime_r( &t, &mytime ) == nullptr ) return( "" ); - // Convert the time of day to local or GMT - t = timenow.tv_sec; - if ( localtime_r( &t, &mytime ) == nullptr ) return( "" ); + // at local noon we want tonight's UTC + mytime.tm_mday += 1; + + // normalize struct handles rollovers + if (timegm(&mytime)==-1) return ""; - current_date.setf(std::ios_base::right); current_date << std::setfill('0') << std::setprecision(0) << std::setw(4) << mytime.tm_year + 1900 << std::setw(2) << mytime.tm_mon + 1 - << std::setw(2) << mytime.tm_mday + 1; // tomorrow! + << std::setw(2) << mytime.tm_mday; - return( current_date.str() ); + return current_date.str(); } int main() { diff --git a/utils/md5.c b/utils/md5.c index 565a2e6b..fea8fad8 100644 --- a/utils/md5.c +++ b/utils/md5.c @@ -11,6 +11,10 @@ * obtained from https://github.com/B-Con/crypto-algorithms *********************************************************************/ +#ifdef __cplusplus +extern "C" { +#endif + /*************************** HEADER FILES ***************************/ #include #include @@ -189,3 +193,6 @@ void md5_final(MD5_CTX *ctx, BYTE hash[]) hash[i + 12] = (ctx->state[3] >> (i * 8)) & 0x000000ff; } } +#ifdef __cplusplus +} +#endif diff --git a/utils/md5.cpp b/utils/md5.cpp deleted file mode 100644 index 47feae42..00000000 --- a/utils/md5.cpp +++ /dev/null @@ -1,193 +0,0 @@ -/********************************************************************* -* Filename: md5.cpp -* Author: Brad Conte (brad AT bradconte.com) -* Copyright: -* Disclaimer: This code is presented "as is" without any guarantees. -* Details: Implementation of the MD5 hashing algorithm. -* Algorithm specification can be found here: -* http://tools.ietf.org/html/rfc1321 -* This implementation uses little endian byte order. -* -* obtained from https://github.com/B-Con/crypto-algorithms -* adopted c++ idiomataic practices, David Hale -*********************************************************************/ - -/*************************** HEADER FILES ***************************/ -#include -#include -#include "md5.h" - -/****************************** MACROS ******************************/ -constexpr uint32_t ROTLEFT(uint32_t a, uint32_t b) { return (a << b) | (a >> (32 - b)); } -constexpr uint32_t F(uint32_t x, uint32_t y, uint32_t z) { return (x & y ) | ( ~x & z ); } -constexpr uint32_t G(uint32_t x, uint32_t y, uint32_t z) { return (x & z) | ( y & ~z ); } -constexpr uint32_t H(uint32_t x, uint32_t y, uint32_t z) { return x ^ y ^ z; } -constexpr uint32_t I(uint32_t x, uint32_t y, uint32_t z) { return y ^ ( x | ~z ); } -constexpr void FF(uint32_t &a, uint32_t b, uint32_t c, uint32_t d, uint32_t m, uint32_t s, uint32_t t) { - a += F(b, c, d) + m + t; - a = b + ROTLEFT(a, s); -} -constexpr void GG(uint32_t &a, uint32_t b, uint32_t c, uint32_t d, uint32_t m, uint32_t s, uint32_t t) { - a += G(b, c, d) + m + t; - a = b + ROTLEFT(a, s); -} -constexpr void HH(uint32_t &a, uint32_t b, uint32_t c, uint32_t d, uint32_t m, uint32_t s, uint32_t t) { - a += H(b, c, d) + m + t; - a = b + ROTLEFT(a, s); -} -constexpr void II(uint32_t &a, uint32_t b, uint32_t c, uint32_t d, uint32_t m, uint32_t s, uint32_t t) { - a += I(b, c, d) + m + t; - a = b + ROTLEFT(a, s); -} - -/*********************** FUNCTION DEFINITIONS ***********************/ -void md5_transform(MD5_CTX *ctx, const BYTE data[]) { - WORD a, b, c, d, m[16], i, j; - - // MD5 specifies big endian byte order, but this implementation assumes a little - // endian byte order CPU. Reverse all the bytes upon input, and re-reverse them - // on output (in md5_final()). - for (i = 0, j = 0; i < 16; ++i, j += 4) - m[i] = (data[j]) + (data[j + 1] << 8) + (data[j + 2] << 16) + (data[j + 3] << 24); - - a = ctx->state[0]; - b = ctx->state[1]; - c = ctx->state[2]; - d = ctx->state[3]; - - FF(a, b, c, d, m[0], 7, 0xd76aa478); - FF(d, a, b, c, m[1], 12, 0xe8c7b756); - FF(c, d, a, b, m[2], 17, 0x242070db); - FF(b, c, d, a, m[3], 22, 0xc1bdceee); - FF(a, b, c, d, m[4], 7, 0xf57c0faf); - FF(d, a, b, c, m[5], 12, 0x4787c62a); - FF(c, d, a, b, m[6], 17, 0xa8304613); - FF(b, c, d, a, m[7], 22, 0xfd469501); - FF(a, b, c, d, m[8], 7, 0x698098d8); - FF(d, a, b, c, m[9], 12, 0x8b44f7af); - FF(c, d, a, b, m[10], 17, 0xffff5bb1); - FF(b, c, d, a, m[11], 22, 0x895cd7be); - FF(a, b, c, d, m[12], 7, 0x6b901122); - FF(d, a, b, c, m[13], 12, 0xfd987193); - FF(c, d, a, b, m[14], 17, 0xa679438e); - FF(b, c, d, a, m[15], 22, 0x49b40821); - - GG(a, b, c, d, m[1], 5, 0xf61e2562); - GG(d, a, b, c, m[6], 9, 0xc040b340); - GG(c, d, a, b, m[11], 14, 0x265e5a51); - GG(b, c, d, a, m[0], 20, 0xe9b6c7aa); - GG(a, b, c, d, m[5], 5, 0xd62f105d); - GG(d, a, b, c, m[10], 9, 0x02441453); - GG(c, d, a, b, m[15], 14, 0xd8a1e681); - GG(b, c, d, a, m[4], 20, 0xe7d3fbc8); - GG(a, b, c, d, m[9], 5, 0x21e1cde6); - GG(d, a, b, c, m[14], 9, 0xc33707d6); - GG(c, d, a, b, m[3], 14, 0xf4d50d87); - GG(b, c, d, a, m[8], 20, 0x455a14ed); - GG(a, b, c, d, m[13], 5, 0xa9e3e905); - GG(d, a, b, c, m[2], 9, 0xfcefa3f8); - GG(c, d, a, b, m[7], 14, 0x676f02d9); - GG(b, c, d, a, m[12], 20, 0x8d2a4c8a); - - HH(a, b, c, d, m[5], 4, 0xfffa3942); - HH(d, a, b, c, m[8], 11, 0x8771f681); - HH(c, d, a, b, m[11], 16, 0x6d9d6122); - HH(b, c, d, a, m[14], 23, 0xfde5380c); - HH(a, b, c, d, m[1], 4, 0xa4beea44); - HH(d, a, b, c, m[4], 11, 0x4bdecfa9); - HH(c, d, a, b, m[7], 16, 0xf6bb4b60); - HH(b, c, d, a, m[10], 23, 0xbebfbc70); - HH(a, b, c, d, m[13], 4, 0x289b7ec6); - HH(d, a, b, c, m[0], 11, 0xeaa127fa); - HH(c, d, a, b, m[3], 16, 0xd4ef3085); - HH(b, c, d, a, m[6], 23, 0x04881d05); - HH(a, b, c, d, m[9], 4, 0xd9d4d039); - HH(d, a, b, c, m[12], 11, 0xe6db99e5); - HH(c, d, a, b, m[15], 16, 0x1fa27cf8); - HH(b, c, d, a, m[2], 23, 0xc4ac5665); - - II(a, b, c, d, m[0], 6, 0xf4292244); - II(d, a, b, c, m[7], 10, 0x432aff97); - II(c, d, a, b, m[14], 15, 0xab9423a7); - II(b, c, d, a, m[5], 21, 0xfc93a039); - II(a, b, c, d, m[12], 6, 0x655b59c3); - II(d, a, b, c, m[3], 10, 0x8f0ccc92); - II(c, d, a, b, m[10], 15, 0xffeff47d); - II(b, c, d, a, m[1], 21, 0x85845dd1); - II(a, b, c, d, m[8], 6, 0x6fa87e4f); - II(d, a, b, c, m[15], 10, 0xfe2ce6e0); - II(c, d, a, b, m[6], 15, 0xa3014314); - II(b, c, d, a, m[13], 21, 0x4e0811a1); - II(a, b, c, d, m[4], 6, 0xf7537e82); - II(d, a, b, c, m[11], 10, 0xbd3af235); - II(c, d, a, b, m[2], 15, 0x2ad7d2bb); - II(b, c, d, a, m[9], 21, 0xeb86d391); - - ctx->state[0] += a; - ctx->state[1] += b; - ctx->state[2] += c; - ctx->state[3] += d; -} - -void md5_init(MD5_CTX *ctx) { - ctx->datalen = 0; - ctx->bitlen = 0; - ctx->state[0] = 0x67452301; - ctx->state[1] = 0xEFCDAB89; - ctx->state[2] = 0x98BADCFE; - ctx->state[3] = 0x10325476; -} - -void md5_update(MD5_CTX *ctx, const BYTE data[], size_t len) { - size_t i; - - for (i = 0; i < len; ++i) { - ctx->data[ctx->datalen] = data[i]; - ctx->datalen++; - if (ctx->datalen == 64) { - md5_transform(ctx, ctx->data); - ctx->bitlen += 512; - ctx->datalen = 0; - } - } -} - -void md5_final(MD5_CTX *ctx, BYTE hash[]) { - size_t i; - - i = ctx->datalen; - - // Pad whatever data is left in the buffer. - if (ctx->datalen < 56) { - ctx->data[i++] = 0x80; - while (i < 56) - ctx->data[i++] = 0x00; - } else if (ctx->datalen >= 56) { - ctx->data[i++] = 0x80; - while (i < 64) - ctx->data[i++] = 0x00; - md5_transform(ctx, ctx->data); - memset(ctx->data, 0, 56); - } - - // Append to the padding the total message's length in bits and transform. - ctx->bitlen += ctx->datalen * 8; - ctx->data[56] = ctx->bitlen; - ctx->data[57] = ctx->bitlen >> 8; - ctx->data[58] = ctx->bitlen >> 16; - ctx->data[59] = ctx->bitlen >> 24; - ctx->data[60] = ctx->bitlen >> 32; - ctx->data[61] = ctx->bitlen >> 40; - ctx->data[62] = ctx->bitlen >> 48; - ctx->data[63] = ctx->bitlen >> 56; - md5_transform(ctx, ctx->data); - - // Since this implementation uses little endian byte ordering and MD uses big endian, - // reverse all the bytes when copying the final state to the output hash. - for (i = 0; i < 4; ++i) { - hash[i] = (ctx->state[0] >> (i * 8)) & 0x000000ff; - hash[i + 4] = (ctx->state[1] >> (i * 8)) & 0x000000ff; - hash[i + 8] = (ctx->state[2] >> (i * 8)) & 0x000000ff; - hash[i + 12] = (ctx->state[3] >> (i * 8)) & 0x000000ff; - } -} diff --git a/utils/md5.h b/utils/md5.h index 09e02978..e88f072b 100644 --- a/utils/md5.h +++ b/utils/md5.h @@ -10,6 +10,9 @@ #ifndef MD5_H #define MD5_H +#ifdef __cplusplus +extern "C" { +#endif /*************************** HEADER FILES ***************************/ #include @@ -33,4 +36,7 @@ void md5_init(MD5_CTX *ctx); void md5_update(MD5_CTX *ctx, const BYTE data[], size_t len); void md5_final(MD5_CTX *ctx, BYTE hash[]); +#ifdef __cplusplus +} +#endif #endif // MD5_H diff --git a/utils/sendcmd.cpp b/utils/sendcmd.cpp new file mode 100644 index 00000000..66b95a16 --- /dev/null +++ b/utils/sendcmd.cpp @@ -0,0 +1,139 @@ +/** + * @file utils/sendcmd.cpp + * @brief TCP command sender + * @author David Hale + */ +#include +#include +#include +#include +#include + +#include +#include +#include +#include +#include + +static constexpr size_t RESPONSE_BUFSIZE = 8192; + +static void usage(const char* exe) { + std::cout << "usage: " << exe + << " [-h hostname] [-p port] [-t timeout] [-m mode] command\n"; +} + +int main(int argc, char* argv[]) { + std::string hostname; + std::string message; + int port = 0; + int timeout = 10; + int mode = 0; // 0: wait for response, 1: fire and forget + + try { + for (int i = 1; i < argc; ++i) { + const std::string arg = argv[i]; + if (arg.size() == 2 && arg[0] == '-') { + if (i + 1 >= argc) { usage(argv[0]); return 0; } + switch (arg[1]) { + case 'h': hostname = argv[++i]; break; + case 'p': port = std::stoi(argv[++i]); break; + case 't': timeout = std::stoi(argv[++i]); break; + case 'm': mode = std::stoi(argv[++i]); break; + default: usage(argv[0]); return 0; + } + } + else { + message = arg; + } + } + } + catch (const std::exception& ex) { + std::cerr << "ERROR invalid argument: " << ex.what() << "\n"; + return 1; + } + + if (hostname.empty() || port <= 0 || message.empty()) { + usage(argv[0]); + return 1; + } + + struct addrinfo hints{}; + hints.ai_family = AF_INET; + hints.ai_socktype = SOCK_STREAM; + + struct addrinfo* res = nullptr; + const int gai_ret = getaddrinfo(hostname.c_str(), std::to_string(port).c_str(), &hints, &res); + if (gai_ret != 0) { + std::cerr << "ERROR getting host: " << gai_strerror(gai_ret) << "\n"; + return 1; + } + + int sock = socket(res->ai_family, res->ai_socktype, res->ai_protocol); + if (sock < 0) { + std::cerr << "ERROR " << errno << " creating socket\n"; + freeaddrinfo(res); + return -errno; + } + + if (fcntl(sock, F_SETFL, O_NONBLOCK) < 0) { + std::cerr << "ERROR " << errno << " setting socket\n"; + close(sock); + freeaddrinfo(res); + return -errno; + } + + const auto tstart = std::chrono::steady_clock::now(); + auto elapsed_sec = [&]() { + return std::chrono::duration_cast( + std::chrono::steady_clock::now() - tstart).count(); + }; + + if (connect(sock, res->ai_addr, res->ai_addrlen) == -1 && errno != EINPROGRESS) { + std::cerr << "ERROR " << errno << " connecting\n"; + close(sock); + freeaddrinfo(res); + return -errno; + } + freeaddrinfo(res); + + message += '\n'; + + ssize_t nwrite; + while ((nwrite = write(sock, message.c_str(), message.size())) < 0) { + if (errno != EAGAIN) { + std::cerr << "ERROR " << errno << " writing message\n"; + close(sock); + return -errno; + } + if (elapsed_sec() >= timeout) { + std::cerr << "ERROR " << ETIME << " TIMEOUT\n"; + close(sock); + return -ETIME; + } + } + + if (mode == 0) { + char response[RESPONSE_BUFSIZE]; + ssize_t nread; + while ((nread = read(sock, response, RESPONSE_BUFSIZE - 1)) < 0) { + if (errno != EAGAIN) { + std::cerr << "(sendcmd) ERROR " << errno << " reading\n"; + close(sock); + return -errno; + } + usleep(10000); + if (elapsed_sec() >= timeout) { + std::cerr << "ERROR " << ETIME << " TIMEOUT\n"; + close(sock); + return -ETIME; + } + } + response[nread] = '\0'; + std::cout << response << "\n"; + } + else { + std::cout << "cmd_sent\n"; + } + + return close(sock); +} diff --git a/utils/seqmon.cpp b/utils/seqmon.cpp new file mode 100644 index 00000000..8220cd7f --- /dev/null +++ b/utils/seqmon.cpp @@ -0,0 +1,492 @@ +/** + * @file seqmon.cpp + * @brief terminal-based subscriber that mimics the sequencer operator display + * @author David Hale + * + * 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) + * 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; + * it only shares the message_keys.h contract. + * + */ + +#include +#include +#include "message_keys.h" + +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace { + + 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 + // + 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"; + + // DECSTBM scrolling-region control. The header occupies HEADER_ROWS fixed + // rows at the top of the terminal; broadcast messages scroll within the + // region starting at row HEADER_ROWS+1 and extending to the bottom. Using + // 999 as the bottom bound lets the terminal clamp to its actual height. + // + // NOTE: the "6" in SCROLL_REGION_SET and CURSOR_LOG_START is HEADER_ROWS+1. + // If HEADER_ROWS changes, update these literals to match. + // + constexpr int HEADER_ROWS = 5; + constexpr const char *SCROLL_REGION_SET = "\033[6;999r"; // top = HEADER_ROWS+1 + constexpr const char *SCROLL_REGION_RESET = "\033[r"; // full-screen scrolling + constexpr const char *CURSOR_LOG_START = "\033[6;1H"; // row HEADER_ROWS+1, col 1 + constexpr const char *CURSOR_LOG_END = "\033[999;1H"; // bottom of scroll region + + 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 = "(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 ******************************************************/ + /** + * @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 ***************************************************/ + + + /***** 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 + * @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 << 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; + } + /***** 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 *****************************************************/ + + + 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 + * @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("") ); + std::string message = j.value( Key::Broadcast::MESSAGE, std::string("(empty)") ); + std::string source = j.value( Key::SOURCE, std::string("unknown") ); + // Park the cursor at the bottom of the scroll region before emitting the + // newline so the message lands inside the region and the header above + // it is not scrolled. + // + std::cout << CURSOR_LOG_END + << color_for_severity(severity) + << "[" << timestamp() << "] " + << "[" << source << "] " + << (severity != Severity::NOTICE ? severity+": " : "") + << message + << ANSI_RESET << "\n" + << std::flush; + } + catch ( const std::exception &e ) { + std::cerr << "broadcast parse error: " << e.what() << "\n"; + } + } + /***** 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 + */ + 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 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"; + 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" + << "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" + << SCROLL_REGION_SET + << CURSOR_LOG_START + << "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; + 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 ); + 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::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 + } + + std::cout << SCROLL_REGION_RESET << "\nseqmon exiting\n" << std::flush; + return 0; +} diff --git a/utils/thread_pool.h b/utils/thread_pool.h new file mode 100644 index 00000000..e01c0892 --- /dev/null +++ b/utils/thread_pool.h @@ -0,0 +1,128 @@ +/** + * @file thread_pool.h + * @brief general purpose thread pool + * @author David Hale + * + */ + +#pragma once + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +class ThreadPool { + private: + std::mutex mtx; + std::condition_variable cv_worker; + std::condition_variable cv_backlog; + std::vector workers; + std::queue> tasks; + size_t max_queue_size; + std::atomic stop; + std::atomic active_tasks; + + void log_exception(std::exception_ptr eptr) { + try { if (eptr) std::rethrow_exception(eptr); } + catch (const std::exception &e) { + std::cerr << get_timestamp() << "(ThreadPool::worker_loop) exception: " << e.what(); + } + } + + void worker_loop() { + while (true) { + std::function task; + { + std::unique_lock lock(mtx); + // blocks until work to do, or stop + cv_worker.wait( lock, [this]() { return stop || !tasks.empty(); } ); + if (stop && tasks.empty()) return; + + task = std::move(tasks.front()); + tasks.pop(); + active_tasks++; + } + // notify backlogged tasks waiting for a free slot + cv_backlog.notify_one(); + // execute the task outside the lock + try { task(); } + catch (...) { + log_exception(std::current_exception()); + } + active_tasks--; + } + } + + public: + explicit ThreadPool(size_t nthreads, size_t max_tasks=100) + : max_queue_size(max_tasks), + stop(false), + active_tasks(0) + { + if (nthreads==0) throw std::invalid_argument("ThreadPool requires at least one thread"); + if (max_queue_size==0) throw std::invalid_argument("max_queue_size must be at least 1"); + workers.reserve(nthreads); + for (size_t n=0; n < nthreads; ++n) { + workers.emplace_back( [this]() { this->worker_loop(); } ); + } + } + + ~ThreadPool() { + { + std::unique_lock lock(mtx); + // signal workers to drain and exit + stop=true; + } + // wake-up all blocked workers + cv_worker.notify_all(); + cv_backlog.notify_all(); + + // wait for clean shutdown + for (auto &thr : workers) { if (thr.joinable()) thr.join(); } + } + + // not copyable + ThreadPool(const ThreadPool&) = delete; + ThreadPool& operator=(const ThreadPool&) = delete; + + // return number of active tasks + size_t get_active() const { return active_tasks.load(std::memory_order_relaxed); } + + // return number of backlogged tasks + size_t get_backlog() { + std::lock_guard lock(mtx); + size_t backlog = tasks.size(); + size_t avail = workers.size() - active_tasks; + return (backlog > avail) ? (backlog-avail) : 0; + } + + // accepts any callable + arguments, returns future + template + auto enqueue(F&& f, Args&&... args) -> std::future> { + using return_type = std::invoke_result_t; + // wrap in a packaged task + auto task = std::make_shared>( + [f = std::forward(f), + args = std::make_tuple(std::forward(args)...)]() mutable { + return std::apply(std::move(f), std::move(args)); + }); + std::future ret = task->get_future(); + { + std::unique_lock lock(mtx); + // block until the queue has space + cv_backlog.wait( lock, [this]() { return stop || tasks.size() < max_queue_size; } ); + if (stop) throw std::runtime_error("job added to stopped ThreadPool"); + tasks.emplace( [task]() { (*task)(); } ); + } + cv_worker.notify_one(); + return ret; + } +}; diff --git a/utils/utilities.h b/utils/utilities.h index 7203c578..87a21899 100644 --- a/utils/utilities.h +++ b/utils/utilities.h @@ -43,6 +43,7 @@ #include #include #include +#include #define TO_DEGREES ( 360. / 24. ) #define TO_HOURS ( 24. / 360. ) @@ -77,6 +78,12 @@ std::chrono::system_clock::time_point next_occurrence( int hour, int minute, int long get_time( int &year, int &mon, int &mday, int &hour, int &min, int &sec, int &usec ); long get_time( const std::string &tmzone_in, int &year, int &mon, int &mday, int &hour, int &min, int &sec, int &usec ); +inline int64_t get_time_us() { + struct timespec ts; + clock_gettime( CLOCK_REALTIME, &ts ); + return int64_t(ts.tv_sec) * 1000000 + ts.tv_nsec/1000; +} + std::string timestamp_from( struct timespec &time_n ); /// return time from input timespec struct in formatted string "YYYY-MM-DDTHH:MM:SS.sss" std::string timestamp_from( const std::string &tmzone_in, struct timespec &time_in ); @@ -144,6 +151,10 @@ double angular_separation( double ra1, double dec1, double ra2, double dec2 ); static inline void rtrim(std::string &s) { /// trim off trailing whitespace from a string s.erase( std::find_if( s.rbegin(), s.rend(), [](unsigned char ch) { return !std::isspace(ch); } ).base(), s.end() ); } +static inline void ltrim(std::string &s) { /// trim off leading white space + s.erase(s.begin(), std::find_if(s.begin(), s.end(), [](unsigned char ch) { return !std::isspace(ch); } ) ); +} +static inline void lrtrim(std::string &s) { rtrim(s); ltrim(s); } inline bool caseCompareChar( char a, char b ) { return ( std::toupper(a) == std::toupper(b) ); }