From 91106386b2e2fca33aa2866a0837dcfc47c1670f Mon Sep 17 00:00:00 2001 From: David Hale Date: Fri, 14 Nov 2025 12:58:14 -0800 Subject: [PATCH 01/16] splits non-Interface code out of slicecam_interface in preparation for slicecam acquisition mode. no code changes yet. --- slicecamd/CMakeLists.txt | 7 +- slicecamd/guimanager.h | 121 ++++ slicecamd/slicecam_camera.cpp | 995 +++++++++++++++++++++++++++++++ slicecamd/slicecam_camera.h | 85 +++ slicecamd/slicecam_interface.cpp | 981 ------------------------------ slicecamd/slicecam_interface.h | 169 +----- 6 files changed, 1208 insertions(+), 1150 deletions(-) create mode 100644 slicecamd/guimanager.h create mode 100644 slicecamd/slicecam_camera.cpp create mode 100644 slicecamd/slicecam_camera.h diff --git a/slicecamd/CMakeLists.txt b/slicecamd/CMakeLists.txt index ecfcfcd0..46009b65 100644 --- a/slicecamd/CMakeLists.txt +++ b/slicecamd/CMakeLists.txt @@ -34,9 +34,10 @@ include_directories( ${PROJECT_BASE_DIR}/tcsd ) include_directories( ${PYTHON_DEV} ) add_executable(slicecamd - ${SLICECAMD_DIR}/slicecamd.cpp - ${SLICECAMD_DIR}/slicecam_server.cpp - ${SLICECAMD_DIR}/slicecam_interface.cpp + ${SLICECAMD_DIR}/slicecamd.cpp + ${SLICECAMD_DIR}/slicecam_server.cpp + ${SLICECAMD_DIR}/slicecam_interface.cpp + ${SLICECAMD_DIR}/slicecam_camera.cpp ${SLICECAMD_DIR}/slicecam_fits.cpp ${PYTHON_DEV} ) diff --git a/slicecamd/guimanager.h b/slicecamd/guimanager.h new file mode 100644 index 00000000..2758fe41 --- /dev/null +++ b/slicecamd/guimanager.h @@ -0,0 +1,121 @@ +/** --------------------------------------------------------------------------- + * @file guimanager.h + * @brief slicecam display GUI manager + * @author David Hale + * + */ + +#pragma once + +/***** Slicecam ***************************************************************/ +/** + * @namespace Slicecam + * @brief namespace for slicer cameras + * + */ +namespace Slicecam { + + /***** Slicecam::GUIManager *************************************************/ + /** + * @class GUIManager + * @brief defines functions and settings for the display GUI + * + */ + class GUIManager { + private: + const std::string camera_name = "slicev"; + std::atomic update; ///push_settings=sh; } + + // sets the private variable push_image, call on config + inline void set_push_image( std::string sh ) { this->push_image=sh; } + + // sets the update flag true + inline void set_update() { this->update.store( true ); } + + /** + * @fn get_update + * @brief returns the update flag then clears it + * @return boolean true|false + */ + inline bool get_update() { return this->update.exchange( false ); } + + /** + * @fn get_message_string + * @brief returns a formatted message of all gui settings + * @details This message is the return string to guideset command. + * @return string in form of + */ + std::string get_message_string() { + std::ostringstream oss; + if ( this->exptime < 0 ) oss << "ERR"; else { oss << std::fixed << std::setprecision(3) << this->exptime; } + oss << " "; + if ( this->gain < 1 ) oss << "ERR"; else { oss << std::fixed << std::setprecision(3) << this->gain; } + oss << " "; + if ( this->bin < 1 ) oss << "x"; else { oss << std::fixed << std::setprecision(3) << this->bin; } + oss << " "; + if ( std::isnan(this->navg) ) oss << "NaN"; else { oss << std::fixed << std::setprecision(2) << this->navg; } + return oss.str(); + } + + /** + * @brief calls the push_settings script with the formatted message string + * @details the script pushes the settings to the Guider GUI + */ + void push_gui_settings() { + const std::string function("Slicecam::GUIManager::push_gui_settings"); + std::ostringstream cmd; + cmd << push_settings << " " + << ( get_update() ? "true" : "false" ) << " " + << get_message_string(); + + if ( std::system( cmd.str().c_str() ) && errno!=ECHILD ) { + logwrite( function, "ERROR updating GUI" ); + } + } + + void send_fifo_warning(const std::string &message) { + const std::string fifo_name("/tmp/.slicev_warning.fifo"); + std::ofstream fifo(fifo_name); + if (!fifo.is_open()) { + logwrite("Slicecam::GUIManager::send_fifo_warning", "failed to open " + fifo_name + " for writing"); + } + else { + fifo << message << std::endl; + fifo.close(); + } + } + + /** + * @brief calls the push_image script with the formatted message string + * @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 ) { + const std::string function("Slicecam::GUIManager::push_gui_image"); + std::ostringstream cmd; + cmd << push_image << " " + << camera_name << " " + << filename; + + if ( std::system( cmd.str().c_str() ) && errno!=ECHILD ) { + logwrite( function, "ERROR pushing image to GUI" ); + } + } + }; + /***** Slicecam::GUIManager *************************************************/ +} diff --git a/slicecamd/slicecam_camera.cpp b/slicecamd/slicecam_camera.cpp new file mode 100644 index 00000000..56e4c068 --- /dev/null +++ b/slicecamd/slicecam_camera.cpp @@ -0,0 +1,995 @@ +/** + * @file slicecam_camera.cpp + * @brief this contains the implementation for Slicecam::Camera code + * @author David Hale + * + * This file contains the code for the Camera class in the Slicecam namespace, + * which deals directly with the camera. + * + */ + +#include "slicecam_camera.h" + +namespace Slicecam { + + /***** Slicecam::Camera::emulator *******************************************/ + /** + * @brief enable/disable Andor emulator + * @param[in] args optional state { ? help true false } + * @param[out] retstring return status { true false } + * @return ERROR | NO_ERROR | HELP + * + */ + long Camera::emulator( std::string args, std::string &retstring ) { + std::string function = "Slicecam::Camera::emulator"; + std::stringstream message; + + // Help + // + if ( args == "?" || args == "help" ) { + retstring = SLICECAMD_EMULATOR; + retstring.append( " [ ] [ true | false ]\n" ); + retstring.append( " Enable Andor emulator.\n" ); + retstring.append( " If the optional is omitted then command applies to both cameras.\n" ); + retstring.append( " If the optional { true false } argument is omitted then the current\n" ); + retstring.append( " state is returned.\n" ); + return HELP; + } + + std::vector tokens; + + Tokenize( args, tokens, " " ); + + if ( tokens.size() == 0 ) { + } + else + if ( tokens.size() == 1 ) { + } + else + if ( tokens.size() == 2 ) { + } + else { + } + + // If the STL map of Andors is empty then something went wrong + // with the configuration. + // + if ( this->andor.empty() ) { + logwrite( function, "ERROR no cameras defined" ); + retstring="bad_config"; + return ERROR; + } + + // Set the Andor state + // + if ( args == "true" ) { + for ( const auto &pair : this->andor ) { + pair.second->andor_emulator( true ); + } + } + else + if ( args == "false" ) { + for ( const auto &pair : this->andor ) { + pair.second->andor_emulator( false ); + } + } + else + if ( ! args.empty() ) { + message.str(""); message << "ERROR unrecognized arg " << args << ": expected \"true\" or \"false\""; + logwrite( function, message.str() ); + return ERROR; + } + + // Set the return string + // + message.str(""); + for ( const auto &pair : this->andor ) { + std::string_view which_andor = pair.second->get_andor_object(); + if ( which_andor == "sim" ) message << "true "; + else + if ( which_andor == "sdk" ) message << "false "; + else { + retstring="unknown "; + } + } + + retstring = message.str(); + + rtrim( retstring ); + + return NO_ERROR; + } + /***** Slicecam::Camera::emulator *******************************************/ + + + /***** Slicecam::Camera::open ***********************************************/ + /** + * @brief open connection to Andor and initialize SDK + * @param[in] which optionally specify which camera to open + * @param[in] args optional args to send to camera(s) + * @return ERROR | NO_ERROR + * + */ + long Camera::open( std::string which, std::string args ) { + std::string function = "Slicecam::Camera::open"; + std::stringstream message; + long error=NO_ERROR; + + // If the STL map of Andors is empty then something went wrong + // with the configuration. + // + if ( this->andor.empty() ) { + logwrite( function, "ERROR no cameras defined" ); + return ERROR; + } + + // Get a map of camera handles, indexed by serial number. + // This must be called before open() because open() uses handles. + // + if ( this->handlemap.size() == 0 ) { + error = this->andor.begin()->second->get_handlemap( this->handlemap ); + } + + if (error==ERROR) { + logwrite( function, "ERROR no camera handles found!" ); + return ERROR; + } + + // make sure each configured Andor has an associated handle for his s/n + // + for ( const auto &pair : this->andor ) { + auto it = this->handlemap.find(pair.second->camera_info.serial_number); + if ( it == this->handlemap.end() ) { + message.str(""); message << "ERROR no camera handle found for s/n " << pair.second->camera_info.serial_number; + logwrite( function, message.str() ); + return ERROR; + } + pair.second->camera_info.handle = this->handlemap[pair.second->camera_info.serial_number]; + } + + long ret; + + // Loop through all defined Andors + // + for ( const auto &pair : this->andor ) { + + // get a copy of the Andor::DefaultValues object for + // the currently indexed andor + // + auto cfg = this->default_config[pair.first]; + + // If a "which" was specified AND it's not this one, then skip it + // + if ( !which.empty() && pair.first != which ) continue; + + // otherwise, open this camera if not already open + // + if ( !pair.second->is_open() ) { + if ( ( ret=pair.second->open( pair.second->camera_info.handle )) != NO_ERROR ) { + message.str(""); message << "ERROR opening slicecam " << pair.second->camera_info.camera_name + << " S/N " << pair.second->camera_info.serial_number; + logwrite( function, message.str() ); + error = ret; // preserve the error state for the return value but try all + continue; + } + } + + // Now set up for single scan readout -- cannot software-trigger acquisition to + // support continuous readout for multiple cameras in the same process. + // + error |= pair.second->set_acquisition_mode( 1 ); // single scan + error |= pair.second->set_read_mode( 4 ); // image mode + error |= pair.second->set_vsspeed( 4.33 ); // vertical shift speed + error |= pair.second->set_hsspeed( 1.0 ); // horizontal shift speed + error |= pair.second->set_shutter( "open" ); // shutter always open + error |= pair.second->set_imrot( cfg.rotstr ); // set imrot to configured value + error |= pair.second->set_imflip( cfg.hflip, cfg.vflip ); // set imflip to configured value + error |= pair.second->set_binning( cfg.hbin, cfg.vbin ); // set binning to configured value + error |= pair.second->set_temperature( cfg.setpoint ); // set temp setpoint to configured value + error |= this->set_gain(pair.first, 1); + error |= this->set_exptime(pair.first, 1 ); + + if ( error != NO_ERROR ) { + message.str(""); message << "ERROR configuring slicecam " << pair.second->camera_info.camera_name + << " S/N " << pair.second->camera_info.serial_number; + logwrite( function, message.str() ); + } + } + + return error; + } + /***** Slicecam::Camera::open ***********************************************/ + + + /***** Slicecam::Camera::close **********************************************/ + /** + * @brief close connection to Andor + * @return ERROR or NO_ERROR + * + */ + long Camera::close() { + std::string function = "Slicecam::Camera::close"; + std::stringstream message; + long error=NO_ERROR; + + // If the STL map of Andors is empty then something went wrong + // with the configuration. + // + if ( this->andor.empty() ) { + logwrite( function, "ERROR no cameras defined" ); + return ERROR; + } + + // loop through and close all (configured) Andors + // + for ( const auto &pair : this->andor ) { + long ret = pair.second->close(); + if ( ret != NO_ERROR ) { + message.str(""); message << "ERROR closing slicecam " << pair.second->camera_info.camera_name + << " S/N " << pair.second->camera_info.serial_number; + logwrite( function, message.str() ); + error = ret; // preserve the error state for the return value + } + } + + this->handlemap.clear(); + + return error; + } + /***** Slicecam::Camera::close **********************************************/ + + + /***** Slicecam::Camera::bin ************************************************/ + /** + * @brief set camera binning + * @param[in] hbin horizontal binning factor + * @param[in] vbin vertical binning factor + * @return ERROR | NO_ERROR | HELP + * + */ + long Camera::bin( const int hbin, const int vbin ) { + std::string function = "Slicecam::Camera::bin"; + std::stringstream message; + long error = NO_ERROR; + + // If the STL map of Andors is empty then something went wrong + // with the configuration. + // + if ( this->andor.empty() ) { + logwrite( function, "ERROR no cameras defined" ); + return ERROR; + } + + // all configured Andors must have been initialized + // + for ( const auto &pair : this->andor ) { + if ( ! pair.second->is_open() ) { + message.str(""); message << "ERROR no connection to slicecam " << pair.second->camera_info.camera_name + << " S/N " << pair.second->camera_info.serial_number; + logwrite( function, message.str() ); + error=ERROR; + } + } + if ( error==ERROR ) return ERROR; + + // Set the binning parameters now for each sequentially + // + for ( auto &[name, cam] : this->andor ) { + error |= cam->set_binning( hbin, vbin ); + } + + return error; + } + /***** Slicecam::Camera::bin ************************************************/ + + + /***** Slicecam::Camera::set_fan ********************************************/ + /** + * @brief set fan mode + * @param[in] which { L R } + * @param[in] mode { 0 1 2 } + * @return ERROR | NO_ERROR | HELP + * + */ + long Camera::set_fan( std::string which, int mode ) { + std::string function = "Slicecam::Camera::set_fan"; + std::stringstream message; + + // make sure requested camera is in the map + // + auto it = this->andor.find( which ); + if ( it == this->andor.end() ) { + message.str(""); message << "ERROR invalid camera name \"" << which << "\""; + logwrite( function, message.str() ); + return ERROR; + } + + // make sure requested camera is open + // + if ( ! this->andor[which]->is_open() ) { + message.str(""); message << "ERROR no connection to slicecam " << which + << " S/N " << this->andor[which]->camera_info.serial_number; + logwrite( function, message.str() ); + return ERROR; + } + + // set the mode + // + return this->andor[which]->set_fan( mode ); + } + /***** Slicecam::Camera::set_fan ********************************************/ + + + /***** Slicecam::Camera::imflip *********************************************/ + /** + * @brief set or get camera image flip + * @param[in] args optionally contains (0=false 1=true) + * @param[out] retstring return string contains + * @return ERROR | NO_ERROR | HELP + * + */ + long Camera::imflip( std::string args, std::string &retstring ) { + std::string function = "Slicecam::Camera::imflip"; + std::stringstream message; + long error = NO_ERROR; + + // Help + // + if ( args == "?" || args == "help" ) { + retstring = SLICECAMD_IMFLIP; + retstring.append( " [ ]\n" ); + retstring.append( " Set or get CCD image flip for camera = L | R.\n" ); + retstring.append( " and indicate to flip horizontally and\n" ); + retstring.append( " vertically, respectively. Set these =1 to enable flipping,\n" ); + retstring.append( " or =0 to disable flipping the indicated axis. When setting\n" ); + retstring.append( " either, both must be supplied. If both omitted then the\n" ); + retstring.append( " current flip states are returned.\n" ); + return HELP; + } + + // If the STL map of Andors is empty then something went wrong + // with the configuration. + // + if ( this->andor.empty() ) { + logwrite( function, "ERROR no cameras defined" ); + retstring="bad_config"; + return ERROR; + } + + // tokenize the args to get the camera name and the flip args + // + std::vector tokens; + Tokenize( args, tokens, " " ); + + if ( tokens.size() != 3 ) { + logwrite( function, "ERROR expected 3 args L|R " ); + retstring="invalid_argument"; + return ERROR; + } + + std::string which; + int hflip, vflip; + + try { + which = tokens.at(0); + hflip = std::stoi(tokens.at(1)); + vflip = std::stoi(tokens.at(2)); + } + catch ( const std::exception &e ) { + message.str(""); message << "ERROR processing args: " << e.what(); + logwrite( function, message.str() ); + retstring="argument_exception"; + return ERROR; + } + + // make sure requested camera is in the map + // + auto it = this->andor.find( which ); + if ( it == this->andor.end() ) { + message.str(""); message << "ERROR invalid camera name \"" << which << "\""; + logwrite( function, message.str() ); + retstring="invalid_argument"; + return ERROR; + } + + // make sure the requested camera is open + // + if ( ! this->andor[which]->is_open() ) { + message.str(""); message << "ERROR no connection to slicecam " << which + << " S/N " << this->andor[which]->camera_info.serial_number; + logwrite( function, message.str() ); + retstring="not_open"; + return ERROR; + } + + // perform the flip + // + error = this->andor[which]->set_imflip( hflip, vflip ); + + if ( error == NO_ERROR ) { + hflip = this->andor[which]->camera_info.hflip; + vflip = this->andor[which]->camera_info.vflip; + } + + message.str(""); message << hflip << " " << vflip; + retstring = message.str(); + logwrite( function, retstring ); + + return error; + } + /***** Slicecam::Camera::imflip *********************************************/ + + + /***** Slicecam::Camera::imrot **********************************************/ + /** + * @brief set camera image rotation + * @param[in] args optionally contains "cw" or "ccw" + * @param[out] retstring return string contains + * @return ERROR | NO_ERROR | HELP + * + */ + long Camera::imrot( std::string args, std::string &retstring ) { + std::string function = "Slicecam::Camera::imrot"; + std::stringstream message; + long error = NO_ERROR; + + // Help + // + if ( args == "?" || args == "help" ) { + retstring = SLICECAMD_IMROT; + retstring.append( " [ ]\n" ); + retstring.append( " Set CCD image rotation for camera where is { none cw ccw }\n" ); + retstring.append( " is L | R\n" ); + retstring.append( " and \"cw\" will rotate 90 degrees clockwise,\n" ); + retstring.append( " \"ccw\" will rotate 90 degrees counter-clockwise,\n" ); + retstring.append( " \"none\" will set the rotation to none.\n" ); + retstring.append( " If used in conjuction with \"" + SLICECAMD_IMFLIP + "\" the rotation will\n" ); + retstring.append( " occur before the flip regardless of which order the commands are\n" ); + retstring.append( " sent. 180 degree rotation can be achieved using the \"" + SLICECAMD_IMFLIP + "\"\n" ); + retstring.append( " command by selecting both horizontal and vertical flipping.\n" ); + return HELP; + } + + // If the STL map of Andors is empty then something went wrong + // with the configuration. + // + if ( this->andor.empty() ) { + logwrite( function, "ERROR no cameras defined" ); + retstring="bad_config"; + return ERROR; + } + + // tokenize the args to get the camera name and the rot arg + // + std::vector tokens; + Tokenize( args, tokens, " " ); + + if ( tokens.size() != 2 ) { + logwrite( function, "ERROR expected 2 args L|R " ); + retstring="invalid_argument"; + return ERROR; + } + + std::string which; + int rotdir; + + // assign the numeric rotdir value from the string argument + // + try { + which = tokens.at(0); + // convert to lowercase + std::transform( tokens.at(1).begin(), tokens.at(1).end(), tokens.at(1).begin(), ::tolower ); + if ( tokens.at(1) == "none" ) rotdir = 0; + else + if ( tokens.at(1) == "cw" ) rotdir = 1; + else + if ( tokens.at(1) == "ccw" ) rotdir = 2; + else { + message.str(""); message << "ERROR bad arg " << tokens.at(1) << ": expected { none cw ccw }"; + logwrite( function, message.str() ); + return ERROR; + } + } + catch ( const std::exception &e ) { + message.str(""); message << "ERROR processing args: " << e.what(); + logwrite( function, message.str() ); + retstring="argument_exception"; + return ERROR; + } + + // make sure requested camera is in the map + // + auto it = this->andor.find( which ); + if ( it == this->andor.end() ) { + message.str(""); message << "ERROR invalid camera name \"" << which << "\""; + logwrite( function, message.str() ); + retstring="invalid_argument"; + return ERROR; + } + + // make sure requested camera is open + // + if ( ! this->andor[which]->is_open() ) { + message.str(""); message << "ERROR no connection to slicecam " << which + << " S/N " << this->andor[which]->camera_info.serial_number; + logwrite( function, message.str() ); + retstring="not_open"; + return ERROR; + } + + // perform the image rotation + // + error = this->andor[which]->set_imrot( rotdir ); + + return error; + } + /***** Slicecam::Camera::imrot **********************************************/ + + + /***** Slicecam::Camera::set_exptime ****************************************/ + /** + * @brief set exposure time + * @details This will stop an acquisition in progress before setting the + * exposure time. The actual exposure time is returned in the + * reference argument. + * @param[in] fval reference to exposure time + * @return ERROR | NO_ERROR + * + * This function is overloaded + * + */ + long Camera::set_exptime( float &fval ) { + return set_exptime("", fval); + } + /***** Slicecam::Camera::set_exptime ****************************************/ + long Camera::set_exptime( std::string which, float &fval ) { + + long error = NO_ERROR; + + for ( const auto &pair : this->andor ) { + // If a "which" was specified AND it's not this one, then skip it + // + if ( !which.empty() && pair.first != which ) continue; + + // Ensure aquisition has stopped + // + error |= pair.second->abort_acquisition(); + + // Set the exposure time on the Andor. + // This will modify val with actual exptime. + // + if (error==NO_ERROR) error |= pair.second->set_exptime( fval ); + std::stringstream message; + message.str(""); message << "[DEBUG] set exptime to " << fval + << " for camera " << pair.second->camera_info.camera_name; + logwrite( "Slicecam::Camera::set_exptime", message.str() ); + } + + return error; + } + /***** Slicecam::Camera::set_exptime ****************************************/ + /** + * @brief set exposure time + * @details This overloaded version takes an rvalue reference to accept a + * temporary float used to call the other set_exptime function. + * Use this to set exptime with an rvalue instead of an lvalue. + * @param[in] fval rvalue reference to exposure time + * @return ERROR | NO_ERROR + */ + long Camera::set_exptime( float &&fval ) { + return set_exptime("", fval); + } + long Camera::set_exptime( std::string which, float &&fval ) { + float retval=fval; + return set_exptime(which, retval); + } + /***** Slicecam::Camera::set_exptime ****************************************/ + + + /***** Slicecam::Camera::set_gain *******************************************/ + /** + * @brief set or get the CCD gain + * @details The output amplifier is automatically set based on gain. + * If gain=1 then set to conventional amp and if gain > 1 + * then set the EMCCD gain register. + * @param[in] args optionally contains new gain + * @param[out] retstring return string contains temp, setpoint, and status + * @return ERROR | NO_ERROR | HELP + * + */ + long Camera::set_gain( int &gain ) { + return set_gain("", gain); + } + long Camera::set_gain( std::string which, int &gain ) { + std::string function = "Slicecam::Camera::set_gain"; + std::stringstream message; + long error = NO_ERROR; + + // get gain range + // + int low=999, high=-1; + error = this->andor.begin()->second->get_emgain_range( low, high ); + + // Loop through all defined Andors + // + for ( const auto &pair : this->andor ) { + // If a "which" was specified AND it's not this one, then skip it + // + if ( !which.empty() && pair.first != which ) continue; + + // camera must be open + // + if ( ! pair.second->is_open() ) { + message.str(""); message << "ERROR no connection to slicecam " << pair.second->camera_info.camera_name + << " S/N " << pair.second->camera_info.serial_number; + logwrite( function, message.str() ); + error=ERROR; + continue; + } + + message.str(""); message << "[DEBUG] set gain to " << gain + << " for camera " << pair.second->camera_info.camera_name; + logwrite( function, message.str() ); + + if ( error==NO_ERROR && gain == 1 ) { + error = pair.second->set_output_amplifier( Andor::AMPTYPE_CONV ); + if (error==NO_ERROR) { + for ( const auto &pair : this->andor ) { + pair.second->camera_info.gain = 1; + } + } + else { message << "ERROR gain not set"; } + } + else + if ( error==NO_ERROR && gain >= low && gain <= high ) { + error |= pair.second->set_output_amplifier( Andor::AMPTYPE_EMCCD ); + if (error==NO_ERROR) pair.second->set_emgain( gain ); + if (error==NO_ERROR) pair.second->camera_info.gain = gain; + else { message << "ERROR gain not set"; } + } + else + if ( error==NO_ERROR ) { + message.str(""); message << "ERROR: gain " << gain << " outside range { 1, " + << low << ":" << high << " }"; + error = ERROR; + } + if ( !message.str().empty() ) logwrite( function, message.str() ); + + // The image gets flipped when the EM gain is used. + // This flips it back. + // + if (error==NO_ERROR && pair.first=="L") { + if (gain>1) { + error=this->andor["L"]->set_imflip( (default_config["L"].hflip==1?0:1), default_config["L"].vflip ); + } + else error=this->andor["L"]->set_imflip( default_config["L"].hflip, default_config["L"].vflip ); + } + if (error==NO_ERROR && pair.first=="R") { + if (gain>1) { + error=this->andor["R"]->set_imflip( (default_config["R"].hflip==1?0:1), default_config["R"].vflip ); + } + else error=this->andor["R"]->set_imflip( default_config["R"].hflip, default_config["R"].vflip ); + } + } + + // Regardless of setting the gain, always return what's in the camera + // + gain = this->andor.begin()->second->camera_info.gain; + + return error; + } + long Camera::set_gain( int &&gain ) { + return set_gain("", gain); + } + long Camera::set_gain( std::string which, int &&gain ) { + return set_gain(which, gain); + } + /***** Slicecam::Camera::set_gain *******************************************/ + + + /***** Slicecam::Camera::speed **********************************************/ + /** + * @brief set or get the CCD clocking speeds + * @param[in] args optionally contains new clocking speeds + * @param[out] retstring return string contains clocking speeds + * @return ERROR | NO_ERROR | HELP + * + */ + long Camera::speed( std::string args, std::string &retstring ) { + std::string function = "Slicecam::Camera::speed"; + std::stringstream message; + long error = NO_ERROR; + float hori=-1, vert=-1; + + // Help + // + if ( args == "?" || args == "help" ) { + retstring = SLICECAMD_SPEED; + retstring.append( " [ ]\n" ); + retstring.append( " Set or get CCD clocking speeds for horizontal and vertical \n" ); + retstring.append( " If speeds are omitted then the current speeds are returned.\n" ); +/*** + if ( !this->andor.empty() && this->andor.begin()->second->is_open() ) { + auto cam = this->andor.begin()->second; // make a smart pointer to the first andor in the map + retstring.append( " Current amp type is " ); + retstring.append( ( cam->camera_info.amptype == Andor::AMPTYPE_EMCCD ? "EMCCD\n" : "conventional\n" ) ); + retstring.append( " Select from {" ); + for ( const auto &hspeed : cam->camera_info.hsspeeds[ cam->camera_info.amptype] ) { + retstring.append( " " ); + retstring.append( std::to_string( hspeed ) ); + } + retstring.append( " }\n" ); + retstring.append( " Select from {" ); + for ( const auto &vspeed : cam->camera_info.vsspeeds ) { + retstring.append( " " ); + retstring.append( std::to_string( vspeed ) ); + } + retstring.append( " }\n" ); + retstring.append( " Units are MHz\n" ); + } + else { + retstring.append( " Open connection to camera to see possible speeds.\n" ); + } +***/ + return HELP; + } + + // If the STL map of Andors is empty then something went wrong + // with the configuration. + // + if ( this->andor.empty() ) { + logwrite( function, "ERROR no cameras defined" ); + retstring="bad_config"; + return ERROR; + } + + // all configured Andors must have been initialized + // +// for ( const auto &pair : this->andor ) { +// if ( ! pair.second->is_open() ) { + for ( auto &[name, cam] : this->andor ) { + if ( ! cam->is_open() ) { + message.str(""); message << "ERROR no connection to slicecam " << cam->camera_info.camera_name + << " S/N " << cam->camera_info.serial_number; + logwrite( function, message.str() ); + error=ERROR; + } + } + if ( error==ERROR ) return ERROR; + + // Parse args if present + // + if ( !args.empty() ) { + + std::vector tokens; + Tokenize( args, tokens, " " ); + + // There must be only two args (the speeds) + // + if ( tokens.size() != 2 ) { + logwrite( function, "ERROR expected speeds" ); + return ERROR; + } + + // Parse the gain from the token + // + try { + hori = std::stof( tokens.at(0) ); + vert = std::stof( tokens.at(1) ); + } + catch ( std::out_of_range &e ) { + message.str(""); message << "ERROR reading speeds: " << e.what(); + error = ERROR; + } + catch ( std::invalid_argument &e ) { + message.str(""); message << "ERROR reading speeds: " << e.what(); + error = ERROR; + } + if (error==ERROR) logwrite( function, message.str() ); + + for ( const auto &pair : this->andor ) { + if (error!=ERROR ) error = pair.second->set_hsspeed( hori ); + if (error!=ERROR ) error = pair.second->set_vsspeed( vert ); + } + } + + message.str(""); + + for ( auto &[name, cam] : this->andor ) { + if ( ( cam->camera_info.hspeed < 0 ) || + ( cam->camera_info.vspeed < 0 ) ) { + message.str(""); message << "ERROR speeds not set for camera " << cam->camera_info.camera_name; + logwrite( function, message.str() ); + error = ERROR; + } + + message << cam->camera_info.camera_name << " " + << cam->camera_info.hspeed << " " << cam->camera_info.vspeed << " "; + } + + retstring = message.str(); + logwrite( function, retstring ); + + return error; + } + /***** Slicecam::Camera::speed **********************************************/ + + + /***** Slicecam::Camera::temperature ****************************************/ + /** + * @brief set or get the camera temperature setpoint + * @param[in] args optionally contains new setpoint + * @param[out] retstring return string contains + * @return ERROR | NO_ERROR | HELP + * + */ + long Camera::temperature( std::string args, std::string &retstring ) { + std::string function = "Slicecam::Camera::temperature"; + std::stringstream message; + long error = NO_ERROR; + int temp = 999; + + // Help + // + if ( args == "?" || args == "help" ) { + retstring = SLICECAMD_TEMP; + retstring.append( " [ ]\n" ); + retstring.append( " Set or get camera temperature in integer degrees C,\n" ); +/*** + if ( !this->andor.empty() && this->andor.begin()->second->is_open() ) { + auto cam = this->andor.begin()->second; // make a smart pointer to the first andor in the map + retstring.append( " where is in range { " ); + retstring.append( std::to_string( cam->camera_info.temp_min ) ); + retstring.append( " " ); + retstring.append( std::to_string( cam->camera_info.temp_max ) ); + retstring.append( " }.\n" ); + } + else { + retstring.append( " open connection to camera to see acceptable range.\n" ); + } +***/ + retstring.append( " If optional is provided then the camera setpoint is changed,\n" ); + retstring.append( " else the current temperature, setpoint, and status are returned.\n" ); + retstring.append( " Format of return value is \n" ); + retstring.append( " Camera cooling is turned on/off automatically, as needed.\n" ); + return HELP; + } + + // If the STL map of Andors is empty then something went wrong + // with the configuration. + // + if ( this->andor.empty() ) { + logwrite( function, "ERROR no cameras defined" ); + retstring="bad_config"; + return ERROR; + } + + // all configured Andors must have been initialized + // + for ( const auto &pair : this->andor ) { + if ( ! pair.second->is_open() ) { + message.str(""); message << "ERROR no connection to slicecam " << pair.second->camera_info.camera_name + << " S/N " << pair.second->camera_info.serial_number; + logwrite( function, message.str() ); + error=ERROR; + } + } + if ( error==ERROR ) return ERROR; + + // Parse args if present + // + if ( !args.empty() ) { + + std::vector tokens; + Tokenize( args, tokens, " " ); + + // There can be only one arg (the requested temperature) + // + if ( tokens.size() != 1 ) { + logwrite( function, "ERROR too many arguments" ); + return ERROR; + } + + // Convert the temperature to int and set the temperature. + // Cooling will be automatically enabled/disabled as needed. + // + try { + temp = static_cast( std::round( std::stof( tokens.at(0) ) ) ); + for ( const auto &pair : this->andor ) { + message.str(""); message << "[DEBUG] set temp to " << temp + << " for camera " << pair.second->camera_info.camera_name; + logwrite( function, message.str() ); + error |= pair.second->set_temperature( temp ); + } + } + catch ( const std::exception &e ) { + message.str(""); message << "ERROR setting temperature: " << e.what(); + error = ERROR; + } + } + if (error==ERROR) logwrite( function, message.str() ); + + // Regardless of setting the temperature, always read it. + // + message.str(""); + for ( const auto &pair : this->andor ) { + error |= pair.second->get_temperature(temp); + message << pair.second->camera_info.camera_name << " " << temp << " " + << pair.second->camera_info.setpoint << " " + << pair.second->camera_info.temp_status << " "; + } + logwrite( function, message.str() ); + + retstring = message.str(); + + return error; + } + /***** Slicecam::Camera::temperature ****************************************/ + + + /***** Slicecam::Camera::write_frame ****************************************/ + /** + * @brief write the Andor image data to FITS file + * @return ERROR or NO_ERROR + * + */ + long Camera::write_frame( std::string source_file, std::string &outfile, const bool _tcs_online ) { + std::string function = "Slicecam::Camera::write_frame"; + std::stringstream message; + + long error = NO_ERROR; + + fitsinfo.fits_name = outfile; +// fitsinfo.datatype = USHORT_IMG; + fitsinfo.datatype = FLOAT_IMG; + + fits_file.copy_info( fitsinfo ); // copy from fitsinfo to the fits_file class + + error = fits_file.open_file(); // open the fits file for writing + + if ( !source_file.empty() ) { + if (error==NO_ERROR) error = fits_file.copy_header_from( source_file ); + } + else { + if (error==NO_ERROR) error = fits_file.create_header(); // create basic header + } + + for ( auto &[name, cam] : this->andor ) { + cam->camera_info.section_size = cam->camera_info.axes[0] * cam->camera_info.axes[1]; + if ( cam->camera_info.section_size == 0 ) { + message.str(""); message << "ERROR section size 0 for slicecam " << cam->camera_info.camera_name; + logwrite( function, message.str() ); + error = ERROR; + break; + } + // cam is passed by reference + // + fits_file.write_image( cam ); // write the image data + } + + fits_file.close_file(); // close the file + + // This is the one extra call that is outside the normal workflow. + // If emulator is enabled then the skysim generator will create a simulated + // image. The image written above by fits_file.write_image() is used as + // input to skysim because it contains the correct WCS headers, but will + // ultimately be overwritten by the simulated image. + // + // Need only to make one call since it will generate a multi-extension + // image. + // + if ( !this->andor.empty() ) { + if ( this->andor.begin()->second->is_emulated() && _tcs_online ) { + this->andor.begin()->second->simulate_frame( fitsinfo.fits_name, + true, // multi-extension + this->simsize ); + } + } + + outfile = fitsinfo.fits_name; + + return error; + } + /***** Slicecam::Camera::write_frame ****************************************/ + +} diff --git a/slicecamd/slicecam_camera.h b/slicecamd/slicecam_camera.h new file mode 100644 index 00000000..d20b3e4c --- /dev/null +++ b/slicecamd/slicecam_camera.h @@ -0,0 +1,85 @@ +/** --------------------------------------------------------------------------- + * @file slicecam_camera.h + * @brief slicecam camera include + * @details defines the Slicecam Camera class + * @author David Hale + * + */ + +#pragma once + +#include +#include +#include +#include +#include + +#include "common.h" +#include "logentry.h" +#include "slicecam_fits.h" +#include "slicecamd_commands.h" + +/***** Slicecam ***************************************************************/ +/** + * @namespace Slicecam + * @brief namespace for slicer cameras + * + */ +namespace Slicecam { + + /***** Slicecam::Camera *****************************************************/ + /** + * @class Camera + * @brief Camera class + * + * This class is used for communicating with the slicecam camera directly (which is an Andor) + * + */ + class Camera { + private: + uint16_t* image_data; + int simsize; /// for the sky simulator + std::map handlemap; + + public: + Camera() : image_data( nullptr ), simsize(1024) { }; + + FITS_file fits_file; /// instantiate a FITS container object + FitsInfo fitsinfo; + + std::mutex framegrab_mutex; + + std::map> andor; ///< container for Andor::Interface objects + + std::map default_config; ///< container to hold defaults for each camera + + inline void copy_info() { fits_file.copy_info( fitsinfo ); } + inline void set_simsize( int val ) { if ( val > 0 ) this->simsize = val; else throw std::out_of_range("simsize must be greater than 0"); } + + inline long init_handlemap() { + this->handlemap.clear(); + return this->andor.begin()->second->get_handlemap( this->handlemap ); + } + + long emulator( std::string args, std::string &retstring ); + long open( std::string which, std::string args ); + long close(); + long get_frame(); + long write_frame( std::string source_file, std::string &outfile, const bool _tcs_online ); + long bin( const int hbin, const int vbin ); + long set_fan( std::string which, int mode ); + long imflip( std::string args, std::string &retstring ); + long imrot( std::string args, std::string &retstring ); + long set_gain( int &gain ); + long set_gain( std::string which, int &gain ); + long set_gain( int &&gain ); + long set_gain( std::string which, int &&gain ); + long set_exptime( float &val ); + long set_exptime( std::string which, float &val ); + long set_exptime( float &&val ); + long set_exptime( std::string which, float &&val ); + long speed( std::string args, std::string &retstring ); + long temperature( std::string args, std::string &retstring ); + }; + /***** Slicecam::Camera *****************************************************/ +} diff --git a/slicecamd/slicecam_interface.cpp b/slicecamd/slicecam_interface.cpp index c793944f..ed1b3712 100644 --- a/slicecamd/slicecam_interface.cpp +++ b/slicecamd/slicecam_interface.cpp @@ -16,987 +16,6 @@ namespace Slicecam { int npreserve=0; ///< counter used for Interface::preserve_framegrab() - /***** Slicecam::Camera::emulator *******************************************/ - /** - * @brief enable/disable Andor emulator - * @param[in] args optional state { ? help true false } - * @param[out] retstring return status { true false } - * @return ERROR | NO_ERROR | HELP - * - */ - long Camera::emulator( std::string args, std::string &retstring ) { - std::string function = "Slicecam::Camera::emulator"; - std::stringstream message; - - // Help - // - if ( args == "?" || args == "help" ) { - retstring = SLICECAMD_EMULATOR; - retstring.append( " [ ] [ true | false ]\n" ); - retstring.append( " Enable Andor emulator.\n" ); - retstring.append( " If the optional is omitted then command applies to both cameras.\n" ); - retstring.append( " If the optional { true false } argument is omitted then the current\n" ); - retstring.append( " state is returned.\n" ); - return HELP; - } - - std::vector tokens; - - Tokenize( args, tokens, " " ); - - if ( tokens.size() == 0 ) { - } - else - if ( tokens.size() == 1 ) { - } - else - if ( tokens.size() == 2 ) { - } - else { - } - - // If the STL map of Andors is empty then something went wrong - // with the configuration. - // - if ( this->andor.empty() ) { - logwrite( function, "ERROR no cameras defined" ); - retstring="bad_config"; - return ERROR; - } - - // Set the Andor state - // - if ( args == "true" ) { - for ( const auto &pair : this->andor ) { - pair.second->andor_emulator( true ); - } - } - else - if ( args == "false" ) { - for ( const auto &pair : this->andor ) { - pair.second->andor_emulator( false ); - } - } - else - if ( ! args.empty() ) { - message.str(""); message << "ERROR unrecognized arg " << args << ": expected \"true\" or \"false\""; - logwrite( function, message.str() ); - return ERROR; - } - - // Set the return string - // - message.str(""); - for ( const auto &pair : this->andor ) { - std::string_view which_andor = pair.second->get_andor_object(); - if ( which_andor == "sim" ) message << "true "; - else - if ( which_andor == "sdk" ) message << "false "; - else { - retstring="unknown "; - } - } - - retstring = message.str(); - - rtrim( retstring ); - - return NO_ERROR; - } - /***** Slicecam::Camera::emulator *******************************************/ - - - /***** Slicecam::Camera::open ***********************************************/ - /** - * @brief open connection to Andor and initialize SDK - * @param[in] which optionally specify which camera to open - * @param[in] args optional args to send to camera(s) - * @return ERROR | NO_ERROR - * - */ - long Camera::open( std::string which, std::string args ) { - std::string function = "Slicecam::Camera::open"; - std::stringstream message; - long error=NO_ERROR; - - // If the STL map of Andors is empty then something went wrong - // with the configuration. - // - if ( this->andor.empty() ) { - logwrite( function, "ERROR no cameras defined" ); - return ERROR; - } - - // Get a map of camera handles, indexed by serial number. - // This must be called before open() because open() uses handles. - // - if ( this->handlemap.size() == 0 ) { - error = this->andor.begin()->second->get_handlemap( this->handlemap ); - } - - if (error==ERROR) { - logwrite( function, "ERROR no camera handles found!" ); - return ERROR; - } - - // make sure each configured Andor has an associated handle for his s/n - // - for ( const auto &pair : this->andor ) { - auto it = this->handlemap.find(pair.second->camera_info.serial_number); - if ( it == this->handlemap.end() ) { - message.str(""); message << "ERROR no camera handle found for s/n " << pair.second->camera_info.serial_number; - logwrite( function, message.str() ); - return ERROR; - } - pair.second->camera_info.handle = this->handlemap[pair.second->camera_info.serial_number]; - } - - long ret; - - // Loop through all defined Andors - // - for ( const auto &pair : this->andor ) { - - // get a copy of the Andor::DefaultValues object for - // the currently indexed andor - // - auto cfg = this->default_config[pair.first]; - - // If a "which" was specified AND it's not this one, then skip it - // - if ( !which.empty() && pair.first != which ) continue; - - // otherwise, open this camera if not already open - // - if ( !pair.second->is_open() ) { - if ( ( ret=pair.second->open( pair.second->camera_info.handle )) != NO_ERROR ) { - message.str(""); message << "ERROR opening slicecam " << pair.second->camera_info.camera_name - << " S/N " << pair.second->camera_info.serial_number; - logwrite( function, message.str() ); - error = ret; // preserve the error state for the return value but try all - continue; - } - } - - // Now set up for single scan readout -- cannot software-trigger acquisition to - // support continuous readout for multiple cameras in the same process. - // - error |= pair.second->set_acquisition_mode( 1 ); // single scan - error |= pair.second->set_read_mode( 4 ); // image mode - error |= pair.second->set_vsspeed( 4.33 ); // vertical shift speed - error |= pair.second->set_hsspeed( 1.0 ); // horizontal shift speed - error |= pair.second->set_shutter( "open" ); // shutter always open - error |= pair.second->set_imrot( cfg.rotstr ); // set imrot to configured value - error |= pair.second->set_imflip( cfg.hflip, cfg.vflip ); // set imflip to configured value - error |= pair.second->set_binning( cfg.hbin, cfg.vbin ); // set binning to configured value - error |= pair.second->set_temperature( cfg.setpoint ); // set temp setpoint to configured value - error |= this->set_gain(pair.first, 1); - error |= this->set_exptime(pair.first, 1 ); - - if ( error != NO_ERROR ) { - message.str(""); message << "ERROR configuring slicecam " << pair.second->camera_info.camera_name - << " S/N " << pair.second->camera_info.serial_number; - logwrite( function, message.str() ); - } - } - - return error; - } - /***** Slicecam::Camera::open ***********************************************/ - - - /***** Slicecam::Camera::close **********************************************/ - /** - * @brief close connection to Andor - * @return ERROR or NO_ERROR - * - */ - long Camera::close() { - std::string function = "Slicecam::Camera::close"; - std::stringstream message; - long error=NO_ERROR; - - // If the STL map of Andors is empty then something went wrong - // with the configuration. - // - if ( this->andor.empty() ) { - logwrite( function, "ERROR no cameras defined" ); - return ERROR; - } - - // loop through and close all (configured) Andors - // - for ( const auto &pair : this->andor ) { - long ret = pair.second->close(); - if ( ret != NO_ERROR ) { - message.str(""); message << "ERROR closing slicecam " << pair.second->camera_info.camera_name - << " S/N " << pair.second->camera_info.serial_number; - logwrite( function, message.str() ); - error = ret; // preserve the error state for the return value - } - } - - this->handlemap.clear(); - - return error; - } - /***** Slicecam::Camera::close **********************************************/ - - - /***** Slicecam::Camera::bin ************************************************/ - /** - * @brief set camera binning - * @param[in] hbin horizontal binning factor - * @param[in] vbin vertical binning factor - * @return ERROR | NO_ERROR | HELP - * - */ - long Camera::bin( const int hbin, const int vbin ) { - std::string function = "Slicecam::Camera::bin"; - std::stringstream message; - long error = NO_ERROR; - - // If the STL map of Andors is empty then something went wrong - // with the configuration. - // - if ( this->andor.empty() ) { - logwrite( function, "ERROR no cameras defined" ); - return ERROR; - } - - // all configured Andors must have been initialized - // - for ( const auto &pair : this->andor ) { - if ( ! pair.second->is_open() ) { - message.str(""); message << "ERROR no connection to slicecam " << pair.second->camera_info.camera_name - << " S/N " << pair.second->camera_info.serial_number; - logwrite( function, message.str() ); - error=ERROR; - } - } - if ( error==ERROR ) return ERROR; - - // Set the binning parameters now for each sequentially - // - for ( auto &[name, cam] : this->andor ) { - error |= cam->set_binning( hbin, vbin ); - } - - return error; - } - /***** Slicecam::Camera::bin ************************************************/ - - - /***** Slicecam::Camera::set_fan ********************************************/ - /** - * @brief set fan mode - * @param[in] which { L R } - * @param[in] mode { 0 1 2 } - * @return ERROR | NO_ERROR | HELP - * - */ - long Camera::set_fan( std::string which, int mode ) { - std::string function = "Slicecam::Camera::set_fan"; - std::stringstream message; - - // make sure requested camera is in the map - // - auto it = this->andor.find( which ); - if ( it == this->andor.end() ) { - message.str(""); message << "ERROR invalid camera name \"" << which << "\""; - logwrite( function, message.str() ); - return ERROR; - } - - // make sure requested camera is open - // - if ( ! this->andor[which]->is_open() ) { - message.str(""); message << "ERROR no connection to slicecam " << which - << " S/N " << this->andor[which]->camera_info.serial_number; - logwrite( function, message.str() ); - return ERROR; - } - - // set the mode - // - return this->andor[which]->set_fan( mode ); - } - /***** Slicecam::Camera::set_fan ********************************************/ - - - /***** Slicecam::Camera::imflip *********************************************/ - /** - * @brief set or get camera image flip - * @param[in] args optionally contains (0=false 1=true) - * @param[out] retstring return string contains - * @return ERROR | NO_ERROR | HELP - * - */ - long Camera::imflip( std::string args, std::string &retstring ) { - std::string function = "Slicecam::Camera::imflip"; - std::stringstream message; - long error = NO_ERROR; - - // Help - // - if ( args == "?" || args == "help" ) { - retstring = SLICECAMD_IMFLIP; - retstring.append( " [ ]\n" ); - retstring.append( " Set or get CCD image flip for camera = L | R.\n" ); - retstring.append( " and indicate to flip horizontally and\n" ); - retstring.append( " vertically, respectively. Set these =1 to enable flipping,\n" ); - retstring.append( " or =0 to disable flipping the indicated axis. When setting\n" ); - retstring.append( " either, both must be supplied. If both omitted then the\n" ); - retstring.append( " current flip states are returned.\n" ); - return HELP; - } - - // If the STL map of Andors is empty then something went wrong - // with the configuration. - // - if ( this->andor.empty() ) { - logwrite( function, "ERROR no cameras defined" ); - retstring="bad_config"; - return ERROR; - } - - // tokenize the args to get the camera name and the flip args - // - std::vector tokens; - Tokenize( args, tokens, " " ); - - if ( tokens.size() != 3 ) { - logwrite( function, "ERROR expected 3 args L|R " ); - retstring="invalid_argument"; - return ERROR; - } - - std::string which; - int hflip, vflip; - - try { - which = tokens.at(0); - hflip = std::stoi(tokens.at(1)); - vflip = std::stoi(tokens.at(2)); - } - catch ( const std::exception &e ) { - message.str(""); message << "ERROR processing args: " << e.what(); - logwrite( function, message.str() ); - retstring="argument_exception"; - return ERROR; - } - - // make sure requested camera is in the map - // - auto it = this->andor.find( which ); - if ( it == this->andor.end() ) { - message.str(""); message << "ERROR invalid camera name \"" << which << "\""; - logwrite( function, message.str() ); - retstring="invalid_argument"; - return ERROR; - } - - // make sure the requested camera is open - // - if ( ! this->andor[which]->is_open() ) { - message.str(""); message << "ERROR no connection to slicecam " << which - << " S/N " << this->andor[which]->camera_info.serial_number; - logwrite( function, message.str() ); - retstring="not_open"; - return ERROR; - } - - // perform the flip - // - error = this->andor[which]->set_imflip( hflip, vflip ); - - if ( error == NO_ERROR ) { - hflip = this->andor[which]->camera_info.hflip; - vflip = this->andor[which]->camera_info.vflip; - } - - message.str(""); message << hflip << " " << vflip; - retstring = message.str(); - logwrite( function, retstring ); - - return error; - } - /***** Slicecam::Camera::imflip *********************************************/ - - - /***** Slicecam::Camera::imrot **********************************************/ - /** - * @brief set camera image rotation - * @param[in] args optionally contains "cw" or "ccw" - * @param[out] retstring return string contains - * @return ERROR | NO_ERROR | HELP - * - */ - long Camera::imrot( std::string args, std::string &retstring ) { - std::string function = "Slicecam::Camera::imrot"; - std::stringstream message; - long error = NO_ERROR; - - // Help - // - if ( args == "?" || args == "help" ) { - retstring = SLICECAMD_IMROT; - retstring.append( " [ ]\n" ); - retstring.append( " Set CCD image rotation for camera where is { none cw ccw }\n" ); - retstring.append( " is L | R\n" ); - retstring.append( " and \"cw\" will rotate 90 degrees clockwise,\n" ); - retstring.append( " \"ccw\" will rotate 90 degrees counter-clockwise,\n" ); - retstring.append( " \"none\" will set the rotation to none.\n" ); - retstring.append( " If used in conjuction with \"" + SLICECAMD_IMFLIP + "\" the rotation will\n" ); - retstring.append( " occur before the flip regardless of which order the commands are\n" ); - retstring.append( " sent. 180 degree rotation can be achieved using the \"" + SLICECAMD_IMFLIP + "\"\n" ); - retstring.append( " command by selecting both horizontal and vertical flipping.\n" ); - return HELP; - } - - // If the STL map of Andors is empty then something went wrong - // with the configuration. - // - if ( this->andor.empty() ) { - logwrite( function, "ERROR no cameras defined" ); - retstring="bad_config"; - return ERROR; - } - - // tokenize the args to get the camera name and the rot arg - // - std::vector tokens; - Tokenize( args, tokens, " " ); - - if ( tokens.size() != 2 ) { - logwrite( function, "ERROR expected 2 args L|R " ); - retstring="invalid_argument"; - return ERROR; - } - - std::string which; - int rotdir; - - // assign the numeric rotdir value from the string argument - // - try { - which = tokens.at(0); - // convert to lowercase - std::transform( tokens.at(1).begin(), tokens.at(1).end(), tokens.at(1).begin(), ::tolower ); - if ( tokens.at(1) == "none" ) rotdir = 0; - else - if ( tokens.at(1) == "cw" ) rotdir = 1; - else - if ( tokens.at(1) == "ccw" ) rotdir = 2; - else { - message.str(""); message << "ERROR bad arg " << tokens.at(1) << ": expected { none cw ccw }"; - logwrite( function, message.str() ); - return ERROR; - } - } - catch ( const std::exception &e ) { - message.str(""); message << "ERROR processing args: " << e.what(); - logwrite( function, message.str() ); - retstring="argument_exception"; - return ERROR; - } - - // make sure requested camera is in the map - // - auto it = this->andor.find( which ); - if ( it == this->andor.end() ) { - message.str(""); message << "ERROR invalid camera name \"" << which << "\""; - logwrite( function, message.str() ); - retstring="invalid_argument"; - return ERROR; - } - - // make sure requested camera is open - // - if ( ! this->andor[which]->is_open() ) { - message.str(""); message << "ERROR no connection to slicecam " << which - << " S/N " << this->andor[which]->camera_info.serial_number; - logwrite( function, message.str() ); - retstring="not_open"; - return ERROR; - } - - // perform the image rotation - // - error = this->andor[which]->set_imrot( rotdir ); - - return error; - } - /***** Slicecam::Camera::imrot **********************************************/ - - - /***** Slicecam::Camera::set_exptime ****************************************/ - /** - * @brief set exposure time - * @details This will stop an acquisition in progress before setting the - * exposure time. The actual exposure time is returned in the - * reference argument. - * @param[in] fval reference to exposure time - * @return ERROR | NO_ERROR - * - * This function is overloaded - * - */ - long Camera::set_exptime( float &fval ) { - return set_exptime("", fval); - } - /***** Slicecam::Camera::set_exptime ****************************************/ - long Camera::set_exptime( std::string which, float &fval ) { - - long error = NO_ERROR; - - for ( const auto &pair : this->andor ) { - // If a "which" was specified AND it's not this one, then skip it - // - if ( !which.empty() && pair.first != which ) continue; - - // Ensure aquisition has stopped - // - error |= pair.second->abort_acquisition(); - - // Set the exposure time on the Andor. - // This will modify val with actual exptime. - // - if (error==NO_ERROR) error |= pair.second->set_exptime( fval ); - std::stringstream message; - message.str(""); message << "[DEBUG] set exptime to " << fval - << " for camera " << pair.second->camera_info.camera_name; - logwrite( "Slicecam::Camera::set_exptime", message.str() ); - } - - return error; - } - /***** Slicecam::Camera::set_exptime ****************************************/ - /** - * @brief set exposure time - * @details This overloaded version takes an rvalue reference to accept a - * temporary float used to call the other set_exptime function. - * Use this to set exptime with an rvalue instead of an lvalue. - * @param[in] fval rvalue reference to exposure time - * @return ERROR | NO_ERROR - */ - long Camera::set_exptime( float &&fval ) { - return set_exptime("", fval); - } - long Camera::set_exptime( std::string which, float &&fval ) { - float retval=fval; - return set_exptime(which, retval); - } - /***** Slicecam::Camera::set_exptime ****************************************/ - - - /***** Slicecam::Camera::set_gain *******************************************/ - /** - * @brief set or get the CCD gain - * @details The output amplifier is automatically set based on gain. - * If gain=1 then set to conventional amp and if gain > 1 - * then set the EMCCD gain register. - * @param[in] args optionally contains new gain - * @param[out] retstring return string contains temp, setpoint, and status - * @return ERROR | NO_ERROR | HELP - * - */ - long Camera::set_gain( int &gain ) { - return set_gain("", gain); - } - long Camera::set_gain( std::string which, int &gain ) { - std::string function = "Slicecam::Camera::set_gain"; - std::stringstream message; - long error = NO_ERROR; - - // get gain range - // - int low=999, high=-1; - error = this->andor.begin()->second->get_emgain_range( low, high ); - - // Loop through all defined Andors - // - for ( const auto &pair : this->andor ) { - // If a "which" was specified AND it's not this one, then skip it - // - if ( !which.empty() && pair.first != which ) continue; - - // camera must be open - // - if ( ! pair.second->is_open() ) { - message.str(""); message << "ERROR no connection to slicecam " << pair.second->camera_info.camera_name - << " S/N " << pair.second->camera_info.serial_number; - logwrite( function, message.str() ); - error=ERROR; - continue; - } - - message.str(""); message << "[DEBUG] set gain to " << gain - << " for camera " << pair.second->camera_info.camera_name; - logwrite( function, message.str() ); - - if ( error==NO_ERROR && gain == 1 ) { - error = pair.second->set_output_amplifier( Andor::AMPTYPE_CONV ); - if (error==NO_ERROR) { - for ( const auto &pair : this->andor ) { - pair.second->camera_info.gain = 1; - } - } - else { message << "ERROR gain not set"; } - } - else - if ( error==NO_ERROR && gain >= low && gain <= high ) { - error |= pair.second->set_output_amplifier( Andor::AMPTYPE_EMCCD ); - if (error==NO_ERROR) pair.second->set_emgain( gain ); - if (error==NO_ERROR) pair.second->camera_info.gain = gain; - else { message << "ERROR gain not set"; } - } - else - if ( error==NO_ERROR ) { - message.str(""); message << "ERROR: gain " << gain << " outside range { 1, " - << low << ":" << high << " }"; - error = ERROR; - } - if ( !message.str().empty() ) logwrite( function, message.str() ); - - // The image gets flipped when the EM gain is used. - // This flips it back. - // - if (error==NO_ERROR && pair.first=="L") { - if (gain>1) { - error=this->andor["L"]->set_imflip( (default_config["L"].hflip==1?0:1), default_config["L"].vflip ); - } - else error=this->andor["L"]->set_imflip( default_config["L"].hflip, default_config["L"].vflip ); - } - if (error==NO_ERROR && pair.first=="R") { - if (gain>1) { - error=this->andor["R"]->set_imflip( (default_config["R"].hflip==1?0:1), default_config["R"].vflip ); - } - else error=this->andor["R"]->set_imflip( default_config["R"].hflip, default_config["R"].vflip ); - } - } - - // Regardless of setting the gain, always return what's in the camera - // - gain = this->andor.begin()->second->camera_info.gain; - - return error; - } - long Camera::set_gain( int &&gain ) { - return set_gain("", gain); - } - long Camera::set_gain( std::string which, int &&gain ) { - return set_gain(which, gain); - } - /***** Slicecam::Camera::set_gain *******************************************/ - - - /***** Slicecam::Camera::speed **********************************************/ - /** - * @brief set or get the CCD clocking speeds - * @param[in] args optionally contains new clocking speeds - * @param[out] retstring return string contains clocking speeds - * @return ERROR | NO_ERROR | HELP - * - */ - long Camera::speed( std::string args, std::string &retstring ) { - std::string function = "Slicecam::Camera::speed"; - std::stringstream message; - long error = NO_ERROR; - float hori=-1, vert=-1; - - // Help - // - if ( args == "?" || args == "help" ) { - retstring = SLICECAMD_SPEED; - retstring.append( " [ ]\n" ); - retstring.append( " Set or get CCD clocking speeds for horizontal and vertical \n" ); - retstring.append( " If speeds are omitted then the current speeds are returned.\n" ); -/*** - if ( !this->andor.empty() && this->andor.begin()->second->is_open() ) { - auto cam = this->andor.begin()->second; // make a smart pointer to the first andor in the map - retstring.append( " Current amp type is " ); - retstring.append( ( cam->camera_info.amptype == Andor::AMPTYPE_EMCCD ? "EMCCD\n" : "conventional\n" ) ); - retstring.append( " Select from {" ); - for ( const auto &hspeed : cam->camera_info.hsspeeds[ cam->camera_info.amptype] ) { - retstring.append( " " ); - retstring.append( std::to_string( hspeed ) ); - } - retstring.append( " }\n" ); - retstring.append( " Select from {" ); - for ( const auto &vspeed : cam->camera_info.vsspeeds ) { - retstring.append( " " ); - retstring.append( std::to_string( vspeed ) ); - } - retstring.append( " }\n" ); - retstring.append( " Units are MHz\n" ); - } - else { - retstring.append( " Open connection to camera to see possible speeds.\n" ); - } -***/ - return HELP; - } - - // If the STL map of Andors is empty then something went wrong - // with the configuration. - // - if ( this->andor.empty() ) { - logwrite( function, "ERROR no cameras defined" ); - retstring="bad_config"; - return ERROR; - } - - // all configured Andors must have been initialized - // -// for ( const auto &pair : this->andor ) { -// if ( ! pair.second->is_open() ) { - for ( auto &[name, cam] : this->andor ) { - if ( ! cam->is_open() ) { - message.str(""); message << "ERROR no connection to slicecam " << cam->camera_info.camera_name - << " S/N " << cam->camera_info.serial_number; - logwrite( function, message.str() ); - error=ERROR; - } - } - if ( error==ERROR ) return ERROR; - - // Parse args if present - // - if ( !args.empty() ) { - - std::vector tokens; - Tokenize( args, tokens, " " ); - - // There must be only two args (the speeds) - // - if ( tokens.size() != 2 ) { - logwrite( function, "ERROR expected speeds" ); - return ERROR; - } - - // Parse the gain from the token - // - try { - hori = std::stof( tokens.at(0) ); - vert = std::stof( tokens.at(1) ); - } - catch ( std::out_of_range &e ) { - message.str(""); message << "ERROR reading speeds: " << e.what(); - error = ERROR; - } - catch ( std::invalid_argument &e ) { - message.str(""); message << "ERROR reading speeds: " << e.what(); - error = ERROR; - } - if (error==ERROR) logwrite( function, message.str() ); - - for ( const auto &pair : this->andor ) { - if (error!=ERROR ) error = pair.second->set_hsspeed( hori ); - if (error!=ERROR ) error = pair.second->set_vsspeed( vert ); - } - } - - message.str(""); - - for ( auto &[name, cam] : this->andor ) { - if ( ( cam->camera_info.hspeed < 0 ) || - ( cam->camera_info.vspeed < 0 ) ) { - message.str(""); message << "ERROR speeds not set for camera " << cam->camera_info.camera_name; - logwrite( function, message.str() ); - error = ERROR; - } - - message << cam->camera_info.camera_name << " " - << cam->camera_info.hspeed << " " << cam->camera_info.vspeed << " "; - } - - retstring = message.str(); - logwrite( function, retstring ); - - return error; - } - /***** Slicecam::Camera::speed **********************************************/ - - - /***** Slicecam::Camera::temperature ****************************************/ - /** - * @brief set or get the camera temperature setpoint - * @param[in] args optionally contains new setpoint - * @param[out] retstring return string contains - * @return ERROR | NO_ERROR | HELP - * - */ - long Camera::temperature( std::string args, std::string &retstring ) { - std::string function = "Slicecam::Camera::temperature"; - std::stringstream message; - long error = NO_ERROR; - int temp = 999; - - // Help - // - if ( args == "?" || args == "help" ) { - retstring = SLICECAMD_TEMP; - retstring.append( " [ ]\n" ); - retstring.append( " Set or get camera temperature in integer degrees C,\n" ); -/*** - if ( !this->andor.empty() && this->andor.begin()->second->is_open() ) { - auto cam = this->andor.begin()->second; // make a smart pointer to the first andor in the map - retstring.append( " where is in range { " ); - retstring.append( std::to_string( cam->camera_info.temp_min ) ); - retstring.append( " " ); - retstring.append( std::to_string( cam->camera_info.temp_max ) ); - retstring.append( " }.\n" ); - } - else { - retstring.append( " open connection to camera to see acceptable range.\n" ); - } -***/ - retstring.append( " If optional is provided then the camera setpoint is changed,\n" ); - retstring.append( " else the current temperature, setpoint, and status are returned.\n" ); - retstring.append( " Format of return value is \n" ); - retstring.append( " Camera cooling is turned on/off automatically, as needed.\n" ); - return HELP; - } - - // If the STL map of Andors is empty then something went wrong - // with the configuration. - // - if ( this->andor.empty() ) { - logwrite( function, "ERROR no cameras defined" ); - retstring="bad_config"; - return ERROR; - } - - // all configured Andors must have been initialized - // - for ( const auto &pair : this->andor ) { - if ( ! pair.second->is_open() ) { - message.str(""); message << "ERROR no connection to slicecam " << pair.second->camera_info.camera_name - << " S/N " << pair.second->camera_info.serial_number; - logwrite( function, message.str() ); - error=ERROR; - } - } - if ( error==ERROR ) return ERROR; - - // Parse args if present - // - if ( !args.empty() ) { - - std::vector tokens; - Tokenize( args, tokens, " " ); - - // There can be only one arg (the requested temperature) - // - if ( tokens.size() != 1 ) { - logwrite( function, "ERROR too many arguments" ); - return ERROR; - } - - // Convert the temperature to int and set the temperature. - // Cooling will be automatically enabled/disabled as needed. - // - try { - temp = static_cast( std::round( std::stof( tokens.at(0) ) ) ); - for ( const auto &pair : this->andor ) { - message.str(""); message << "[DEBUG] set temp to " << temp - << " for camera " << pair.second->camera_info.camera_name; - logwrite( function, message.str() ); - error |= pair.second->set_temperature( temp ); - } - } - catch ( const std::exception &e ) { - message.str(""); message << "ERROR setting temperature: " << e.what(); - error = ERROR; - } - } - if (error==ERROR) logwrite( function, message.str() ); - - // Regardless of setting the temperature, always read it. - // - message.str(""); - for ( const auto &pair : this->andor ) { - error |= pair.second->get_temperature(temp); - message << pair.second->camera_info.camera_name << " " << temp << " " - << pair.second->camera_info.setpoint << " " - << pair.second->camera_info.temp_status << " "; - } - logwrite( function, message.str() ); - - retstring = message.str(); - - return error; - } - /***** Slicecam::Camera::temperature ****************************************/ - - - /***** Slicecam::Camera::write_frame ****************************************/ - /** - * @brief write the Andor image data to FITS file - * @return ERROR or NO_ERROR - * - */ - long Camera::write_frame( std::string source_file, std::string &outfile, const bool _tcs_online ) { - std::string function = "Slicecam::Camera::write_frame"; - std::stringstream message; - - long error = NO_ERROR; - - fitsinfo.fits_name = outfile; -// fitsinfo.datatype = USHORT_IMG; - fitsinfo.datatype = FLOAT_IMG; - - fits_file.copy_info( fitsinfo ); // copy from fitsinfo to the fits_file class - - error = fits_file.open_file(); // open the fits file for writing - - if ( !source_file.empty() ) { - if (error==NO_ERROR) error = fits_file.copy_header_from( source_file ); - } - else { - if (error==NO_ERROR) error = fits_file.create_header(); // create basic header - } - - for ( auto &[name, cam] : this->andor ) { - cam->camera_info.section_size = cam->camera_info.axes[0] * cam->camera_info.axes[1]; - if ( cam->camera_info.section_size == 0 ) { - message.str(""); message << "ERROR section size 0 for slicecam " << cam->camera_info.camera_name; - logwrite( function, message.str() ); - error = ERROR; - break; - } - // cam is passed by reference - // - fits_file.write_image( cam ); // write the image data - } - - fits_file.close_file(); // close the file - - // This is the one extra call that is outside the normal workflow. - // If emulator is enabled then the skysim generator will create a simulated - // image. The image written above by fits_file.write_image() is used as - // input to skysim because it contains the correct WCS headers, but will - // ultimately be overwritten by the simulated image. - // - // Need only to make one call since it will generate a multi-extension - // image. - // - if ( !this->andor.empty() ) { - if ( this->andor.begin()->second->is_emulated() && _tcs_online ) { - this->andor.begin()->second->simulate_frame( fitsinfo.fits_name, - true, // multi-extension - this->simsize ); - } - } - - outfile = fitsinfo.fits_name; - - return error; - } - /***** Slicecam::Camera::write_frame ****************************************/ - - /***** Slicecam::Interface::bin *********************************************/ /** * @brief set or get camera binning diff --git a/slicecamd/slicecam_interface.h b/slicecamd/slicecam_interface.h index 7ec87161..d10fa7d8 100644 --- a/slicecamd/slicecam_interface.h +++ b/slicecamd/slicecam_interface.h @@ -24,6 +24,7 @@ #include "acamd_commands.h" #include "tcsd_client.h" #include "skyinfo.h" +#include "slicecam_camera.h" #define PYTHON_PATH "/home/developer/Software/Python:/home/developer/Software/Python/acam_skyinfo" #define PYTHON_ASTROMETRY_MODULE "astrometry" @@ -37,6 +38,8 @@ #include "andor.h" #endif +#include "guimanager.h" + /***** Slicecam ***************************************************************/ /** * @namespace Slicecam @@ -53,172 +56,6 @@ namespace Slicecam { class Interface; // forward declaration - /***** Slicecam::Camera *****************************************************/ - /** - * @class Camera - * @brief Camera class - * - * This class is used for communicating with the slicecam camera directly (which is an Andor) - * - */ - class Camera { - private: - uint16_t* image_data; - int simsize; /// for the sky simulator - std::map handlemap; - - public: - Camera() : image_data( nullptr ), simsize(1024) { }; - - FITS_file fits_file; /// instantiate a FITS container object - FitsInfo fitsinfo; - - std::mutex framegrab_mutex; - - std::map> andor; ///< container for Andor::Interface objects - - std::map default_config; ///< container to hold defaults for each camera - - inline void copy_info() { fits_file.copy_info( fitsinfo ); } - inline void set_simsize( int val ) { if ( val > 0 ) this->simsize = val; else throw std::out_of_range("simsize must be greater than 0"); } - - inline long init_handlemap() { - this->handlemap.clear(); - return this->andor.begin()->second->get_handlemap( this->handlemap ); - } - - long emulator( std::string args, std::string &retstring ); - long open( std::string which, std::string args ); - long close(); - long get_frame(); - long write_frame( std::string source_file, std::string &outfile, const bool _tcs_online ); - long bin( const int hbin, const int vbin ); - long set_fan( std::string which, int mode ); - long imflip( std::string args, std::string &retstring ); - long imrot( std::string args, std::string &retstring ); - long set_gain( int &gain ); - long set_gain( std::string which, int &gain ); - long set_gain( int &&gain ); - long set_gain( std::string which, int &&gain ); - long set_exptime( float &val ); - long set_exptime( std::string which, float &val ); - long set_exptime( float &&val ); - long set_exptime( std::string which, float &&val ); - long speed( std::string args, std::string &retstring ); - long temperature( std::string args, std::string &retstring ); - }; - /***** Slicecam::Camera *****************************************************/ - - - /***** Slicecam::GUIManager *************************************************/ - /** - * @class GUIManager - * @brief defines functions and settings for the display GUI - * - */ - class GUIManager { - private: - const std::string camera_name = "slicev"; - std::atomic update; ///push_settings=sh; } - - // sets the private variable push_image, call on config - inline void set_push_image( std::string sh ) { this->push_image=sh; } - - // sets the update flag true - inline void set_update() { this->update.store( true ); return; } - - /** - * @fn get_update - * @brief returns the update flag then clears it - * @return boolean true|false - */ - inline bool get_update() { return this->update.exchange( false ); } - - /** - * @fn get_message_string - * @brief returns a formatted message of all gui settings - * @details This message is the return string to guideset command. - * @return string in form of - */ - std::string get_message_string() { - std::stringstream message; - if ( this->exptime < 0 ) message << "ERR"; else { message << std::fixed << std::setprecision(3) << this->exptime; } - message << " "; - if ( this->gain < 1 ) message << "ERR"; else { message << std::fixed << std::setprecision(3) << this->gain; } - message << " "; - if ( this->bin < 1 ) message << "x"; else { message << std::fixed << std::setprecision(3) << this->bin; } - message << " "; - if ( std::isnan(this->navg) ) message << "NaN"; else { message << std::fixed << std::setprecision(2) << this->navg; } - return message.str(); - } - - /** - * @brief calls the push_settings script with the formatted message string - * @details the script pushes the settings to the Guider GUI - */ - void push_gui_settings() { - std::string function = "Slicecam::GUIManager::push_gui_settings"; - std::stringstream cmd; - cmd << push_settings << " " - << ( get_update() ? "true" : "false" ) << " " - << get_message_string(); - - if ( std::system( cmd.str().c_str() ) && errno!=ECHILD ) { - logwrite( function, "ERROR updating GUI" ); - } - - return; - } - - void send_fifo_warning(const std::string &message) { - const std::string fifo_name("/tmp/.slicev_warning.fifo"); - std::ofstream fifo(fifo_name); - if (!fifo.is_open()) { - logwrite("Slicecam::GUIManager::send_fifo_warning", "failed to open " + fifo_name + " for writing"); - } - else { - fifo << message << std::endl; - fifo.close(); - } - } - - /** - * @brief calls the push_image script with the formatted message string - * @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 ) { - std::string function = "Slicecam::GUIManager::push_gui_image"; - std::stringstream cmd; - cmd << push_image << " " - << camera_name << " " - << filename; - - if ( std::system( cmd.str().c_str() ) && errno!=ECHILD ) { - logwrite( function, "ERROR pushing image to GUI" ); - } - - return; - } - }; - /***** Slicecam::GUIManager *************************************************/ - - /***** Slicecam::Interface **************************************************/ /** * @class Interface From 79e86fe298b8899425b5d91ce414a7dbe8af06a7 Mon Sep 17 00:00:00 2001 From: David Hale Date: Fri, 14 Nov 2025 14:31:26 -0800 Subject: [PATCH 02/16] adds acquire_target function, and placeholders for calculate_centroid() and calculate_acquisition_offsets() --- common/slicecamd_commands.h | 2 + slicecamd/slicecam_interface.cpp | 206 +++++++++++++++++++++++++------ slicecamd/slicecam_interface.h | 10 ++ 3 files changed, 182 insertions(+), 36 deletions(-) diff --git a/common/slicecamd_commands.h b/common/slicecamd_commands.h index 1ddad325..0239bb48 100644 --- a/common/slicecamd_commands.h +++ b/common/slicecamd_commands.h @@ -9,6 +9,7 @@ #pragma once +const std::string SLICECAMD_ACQUIRETARGET= "targetacquire"; ///< target acquisition const std::string SLICECAMD_AVGFRAMES= "avgframes"; ///< set/get camera binning const std::string SLICECAMD_BIN = "bin"; ///< set/get camera binning const std::string SLICECAMD_CLOSE = "close"; ///< *** close connection to all devices @@ -53,6 +54,7 @@ const std::vector SLICECAMD_SYNTAX = { SLICECAMD_TCSISCONNECTED+" [ ? ]", SLICECAMD_TCSISOPEN+" [ ? ]", " CAMERA COMMANDS:", + SLICECAMD_ACQUIRETARGET+" [ ? ]", SLICECAMD_AVGFRAMES+" [ ? | ]", SLICECAMD_FRAMEGRAB+" [ ? | start | stop | one [ ] | status ]", SLICECAMD_FRAMEGRABFIX+" [ ? ]", diff --git a/slicecamd/slicecam_interface.cpp b/slicecamd/slicecam_interface.cpp index ed1b3712..f5e0d9ad 100644 --- a/slicecamd/slicecam_interface.cpp +++ b/slicecamd/slicecam_interface.cpp @@ -16,6 +16,108 @@ namespace Slicecam { int npreserve=0; ///< counter used for Interface::preserve_framegrab() + /***** Slicecam::Interface::acquire_target **********************************/ + /** + * @brief + * @param[in] + * @param[out] + * @return + * + */ + long Interface::acquire_target(std::string args, std::string &retstring) { + const std::string function("Slicecam::Interface::acquire_target"); + std::stringstream message; + + // Help + if ( args == "?" || args == "help" ) { + retstring = SLICECAMD_ACQUIRETARGET; + retstring.append( " \n" ); + retstring.append( " where are the coordinates of the destination\n" ); + retstring.append( " and is the size of a box centered on \n" ); + return HELP; + } + + if (args.empty()) { + logwrite(function, "ERROR missing args.... TBD"); + return ERROR; + } + + // don't allow someone to run this if already running + if (this->is_targetacquire_running.load(std::memory_order_acquire)) { + logwrite(function, "ERROR target acquisition already running"); + return ERROR; + } + + // sets the is_targetacquire_running state true, clears automatically + BoolState targetacquire_running(this->is_targetacquire_running); + + // framegrabbing must be running + if (!this->is_framegrab_running.load(std::memory_order_acquire)) { + logwrite(function, "ERROR framegrabbing is not running"); + return ERROR; + } + + std::pair centroid; // { RA, DEC } of centroid + std::pair offsets; // { dRA, dDEC } puts centroid on target + + std::string camera("L"); // somehow need to determine camera name! + + this->calculate_centroid(camera, centroid); + + this->calculate_acquisition_offsets(centroid, offsets); + + this->offset_acam_goal(offsets); + + return NO_ERROR; + } + /***** Slicecam::Interface::acquire_target **********************************/ + + + /***** Slicecam::Interface::calculate_centroid ******************************/ + /** + * @brief calculate offsets required to move centroid on target + * @param[in] which "L" or "R" indicates which camera + * @param[out] centroid pair { RA, DEC } coordinates of centroid + * + * CHAZ + * + */ + void Interface::calculate_centroid(const std::string &which, + std::pair ¢roid) { + + // pointer to the selected camera + // + auto* cam = this->camera.andor[which].get(); + + // pointer to a buffer containing the image + // + float* data = cam->get_avg_data(); + + // In case you want it, the size of the image can be gotten from: + // + long cols = cam->camera_info.axes[0]; + long rows = cam->camera_info.axes[1]; + unsigned long imsize = cam->camera_info.section_size; // rows*cols + } + /***** Slicecam::Interface::calculate_centroid ******************************/ + + + /***** Slicecam::Interface::calculate_acquisition_offsets *******************/ + /** + * @brief calculate offsets required to move centroid on target + * @param[in] centroid pair { RA, DEC } coordinates of centroid + * @param[out] offsets pair { dRA, dDEC } offsets to put centroid on target + * + * CHAZ + * + */ + void Interface::calculate_acquisition_offsets(const std::pair ¢roid, + std::pair &offsets) { + + } + /***** Slicecam::Interface::calculate_acquisition_offsets *******************/ + + /***** Slicecam::Interface::bin *********************************************/ /** * @brief set or get camera binning @@ -1346,6 +1448,73 @@ namespace Slicecam { /***** Slicecam::Interface::get_acam_guide_state ****************************/ + /***** Slicecam::Interface::offset_acam_goal ********************************/ + /** + * @brief applies offset to ACAM goal, or move telescope directly + * @details When guiding is enabled, the offsets will be applied to the ACAM + * goal so that the ACAM will guide on the offset position. When + * not guiding, the offsets are sent directly to the TCS as PT offsets. + * @param[in] offsets pair { dRA, dDEC } + * @return ERROR | NO_ERROR + * + */ + long Interface::offset_acam_goal(const std::pair &offsets) { + const std::string function("Slicecam::Interface::offset_acam_goal"); + + auto [ra_off, dec_off] = offsets; // local copy + + // If ACAM is guiding then slicecam must not move the telescope, + // but must allow ACAM to perform the offset. + // + bool is_guiding; + long error = this->get_acam_guide_state( is_guiding ); + + if ( error != NO_ERROR ) { + logwrite( function, "ERROR getting guide state" ); + return ERROR; + } + + // send the offsets now + // + if ( is_guiding ) { + // Send to acamd if guiding + // + std::stringstream cmd; + cmd << ACAMD_OFFSETGOAL << " " << std::fixed << std::setprecision(6) << ra_off << " " << dec_off; + error = this->acamd.command( cmd.str() ); + if ( error != NO_ERROR ) { + logwrite( function, "ERROR adding offset to acam goal" ); + return ERROR; + } + } + else + if ( !is_guiding && this->tcs_online.load(std::memory_order_acquire) && this->tcsd.client.is_open() ) { + // offsets are in degrees, convert to arcsec (required for PT command) + // + ra_off *= 3600.; + dec_off *= 3600.; + + // Send them directly to the TCS when not guiding + // + if ( this->tcsd.pt_offset( ra_off, dec_off, OFFSETRATE ) != NO_ERROR ) { + logwrite( function, "ERROR offsetting telescope" ); + return ERROR; + } + } + else if ( !is_guiding ) { + logwrite( function, "ERROR not connected to tcsd" ); + return ERROR; + } + + std::ostringstream message; + message << "requested offsets dRA=" << ra_off << " dDEC=" << dec_off << " arcsec"; + logwrite(function, message.str()); + + return NO_ERROR; + } + /***** Slicecam::Interface::offset_acam_goal ********************************/ + + /***** Slicecam::Interface::put_on_slit *************************************/ /** * @brief put target on slit @@ -1422,42 +1591,7 @@ namespace Slicecam { // send the offsets now // - if ( is_guiding ) { - // Send to acamd if guiding - // - std::stringstream cmd; - cmd << ACAMD_OFFSETGOAL << " " << std::fixed << std::setprecision(6) << ra_off << " " << dec_off; - error = this->acamd.command( cmd.str() ); - if ( error != NO_ERROR ) { - logwrite( function, "ERROR adding offset to acam goal" ); - return ERROR; - } - } - else - if ( !is_guiding && this->tcs_online.load(std::memory_order_acquire) && this->tcsd.client.is_open() ) { - // offsets are in degrees, convert to arcsec (required for PT command) - // - ra_off *= 3600.; - dec_off *= 3600.; - - // Send them directly to the TCS when not guiding - // - if ( this->tcsd.pt_offset( ra_off, dec_off, OFFSETRATE ) != NO_ERROR ) { - logwrite( function, "ERROR offsetting telescope" ); - retstring="tcs_error"; - return ERROR; - } - } - else if ( !is_guiding ) { - logwrite( function, "ERROR not connected to tcsd" ); - retstring="tcs_not_connected"; - return ERROR; - } - - message.str(""); message << "requested offsets dRA=" << ra_off << " dDEC=" << dec_off << " arcsec"; - logwrite( function, message.str() ); - - return NO_ERROR; + return this->offset_acam_goal( { ra_off, dec_off } ); } /***** Slicecam::Interface::put_on_slit *************************************/ diff --git a/slicecamd/slicecam_interface.h b/slicecamd/slicecam_interface.h index d10fa7d8..677c5bd2 100644 --- a/slicecamd/slicecam_interface.h +++ b/slicecamd/slicecam_interface.h @@ -92,6 +92,7 @@ namespace Slicecam { std::atomic should_framegrab_run; ///< set if framegrab loop should run std::atomic is_framegrab_running; ///< set if framegrab loop is running + std::atomic is_targetacquire_running; ///< set if target acquisition is running /** these are set by Interface::saveframes() */ @@ -129,6 +130,7 @@ namespace Slicecam { should_subscriber_thread_run(false), should_framegrab_run(false), is_framegrab_running(false), + is_targetacquire_running(false), nsave_preserve_frames(0), nskip_preserve_frames(0), snapshot_status { { "slitd", false }, {"tcsd", false} } @@ -182,6 +184,12 @@ namespace Slicecam { void request_snapshot(); bool wait_for_snapshots(); + long acquire_target(std::string args, std::string &retstring); + void calculate_centroid(const std::string &which, + std::pair ¢roid); + void calculate_acquisition_offsets(const std::pair ¢roid, + std::pair &offsets); + long avg_frames( std::string args, std::string &retstring ); long bin( std::string args, std::string &retstring ); long test_image(); /// @@ -210,6 +218,8 @@ namespace Slicecam { long get_acam_guide_state( bool &is_guiding ); + long offset_acam_goal(const std::pair &offsets); + long collect_header_info( std::unique_ptr &slicecam ); inline void init_names() { imagename=""; wcsname=""; return; } // TODO still needed? From 507bcb82af78db39ac519eb67be7baac1eb8b5a4 Mon Sep 17 00:00:00 2001 From: David Hale Date: Tue, 17 Mar 2026 12:57:06 -0700 Subject: [PATCH 03/16] fills functions with CF's AI code translated into C++, adds a new convenience function Common::FitsKeys::get_key --- acamd/acam_interface.cpp | 57 ++++--- common/common.h | 28 ++++ common/slicecamd_commands.h | 4 +- slicecamd/CMakeLists.txt | 1 + slicecamd/slicecam_interface.cpp | 268 +++++++++++++++++++++++------- slicecamd/slicecam_interface.h | 55 ++++++- slicecamd/slicecam_math.cpp | 270 +++++++++++++++++++++++++++++++ slicecamd/slicecam_math.h | 52 ++++++ 8 files changed, 638 insertions(+), 97 deletions(-) create mode 100644 slicecamd/slicecam_math.cpp create mode 100644 slicecamd/slicecam_math.h diff --git a/acamd/acam_interface.cpp b/acamd/acam_interface.cpp index 5d314eb6..212fdb6e 100644 --- a/acamd/acam_interface.cpp +++ b/acamd/acam_interface.cpp @@ -5479,8 +5479,8 @@ logwrite( function, message.str() ); * */ long Interface::offset_goal( const std::string args, std::string &retstring ) { - std::string function = "Acam::Interface::offset_goal"; - std::stringstream message; + const std::string function("Acam::Interface::offset_goal"); + std::ostringstream message; if ( args.empty() ) { message << this->target.dRA << " " << this->target.dDEC; @@ -5492,39 +5492,29 @@ logwrite( function, message.str() ); // if ( args == "?" ) { retstring = ACAMD_OFFSETGOAL; - retstring.append( " [ ]\n" ); + retstring.append( " [ [ fineguiding ]\n" ); retstring.append( " Apply offsets to the ACAM goal coordinates.\n" ); retstring.append( " These offsets are applied only while guiding. If omitted,\n" ); retstring.append( " the current offsets are returned. Units are in degrees.\n" ); + retstring.append( " The optional 'fineguiding' is used for slicecam fine acquisition.\n" ); return HELP; } - std::vector tokens; - Tokenize( args, tokens, " " ); + std::istringstream iss(args); - if ( tokens.size() != 2 ) { - logwrite( function, "ERROR expected " ); + double dRA=NAN, dDEC=NAN; + if (!(iss >> dRA >> dDEC) || + (std::isnan(dRA) || std::isnan(dDEC)) ) { + logwrite( function, "ERROR expected [ fineguiding ]" ); retstring="invalid_argument"; return ERROR; } + this->target.dRA = dRA; + this->target.dDEC = dDEC; - // Convert the input string to double and save in the class - // - try { - double dRA = std::stod( tokens.at(0) ); - double dDEC = std::stod( tokens.at(1) ); - - if (std::isnan(dRA) || std::isnan(dDEC)) throw std::invalid_argument("NaN value encountered"); - - this->target.dRA = dRA; - this->target.dDEC = dDEC; - } - catch ( const std::exception &e ) { - message.str(""); message << "ERROR parsing " << args << ": " << e.what(); - logwrite( function, message.str() ); - retstring="argument_exception"; - return ERROR; - } + // optional fineguiding flag used for slicecam fineacquisition mode + std::string flag; + bool is_fineguiding = (iss >> flag && flag == "fineguiding"); // Apply any dRA, dDEC goal offsets from the "put on slit" action to // acam_ra_goal, acam_dec_goal. These dRA,dDEC offsets can come from @@ -5540,12 +5530,19 @@ logwrite( function, message.str() ); retstring = message.str(); if ( this->target.acquire_mode == Acam::TARGET_GUIDE ) { - this->target.acquire_mode = Acam::TARGET_ACQUIRE; - this->target.nacquired = 0; - this->target.attempts = 0; - this->target.sequential_failures = 0; - this->target.timeout_time = std::chrono::steady_clock::now() - + std::chrono::duration(this->target.timeout); + // for slicecam fine aquisition/guiding, stay in TARGET_GUIDE but + // reset the filtering so the goal takes effect quickly + if ( is_fineguiding ) { + this->target.reset_offset_params(); + } + else { + this->target.acquire_mode = Acam::TARGET_ACQUIRE; + this->target.nacquired = 0; + this->target.attempts = 0; + this->target.sequential_failures = 0; + this->target.timeout_time = std::chrono::steady_clock::now() + + std::chrono::duration(this->target.timeout); + } } return NO_ERROR; diff --git a/common/common.h b/common/common.h index 960a3120..d540dbd3 100644 --- a/common/common.h +++ b/common/common.h @@ -733,6 +733,34 @@ namespace Common { } return; } + + template + T get_key(const std::string &keyname) const { + auto it = this->keydb.find(keyname); + if (it == this->keydb.end()) { + throw std::out_of_range("FitsKeys::get_key '"+keyname+"' not found"); + } + const std::string &val = it->second.keyvalue; + + try { + if constexpr(std::is_same_v) return std::stod(val); + else + if constexpr(std::is_same_v) return std::stof(val); + else + if constexpr(std::is_same_v) return std::stoi(val); + else + if constexpr(std::is_same_v) return std::stol(val); + else + if constexpr(std::is_same_v) return (val=="T"||val=="true"||val=="1"); + else + if constexpr(std::is_same_v) return val; + else + static_assert(std::is_same_v, "FitsKeys::get_key unsupported type"); + } + catch (const std::exception &e) { + throw std::runtime_error("FitsKeys::get_key '"+keyname+"' could not convert '"+val+"'"); + } + } }; /**************** Common::FitsKeys ******************************************/ diff --git a/common/slicecamd_commands.h b/common/slicecamd_commands.h index 0239bb48..d758dca1 100644 --- a/common/slicecamd_commands.h +++ b/common/slicecamd_commands.h @@ -9,7 +9,6 @@ #pragma once -const std::string SLICECAMD_ACQUIRETARGET= "targetacquire"; ///< target acquisition const std::string SLICECAMD_AVGFRAMES= "avgframes"; ///< set/get camera binning const std::string SLICECAMD_BIN = "bin"; ///< set/get camera binning const std::string SLICECAMD_CLOSE = "close"; ///< *** close connection to all devices @@ -25,6 +24,7 @@ const std::string SLICECAMD_EMULATOR = "emulator"; ///< set/get Andor emulator const std::string SLICECAMD_EXIT = "exit"; ///< const std::string SLICECAMD_EXPTIME = "exptime"; ///< set/get camera exposure time const std::string SLICECAMD_FAN = "fan"; ///< set Andor fan mode +const std::string SLICECAMD_FINEACQUIRE = "fineacquire"; ///< fine acquisition const std::string SLICECAMD_GUISET = "guiset"; ///< set params for gui display const std::string SLICECAMD_INIT = "init"; ///< *** const std::string SLICECAMD_ISACQUIRED = "isacquired"; ///< is the target acquired? @@ -54,7 +54,7 @@ const std::vector SLICECAMD_SYNTAX = { SLICECAMD_TCSISCONNECTED+" [ ? ]", SLICECAMD_TCSISOPEN+" [ ? ]", " CAMERA COMMANDS:", - SLICECAMD_ACQUIRETARGET+" [ ? ]", + SLICECAMD_FINEACQUIRE+" [ ? ]", SLICECAMD_AVGFRAMES+" [ ? | ]", SLICECAMD_FRAMEGRAB+" [ ? | start | stop | one [ ] | status ]", SLICECAMD_FRAMEGRABFIX+" [ ? ]", diff --git a/slicecamd/CMakeLists.txt b/slicecamd/CMakeLists.txt index 46009b65..42969138 100644 --- a/slicecamd/CMakeLists.txt +++ b/slicecamd/CMakeLists.txt @@ -39,6 +39,7 @@ add_executable(slicecamd ${SLICECAMD_DIR}/slicecam_interface.cpp ${SLICECAMD_DIR}/slicecam_camera.cpp ${SLICECAMD_DIR}/slicecam_fits.cpp + ${SLICECAMD_DIR}/slicecam_math.cpp ${PYTHON_DEV} ) diff --git a/slicecamd/slicecam_interface.cpp b/slicecamd/slicecam_interface.cpp index f5e0d9ad..acf02462 100644 --- a/slicecamd/slicecam_interface.cpp +++ b/slicecamd/slicecam_interface.cpp @@ -9,6 +9,7 @@ */ #include "slicecam_interface.h" +#include "slicecam_math.h" namespace Slicecam { @@ -16,106 +17,247 @@ namespace Slicecam { int npreserve=0; ///< counter used for Interface::preserve_framegrab() - /***** Slicecam::Interface::acquire_target **********************************/ + /***** Slicecam::Interface::fineacquire *************************************/ /** - * @brief - * @param[in] - * @param[out] - * @return + * @brief user-interface to start/stop fine target acquisition + * @param[in] args contains stop | + * @param[out] return string + * @return ERROR|NO_ERROR|HELP * */ - long Interface::acquire_target(std::string args, std::string &retstring) { - const std::string function("Slicecam::Interface::acquire_target"); + long Interface::fineacquire(std::string args, std::string &retstring) { + const std::string function("Slicecam::Interface::fineacquire"); std::stringstream message; // Help if ( args == "?" || args == "help" ) { - retstring = SLICECAMD_ACQUIRETARGET; - retstring.append( " \n" ); - retstring.append( " where are the coordinates of the destination\n" ); - retstring.append( " and is the size of a box centered on \n" ); + retstring = SLICECAMD_FINEACQUIRE; + retstring.append( " stop | start { L | R } \n" ); + retstring.append( " start or stop fine target acquisition.\n" ); + retstring.append( " L | R specifies which camera\n" ); return HELP; } - if (args.empty()) { - logwrite(function, "ERROR missing args.... TBD"); + std::vector tokens; + Tokenize(args, tokens, " "); + const std::string action = tokens.empty() ? "status" : tokens.at(0); + + // empty args returns status + if (action=="status") { + retstring=this->is_fineacquire_running.load(std::memory_order_acquire)?"running":"stopped"; + return NO_ERROR; + } + else + + // 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); + logwrite(function, "stop requested"); + } + 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) { + logwrite(function, "ERROR expected stop | start { L | R }"); + retstring="invalid_argument"; return ERROR; } + // at this point, action=="start" + // don't allow someone to run this if already running - if (this->is_targetacquire_running.load(std::memory_order_acquire)) { + if (this->is_fineacquire_running.load(std::memory_order_acquire)) { logwrite(function, "ERROR target acquisition already running"); + retstring="running"; return ERROR; } - // sets the is_targetacquire_running state true, clears automatically - BoolState targetacquire_running(this->is_targetacquire_running); - // framegrabbing must be running if (!this->is_framegrab_running.load(std::memory_order_acquire)) { logwrite(function, "ERROR framegrabbing is not running"); + retstring="stopped"; return ERROR; } - std::pair centroid; // { RA, DEC } of centroid - std::pair offsets; // { dRA, dDEC } puts centroid on target + const std::string which = tokens.at(1); + if (which != "L" && which != "R") { + logwrite(function, "ERROR expected stop | start { L | R }"); + retstring="invalid_argument"; + return ERROR; + } - std::string camera("L"); // somehow need to determine camera name! + this->fineacquire_state.which = which; - this->calculate_centroid(camera, centroid); + // start the state machine + this->fineacquire_state.reset(); + this->is_fineacquire_locked.store(false, std::memory_order_release); + this->is_fineacquire_running.store(true, std::memory_order_release); - this->calculate_acquisition_offsets(centroid, offsets); + logwrite(function, "fine target acquisition enabled"); - this->offset_acam_goal(offsets); + retstring=is_fineacquire_running.load(std::memory_order_acquire)?"running":"stopped"; return NO_ERROR; } - /***** Slicecam::Interface::acquire_target **********************************/ + /***** Slicecam::Interface::fineacquire *************************************/ - /***** Slicecam::Interface::calculate_centroid ******************************/ + /***** Slicecam::Interface::do_fineacquire **********************************/ /** - * @brief calculate offsets required to move centroid on target - * @param[in] which "L" or "R" indicates which camera - * @param[out] centroid pair { RA, DEC } coordinates of centroid - * - * CHAZ + * @brief Evaluates fine acquisition natively per-frame + * @details Called synchronously inside dothread_framegrab. Acts as a + * state machine to accumulate samples, calculate medians, and + * publish acam goal corrections, to which acam responds by + * sending offsets to the telescope. + * @param[in] which "L" or "R" indicates which camera * */ - void Interface::calculate_centroid(const std::string &which, - std::pair ¢roid) { + void Interface::do_fineacquire() { + const std::string function = "Slicecam::Interface::do_fineacquire"; - // pointer to the selected camera + // skip frames if we are waiting for the telescope to settle after a move // - auto* cam = this->camera.andor[which].get(); + if (this->fineacquire_state.skip_frames > 0) { + this->fineacquire_state.skip_frames--; + return; + } - // pointer to a buffer containing the image + const std::string which = this->fineacquire_state.which; + + // get a reference to the requested slicecam and + // a pointer to that image buffer // - float* data = cam->get_avg_data(); + auto it = this->camera.andor.find(which); + if (it==this->camera.andor.end() || it->second==nullptr) { + logwrite(function, "slicecam '"+which+"' not found!"); + this->is_fineacquire_running.store( false, std::memory_order_release ); + logwrite(function, "fine target acquisition disabled"); + return; + } + auto* cam = it->second.get(); + float* img_data = cam->get_avg_data(); + + if (img_data==nullptr) { + logwrite(function, "bad image data buffer for slicecam '"+which+"'"); + return; + } + + const long ncols = cam->camera_info.cols; + const long nrows = cam->camera_info.rows; - // In case you want it, the size of the image can be gotten from: + // find the star centroid near the aim point // - long cols = cam->camera_info.axes[0]; - long rows = cam->camera_info.axes[1]; - unsigned long imsize = cam->camera_info.section_size; // rows*cols - } - /***** Slicecam::Interface::calculate_centroid ******************************/ + Point centroid; + if ( Math::calculate_centroid( img_data, ncols, nrows, + this->fineacquire_state.bg_region, + this->fineacquire_state.aimpoint, + centroid) != NO_ERROR ) { + logwrite(function, "WARNING: failed to find centroid, skipping frame"); + return; + } - /***** Slicecam::Interface::calculate_acquisition_offsets *******************/ - /** - * @brief calculate offsets required to move centroid on target - * @param[in] centroid pair { RA, DEC } coordinates of centroid - * @param[out] offsets pair { dRA, dDEC } offsets to put centroid on target - * - * CHAZ - * - */ - void Interface::calculate_acquisition_offsets(const std::pair ¢roid, - std::pair &offsets) { + // convert centroid pixel -> sky using WCS from FITS header + // + World star_sky; + try { + Math::pix2world( cam->fitskeys, centroid, star_sky ); + } + catch (const std::exception &e) { + logwrite(function, "WARNING pix2world (centroid) failed: "+std::string(e.what())+", skipping frame"); + return; + } + if ( !std::isfinite( star_sky.ra ) || !std::isfinite( star_sky.dec ) ) { + logwrite( function, "WARNING pix2world returned non-finite coords, skipping frame" ); + return; + } + + // convert aim point pixel -> sky + // + World aimpoint_sky; + try { + Math::pix2world( cam->fitskeys, this->fineacquire_state.aimpoint, aimpoint_sky ); + } + catch (const std::exception &e) { + logwrite(function, "WARNING pix2world (aimpoint) failed: "+std::string(e.what())+", skipping frame"); + return; + } + + // compute dRA,dDEC in degrees and accumulate + // + std::pair offsets; + + Math::calculate_acquisition_offsets( star_sky, aimpoint_sky, offsets ); + + 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 max_samples = this->fineacquire_state.max_samples; + if ( static_cast(this->fineacquire_state.dra_samp.size()) < max_samples ) { + return; + } + + // calculate median from accumulated samples + // + 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 ]; + + // convert to arcsec only for the threshold comparison and logging + // + const double offset_arcsec = std::hypot( med_dra, med_ddec ) * 3600.0; + + + // convergence check + // + if ( offset_arcsec <= this->fineacquire_state.goal_arcsec ) { + if ( !this->is_fineacquire_locked.load(std::memory_order_acquire) ) { + logwrite( function, "NOTICE fine acquisition converged" ); + this->is_fineacquire_locked.store( true, std::memory_order_release ); + } + this->fineacquire_state.reset(); + return; + } + // drifted outside threshold so clear the locked flag + this->is_fineacquire_locked.store(false, std::memory_order_release); + + // 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"; + logwrite( function, oss.str() ); + + const double cmd_dra = this->fineacquire_state.gain * med_dra; + const double cmd_ddec = this->fineacquire_state.gain * med_ddec; + + if ( this->offset_acam_goal( { cmd_dra, cmd_ddec }, true ) != NO_ERROR ) { + logwrite( function, "ERROR failed to send offset to ACAM" ); + this->is_fineacquire_running.store( false, std::memory_order_release ); + return; + } + + // reset samples and skip a couple of frames for telescope settling + this->fineacquire_state.reset(); + this->fineacquire_state.skip_frames = 2; } - /***** Slicecam::Interface::calculate_acquisition_offsets *******************/ + /***** Slicecam::Interface::do_fineacquire **********************************/ /***** Slicecam::Interface::bin *********************************************/ @@ -1062,13 +1204,18 @@ namespace Slicecam { collect_header_info( cam ); } + // run the fine target acquisition if enabled + if ( is_fineacquire_running.load() ) { do_fineacquire(); } + + // write to FITS file if (error==NO_ERROR) error = this->camera.write_frame( sourcefile, this->imagename, - this->tcs_online.load(std::memory_order_acquire) ); // write to FITS file + this->tcs_online.load(std::memory_order_acquire) ); this->framegrab_time = std::chrono::steady_clock::time_point::min(); - this->gui_manager.push_gui_image( this->imagename ); // send frame to GUI + // send frame to GUI + this->gui_manager.push_gui_image( this->imagename ); // Normally, framegrabs are overwritten to the same file. // This optionally saves them at the requested cadence by @@ -1458,11 +1605,14 @@ namespace Slicecam { * @return ERROR | NO_ERROR * */ - long Interface::offset_acam_goal(const std::pair &offsets) { + long Interface::offset_acam_goal(const std::pair &offsets, std::optional fineacquire) { const std::string function("Slicecam::Interface::offset_acam_goal"); auto [ra_off, dec_off] = offsets; // local copy + bool is_fineacquire=false; + if (fineacquire) is_fineacquire = *fineacquire; + // If ACAM is guiding then slicecam must not move the telescope, // but must allow ACAM to perform the offset. // @@ -1481,6 +1631,10 @@ namespace Slicecam { // std::stringstream cmd; cmd << ACAMD_OFFSETGOAL << " " << std::fixed << std::setprecision(6) << ra_off << " " << dec_off; + + // add fineguiding arg when used for fine acquisition mode + if (is_fineacquire) cmd << " fineguiding"; + error = this->acamd.command( cmd.str() ); if ( error != NO_ERROR ) { logwrite( function, "ERROR adding offset to acam goal" ); diff --git a/slicecamd/slicecam_interface.h b/slicecamd/slicecam_interface.h index 677c5bd2..4f096a3f 100644 --- a/slicecamd/slicecam_interface.h +++ b/slicecamd/slicecam_interface.h @@ -8,6 +8,7 @@ #pragma once +#include #include #include #include @@ -25,6 +26,7 @@ #include "tcsd_client.h" #include "skyinfo.h" #include "slicecam_camera.h" +#include "slicecam_math.h" #define PYTHON_PATH "/home/developer/Software/Python:/home/developer/Software/Python/acam_skyinfo" #define PYTHON_ASTROMETRY_MODULE "astrometry" @@ -56,6 +58,42 @@ namespace Slicecam { class Interface; // forward declaration + /***** Slicecam::FineAcqState ***********************************************/ + /** + * @brief Persistent state for the fine-acquisition per-frame state machine + * + * @details + * which - which camera to use ("L" or "R") + * aimpoint - desired star location on the chip, FITS 1-based pixels. + * This is the pixel analogue of CF's --goal-x / --goal-y. + * The star is driven toward this pixel, not toward an ra/dec. + * bg_region - background estimation ROI, 1-based inclusive. + * Matches the --bg-x1/x2/y1/y2 defaults in slicecamd.cfg. + * dra_samp - accumulated dRA*cos(dec) samples in degrees + * ddec_samp - accumulated dDEC samples in degrees + * max_samples - samples to gather before evaluating a move + * 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 + */ + struct FineAcqState { + std::string which = "L"; + Point aimpoint = { 150.0, 115.5 }; ///< 1-based pixel aim point + Rect bg_region = { 80, 165, 30, 210 }; ///< 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 + 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; } + }; + /***** Slicecam::FineAcqState ***********************************************/ + + /***** Slicecam::Interface **************************************************/ /** * @class Interface @@ -77,6 +115,8 @@ namespace Slicecam { std::mutex framegrab_mtx; std::condition_variable cv; + FineAcqState fineacquire_state; + public: std::unique_ptr publisher; ///< publisher object std::string publisher_address; ///< publish socket endpoint @@ -92,7 +132,8 @@ namespace Slicecam { std::atomic should_framegrab_run; ///< set if framegrab loop should run std::atomic is_framegrab_running; ///< set if framegrab loop is running - std::atomic is_targetacquire_running; ///< set if target acquisition is running + std::atomic is_fineacquire_running; ///< set if fine target acquisition is running + std::atomic is_fineacquire_locked; ///< set when fine acquire target acquired /** these are set by Interface::saveframes() */ @@ -130,7 +171,8 @@ namespace Slicecam { should_subscriber_thread_run(false), should_framegrab_run(false), is_framegrab_running(false), - is_targetacquire_running(false), + is_fineacquire_running(false), + is_fineacquire_locked(false), nsave_preserve_frames(0), nskip_preserve_frames(0), snapshot_status { { "slitd", false }, {"tcsd", false} } @@ -184,11 +226,8 @@ namespace Slicecam { void request_snapshot(); bool wait_for_snapshots(); - long acquire_target(std::string args, std::string &retstring); - void calculate_centroid(const std::string &which, - std::pair ¢roid); - void calculate_acquisition_offsets(const std::pair ¢roid, - std::pair &offsets); + long fineacquire(std::string args, std::string &retstring); + void do_fineacquire(); long avg_frames( std::string args, std::string &retstring ); long bin( std::string args, std::string &retstring ); @@ -218,7 +257,7 @@ namespace Slicecam { long get_acam_guide_state( bool &is_guiding ); - long offset_acam_goal(const std::pair &offsets); + long offset_acam_goal(const std::pair &offsets, std::optional fineacquire=std::nullopt); long collect_header_info( std::unique_ptr &slicecam ); diff --git a/slicecamd/slicecam_math.cpp b/slicecamd/slicecam_math.cpp new file mode 100644 index 00000000..c99f49b0 --- /dev/null +++ b/slicecamd/slicecam_math.cpp @@ -0,0 +1,270 @@ +/** --------------------------------------------------------------------------- + * @file slicecam_math.cpp + * @brief slicecam math utilities implementation for fine target acquisition + * @details Implements centroid detection, WCS pixel-to-sky conversion, and + * fine-acquisition offset calculation for the slicecam fine-acquire + * loop. + * @author David Hale, Christoffer Fremling + * + */ + +#include "slicecam_math.h" + +#include +#include +#include + +namespace Slicecam { + + /***** Slicecam::Math::calculate_centroid ************************************/ + /** + * @brief compute the centroid of the brightest source near an aim point + * @details Translated from CF's ngps_acq.c. Uses MAD background estimation, + * peak finding, and iterative Gaussian centroiding. + * @param[in] image pointer to row-major camera image buffer (cols * rows elements) + * @param[in] cols number of image columns + * @param[in] rows number of image rows + * @param[in] background ROI for background estimation (1-based, inclusive) + * @param[in] aimpoint pixel search centre (1-based) + * @param[out] centroid detected centroid position (1-based on success) + * @return NO_ERROR|ERROR + * + */ + long Math::calculate_centroid( float* image, + long ncols, long nrows, + Rect background, + Point aimpoint, + Point ¢roid ) { + if ( !image || ncols <= 0 || nrows <= 0 ) return ERROR; + + // Convert 1-based inclusive ROI to 0-based, clamped to image boundaries. + long bx1 = std::max( 0L, background.x1 - 1 ); + long bx2 = std::min( ncols - 1L, background.x2 - 1 ); + long by1 = std::max( 0L, background.y1 - 1 ); + long by2 = std::min( nrows - 1L, background.y2 - 1 ); + + if ( bx2 < bx1 || by2 < by1 ) return ERROR; + + // estimate background and noise via sigma-clipped median/MAD + std::vector samples; + samples.reserve( static_cast( (bx2 - bx1 + 1) * (by2 - by1 + 1) ) ); + + for ( long y = by1; y <= by2; y++ ) { + for ( long x = bx1; x <= bx2; x++ ) { + samples.push_back( image[y * ncols + x] ); + } + } + + if ( samples.size() < 16 ) return ERROR; + + std::sort( samples.begin(), samples.end() ); + + const size_t n = samples.size(); + + double median = ( n % 2 ) + ? static_cast( samples[n / 2] ) + : 0.5 * ( static_cast( samples[n / 2 - 1] ) + + static_cast( samples[n / 2] ) ); + + // MAD gives a robust initial sigma estimate: sigma ~= 1.4826 * MAD + std::vector dev( n ); + for ( size_t i = 0; i < n; i++ ) { + dev[i] = std::abs( samples[i] - static_cast( median ) ); + } + std::sort( dev.begin(), dev.end() ); + + double mad = ( n % 2 ) + ? static_cast( dev[n / 2] ) + : 0.5 * ( static_cast( dev[n / 2 - 1] ) + + static_cast( dev[n / 2] ) ); + double sigma = 1.4826 * mad; + if ( !std::isfinite( sigma ) || sigma <= 0.0 ) sigma = 1.0; + + // refine background with iterative sigma clipping (5 passes, 3-sigma clip). + double bkg = median; + const double clip_sig = 3.0; + + for ( int it = 0; it < 5; it++ ) { + const double lo = bkg - clip_sig * sigma; + const double hi = bkg + clip_sig * sigma; + + double sum = 0.0; + double sum2 = 0.0; + long cnt = 0; + + for ( float v : samples ) { + if ( v < lo || v > hi ) continue; + const double dv = static_cast( v ); + sum += dv; + sum2 += dv * dv; + cnt++; + } + + if ( cnt < 2 ) break; + + bkg = sum / cnt; + sigma = std::sqrt( sum2 / cnt - bkg * bkg ); + if ( !std::isfinite( sigma ) || sigma <= 0.0 ) { sigma = 1.0; break; } + } + + // locate the brightest pixel above threshold near the aim point + // + const double snr_threshold = 3.0; ///< minimum SNR for detection + const double search_radius = 40.0; ///< search radius in pixels + + const double detection_level = bkg + snr_threshold * sigma; + + // convert 1-based aim point to 0-based for array indexing. + const double aim_x0 = aimpoint.x - 1.0; + const double aim_y0 = aimpoint.y - 1.0; + + long sx1 = std::max( 0L, static_cast( aim_x0 - search_radius ) ); + long sx2 = std::min( ncols - 1L, static_cast( aim_x0 + search_radius ) ); + long sy1 = std::max( 0L, static_cast( aim_y0 - search_radius ) ); + long sy2 = std::min( nrows - 1L, static_cast( aim_y0 + search_radius ) ); + + double best_val = detection_level; + long best_x = -1; + long best_y = -1; + + for ( long y = sy1; y <= sy2; y++ ) { + for ( long x = sx1; x <= sx2; x++ ) { + const double dx = static_cast(x) - aim_x0; + const double dy = static_cast(y) - aim_y0; + if ( dx * dx + dy * dy > search_radius * search_radius ) continue; + + const double v = static_cast( image[y * ncols + x] ); + if ( v > best_val ) { + best_val = v; + best_x = x; + best_y = y; + } + } + } + + if ( best_x < 0 ) return ERROR; // no source found above threshold + + // iterative Gaussian-windowed first-moment centroid + const int centroid_halfwin = 4; + const double centroid_sigma = 1.2; + const double centroid_sigma_sq = centroid_sigma * centroid_sigma; + + double cx = static_cast( best_x ); + double cy = static_cast( best_y ); + + for ( int it = 0; it < 20; it++ ) { + long xlo = std::max( 0L, static_cast(cx) - centroid_halfwin ); + long xhi = std::min( ncols - 1L, static_cast(cx) + centroid_halfwin ); + long ylo = std::max( 0L, static_cast(cy) - centroid_halfwin ); + long yhi = std::min( nrows - 1L, static_cast(cy) + centroid_halfwin ); + + double sumI = 0.0; + double sumX = 0.0; + double sumY = 0.0; + + for ( long y = ylo; y <= yhi; y++ ) { + for ( long x = xlo; x <= xhi; x++ ) { + const double I = static_cast( image[y * ncols + x] ) - bkg; + if ( I <= 0.0 ) continue; + + const double dx = static_cast(x) - cx; + const double dy = static_cast(y) - cy; + const double w = std::exp( -0.5 * ( dx * dx + dy * dy ) / centroid_sigma_sq ) * I; + + sumI += w; + sumX += w * static_cast(x); + sumY += w * static_cast(y); + } + } + + if ( sumI <= 0.0 ) break; + + const double ncx = sumX / sumI; + const double ncy = sumY / sumI; + const double shift = std::hypot( ncx - cx, ncy - cy ); + cx = ncx; + cy = ncy; + + if ( shift < 0.01 ) break; // sub-hundredth pixel convergence + } + + if ( !std::isfinite( cx ) || !std::isfinite( cy ) ) return ERROR; + + // return centroid in 1-based FITS pixel coordinates. + centroid.x = cx + 1.0; + centroid.y = cy + 1.0; + + return NO_ERROR; + } + /***** Slicecam::Math::calculate_centroid ************************************/ + + + /***** Slicecam::Math::pix2world *********************************************/ + /** + * @brief convert 1-based pixel coordinate to sky (RA/DEC) in degrees + * @details Applies the standard FITS affine WCS: + * u = pix.x - CRPIX1 + * v = pix.y - CRPIX2 + * world.ra = CRVAL1 + CDELT1 * (PC1_1 * u + PC1_2 * v) + * world.dec = CRVAL2 + CDELT2 * (PC2_1 * u + PC2_2 * v) + * This is exact for a linear WCS and an accurate approximation + * for the gnomonic (TAN) projection over a small field. + * @param[in] keys FITS keyword database populated by collect_header_info + * @param[in] pix pixel position (1-based) + * @param[out] world corresponding RA / DEC in degrees + * @throws Common::FitsKeys::get_key can throw std::runtime_error + * + */ + void Math::pix2world( const Common::FitsKeys &keys, Point pix, World &world ) { + const double crpix1 = keys.get_key( "CRPIX1" ); + const double crpix2 = keys.get_key( "CRPIX2" ); + const double crval1 = keys.get_key( "CRVAL1" ); + const double crval2 = keys.get_key( "CRVAL2" ); + const double cdelt1 = keys.get_key( "CDELT1" ); + const double cdelt2 = keys.get_key( "CDELT2" ); + const double pc1_1 = keys.get_key( "PC1_1" ); + const double pc1_2 = keys.get_key( "PC1_2" ); + const double pc2_1 = keys.get_key( "PC2_1" ); + const double pc2_2 = keys.get_key( "PC2_2" ); + + const double u = pix.x - crpix1; + const double v = pix.y - crpix2; + + world.ra = crval1 + cdelt1 * ( pc1_1 * u + pc1_2 * v ); + world.dec = crval2 + cdelt2 * ( pc2_1 * u + pc2_2 * v ); + } + /***** Slicecam::Math::pix2world *********************************************/ + + + /***** Slicecam::Math::calculate_acquisition_offsets *************************/ + /** + * @brief compute the (dRA, dDEC) offset from a goal position to a star + * @details Returns the angular offset (star - goal) which, when sent to + * the telescope, will move the star onto the goal position. + * The RA component is a true great-circle offset + * (multiplied by cos(dec)). + * @param[in] star detected sky position of the star (degrees) + * @param[in] goal desired sky position on the chip (degrees) + * @param[out] offsets (dRA * cos(dec), dDEC) in degrees + * + */ + void Math::calculate_acquisition_offsets( World star, World goal, + std::pair &offsets ) { + double dra = star.ra - goal.ra; + + // Wrap RA difference into [-180, +180] degrees. + // + while ( dra > 180.0 ) dra -= 360.0; + while ( dra < -180.0 ) dra += 360.0; + + // Project onto the sky: multiply by cos(dec) so the RA offset is a + // true angular separation rather than a coordinate difference. + // + const double cosdec = std::cos( goal.dec * M_PI / 180.0 ); + const double ddec = star.dec - goal.dec; + + offsets = { dra * cosdec, ddec }; + } + /***** Slicecam::Math::calculate_acquisition_offsets *************************/ + +} diff --git a/slicecamd/slicecam_math.h b/slicecamd/slicecam_math.h new file mode 100644 index 00000000..6c81c010 --- /dev/null +++ b/slicecamd/slicecam_math.h @@ -0,0 +1,52 @@ +/** --------------------------------------------------------------------------- + * @file slicecam_math.h + * @brief slicecam math utilities + * @details Declares structs and the Math class used for centroid detection, + * WCS pixel-to-sky conversion, and fine-acquisition offset calculation. + * @author David Hale, Christoffer Fremling + * + */ + +#pragma once + +#include +#include +#include +#include +#include "common.h" ///< for Common::FitsKeys + +namespace Slicecam { + + struct Point { double x = 0.0; double y = 0.0; }; ///< pixel coordinate + struct Rect { long x1 = 1; long x2 = 1; long y1 = 1; long y2 = 1; }; ///< rectangular region + struct World { double ra = 0.0; double dec = 0.0; }; ///< sky coordinates + + /***** Slicecam::Math *******************************************************/ + /** + * @brief Static math utilities for slicecam fine acquisition + * + */ + class Math { + public: + /** + * @brief compute the centroid of the brightest source near an aim point + */ + static long calculate_centroid( float* image, + long cols, long rows, + Rect background, + Point aimpoint, + Point ¢roid ); + /** + * @brief convert pixel coordinates to sky coordinates using WCS keys + */ + static void pix2world( const Common::FitsKeys &keys, Point pix, World &world ); + + /** + * @brief compute the (dRA, dDEC) offset from a goal position to a star + */ + static void calculate_acquisition_offsets( World star, World goal, + std::pair &offsets ); + }; + /***** Slicecam::Math *******************************************************/ + +} From 6d3af5f51daea3fd8cdb44189bf7016866b27162 Mon Sep 17 00:00:00 2001 From: David Hale Date: Tue, 17 Mar 2026 13:32:13 -0700 Subject: [PATCH 04/16] operational bug fix on convergence in Slicecam::Interface::do_fineacquire --- slicecamd/slicecam_interface.cpp | 10 +++------- 1 file changed, 3 insertions(+), 7 deletions(-) diff --git a/slicecamd/slicecam_interface.cpp b/slicecamd/slicecam_interface.cpp index acf02462..d515b366 100644 --- a/slicecamd/slicecam_interface.cpp +++ b/slicecamd/slicecam_interface.cpp @@ -221,19 +221,15 @@ namespace Slicecam { // const double offset_arcsec = std::hypot( med_dra, med_ddec ) * 3600.0; - // convergence check // if ( offset_arcsec <= this->fineacquire_state.goal_arcsec ) { - if ( !this->is_fineacquire_locked.load(std::memory_order_acquire) ) { - logwrite( function, "NOTICE fine acquisition converged" ); - this->is_fineacquire_locked.store( true, std::memory_order_release ); - } + logwrite( function, "fine acquisition converged" ); + this->is_fineacquire_locked.store( true, std::memory_order_release ); + this->is_fineacquire_running.store( false, std::memory_order_release ); this->fineacquire_state.reset(); return; } - // drifted outside threshold so clear the locked flag - this->is_fineacquire_locked.store(false, std::memory_order_release); // send gain-weighted offsets to acam // From 9fc03c1ed16b471042a2902e3b539c41dc65bb5b Mon Sep 17 00:00:00 2001 From: David Hale Date: Tue, 17 Mar 2026 14:27:58 -0700 Subject: [PATCH 05/16] adds a way to actually call the new slicecam fineacquire function --- common/slicecamd_commands.h | 2 +- slicecamd/slicecam_interface.cpp | 2 +- slicecamd/slicecam_server.cpp | 4 ++++ 3 files changed, 6 insertions(+), 2 deletions(-) diff --git a/common/slicecamd_commands.h b/common/slicecamd_commands.h index d758dca1..45423c2f 100644 --- a/common/slicecamd_commands.h +++ b/common/slicecamd_commands.h @@ -54,7 +54,7 @@ const std::vector SLICECAMD_SYNTAX = { SLICECAMD_TCSISCONNECTED+" [ ? ]", SLICECAMD_TCSISOPEN+" [ ? ]", " CAMERA COMMANDS:", - SLICECAMD_FINEACQUIRE+" [ ? ]", + SLICECAMD_FINEACQUIRE+" [ ? | status | stop | start { L | R } ]", SLICECAMD_AVGFRAMES+" [ ? | ]", SLICECAMD_FRAMEGRAB+" [ ? | start | stop | one [ ] | status ]", SLICECAMD_FRAMEGRABFIX+" [ ? ]", diff --git a/slicecamd/slicecam_interface.cpp b/slicecamd/slicecam_interface.cpp index d515b366..4e7b70f1 100644 --- a/slicecamd/slicecam_interface.cpp +++ b/slicecamd/slicecam_interface.cpp @@ -32,7 +32,7 @@ namespace Slicecam { // Help if ( args == "?" || args == "help" ) { retstring = SLICECAMD_FINEACQUIRE; - retstring.append( " stop | start { L | R } \n" ); + retstring.append( " stop | start { L | R } | [ status ]\n" ); retstring.append( " start or stop fine target acquisition.\n" ); retstring.append( " L | R specifies which camera\n" ); return HELP; diff --git a/slicecamd/slicecam_server.cpp b/slicecamd/slicecam_server.cpp index 226227de..8ec19e6d 100644 --- a/slicecamd/slicecam_server.cpp +++ b/slicecamd/slicecam_server.cpp @@ -531,6 +531,10 @@ namespace Slicecam { ret = this->interface.fan_mode( args, retstring ); } else + if ( cmd == SLICECAMD_FINEACQUIRE ) { + ret = this->interface.fineacquire( args, retstring ); + } + else if ( cmd == SLICECAMD_GAIN ) { ret = this->interface.gain( args, retstring ); // set gain if (ret==NO_ERROR) this->interface.gui_settings_control(); // update GUI display igores ret From 10a91da2ff1a825dbba6a0ebb4a5ebed3b1a1e31 Mon Sep 17 00:00:00 2001 From: David Hale Date: Tue, 17 Mar 2026 16:50:52 -0700 Subject: [PATCH 06/16] acamd publishes status on change (sequencerd will need this) --- acamd/acam_interface.cpp | 55 ++++++++++++++++++++++++++++++++++++++++ acamd/acam_interface.h | 8 ++++++ 2 files changed, 63 insertions(+) diff --git a/acamd/acam_interface.cpp b/acamd/acam_interface.cpp index 212fdb6e..ae06d582 100644 --- a/acamd/acam_interface.cpp +++ b/acamd/acam_interface.cpp @@ -1465,6 +1465,55 @@ namespace Acam { /***** Acam::Interface::publish_snapshot ************************************/ + /***** Acam::Interface::publish_status **************************************/ + /** + * @brief publishes my acam-related (important) status on change + * @details This publishes a JSON message containing important telemetry. + * + */ + void Interface::publish_status() { + const std::string acquire_mode = this->target.acquire_mode_string(); + const bool is_acquired = this->target.is_acquired.load(); + const int nacquired = this->target.nacquired; + const int attempts = this->target.attempts; + const double seeing = this->astrometry.get_seeing(); + const double background = this->astrometry.get_background(); + + // only will publish if there was a change in any one of these + // + if ( acquire_mode == this->last_status.acquire_mode && + is_acquired == this->last_status.is_acquired && + nacquired == this->last_status.nacquired && + attempts == this->last_status.attempts ) return; + + this->last_status.acquire_mode = acquire_mode; + this->last_status.is_acquired = is_acquired; + this->last_status.nacquired = nacquired; + this->last_status.attempts = attempts; + + // assemble the telemetry into a json message + // + nlohmann::json jmessage_out; + jmessage_out["source"] = "acamd"; + jmessage_out["ACQUIRE_MODE"] = this->target.acquire_mode_string(); + jmessage_out["IS_ACQUIRED"] = this->target.is_acquired.load(); + jmessage_out["NACQUIRED"] = this->target.nacquired; + jmessage_out["ATTEMPTS"] = this->target.attempts; + jmessage_out["SEEING"] = this->astrometry.get_seeing(); + jmessage_out["BACKGROUND"] = this->astrometry.get_background(); + + try { + this->publisher->publish( jmessage_out, "acamd" ); + } + catch ( const std::exception &e ) { + logwrite( "Acam::Interface::publish_status", + "ERROR publishing message: "+std::string(e.what()) ); + return; + } + } + /***** Acam::Interface::publish_status **************************************/ + + /***** Acam::Interface::request_snapshot ************************************/ /** * @brief sends request for snapshot @@ -3412,6 +3461,8 @@ logwrite( function, message.str() ); this->acquire_mode = requested_mode; + iface->publish_status(); + return NO_ERROR; } /***** Acam::Target::acquire ************************************************/ @@ -3797,6 +3848,8 @@ logwrite( function, message.str() ); logwrite( function, "ERROR writing to database: "+std::string(e.what()) ); } + iface->publish_status(); + return error; } /***** Acam::Target::do_acquire *********************************************/ @@ -5545,6 +5598,8 @@ logwrite( function, message.str() ); } } + this->publish_status(); + return NO_ERROR; } /***** Acam::Interface::offset_goal *****************************************/ diff --git a/acamd/acam_interface.h b/acamd/acam_interface.h index 673d26ed..729e60d6 100644 --- a/acamd/acam_interface.h +++ b/acamd/acam_interface.h @@ -510,6 +510,13 @@ namespace Acam { std::mutex framegrab_mtx; std::condition_variable cv; + struct { + std::string acquire_mode = ""; + bool is_acquired = false; + int nacquired = 0; + int attempts = 0; + } last_status; + public: std::string motion_host; @@ -645,6 +652,7 @@ namespace Acam { long bin( std::string args, std::string &retstring ); void publish_snapshot(); + void publish_status(); void request_snapshot(); bool wait_for_snapshots(); long handle_json_message( std::string message_in ); From ee0d3fc801805b9c81f7110dccfc417eccf06c43 Mon Sep 17 00:00:00 2001 From: David Hale Date: Tue, 17 Mar 2026 17:31:48 -0700 Subject: [PATCH 07/16] starts preparing sequencer for fine acquisition, not too much in terms of new functionality, mostly splits old functionality into another file for readability. does add a few fine acquisition references --- sequencerd/CMakeLists.txt | 1 + sequencerd/sequence.cpp | 97 +++++++++++++--------- sequencerd/sequence.h | 14 ++++ sequencerd/sequence_acquisition.cpp | 120 ++++++++++++++++++++++++++++ 4 files changed, 193 insertions(+), 39 deletions(-) create mode 100644 sequencerd/sequence_acquisition.cpp diff --git a/sequencerd/CMakeLists.txt b/sequencerd/CMakeLists.txt index fb685cac..dda2f133 100644 --- a/sequencerd/CMakeLists.txt +++ b/sequencerd/CMakeLists.txt @@ -37,6 +37,7 @@ add_executable(sequencerd ${SEQUENCER_DIR}/sequencerd.cpp ${SEQUENCER_DIR}/sequencer_server.cpp ${SEQUENCER_DIR}/sequencer_interface.cpp + ${SEQUENCER_DIR}/sequence_acquisition.cpp ${SEQUENCER_DIR}/sequence.cpp ${MYSQL_INCLUDES} ${PYTHON_DEV} diff --git a/sequencerd/sequence.cpp b/sequencerd/sequence.cpp index 7a265c19..c75e23d4 100644 --- a/sequencerd/sequence.cpp +++ b/sequencerd/sequence.cpp @@ -373,6 +373,42 @@ namespace Sequencer { } + /***** 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. + * + */ + void 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; + } + + this->is_usercontinue.store(false); + + this->async.enqueue_and_log( function, "NOTICE: received USER continue signal!" ); + } + /***** Sequencer::Sequence::wait_for_user ***********************************/ + + /***** Sequencer::Sequence::sequence_start **********************************/ /** * @brief main sequence start thread @@ -569,52 +605,35 @@ namespace Sequencer { break; } -/*** 12/17/24 move acquisition elsewhere? - * - * logwrite( function, "starting acquisition thread" ); ///< TODO @todo log to telemetry! - - * this->seq_state.set( Sequencer::SEQ_WAIT_ACQUIRE ); - * this->broadcast_seqstate(); - * std::thread( &Sequencer::Sequence::dothread_acquisition, this ).detach(); - ***/ - - // If not a calibration target then introduce a pause for the user - // to make adjustments, send offsets, etc. + // If not a calibration target and dotype is ALL then auto acquire, + // first acam then slicecam // - if ( !this->target.iscal ) { - - // waiting for user signal (or cancel) - // - // The sequencer is effectively paused waiting for user input. This - // gives the user a chance to ensure the correct target is on the slit, - // select offset stars, etc. - // - { - 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->target.iscal && !this->do_once.load() ) { - if ( this->cancel_flag.load() ) { - this->async.enqueue_and_log( function, "NOTICE: sequence cancelled" ); + // start ACAM acquisition + if ( this->do_acam_acquire() != NO_ERROR ) { + this->async.enqueue_and_log( function, "ERROR acam acquisition failed" ); + this->thread_error_manager.set( THR_ACQUISITION ); return; } - this->is_usercontinue.store(false); + // start SLICECAM fine acquisition + if ( this->do_slicecam_fineacquire() != NO_ERROR ) { + this->async.enqueue_and_log( function, "WARNING slicecam fine acquisition failed" ); + } + } + else - this->async.enqueue_and_log( function, "NOTICE: received USER continue signal!" ); + // Not a calibration target but do-one, i.e. "manual" then + // wait for a user continue + // + if ( !this->target.iscal && this->do_once.load() ) { + this->wait_for_user(); + } - // Ensure slit offset is in "expose" position - // + // Ensure slit offset is in "expose" position when needed + // + if ( !this->target.iscal ) { auto slitset = std::async(std::launch::async, &Sequence::slit_set, this, Sequencer::VSM_EXPOSE); try { error |= slitset.get(); diff --git a/sequencerd/sequence.h b/sequencerd/sequence.h index c931cce2..b296c0a4 100644 --- a/sequencerd/sequence.h +++ b/sequencerd/sequence.h @@ -288,6 +288,8 @@ 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 is_fineacquire_locked{false}; ///< is slicecam fine acquisition locked? + std::atomic is_acam_guiding{false}; ///< is acam guiding? /** @brief safely runs function in a detached thread using lambda to catch exceptions */ @@ -384,6 +386,10 @@ namespace Sequencer { /// std::mutex tcs_ontarget_mtx; /// std::condition_variable tcs_ontarget_cv; + std::mutex fineacquire_mtx; + std::condition_variable fineacquire_cv; + std::mutex acam_mtx; + std::condition_variable acam_cv; std::mutex camerad_mtx; std::condition_variable camerad_cv; std::mutex wait_mtx; @@ -549,6 +555,7 @@ namespace Sequencer { void dothread_acquisition(); /// performs the acquisition sequence when signalled void dothread_test(); + void wait_for_user(); ///< wait for the user to do something 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 @@ -558,6 +565,13 @@ namespace Sequencer { long focus_set(); long flexure_set(); + /** + * these are in sequence_acquisition.cpp + */ + long do_acam_acquire(); + long do_slicecam_fineacquire(); + + long acam_init(); ///< initializes connection to acamd long calib_init(); ///< initializes connection to calibd long camera_init(); ///< initializes connection to camerad diff --git a/sequencerd/sequence_acquisition.cpp b/sequencerd/sequence_acquisition.cpp new file mode 100644 index 00000000..7beab093 --- /dev/null +++ b/sequencerd/sequence_acquisition.cpp @@ -0,0 +1,120 @@ +/** + * @file sequence_acquisition.cpp + * @brief target acquisition code for the Sequence class + * @author David Hale + * + */ + +#include "sequence.h" + +namespace Sequencer { + + /***** Sequencer::Sequence::do_acam_acquire **********************************/ + /** + * @brief trigger ACAM acquisition and wait until guiding state reached + * @return NO_ERROR | ERROR | TIMEOUT + * + */ + long Sequence::do_acam_acquire() { + 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 ); + + // form and send the ACQUIRE command to ACAM + // + double ra_in = radec_to_decimal( this->target.ra_hms ) * TO_DEGREES; + double dec_in = radec_to_decimal( this->target.dec_dms ); + 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" ); + return ERROR; + } + + std::ostringstream cmd; + cmd << ACAMD_ACQUIRE << " " << ra_in << " " << dec_in << " " << angle_in; + + this->async.enqueue_and_log( function, "NOTICE: starting ACAM acquisition" ); + + if ( this->acamd.command( cmd.str(), reply ) != NO_ERROR ) { + this->async.enqueue_and_log( function, "ERROR sending acquire command to acamd" ); + 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_acam_guiding (I subscribe to this) + // or cancel, or timeout + // + 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->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; + } + + this->async.enqueue_and_log(function, "ACAM target acquired"); + return NO_ERROR; + } + /***** Sequencer::Sequence::do_acam_acquire **********************************/ + + + /***** Sequencer::Sequence::do_slicecam_fineacquire **************************/ + /** + * @brief trigger SLICECAM fine acquisition and wait until locked + * @return NO_ERROR | ERROR | TIMEOUT + * + */ + long Sequence::do_slicecam_fineacquire() { + const std::string function("Sequencer::Sequence::do_slicecam_fineacquire"); + + ScopedState wait_state(wait_state_manager, Sequencer::SEQ_WAIT_ACQUIRE); + + // 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"); + return ERROR; + } + + if ( reply.find("ERROR") != std::string::npos ) { + this->async.enqueue_and_log(function, "slicecam fine acquisition mode: "+reply); + return ERROR; + } + + this->async.enqueue_and_log(function, "NOTICE: 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 + // + 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); + }); + + 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; + } + + this->async.enqueue_and_log(function, "slicecam fine acquisition target acquired"); + return NO_ERROR; + } + /***** Sequencer::Sequence::do_slicecam_fineacquire **************************/ + +} From 581c6b3c3d5be0eb9bd981504555c3305f561103 Mon Sep 17 00:00:00 2001 From: David Hale Date: Wed, 18 Mar 2026 14:39:25 -0700 Subject: [PATCH 08/16] adds the necessary publish-subscribe functionality --- acamd/acam_interface.cpp | 47 ++++++++++++++------------------ acamd/acam_interface.h | 1 + acamd/acamd.cpp | 4 ++- common/message_keys.h | 21 +++++++++++++- sequencerd/sequence.cpp | 43 ++++++++++++++++++++++++++--- sequencerd/sequence.h | 6 ++++ sequencerd/sequencerd.cpp | 6 ++-- slicecamd/slicecam_interface.cpp | 45 ++++++++++++++++++++++++++++-- slicecamd/slicecam_interface.h | 7 +++++ slicecamd/slicecamd.cpp | 3 +- thermald/thermal_interface.cpp | 2 +- thermald/thermal_interface.h | 1 + 12 files changed, 148 insertions(+), 38 deletions(-) diff --git a/acamd/acam_interface.cpp b/acamd/acam_interface.cpp index ae06d582..ff50b572 100644 --- a/acamd/acam_interface.cpp +++ b/acamd/acam_interface.cpp @@ -1433,28 +1433,23 @@ namespace Acam { * */ void Interface::publish_snapshot() { - - // assemble the telemetry into a json message - // nlohmann::json jmessage_out; - - jmessage_out["source"] = "acamd"; // source of this telemetry + jmessage_out[Key::SOURCE] = Topic::ACAMD; int ccdtemp=99; - this->camera.andor.get_temperature( ccdtemp ); // temp is int - jmessage_out["TANDOR_ACAM"] = ( this->isopen("camera") ? - static_cast(ccdtemp) : // but the database wants floats - NAN ); - - jmessage_out["ACAM_FILTER"] = ( this->isopen("motion" ) ? - this->motion.get_current_filtername() : - "not_connected" ); - jmessage_out["ACAM_COVER"] = ( this->isopen("motion" ) ? - this->motion.get_current_coverpos() : - "not_connected" ); + this->camera.andor.get_temperature( ccdtemp ); // temp is int + jmessage_out[Key::Acamd::TANDOR] = ( this->isopen("camera") ? + static_cast(ccdtemp) : // but the database wants floats + NAN ); + jmessage_out[Key::Acamd::FILTER] = ( this->isopen("motion" ) ? + this->motion.get_current_filtername() : + "not_connected" ); + jmessage_out[Key::Acamd::COVER] = ( this->isopen("motion" ) ? + this->motion.get_current_coverpos() : + "not_connected" ); try { - this->publisher->publish( jmessage_out ); + this->publisher->publish( jmessage_out, Topic::SNAPSHOT ); } catch ( const std::exception &e ) { logwrite( "Acam::Interface::publish_snapshot", @@ -1494,16 +1489,16 @@ namespace Acam { // assemble the telemetry into a json message // nlohmann::json jmessage_out; - jmessage_out["source"] = "acamd"; - jmessage_out["ACQUIRE_MODE"] = this->target.acquire_mode_string(); - jmessage_out["IS_ACQUIRED"] = this->target.is_acquired.load(); - jmessage_out["NACQUIRED"] = this->target.nacquired; - jmessage_out["ATTEMPTS"] = this->target.attempts; - jmessage_out["SEEING"] = this->astrometry.get_seeing(); - jmessage_out["BACKGROUND"] = this->astrometry.get_background(); + jmessage_out[Key::SOURCE] = Topic::ACAMD; + jmessage_out[Key::Acamd::ACQUIRE_MODE] = this->target.acquire_mode_string(); + jmessage_out[Key::Acamd::IS_ACQUIRED] = this->target.is_acquired.load(); + jmessage_out[Key::Acamd::NACQUIRED] = this->target.nacquired; + 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(); try { - this->publisher->publish( jmessage_out, "acamd" ); + this->publisher->publish( jmessage_out, Topic::ACAMD ); } catch ( const std::exception &e ) { logwrite( "Acam::Interface::publish_status", @@ -1528,7 +1523,7 @@ namespace Acam { } } try { - this->publisher->publish( jmessage, "_snapshot" ); + this->publisher->publish( jmessage, Topic::SNAPSHOT ); } catch ( const std::exception &e ) { logwrite( "Acam::Interface::request_snapshot", diff --git a/acamd/acam_interface.h b/acamd/acam_interface.h index 729e60d6..b8989845 100644 --- a/acamd/acam_interface.h +++ b/acamd/acam_interface.h @@ -24,6 +24,7 @@ #include "tcsd_client.h" #include "skyinfo.h" #include "database.h" +#include "message_keys.h" #ifdef ANDORSIM #include "andorsim.h" diff --git a/acamd/acamd.cpp b/acamd/acamd.cpp index 52028d0d..0bc34dcb 100644 --- a/acamd/acamd.cpp +++ b/acamd/acamd.cpp @@ -174,7 +174,9 @@ int main(int argc, char **argv) { // initialize the pub/sub handler and give it time to start // - if ( acamd.interface.init_pubsub( {"tcsd", "targetinfo", "slitd"} ) == ERROR ) { + if ( acamd.interface.init_pubsub( { Topic::TCSD, + Topic::TARGETINFO, + Topic::SLITD } ) == ERROR ) { logwrite(function, "ERROR initializing publisher-subscriber handler"); acamd.exit_cleanly(); } diff --git a/common/message_keys.h b/common/message_keys.h index 0155cc8c..121db80b 100644 --- a/common/message_keys.h +++ b/common/message_keys.h @@ -10,10 +10,12 @@ namespace Topic { inline const std::string SNAPSHOT = "_snapshot"; + inline const std::string TARGETINFO = "targetinfo"; inline const std::string TCSD = "tcsd"; - inline const std::string TARGETINFO = "tcsd"; inline const std::string SLITD = "slitd"; inline const std::string CAMERAD = "camerad"; + inline const std::string ACAMD = "acamd"; + inline const std::string SLICECAMD = "slicecamd"; } namespace Key { @@ -23,4 +25,21 @@ namespace Key { namespace Camerad { inline const std::string READY = "ready"; } + + namespace Acamd { + inline const std::string TANDOR = "tandor"; + inline const std::string FILTER = "filter"; + inline const std::string COVER = "cover"; + inline const std::string ACQUIRE_MODE = "acquire_mode"; + inline const std::string IS_ACQUIRED = "is_acquired"; + inline const std::string NACQUIRED = "nacquired"; + inline const std::string ATTEMPTS = "attempts"; + inline const std::string SEEING = "seeing"; + inline const std::string BACKGROUND = "background"; + } + + namespace Slicecamd { + inline const std::string FINEACQUIRE_LOCKED = "fineacquire_locked"; + inline const std::string FINEACQUIRE_RUNNING = "fineacquire_running"; + } } diff --git a/sequencerd/sequence.cpp b/sequencerd/sequence.cpp index c75e23d4..518b3a18 100644 --- a/sequencerd/sequence.cpp +++ b/sequencerd/sequence.cpp @@ -22,7 +22,7 @@ namespace Sequencer { /** * @brief publishes snapshot of my telemetry * @details This publishes a JSON message containing a snapshot of my - * telemetry info when the subscriber receives the "_snapshot" + * telemetry info when the subscriber receives the Topic::SNAPSHOT * topic and the payload contains my daemon name. * @param[in] jmessage_in subscribed-received JSON message * @@ -43,7 +43,7 @@ namespace Sequencer { /***** Sequencer::Sequence::handletopic_camerad ****************************/ /** - * @brief handles camerad telemetry + * @brief handles Topic::CAMERAD telemetry * @param[in] jmessage subscribed-received JSON message * */ @@ -58,6 +58,40 @@ namespace Sequencer { /***** Sequencer::Sequence::handletopic_camerad ****************************/ + /***** Sequencer::Sequence::handletopic_slicecamd **************************/ + /** + * @brief handles Topic::SLICECAMD telemetry + * @param[in] jmessage subscribed-received JSON message + * + */ + 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(); + } + /***** Sequencer::Sequence::handletopic_slicecamd **************************/ + + + /***** Sequencer::Sequence::handletopic_acamd ******************************/ + /** + * @brief handles Topic::ACAMD telemetry + * @param[in] jmessage subscribed-received JSON message + * + */ + 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); + std::lock_guard lock(this->acam_mtx); + this->acam_cv.notify_all(); + } + /***** Sequencer::Sequence::handletopic_acamd ******************************/ + + /***** Sequencer::Sequence::publish_snapshot *******************************/ /** * @brief publishes snapshot of my telemetry @@ -549,10 +583,11 @@ namespace Sequencer { 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. // - else { // set pointmode explicitly, in case it's empty this->target.pointmode = Acam::POINTMODE_SLIT; @@ -624,7 +659,7 @@ namespace Sequencer { } else - // Not a calibration target but do-one, i.e. "manual" then + // Not a calibration target but dotype is ONE, i.e. "manual" then // wait for a user continue // if ( !this->target.iscal && this->do_once.load() ) { diff --git a/sequencerd/sequence.h b/sequencerd/sequence.h index b296c0a4..246a35b7 100644 --- a/sequencerd/sequence.h +++ b/sequencerd/sequence.h @@ -341,6 +341,10 @@ namespace Sequencer { topic_handlers = { { Topic::SNAPSHOT, std::function( [this](const nlohmann::json &msg) { handletopic_snapshot(msg); } ) }, + { Topic::ACAMD, std::function( + [this](const nlohmann::json &msg) { handletopic_acamd(msg); } ) }, + { Topic::SLICECAMD, std::function( + [this](const nlohmann::json &msg) { handletopic_slicecamd(msg); } ) }, { Topic::CAMERAD, std::function( [this](const nlohmann::json &msg) { handletopic_camerad(msg); } ) } }; @@ -462,6 +466,8 @@ namespace Sequencer { void handletopic_snapshot( const nlohmann::json &jmessage ); void handletopic_camerad( const nlohmann::json &jmessage ); + void handletopic_acamd( const nlohmann::json &jmessage ); + void handletopic_slicecamd( const nlohmann::json &jmessage ); void publish_snapshot(); void publish_snapshot(std::string &retstring); void publish_seqstate(); diff --git a/sequencerd/sequencerd.cpp b/sequencerd/sequencerd.cpp index 841bb4f0..3b9ba63f 100644 --- a/sequencerd/sequencerd.cpp +++ b/sequencerd/sequencerd.cpp @@ -127,9 +127,11 @@ int main(int argc, char **argv) { sequencerd.exit_cleanly(); } - // initialize the pub/sub handler + // initialize the pub-sub handler with my subscriber topics // - if ( sequencerd.sequence.init_pubsub( {"camerad"} ) == ERROR ) { + if ( sequencerd.sequence.init_pubsub( { Topic::CAMERAD, + Topic::ACAMD, + Topic::SLICECAMD } ) == ERROR ) { logwrite(function, "ERROR initializing publisher-subscriber handler"); sequencerd.exit_cleanly(); } diff --git a/slicecamd/slicecam_interface.cpp b/slicecamd/slicecam_interface.cpp index 4e7b70f1..2a966e6d 100644 --- a/slicecamd/slicecam_interface.cpp +++ b/slicecamd/slicecam_interface.cpp @@ -56,6 +56,7 @@ namespace Slicecam { } else { this->is_fineacquire_running.store(false); + this->publish_status(); logwrite(function, "stop requested"); } retstring=this->is_fineacquire_running.load(std::memory_order_acquire)?"running":"stopped"; @@ -100,6 +101,9 @@ namespace Slicecam { this->is_fineacquire_locked.store(false, std::memory_order_release); this->is_fineacquire_running.store(true, std::memory_order_release); + // 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"; @@ -138,6 +142,7 @@ namespace Slicecam { if (it==this->camera.andor.end() || it->second==nullptr) { logwrite(function, "slicecam '"+which+"' not found!"); this->is_fineacquire_running.store( false, std::memory_order_release ); + this->publish_status(); logwrite(function, "fine target acquisition disabled"); return; } @@ -228,6 +233,7 @@ namespace Slicecam { this->is_fineacquire_locked.store( true, std::memory_order_release ); this->is_fineacquire_running.store( false, std::memory_order_release ); this->fineacquire_state.reset(); + this->publish_status(); return; } @@ -246,6 +252,7 @@ namespace Slicecam { if ( this->offset_acam_goal( { cmd_dra, cmd_ddec }, true ) != NO_ERROR ) { logwrite( function, "ERROR failed to send offset to ACAM" ); this->is_fineacquire_running.store( false, std::memory_order_release ); + this->publish_status(); return; } @@ -400,6 +407,40 @@ namespace Slicecam { } + /***** Slicecam::Interface::publish_status **********************************/ + /** + * @brief publishes my important status on change + * @details This publishes a JSON message containing important telemetry. + * + */ + void Interface::publish_status() { + const bool is_fineacquire_running_now = this->is_fineacquire_running.load(); + const bool is_fineacquire_locked_now = this->is_fineacquire_locked.load(); + + // only publish if there was a change + // + if ( is_fineacquire_running_now == this->last_status.is_fineacquire_running && + is_fineacquire_locked_now == this->last_status.is_fineacquire_locked) return; + + this->last_status.is_fineacquire_running = is_fineacquire_running_now; + this->last_status.is_fineacquire_locked = is_fineacquire_locked_now; + + nlohmann::json jmessage_out; + jmessage_out[Key::SOURCE] = Topic::SLICECAMD; + jmessage_out[Key::Slicecamd::FINEACQUIRE_RUNNING] = this->is_fineacquire_running.load(); + jmessage_out[Key::Slicecamd::FINEACQUIRE_LOCKED] = this->is_fineacquire_locked.load(); + + try { + this->publisher->publish(jmessage_out, Topic::SLICECAMD); + } + catch (const std::exception &e) { + logwrite("Slicecam::Interface::publish_status", + "ERROR publishing status: "+std::string(e.what())); + } + } + /***** Slicecam::Interface::publish_status **********************************/ + + /***** Slicecam::Interface::publish_snapshot ********************************/ /** * @brief publishes snapshot of my telemetry @@ -409,7 +450,7 @@ namespace Slicecam { */ void Interface::publish_snapshot() { nlohmann::json jmessage_out; - jmessage_out["source"] = "slicecamd"; + jmessage_out[Key::SOURCE] = Topic::SLICECAMD; for ( const auto &[name, cam] : this->camera.andor ) { std::string key="TANDOR_SCAM_"+name; @@ -442,7 +483,7 @@ namespace Slicecam { } try { - this->publisher->publish( jmessage_out, "_snapshot" ); + this->publisher->publish( jmessage_out, Topic::SNAPSHOT ); } catch ( const std::exception &e ) { logwrite( "Slicecam::Interface::request_snapshot", diff --git a/slicecamd/slicecam_interface.h b/slicecamd/slicecam_interface.h index 4f096a3f..3d002436 100644 --- a/slicecamd/slicecam_interface.h +++ b/slicecamd/slicecam_interface.h @@ -27,6 +27,7 @@ #include "skyinfo.h" #include "slicecam_camera.h" #include "slicecam_math.h" +#include "message_keys.h" #define PYTHON_PATH "/home/developer/Software/Python:/home/developer/Software/Python/acam_skyinfo" #define PYTHON_ASTROMETRY_MODULE "astrometry" @@ -160,6 +161,11 @@ namespace Slicecam { std::mutex snapshot_mtx; std::unordered_map snapshot_status; + struct { + bool is_fineacquire_running=false; + bool is_fineacquire_locked=false; + } last_status; + GUIManager gui_manager; Interface() @@ -222,6 +228,7 @@ namespace Slicecam { void handletopic_snapshot( const nlohmann::json &jmessage ); void handletopic_slitd( const nlohmann::json &jmessage ); void handletopic_tcsd( const nlohmann::json &jmessage ); + void publish_status(); void publish_snapshot(); void request_snapshot(); bool wait_for_snapshots(); diff --git a/slicecamd/slicecamd.cpp b/slicecamd/slicecamd.cpp index 332c51d2..694a31f8 100644 --- a/slicecamd/slicecamd.cpp +++ b/slicecamd/slicecamd.cpp @@ -146,7 +146,8 @@ int main(int argc, char **argv) { // initialize the pub/sub handler, which // takes a list of subscription topics // - if ( slicecamd.interface.init_pubsub({"slitd", "tcsd"}) == ERROR ) { + if ( slicecamd.interface.init_pubsub( { Topic::SLITD, + Topic::TCSD }) == ERROR ) { logwrite(function, "ERROR initializing publisher-subscriber handler"); slicecamd.exit_cleanly(); } diff --git a/thermald/thermal_interface.cpp b/thermald/thermal_interface.cpp index bfc5b438..9f59a09c 100644 --- a/thermald/thermal_interface.cpp +++ b/thermald/thermal_interface.cpp @@ -154,7 +154,7 @@ namespace Thermal { // no errors, so disseminate the message contents based on the message type // if ( messagetype == "acaminfo" ) { - this->process_key( jmessage, "TANDOR_ACAM" ); + this->process_key( jmessage, Key::Acamd::TANDOR ); } else if ( messagetype == "slicecaminfo" ) { diff --git a/thermald/thermal_interface.h b/thermald/thermal_interface.h index fa246006..9d3d0c8d 100644 --- a/thermald/thermal_interface.h +++ b/thermald/thermal_interface.h @@ -8,6 +8,7 @@ #pragma once +#include "message_keys.h" #include "network.h" #include "logentry.h" #include "common.h" From 647be12f4520b8d6011d2c5e45999ae00b0fae92 Mon Sep 17 00:00:00 2001 From: David Hale Date: Wed, 18 Mar 2026 15:46:26 -0700 Subject: [PATCH 09/16] adds some missing Topics --- common/message_keys.h | 3 +++ sequencerd/sequence.cpp | 12 ++++++------ 2 files changed, 9 insertions(+), 6 deletions(-) diff --git a/common/message_keys.h b/common/message_keys.h index 121db80b..56e9cfb5 100644 --- a/common/message_keys.h +++ b/common/message_keys.h @@ -15,6 +15,9 @@ namespace Topic { inline const std::string SLITD = "slitd"; inline const std::string CAMERAD = "camerad"; inline const std::string ACAMD = "acamd"; + inline const std::string SEQ_DAEMONSTATE = "seq_daemonstate"; + inline const std::string SEQ_THREADSTATE = "seq_threadstate"; + inline const std::string SEQ_WAITSTATE = "seq_waitstate"; inline const std::string SLICECAMD = "slicecamd"; } diff --git a/sequencerd/sequence.cpp b/sequencerd/sequence.cpp index 518b3a18..4e292e4c 100644 --- a/sequencerd/sequence.cpp +++ b/sequencerd/sequence.cpp @@ -148,7 +148,7 @@ namespace Sequencer { */ void Sequence::publish_waitstate() { nlohmann::json jmessage_out; - jmessage_out["source"] = Sequencer::DAEMON_NAME; + jmessage_out[Key::SOURCE] = Sequencer::DAEMON_NAME; // iterate through map of daemon state bits, add each as a key in the JSON message, // and set true|false if the bit is set or not @@ -158,7 +158,7 @@ namespace Sequencer { } try { - this->publisher->publish( jmessage_out, "seq_waitstate" ); + this->publisher->publish( jmessage_out, Topic::SEQ_WAITSTATE ); } catch ( const std::exception &e ) { logwrite( "Sequencer::Sequence::publish_waitstate", @@ -178,7 +178,7 @@ namespace Sequencer { */ void Sequence::publish_daemonstate() { nlohmann::json jmessage_out; - jmessage_out["source"] = Sequencer::DAEMON_NAME; + jmessage_out[Key::SOURCE] = Sequencer::DAEMON_NAME; // iterate through map of daemon state bits, add each as a key in the JSON message, // and set true|false if the bit is set or not @@ -188,7 +188,7 @@ namespace Sequencer { } try { - this->publisher->publish( jmessage_out, "seq_daemonstate" ); + this->publisher->publish( jmessage_out, Topic::SEQ_DAEMONSTATE ); } catch ( const std::exception &e ) { logwrite( "Sequencer::Sequence::publish_daemonstate", @@ -208,7 +208,7 @@ namespace Sequencer { */ void Sequence::publish_threadstate() { nlohmann::json jmessage_out; - jmessage_out["source"] = Sequencer::DAEMON_NAME; + jmessage_out[Key::SOURCE] = Sequencer::DAEMON_NAME; // 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 @@ -218,7 +218,7 @@ namespace Sequencer { } try { - this->publisher->publish( jmessage_out, "seq_threadstate" ); + this->publisher->publish( jmessage_out, Topic::SEQ_THREADSTATE ); } catch ( const std::exception &e ) { logwrite( "Sequencer::Sequence::publish_threadstate", From 23737ca2f9361055f88428727ed754f4398f794b Mon Sep 17 00:00:00 2001 From: David Hale Date: Wed, 18 Mar 2026 18:03:56 -0700 Subject: [PATCH 10/16] removes some obsolete code --- sequencerd/sequence.cpp | 154 +--------------------------------------- sequencerd/sequence.h | 1 - 2 files changed, 3 insertions(+), 152 deletions(-) diff --git a/sequencerd/sequence.cpp b/sequencerd/sequence.cpp index 4e292e4c..59135476 100644 --- a/sequencerd/sequence.cpp +++ b/sequencerd/sequence.cpp @@ -2083,10 +2083,6 @@ namespace Sequencer { // if ( this->cancel_flag.load() ) return NO_ERROR; - // if ontarget (not cancelled) then acquire target - // - if ( !this->cancel_flag.load() ) this->acamd.command( ACAMD_ACQUIRE ); - this->is_ontarget.store(false); // remember the last target that was tracked on @@ -2580,141 +2576,6 @@ namespace Sequencer { /***** Sequencer::Sequence::modify_exptime **********************************/ - /***** Sequencer::Sequence::dothread_acquisition ****************************/ - /** - * @brief performs the acqusition sequence - * @details this gets called by the move_to_target thread - * - * This function is spawned in a thread. - * - */ - void Sequence::dothread_acquisition() { - const std::string function("Sequencer::Sequence::dothread_acquisition"); - std::stringstream message; - std::stringstream cmd; - std::string reply; - long error = NO_ERROR; - - ScopedState thr_state( thread_state_manager, Sequencer::THR_ACQUISITION ); - ScopedState wait_state( wait_state_manager, Sequencer::SEQ_WAIT_ACQUIRE ); - - // Before sending target coordinates to ACAM, - // convert them to decimal and to ACAM coordinates. - // (fpoffsets.coords_* are always in degrees) - // - double ra_in = radec_to_decimal( this->target.ra_hms ) * TO_DEGREES; - double dec_in = radec_to_decimal( this->target.dec_dms ); - double angle_in = this->target.slitangle; - - // can't be NaN - // - bool ra_isnan = std::isnan( ra_in ); - bool dec_isnan = std::isnan( dec_in ); - - if ( ra_isnan || dec_isnan ) { - message.str(""); message << "ERROR: converting"; - 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, message.str() ); - this->thread_error_manager.set( THR_MOVE_TO_TARGET ); - return; - } - -// // Before sending the target coords to the ACAM, -// // convert them from to ACAM coordinates. -// // -// double ra_out, dec_out, angle_out; -// error = this->target.fpoffsets.compute_offset( this->target.pointmode, "ACAM", -// ra_in, dec_in, angle_in, -// ra_out, dec_out, angle_out ); -// -// // Send the ACQUIRE command to acamd, which requires -// // the target coordinates (from the database). -// // -// message.str(""); message << "starting target acquisition " << ra_out << " " -// << dec_out << " " -// << angle_out << " " -// << this->target.name; - message.str(""); message << "starting target acquisition " << ra_in << " " - << dec_in << " " - << angle_in << " " - << this->target.name; - logwrite( function, message.str() ); - cmd.str(""); cmd << ACAMD_ACQUIRE << " " << ra_in << " " - << dec_in << " " - << angle_in << " "; - - error = this->acamd.command( cmd.str(), reply ); - -/***** DONT CARE ABOUT ERRORS NOW -- NO CONDITION ON ACQ SUCCESS - if ( error != NO_ERROR ) { - this->thread_error_manager.set( THR_ACQUISITION ); // report error - message.str(""); message << "ERROR acquiring target"; - this->async.enqueue_and_log( function, message.str() ); - this->seq_state.clear( Sequencer::SEQ_WAIT_ACQUIRE ); // clear ACQUIRE bit - this->broadcast_seqstate(); - return; - } - - // The reply contains the timeout. - // Acam's acquisition sequence uses that timeout but the Sequencer - // will also use it here, so that it knows when to stop asking acamd - // for its acquisition status. - // - double timeout; - try { - timeout = std::stod( reply ); - } catch( std::out_of_range &e ) { - message.str(""); message << "ERROR parsing timeout \"" << reply << "\" from acam: " << e.what(); - logwrite( function, message.str() ); - this->thread_error_manager.set( THR_ACQUISITION ); // report any error - return; - } - - auto timeout_time = std::chrono::steady_clock::now() - + std::chrono::duration(timeout); - - reply.clear(); - - // Poll acamd while it is acquiring. Once finished, get the state. - // - bool acquiring = true; - do { - std::this_thread::sleep_for( std::chrono::milliseconds(100) ); - if (error==NO_ERROR) error = this->acamd.command( ACAMD_ACQUIRE, reply ); - acquiring = ( reply.find("acquiring") != std::string::npos ); - } while ( error==NO_ERROR && - acquiring && - std::chrono::steady_clock::now() < timeout_time ); - - // Acquisition loop complete so get the state - // - error = this->acamd.command( ACAMD_ISACQUIRED, reply ); - this->target.acquired = ( reply.find("true") != std::string::npos ); - - // set message - // - if ( std::chrono::steady_clock::now() >= timeout_time ) { // Timeout - this->thread_error_manager.set( THR_ACQUISITION ); - message.str(""); message << "ERROR failed to acquire within timeout"; - } - else - if ( error!=NO_ERROR ) { // Error polling - this->thread_error_manager.set( THR_ACQUISITION ); - message.str(""); message << "ERROR acquiring target"; - } - else { // Success - message.str(""); message << "NOTICE: target " << ( this->target.acquired ? "acquired" : "not acquired" ); - } - - this->async.enqueue_and_log( function, message.str() ); // log message -*****/ - - } - /***** Sequencer::Sequence::dothread_acquisition ****************************/ - - /***** Sequencer::Sequence::startup *****************************************/ /** * @brief performs nightly startup @@ -3499,16 +3360,7 @@ namespace Sequencer { const std::string function("Sequencer::Sequence::target_offset"); long error=NO_ERROR; - bool is_guiding = false; - std::string reply; - if ( this->acamd.command( ACAMD_ACQUIRE, reply ) == NO_ERROR ) { - if ( reply.find( "guiding" ) != std::string::npos ) is_guiding = true; - } - else { - logwrite( function, "ERROR reading ACAM guide state, falling back to TCS offset" ); - } - - if ( is_guiding ) { + if ( this->is_acam_guiding.load() ) { // ACAMD_OFFSETGOAL expects degrees; target offsets are arcsec const double dra_deg = this->target.offset_ra / 3600.0; const double ddec_deg = this->target.offset_dec / 3600.0; @@ -4676,8 +4528,8 @@ namespace Sequencer { // Finally, spawn the acquisition thread // - logwrite( function, "spawning dothread_acquisition..." ); - if (error==NO_ERROR) std::thread( &Sequencer::Sequence::dothread_acquisition, this ).detach(); + logwrite( function, "spawning do_acam_acquire..." ); + if (error==NO_ERROR) std::thread( &Sequencer::Sequence::do_acam_acquire, this ).detach(); } else diff --git a/sequencerd/sequence.h b/sequencerd/sequence.h index 246a35b7..3c2cad3f 100644 --- a/sequencerd/sequence.h +++ b/sequencerd/sequence.h @@ -558,7 +558,6 @@ namespace Sequencer { 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_acquisition(); /// performs the acquisition sequence when signalled void dothread_test(); void wait_for_user(); ///< wait for the user to do something From d382c10f4511c539c7651c75d4c54d5d8306bc44 Mon Sep 17 00:00:00 2001 From: David Hale Date: Thu, 19 Mar 2026 13:04:34 -0700 Subject: [PATCH 11/16] replaces Slicecam::Interface::get_acam_guide_state() with handletopic_acamd() --- slicecamd/slicecam_interface.cpp | 113 ++++++------------------------- slicecamd/slicecam_interface.h | 13 ++-- 2 files changed, 27 insertions(+), 99 deletions(-) diff --git a/slicecamd/slicecam_interface.cpp b/slicecamd/slicecam_interface.cpp index 2a966e6d..d06ed95f 100644 --- a/slicecamd/slicecam_interface.cpp +++ b/slicecamd/slicecam_interface.cpp @@ -372,6 +372,21 @@ namespace Slicecam { } + /***** Slicecam::Interface::handletopic_acamd *******************************/ + /** + * @brief handles Topic::ACAMD telemetry + * @param[in] jmessage subscribed-received JSON message + * + */ + void Interface::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); + } + /***** Slicecam::Interface::handletopic_acamd *******************************/ + + void Interface::handletopic_slitd( const nlohmann::json &jmessage ) { { std::lock_guard lock(snapshot_mtx); @@ -1570,68 +1585,6 @@ namespace Slicecam { /***** Slicecam::Interface::dothread_fpoffset *******************************/ - /***** Slicecam::Interface::get_acam_guide_state ****************************/ - /** - * @brief asks if ACAM is guiding - * @details The only way this can fail is if acam is connected but returns - * an error reading the state. - * @param[out] is_guiding bool guiding state - * @return ERROR | NO_ERROR - * - */ - long Interface::get_acam_guide_state( bool &is_guiding ) { - std::string function = "Slicecam::Interface::get_acam_guide_state"; - std::stringstream message; - long error = NO_ERROR; - std::string retstring; - - // If not connected to acamd then try to connect to the daemon. - // If there's an error in doing this then assume acamd is not even - // running, in which case the guiding cannot be running. - // - error = this->acamd.is_connected(retstring); - if ( error == ERROR ) { - logwrite( function, "ERROR no response from acamd -- will assume guiding is inactive" ); - is_guiding = false; - return NO_ERROR; - } - - // Not connected, try to connect - // - if ( retstring.find("false") != std::string::npos ) { - logwrite( function, "connecting to acamd" ); - error = this->acamd.connect(); - if ( error != NO_ERROR ) { - logwrite( function, "ERROR unable to connect to acamd -- will assume guiding is inactive" ); - is_guiding=false; - return NO_ERROR; - } - logwrite( function, "connected to acamd" ); - } - - // Is acamd guiding? At this point slicecam is connected to acamd, so - // consider an error here as a fault and don't continute. - // - error = this->acamd.send( ACAMD_ACQUIRE, retstring ); - if ( error != NO_ERROR ) { - logwrite( function, "ERROR getting guiding state from ACAM" ); - return ERROR; - } - - // If guiding is in the return string then it is enabled. - // - if ( retstring.find( "guiding" ) != std::string::npos ) { - is_guiding = true; - } - else is_guiding = false; - - message.str(""); message << "acam is" << ( is_guiding ? " " : " not " ) << "guiding"; - - return NO_ERROR; - } - /***** Slicecam::Interface::get_acam_guide_state ****************************/ - - /***** Slicecam::Interface::offset_acam_goal ********************************/ /** * @brief applies offset to ACAM goal, or move telescope directly @@ -1653,13 +1606,7 @@ namespace Slicecam { // If ACAM is guiding then slicecam must not move the telescope, // but must allow ACAM to perform the offset. // - bool is_guiding; - long error = this->get_acam_guide_state( is_guiding ); - - if ( error != NO_ERROR ) { - logwrite( function, "ERROR getting guide state" ); - return ERROR; - } + bool is_guiding = this->is_acam_guiding.load(); // send the offsets now // @@ -1672,8 +1619,7 @@ namespace Slicecam { // add fineguiding arg when used for fine acquisition mode if (is_fineacquire) cmd << " fineguiding"; - error = this->acamd.command( cmd.str() ); - if ( error != NO_ERROR ) { + if (this->acamd.command( cmd.str() ) != NO_ERROR) { logwrite( function, "ERROR adding offset to acam goal" ); return ERROR; } @@ -1769,17 +1715,6 @@ namespace Slicecam { return ERROR; } - // If ACAM is guiding then slicecam must not move the telescope, - // but must allow ACAM to perform the offset. - // - bool is_guiding; - long error = this->get_acam_guide_state( is_guiding ); - - if ( error != NO_ERROR ) { - logwrite( function, "ERROR getting guide state" ); - return ERROR; - } - // send the offsets now // return this->offset_acam_goal( { ra_off, dec_off } ); @@ -2104,9 +2039,7 @@ namespace Slicecam { retstring.append( " Return the acam guiding state\n" ); return HELP; } - bool is_guiding; - error = this->get_acam_guide_state( is_guiding ); - message.str(""); message << "request returned " << ( error==ERROR ? "ERROR" : "NO_ERROR" ) << ": guiding is " << ( is_guiding ? "on" : "off" ); + message.str(""); message << ": ACAM guiding is " << ( this->is_acam_guiding.load() ? "on" : "off" ); retstring = message.str(); } else @@ -2122,16 +2055,8 @@ namespace Slicecam { retstring="invalid_argument"; return ERROR; } - bool is_guiding; - long error = this->get_acam_guide_state( is_guiding ); - - if ( error != NO_ERROR ) { - logwrite( function, "ERROR getting guide state" ); - retstring="acamd_error"; - return ERROR; - } - if ( !is_guiding ) { + if ( !this->is_acam_guiding.load() ) { logwrite( function, "ERROR acam is not guiding" ); retstring="not_guiding"; return ERROR; diff --git a/slicecamd/slicecam_interface.h b/slicecamd/slicecam_interface.h index 3d002436..cbc8c2e1 100644 --- a/slicecamd/slicecam_interface.h +++ b/slicecamd/slicecam_interface.h @@ -135,6 +135,7 @@ namespace Slicecam { std::atomic is_framegrab_running; ///< set if framegrab loop is running std::atomic is_fineacquire_running; ///< set if fine target acquisition is running std::atomic is_fineacquire_locked; ///< set when fine acquire target acquired + std::atomic is_acam_guiding; ///< is acam guiding? /** these are set by Interface::saveframes() */ @@ -179,16 +180,19 @@ namespace Slicecam { is_framegrab_running(false), is_fineacquire_running(false), is_fineacquire_locked(false), + is_acam_guiding(false), nsave_preserve_frames(0), nskip_preserve_frames(0), snapshot_status { { "slitd", false }, {"tcsd", false} } { topic_handlers = { - { "_snapshot", std::function( + { Topic::SNAPSHOT, std::function( [this](const nlohmann::json &msg) { handletopic_snapshot(msg); } ) }, - { "tcsd", std::function( + { Topic::ACAMD, std::function( + [this](const nlohmann::json &msg) { handletopic_acamd(msg); } ) }, + { Topic::TCSD, std::function( [this](const nlohmann::json &msg) { handletopic_tcsd(msg); } ) }, - { "slitd", std::function( + { Topic::SLITD, std::function( [this](const nlohmann::json &msg) { handletopic_slitd(msg); } ) } }; } @@ -226,6 +230,7 @@ namespace Slicecam { void stop_subscriber_thread() { Common::PubSubHandler::stop_subscriber_thread(*this); } void handletopic_snapshot( const nlohmann::json &jmessage ); + void handletopic_acamd( const nlohmann::json &jmessage ); void handletopic_slitd( const nlohmann::json &jmessage ); void handletopic_tcsd( const nlohmann::json &jmessage ); void publish_status(); @@ -262,8 +267,6 @@ namespace Slicecam { long fan_mode( std::string args, std::string &retstring ); long gain( std::string args, std::string &retstring ); - long get_acam_guide_state( bool &is_guiding ); - long offset_acam_goal(const std::pair &offsets, std::optional fineacquire=std::nullopt); long collect_header_info( std::unique_ptr &slicecam ); From d3b959d8fd505b77491cd2ca234b3d430a5c32d4 Mon Sep 17 00:00:00 2001 From: David Hale Date: Thu, 19 Mar 2026 13:14:08 -0700 Subject: [PATCH 12/16] added missing acam guiding check in Slicecam::Interface::fineacquire --- slicecamd/slicecam_interface.cpp | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/slicecamd/slicecam_interface.cpp b/slicecamd/slicecam_interface.cpp index d06ed95f..17946882 100644 --- a/slicecamd/slicecam_interface.cpp +++ b/slicecamd/slicecam_interface.cpp @@ -87,6 +87,13 @@ 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"; + return ERROR; + } + const std::string which = tokens.at(1); if (which != "L" && which != "R") { logwrite(function, "ERROR expected stop | start { L | R }"); From 7f547250ae323ddbc8923df24ee31cf287f84b19 Mon Sep 17 00:00:00 2001 From: David Hale Date: Thu, 19 Mar 2026 16:00:43 -0700 Subject: [PATCH 13/16] corrects flow of sequence_start and allows wait_for_user to be cancelled --- common/message_keys.h | 5 +++ sequencerd/sequence.cpp | 82 ++++++++++++++++++++++------------------- sequencerd/sequence.h | 2 +- 3 files changed, 51 insertions(+), 38 deletions(-) diff --git a/common/message_keys.h b/common/message_keys.h index 56e9cfb5..ab7d8a46 100644 --- a/common/message_keys.h +++ b/common/message_keys.h @@ -16,6 +16,7 @@ namespace Topic { inline const std::string CAMERAD = "camerad"; inline const std::string ACAMD = "acamd"; inline const std::string SEQ_DAEMONSTATE = "seq_daemonstate"; + inline const std::string SEQ_SEQSTATE = "seq_seqstate"; inline const std::string SEQ_THREADSTATE = "seq_threadstate"; inline const std::string SEQ_WAITSTATE = "seq_waitstate"; inline const std::string SLICECAMD = "slicecamd"; @@ -25,6 +26,10 @@ namespace Key { inline const std::string SOURCE = "source"; + namespace Sequencer { + inline const std::string SEQSTATE = "seqstate"; + } + namespace Camerad { inline const std::string READY = "ready"; } diff --git a/sequencerd/sequence.cpp b/sequencerd/sequence.cpp index 59135476..5db8147d 100644 --- a/sequencerd/sequence.cpp +++ b/sequencerd/sequence.cpp @@ -119,15 +119,15 @@ namespace Sequencer { */ void Sequence::publish_seqstate() { nlohmann::json jmessage_out; - jmessage_out["source"] = Sequencer::DAEMON_NAME; + jmessage_out[Key::SOURCE] = Sequencer::DAEMON_NAME; // sequencer state std::string seqstate( this->seq_state_manager.get_set_states() ); rtrim( seqstate ); - jmessage_out["seqstate"] = seqstate; + jmessage_out[Key::Sequencer::SEQSTATE] = seqstate; try { - this->publisher->publish( jmessage_out, "seq_seqstate" ); + this->publisher->publish( jmessage_out, Topic::SEQ_SEQSTATE ); } catch ( const std::exception &e ) { logwrite( "Sequencer::Sequence::publish_seqstate", @@ -412,9 +412,10 @@ namespace Sequencer { * @brief waits for the user to click a button, or cancel * @details Use this when you just want to slow things down or get a * cup of coffee instead of observing. + * @return NO_ERROR on continue | ABORT on cancel * */ - void Sequence::wait_for_user() { + long Sequence::wait_for_user() { const std::string function("Sequencer::Sequence::wait_for_user"); { ScopedState wait_state( wait_state_manager, Sequencer::SEQ_WAIT_USER ); @@ -433,12 +434,14 @@ namespace Sequencer { if ( this->cancel_flag.load() ) { this->async.enqueue_and_log( function, "NOTICE: sequence cancelled" ); - return; + 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 ***********************************/ @@ -640,38 +643,36 @@ namespace Sequencer { break; } - // If not a calibration target and dotype is ALL then auto acquire, - // first acam then slicecam + // If not a calibration target then acquire, first acam then slicecam // - if ( !this->target.iscal && !this->do_once.load() ) { + if ( !this->target.iscal ) { - // start ACAM acquisition + // start ACAM acquisition. If it fails then wait for user to continue or cancel. if ( this->do_acam_acquire() != NO_ERROR ) { - this->async.enqueue_and_log( function, "ERROR acam acquisition failed" ); - this->thread_error_manager.set( THR_ACQUISITION ); - return; + 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" ); } } - else - - // Not a calibration target but dotype is ONE, i.e. "manual" then - // wait for a user continue - // - if ( !this->target.iscal && this->do_once.load() ) { - this->wait_for_user(); - } - // Ensure slit offset is in "expose" position when needed - // if ( !this->target.iscal ) { - auto slitset = std::async(std::launch::async, &Sequence::slit_set, this, Sequencer::VSM_EXPOSE); + // 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 |= slitset.get(); + error |= this->slit_set(Sequencer::VSM_EXPOSE); } catch (const std::exception& e) { logwrite( function, "ERROR slit offset exception: "+std::string(e.what()) ); @@ -3358,27 +3359,34 @@ namespace Sequencer { */ long Sequence::target_offset() { const std::string function("Sequencer::Sequence::target_offset"); - long error=NO_ERROR; - if ( this->is_acam_guiding.load() ) { + // 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; + + // zero TCS offsets before applying target offset + long error = this->tcsd.command( TCSD_ZERO_OFFSETS ); + + // when ACAM is guiding, offsets are handled by changing his goal + if (error==NO_ERROR && this->is_acam_guiding.load()) { // ACAMD_OFFSETGOAL expects degrees; target offsets are arcsec const double dra_deg = this->target.offset_ra / 3600.0; const double ddec_deg = this->target.offset_dec / 3600.0; std::stringstream cmd; cmd << ACAMD_OFFSETGOAL << " " << std::fixed << std::setprecision(6) << dra_deg << " " << ddec_deg; error = this->acamd.command( cmd.str() ); - logwrite( function, "sent "+cmd.str()+" (guiding)" ); - return error; + } + else + // if ACAM is not guiding then I send the offsets directly to the TCS + if (error==NO_ERROR) { + std::ostringstream cmd; + cmd << TCSD_PTOFFSET << " " << this->target.offset_ra << " " << this->target.offset_dec; + error = this->tcsd.command( cmd.str() ); } - error = this->tcsd.command( TCSD_ZERO_OFFSETS ); - - std::stringstream cmd; - cmd << TCSD_PTOFFSET << " " << this->target.offset_ra << " " << this->target.offset_dec; - - error |= this->tcsd.command( cmd.str() ); - - logwrite( function, "sent "+cmd.str() ); + std::ostringstream oss; + oss << (error==NO_ERROR?"":"ERROR ") << "target offsets" << (error==NO_ERROR ? " " : " not ") << "applied"; + logwrite(function, oss.str()); return error; } diff --git a/sequencerd/sequence.h b/sequencerd/sequence.h index 3c2cad3f..ab86cc45 100644 --- a/sequencerd/sequence.h +++ b/sequencerd/sequence.h @@ -560,7 +560,7 @@ namespace Sequencer { void modify_exptime( double exptime_in ); ///< modify exptime while exposure running void dothread_test(); - void wait_for_user(); ///< wait for the user to do something + 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 From 961066ee478c37c0a83e8c8813873e653b07a43e Mon Sep 17 00:00:00 2001 From: David Hale Date: Fri, 20 Mar 2026 17:52:16 -0700 Subject: [PATCH 14/16] AI rewrites slicecam_math, I make various small changes, still isn't working --- acamd/acam_interface.cpp | 9 +- acamd/acam_interface.h | 14 +- emulator/power.cpp | 2 +- slicecamd/slicecam_camera.cpp | 31 +++ slicecamd/slicecam_camera.h | 6 +- slicecamd/slicecam_interface.cpp | 53 +++- slicecamd/slicecam_interface.h | 4 +- slicecamd/slicecam_math.cpp | 440 +++++++++++++++++++++---------- slicecamd/slicecam_math.h | 2 +- slicecamd/slicecamd.cpp | 1 + 10 files changed, 396 insertions(+), 166 deletions(-) diff --git a/acamd/acam_interface.cpp b/acamd/acam_interface.cpp index ff50b572..f1ff6fe4 100644 --- a/acamd/acam_interface.cpp +++ b/acamd/acam_interface.cpp @@ -1433,6 +1433,7 @@ namespace Acam { * */ void Interface::publish_snapshot() { + this->publish_status(); nlohmann::json jmessage_out; jmessage_out[Key::SOURCE] = Topic::ACAMD; @@ -1602,7 +1603,7 @@ namespace Acam { void Interface::handletopic_tcsd( const nlohmann::json &jmessage ) { { std::lock_guard lock(snapshot_mtx); - snapshot_status["tcsd"]=true; + snapshot_status[Topic::TCSD]=true; } // extract and store values in the class // @@ -1631,6 +1632,10 @@ 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" ); @@ -1648,7 +1653,7 @@ namespace Acam { void Interface::handletopic_slitd( const nlohmann::json &jmessage ) { { std::lock_guard lock(snapshot_mtx); - snapshot_status["slitd"]=true; + snapshot_status[Topic::SLITD]=true; } this->telemkeys.add_json_key(jmessage, "SLITO", "SLITO", "slit offset in arcsec", "FLOAT", false); this->telemkeys.add_json_key(jmessage, "SLITW", "SLITW", "slit width in arcsec", "FLOAT", false); diff --git a/acamd/acam_interface.h b/acamd/acam_interface.h index b8989845..9e3b5bb6 100644 --- a/acamd/acam_interface.h +++ b/acamd/acam_interface.h @@ -575,8 +575,10 @@ namespace Acam { nskip_preserve_frames(0), newframe_ready(false), snapshot_status { - {"tcsd", false}, - {"slitd", false} + {Topic::TCSD, false}, + {Topic::SLITD, false}, + {Topic::TARGETINFO, false}, + {Topic::ACAMD, false} }, subscriber(std::make_unique(context, Common::PubSub::Mode::SUB)), is_subscriber_thread_running(false), @@ -584,13 +586,13 @@ namespace Acam { { target.set_interface_instance( this ); ///< Set the Interface instance in Target topic_handlers = { - { "_snapshot", std::function( + { Topic::SNAPSHOT, std::function( [this](const nlohmann::json &msg) { handletopic_snapshot(msg); } ) }, - { "tcsd", std::function( + { Topic::TCSD, std::function( [this](const nlohmann::json &msg) { handletopic_tcsd(msg); } ) }, - { "targetinfo", std::function( + { Topic::TARGETINFO, std::function( [this](const nlohmann::json &msg) { handletopic_targetinfo(msg); } ) }, - { "slitd", std::function( + { Topic::SLITD, std::function( [this](const nlohmann::json &msg) { handletopic_slitd(msg); } ) } }; } diff --git a/emulator/power.cpp b/emulator/power.cpp index 55450108..ae21efbb 100644 --- a/emulator/power.cpp +++ b/emulator/power.cpp @@ -324,6 +324,7 @@ namespace PowerEmulator { std::cerr << get_timestamp() << function << "[DEBUG] plugmap for nps" << npsnum << ": " << it->first << " " << it->second << "\n"; } +#endif } catch( std::out_of_range &e ) { @@ -332,7 +333,6 @@ namespace PowerEmulator { retstring = retstream.str(); return( ERROR ); } -#endif return ( NO_ERROR ); } diff --git a/slicecamd/slicecam_camera.cpp b/slicecamd/slicecam_camera.cpp index 56e4c068..797625a2 100644 --- a/slicecamd/slicecam_camera.cpp +++ b/slicecamd/slicecam_camera.cpp @@ -992,4 +992,35 @@ namespace Slicecam { } /***** Slicecam::Camera::write_frame ****************************************/ + + std::vector Camera::get_image(const std::string &which) { + auto it = this->andor.find(which); + if (it==this->andor.end() || it->second==nullptr) return {}; + const auto &cam = it->second; + if (cam->is_emulated()) return this->read_from_file(which); + const float* buf = cam->get_avg_data(); + if (buf==nullptr) return {}; + const long npix = cam->camera_info.axes[0]*cam->camera_info.axes[1]; + return std::vector(buf, buf+npix); + } + + + std::vector Camera::read_from_file(const std::string &extname) { + const char* function = "Slicecam::Camera::read_image"; + try { + std::unique_ptr pInfile(new CCfits::FITS(fitsinfo.fits_name, CCfits::Read, true)); + CCfits::ExtHDU& ext = pInfile->extension(extname); + std::valarray tmp; + ext.read(tmp); + return std::vector(std::begin(tmp), std::end(tmp)); + } + catch (const CCfits::FitsException &e) { + logwrite(function, "ERROR CCfits: "+std::string(e.message())); + return {}; + } + catch (const std::exception &e) { + logwrite(function, "ERROR: "+std::string(e.what())); + return {}; + } + } } diff --git a/slicecamd/slicecam_camera.h b/slicecamd/slicecam_camera.h index d20b3e4c..ddd0a744 100644 --- a/slicecamd/slicecam_camera.h +++ b/slicecamd/slicecam_camera.h @@ -37,12 +37,12 @@ namespace Slicecam { */ class Camera { private: - uint16_t* image_data; + std::unique_ptr simdata; int simsize; /// for the sky simulator std::map handlemap; public: - Camera() : image_data( nullptr ), simsize(1024) { }; + Camera() : simsize(1024) { }; FITS_file fits_file; /// instantiate a FITS container object FitsInfo fitsinfo; @@ -66,6 +66,8 @@ namespace Slicecam { long close(); long get_frame(); long write_frame( std::string source_file, std::string &outfile, const bool _tcs_online ); + std::vector get_image(const std::string &which); + std::vector read_from_file(const std::string &which); long bin( const int hbin, const int vbin ); long set_fan( std::string which, int mode ); long imflip( std::string args, std::string &retstring ); diff --git a/slicecamd/slicecam_interface.cpp b/slicecamd/slicecam_interface.cpp index 17946882..f10c45f5 100644 --- a/slicecamd/slicecam_interface.cpp +++ b/slicecamd/slicecam_interface.cpp @@ -154,21 +154,44 @@ namespace Slicecam { return; } auto* cam = it->second.get(); - float* img_data = cam->get_avg_data(); + const long ncols = cam->camera_info.axes[0]; + const long nrows = cam->camera_info.axes[1]; - if (img_data==nullptr) { - logwrite(function, "bad image data buffer for slicecam '"+which+"'"); + const std::vector img_data = this->camera.get_image(which); + + if (img_data.empty()) { + logwrite(function, "no image data for slicecam '"+which+"'"); return; } - const long ncols = cam->camera_info.cols; - const long nrows = cam->camera_info.rows; - // find the star centroid near the aim point // Point centroid; - - if ( Math::calculate_centroid( img_data, ncols, nrows, + { + std::ostringstream oss; + oss << "[DEBUG] ncols=" << ncols << " nrows=" << nrows << " img_data=" << std::hex << std::uppercase << img_data.data(); + logwrite(function, oss.str()); + oss.str(""); oss << "[DEBUG] pix="; + for (int i=0; i<5; i++) oss << " " << img_data[i]; + logwrite(function, oss.str()); + } + +{ +const int ax = static_cast( this->fineacquire_state.aimpoint.x ) - 1; // 0-based +const int ay = static_cast( this->fineacquire_state.aimpoint.y ) - 1; +std::ostringstream oss; +oss << "[DEBUG] aimpoint=(" << ax+1 << "," << ay+1 << ") pixels around aimpoint:"; +for ( int dy = -2; dy <= 2; dy++ ) { + for ( int dx = -2; dx <= 2; dx++ ) { + const int x = ax + dx; + const int y = ay + dy; + if ( x >= 0 && x < ncols && y >= 0 && y < nrows ) + oss << " (" << x+1 << "," << y+1 << ")=" << img_data.data()[y*ncols+x]; + } +} +logwrite( function, oss.str() ); +} + if ( Math::calculate_centroid( img_data.data(), ncols, nrows, this->fineacquire_state.bg_region, this->fineacquire_state.aimpoint, centroid) != NO_ERROR ) { @@ -386,6 +409,10 @@ namespace Slicecam { * */ void Interface::handletopic_acamd(const nlohmann::json &jmessage) { + { + std::lock_guard lock(snapshot_mtx); + snapshot_status[Topic::ACAMD]=true; + } // set is_acam_guiding flag bool acquired; Common::extract_telemetry_value( jmessage, Key::Acamd::IS_ACQUIRED, acquired ); @@ -397,7 +424,7 @@ namespace Slicecam { void Interface::handletopic_slitd( const nlohmann::json &jmessage ) { { std::lock_guard lock(snapshot_mtx); - snapshot_status["slitd"]=true; + snapshot_status[Topic::SLITD]=true; } Common::extract_telemetry_value( jmessage, "SLITO", telem.slitoffset ); Common::extract_telemetry_value( jmessage, "SLITW", telem.slitwidth ); @@ -410,7 +437,7 @@ namespace Slicecam { void Interface::handletopic_tcsd( const nlohmann::json &jmessage ) { { std::lock_guard lock(snapshot_mtx); - snapshot_status["tcsd"]=true; + snapshot_status[Topic::TCSD]=true; } // extract and store values in the class // @@ -1263,14 +1290,14 @@ namespace Slicecam { collect_header_info( cam ); } - // run the fine target acquisition if enabled - if ( is_fineacquire_running.load() ) { do_fineacquire(); } - // write to FITS file if (error==NO_ERROR) error = this->camera.write_frame( sourcefile, this->imagename, this->tcs_online.load(std::memory_order_acquire) ); + // run the fine target acquisition if enabled + if ( is_fineacquire_running.load() ) { do_fineacquire(); } + this->framegrab_time = std::chrono::steady_clock::time_point::min(); // send frame to GUI diff --git a/slicecamd/slicecam_interface.h b/slicecamd/slicecam_interface.h index cbc8c2e1..16736589 100644 --- a/slicecamd/slicecam_interface.h +++ b/slicecamd/slicecam_interface.h @@ -183,7 +183,9 @@ namespace Slicecam { is_acam_guiding(false), nsave_preserve_frames(0), nskip_preserve_frames(0), - snapshot_status { { "slitd", false }, {"tcsd", false} } + snapshot_status { { Topic::SLITD, false }, + { Topic::TCSD, false }, + { Topic::ACAMD, false } } { topic_handlers = { { Topic::SNAPSHOT, std::function( diff --git a/slicecamd/slicecam_math.cpp b/slicecamd/slicecam_math.cpp index c99f49b0..cb9943c4 100644 --- a/slicecamd/slicecam_math.cpp +++ b/slicecamd/slicecam_math.cpp @@ -3,7 +3,7 @@ * @brief slicecam math utilities implementation for fine target acquisition * @details Implements centroid detection, WCS pixel-to-sky conversion, and * fine-acquisition offset calculation for the slicecam fine-acquire - * loop. + * loop. Direct C++ translation of CF's ngps_acq.c. * @author David Hale, Christoffer Fremling * */ @@ -16,164 +16,340 @@ namespace Slicecam { + // --------------------------------------------------------------------------- + // Internal helpers (file scope only) + // --------------------------------------------------------------------------- + + /** + * @brief Build a normalized 1-D Gaussian kernel, half-radius = ceil(3*sigma). + * @param[in] sigma Gaussian sigma in pixels (clamped to >= 0.2) + * @param[out] radius_out half-radius r; full kernel length = 2r+1 + */ + static std::vector make_gaussian_kernel( double sigma, int &radius_out ) { + if ( sigma < 0.2 ) sigma = 0.2; + const int r = std::max( 1, static_cast( std::ceil( 3.0 * sigma ) ) ); + const int len = 2 * r + 1; + std::vector k( len ); + double sum = 0.0; + for ( int i = -r; i <= r; i++ ) { + const double x = static_cast(i) / sigma; + k[i + r] = std::exp( -0.5 * x * x ); + sum += k[i + r]; + } + if ( sum <= 0.0 ) sum = 1.0; + for ( auto &v : k ) v /= sum; + radius_out = r; + return k; + } + + /** + * @brief Sum of squares of 1-D kernel coefficients. + * For a separable 2-D Gaussian: sigma_filt = sigma_raw * kernel_sum_sq. + */ + static double kernel_sum_sq( const std::vector &k ) { + double s = 0.0; + for ( auto v : k ) s += v * v; + return s; + } + + /** + * @brief Separable 2-D Gaussian convolution on a (w x h) patch. + * Border handling: clamp (mirror-pad would be better but matches CF). + */ + static void convolve_separable( const std::vector &in, + std::vector &tmp, + std::vector &out, + int w, int h, + const std::vector &k, int r ) { + // horizontal pass: in -> tmp + for ( int y = 0; y < h; y++ ) { + for ( int x = 0; x < w; x++ ) { + double acc = 0.0; + for ( int dx = -r; dx <= r; dx++ ) { + const int xx = std::max( 0, std::min( w - 1, x + dx ) ); + acc += static_cast( in[y * w + xx] ) * k[dx + r]; + } + tmp[y * w + x] = static_cast( acc ); + } + } + // vertical pass: tmp -> out + for ( int y = 0; y < h; y++ ) { + for ( int x = 0; x < w; x++ ) { + double acc = 0.0; + for ( int dy = -r; dy <= r; dy++ ) { + const int yy = std::max( 0, std::min( h - 1, y + dy ) ); + acc += static_cast( tmp[yy * w + x] ) * k[dy + r]; + } + out[y * w + x] = static_cast( acc ); + } + } + } + + /** + * @brief SExtractor-like background and sigma estimation. + * @details Translation of CF's bg_sigma_sextractor_like(). + * 1. Initial estimate: median and MAD-derived sigma. + * 2. Iterative 3-sigma clipping around the original median, + * with early exit when sigma converges (rel change < 1%). + * 3. Background via mode = 2.5*median - 1.5*mean, + * falling back to median if distribution is skewed. + */ + static void bg_sigma( const std::vector &samples, + double &bkg_out, double &sigma_out ) { + bkg_out = 0.0; + sigma_out = 1.0; + + const size_t ns = samples.size(); + if ( ns < 64 ) return; + + // samples must be sorted on entry + std::vector sorted = samples; + std::sort( sorted.begin(), sorted.end() ); + + const double median = ( ns % 2 ) + ? static_cast( sorted[ns / 2] ) + : 0.5 * ( static_cast( sorted[ns / 2 - 1] ) + + static_cast( sorted[ns / 2] ) ); + + // MAD -> initial sigma + std::vector dev( ns ); + for ( size_t i = 0; i < ns; i++ ) + dev[i] = std::abs( sorted[i] - static_cast( median ) ); + std::sort( dev.begin(), dev.end() ); + + const double mad = ( ns % 2 ) + ? static_cast( dev[ns / 2] ) + : 0.5 * ( static_cast( dev[ns / 2 - 1] ) + + static_cast( dev[ns / 2] ) ); + double sigma = 1.4826 * mad; + if ( !std::isfinite( sigma ) || sigma <= 0.0 ) sigma = 1.0; + + // Iterative 3-sigma clipping, always centered on original median + double mean = median; + double sigma_prev = sigma; + + for ( int it = 0; it < 8; it++ ) { + const double lo = median - 3.0 * sigma; + const double hi = median + 3.0 * sigma; + double sum = 0.0, sum2 = 0.0; + long cnt = 0; + for ( float v : sorted ) { + if ( v < lo || v > hi ) continue; + const double dv = static_cast( v ); + sum += dv; + sum2 += dv * dv; + cnt++; + } + if ( cnt < 32 ) break; + + mean = sum / static_cast( cnt ); + const double var = ( sum2 / static_cast( cnt ) ) - mean * mean; + sigma = ( var > 0.0 ) ? std::sqrt( var ) : 0.0; + if ( !std::isfinite( sigma ) || sigma <= 0.0 ) { sigma = sigma_prev; break; } + + const double rel = std::abs( sigma - sigma_prev ) + / ( sigma_prev > 0.0 ? sigma_prev : 1.0 ); + sigma_prev = sigma; + if ( rel < 0.01 ) break; + } + + // SExtractor mode estimator: 2.5*median - 1.5*mean + // Fall back to median if distribution is too skewed + double bkg = 2.5 * median - 1.5 * mean; + if ( sigma > 0.0 && ( mean - median ) / sigma > 0.3 ) bkg = median; + if ( !std::isfinite( bkg ) ) bkg = median; + if ( !std::isfinite( sigma ) || sigma <= 0.0 ) sigma = 1.0; + + bkg_out = bkg; + sigma_out = sigma; + } + + /***** Slicecam::Math::calculate_centroid ************************************/ /** * @brief compute the centroid of the brightest source near an aim point - * @details Translated from CF's ngps_acq.c. Uses MAD background estimation, - * peak finding, and iterative Gaussian centroiding. - * @param[in] image pointer to row-major camera image buffer (cols * rows elements) - * @param[in] cols number of image columns - * @param[in] rows number of image rows - * @param[in] background ROI for background estimation (1-based, inclusive) - * @param[in] aimpoint pixel search centre (1-based) - * @param[out] centroid detected centroid position (1-based on success) - * @return NO_ERROR|ERROR + * @details Direct C++ translation of CF's detect_star_near_goal() in + * ngps_acq.c, with parameter defaults matching AUTOACQ_ARGS. + * + * Step 1: SExtractor-like background and sigma estimation over + * the background ROI. + * + * Step 2: extract the search ROI (= background ROI), background- + * subtract, and apply separable Gaussian smoothing + * (filt_sigma_pix = 1.2). + * + * Step 3: scan the filtered patch for local maxima that exceed + * the SNR threshold, have >= 4 adjacent pixels above threshold + * in the raw residual, and are within 40 pixels of the aim point. + * Among all qualifying candidates, select the brightest. + * + * Step 4: refine to sub-pixel centroid via iterative Gaussian- + * windowed first-moment (centroid_sigma_pix = 2.0, 12 iterations, + * eps = 0.01 px). + * + * All pixel coordinates are FITS 1-based on input and output. + * Internally everything is 0-based. * */ - long Math::calculate_centroid( float* image, + long Math::calculate_centroid( const float* image, long ncols, long nrows, Rect background, Point aimpoint, Point ¢roid ) { if ( !image || ncols <= 0 || nrows <= 0 ) return ERROR; - // Convert 1-based inclusive ROI to 0-based, clamped to image boundaries. - long bx1 = std::max( 0L, background.x1 - 1 ); - long bx2 = std::min( ncols - 1L, background.x2 - 1 ); - long by1 = std::max( 0L, background.y1 - 1 ); - long by2 = std::min( nrows - 1L, background.y2 - 1 ); + // Convert 1-based inclusive ROI to 0-based, clamped + const long bx1 = std::max( 0L, background.x1 - 1 ); + const long bx2 = std::min( ncols - 1L, background.x2 - 1 ); + const long by1 = std::max( 0L, background.y1 - 1 ); + const long by2 = std::min( nrows - 1L, background.y2 - 1 ); if ( bx2 < bx1 || by2 < by1 ) return ERROR; - // estimate background and noise via sigma-clipped median/MAD + // Search ROI = background ROI (no separate search ROI configured) + const long sx1 = bx1, sx2 = bx2, sy1 = by1, sy2 = by2; + + // --- Step 1: background and sigma estimation --- std::vector samples; samples.reserve( static_cast( (bx2 - bx1 + 1) * (by2 - by1 + 1) ) ); - - for ( long y = by1; y <= by2; y++ ) { - for ( long x = bx1; x <= bx2; x++ ) { + for ( long y = by1; y <= by2; y++ ) + for ( long x = bx1; x <= bx2; x++ ) samples.push_back( image[y * ncols + x] ); - } - } - - if ( samples.size() < 16 ) return ERROR; - - std::sort( samples.begin(), samples.end() ); - - const size_t n = samples.size(); - double median = ( n % 2 ) - ? static_cast( samples[n / 2] ) - : 0.5 * ( static_cast( samples[n / 2 - 1] ) - + static_cast( samples[n / 2] ) ); - - // MAD gives a robust initial sigma estimate: sigma ~= 1.4826 * MAD - std::vector dev( n ); - for ( size_t i = 0; i < n; i++ ) { - dev[i] = std::abs( samples[i] - static_cast( median ) ); - } - std::sort( dev.begin(), dev.end() ); - - double mad = ( n % 2 ) - ? static_cast( dev[n / 2] ) - : 0.5 * ( static_cast( dev[n / 2 - 1] ) - + static_cast( dev[n / 2] ) ); - double sigma = 1.4826 * mad; - if ( !std::isfinite( sigma ) || sigma <= 0.0 ) sigma = 1.0; + double bkg = 0.0, sigma = 1.0; + bg_sigma( samples, bkg, sigma ); - // refine background with iterative sigma clipping (5 passes, 3-sigma clip). - double bkg = median; - const double clip_sig = 3.0; + if ( !std::isfinite( sigma ) || sigma <= 0.0 ) return ERROR; - for ( int it = 0; it < 5; it++ ) { - const double lo = bkg - clip_sig * sigma; - const double hi = bkg + clip_sig * sigma; + // --- Step 2: background-subtract the search patch and smooth --- + const int w = static_cast( sx2 - sx1 + 1 ); + const int h = static_cast( sy2 - sy1 + 1 ); + if ( w <= 3 || h <= 3 ) return ERROR; - double sum = 0.0; - double sum2 = 0.0; - long cnt = 0; + std::vector patch( w * h ); + std::vector tmp( w * h ); + std::vector filt( w * h ); - for ( float v : samples ) { - if ( v < lo || v > hi ) continue; - const double dv = static_cast( v ); - sum += dv; - sum2 += dv * dv; - cnt++; + for ( int yy = 0; yy < h; yy++ ) { + const long y = sy1 + yy; + for ( int xx = 0; xx < w; xx++ ) { + const long x = sx1 + xx; + // keep negatives — filter uses them too (matches CF) + patch[yy * w + xx] = static_cast( + static_cast( image[y * ncols + x] ) - bkg ); } - - if ( cnt < 2 ) break; - - bkg = sum / cnt; - sigma = std::sqrt( sum2 / cnt - bkg * bkg ); - if ( !std::isfinite( sigma ) || sigma <= 0.0 ) { sigma = 1.0; break; } } - // locate the brightest pixel above threshold near the aim point - // - const double snr_threshold = 3.0; ///< minimum SNR for detection - const double search_radius = 40.0; ///< search radius in pixels + // filt_sigma_pix = 1.2 (CF default) + int kr = 0; + const std::vector kernel = make_gaussian_kernel( 1.2, kr ); + convolve_separable( patch, tmp, filt, w, h, kernel, kr ); - const double detection_level = bkg + snr_threshold * sigma; + const double sumsq1d = kernel_sum_sq( kernel ); + const double sigma_filt = ( sumsq1d > 0.0 ) ? sigma * sumsq1d : sigma; - // convert 1-based aim point to 0-based for array indexing. - const double aim_x0 = aimpoint.x - 1.0; - const double aim_y0 = aimpoint.y - 1.0; + // SNR thresholds (snr_thresh = 3.0, CF default) + const double thr_filt = 3.0 * sigma_filt; // threshold in filtered image + const double thr_raw = 3.0 * sigma; // threshold for adjacency check - long sx1 = std::max( 0L, static_cast( aim_x0 - search_radius ) ); - long sx2 = std::min( ncols - 1L, static_cast( aim_x0 + search_radius ) ); - long sy1 = std::max( 0L, static_cast( aim_y0 - search_radius ) ); - long sy2 = std::min( nrows - 1L, static_cast( aim_y0 + search_radius ) ); + // Aim point in 0-based image coordinates + const double goal_x0 = aimpoint.x - 1.0; + const double goal_y0 = aimpoint.y - 1.0; + const double max_dist = 40.0; // pixels; CF's --max-dist default - double best_val = detection_level; + // --- Step 3: find best local maximum in the filtered patch --- + double best_val = -1.0e300; long best_x = -1; long best_y = -1; - for ( long y = sy1; y <= sy2; y++ ) { - for ( long x = sx1; x <= sx2; x++ ) { - const double dx = static_cast(x) - aim_x0; - const double dy = static_cast(y) - aim_y0; - if ( dx * dx + dy * dy > search_radius * search_radius ) continue; - - const double v = static_cast( image[y * ncols + x] ); - if ( v > best_val ) { - best_val = v; - best_x = x; - best_y = y; + // skip border pixels (yy=0, yy=h-1, xx=0, xx=w-1) — local max test + // requires all 4 neighbours to exist + for ( int yy = 1; yy < h - 1; yy++ ) { + for ( int xx = 1; xx < w - 1; xx++ ) { + const float v = filt[yy * w + xx]; + + // must exceed detection threshold in filtered image + if ( static_cast( v ) < thr_filt ) continue; + + // must be a local maximum in the filtered image (4-connected) + if ( v < filt[ yy * w + (xx - 1)] ) continue; + if ( v < filt[ yy * w + (xx + 1)] ) continue; + if ( v < filt[(yy - 1) * w + xx ] ) continue; + if ( v < filt[(yy + 1) * w + xx ] ) continue; + + // convert patch coordinates to full-image coordinates (0-based) + const long x0 = sx1 + xx; + const long y0 = sy1 + yy; + + // must be within max_dist pixels of the aim point + const double dxg = static_cast( x0 ) - goal_x0; + const double dyg = static_cast( y0 ) - goal_y0; + if ( std::hypot( dxg, dyg ) > max_dist ) continue; + + // adjacency check in the raw residual patch: need >= 4 of 8 neighbours + // above the raw threshold (rejects hot pixels and cosmic rays) + int nadj = 0; + for ( int dy = -1; dy <= 1; dy++ ) { + for ( int dx = -1; dx <= 1; dx++ ) { + if ( dx == 0 && dy == 0 ) continue; + if ( static_cast( patch[(yy + dy) * w + (xx + dx)] ) > thr_raw ) + nadj++; + } + } + if ( nadj < 4 ) continue; // min_adjacent = 4; CF's --min-adj default + + // rank by raw (not filtered) peak value — brightest candidate wins + const double rawv = static_cast( patch[yy * w + xx] ); + if ( rawv > best_val ) { + best_val = rawv; + best_x = x0; + best_y = y0; } } } - if ( best_x < 0 ) return ERROR; // no source found above threshold + { + 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()); + } - // iterative Gaussian-windowed first-moment centroid - const int centroid_halfwin = 4; - const double centroid_sigma = 1.2; - const double centroid_sigma_sq = centroid_sigma * centroid_sigma; + if ( best_x < 0 ) return ERROR; // no source found - double cx = static_cast( best_x ); - double cy = static_cast( best_y ); + // --- Step 4: iterative Gaussian-windowed first-moment centroid --- + // + // centroid_halfwin = 4 (CF's --centroid-hw default) + // centroid_sigma_pix = 2.0 (CF's default, NOTE: different from filt_sigma) + // centroid_maxiter = 12 (CF's default) + // centroid_eps_pix = 0.01 + // + const int hw = 4; + const double s2 = 2.0 * 2.0; // centroid_sigma_pix^2 - for ( int it = 0; it < 20; it++ ) { - long xlo = std::max( 0L, static_cast(cx) - centroid_halfwin ); - long xhi = std::min( ncols - 1L, static_cast(cx) + centroid_halfwin ); - long ylo = std::max( 0L, static_cast(cy) - centroid_halfwin ); - long yhi = std::min( nrows - 1L, static_cast(cy) + centroid_halfwin ); + double cx = static_cast( best_x ); + double cy = static_cast( best_y ); + double sumI = 0.0; - double sumI = 0.0; - double sumX = 0.0; - double sumY = 0.0; + for ( int it = 0; it < 12; it++ ) { + const long xlo = std::max( sx1, static_cast( cx ) - hw ); + const long xhi = std::min( sx2, static_cast( cx ) + hw ); + const long ylo = std::max( sy1, static_cast( cy ) - hw ); + const long yhi = std::min( sy2, static_cast( cy ) + hw ); + + double sumX = 0.0, sumY = 0.0; + sumI = 0.0; for ( long y = ylo; y <= yhi; y++ ) { for ( long x = xlo; x <= xhi; x++ ) { const double I = static_cast( image[y * ncols + x] ) - bkg; if ( I <= 0.0 ) continue; - - const double dx = static_cast(x) - cx; - const double dy = static_cast(y) - cy; - const double w = std::exp( -0.5 * ( dx * dx + dy * dy ) / centroid_sigma_sq ) * I; - - sumI += w; - sumX += w * static_cast(x); - sumY += w * static_cast(y); + const double dx = static_cast( x ) - cx; + const double dy = static_cast( y ) - cy; + const double wgt = std::exp( -0.5 * ( dx * dx + dy * dy ) / s2 ) * I; + sumI += wgt; + sumX += wgt * static_cast( x ); + sumY += wgt * static_cast( y ); } } @@ -185,12 +361,12 @@ namespace Slicecam { cx = ncx; cy = ncy; - if ( shift < 0.01 ) break; // sub-hundredth pixel convergence + if ( shift < 0.01 ) break; // centroid_eps_pix } - if ( !std::isfinite( cx ) || !std::isfinite( cy ) ) return ERROR; + if ( sumI <= 0.0 || !std::isfinite( cx ) || !std::isfinite( cy ) ) return ERROR; - // return centroid in 1-based FITS pixel coordinates. + // Return in FITS 1-based coordinates centroid.x = cx + 1.0; centroid.y = cy + 1.0; @@ -207,12 +383,8 @@ namespace Slicecam { * v = pix.y - CRPIX2 * world.ra = CRVAL1 + CDELT1 * (PC1_1 * u + PC1_2 * v) * world.dec = CRVAL2 + CDELT2 * (PC2_1 * u + PC2_2 * v) - * This is exact for a linear WCS and an accurate approximation - * for the gnomonic (TAN) projection over a small field. - * @param[in] keys FITS keyword database populated by collect_header_info - * @param[in] pix pixel position (1-based) - * @param[out] world corresponding RA / DEC in degrees - * @throws Common::FitsKeys::get_key can throw std::runtime_error + * Valid for the gnomonic (TAN) projection over a small field. + * Throws if any required WCS key is absent. * */ void Math::pix2world( const Common::FitsKeys &keys, Point pix, World &world ) { @@ -238,32 +410,20 @@ namespace Slicecam { /***** Slicecam::Math::calculate_acquisition_offsets *************************/ /** - * @brief compute the (dRA, dDEC) offset from a goal position to a star - * @details Returns the angular offset (star - goal) which, when sent to - * the telescope, will move the star onto the goal position. - * The RA component is a true great-circle offset - * (multiplied by cos(dec)). - * @param[in] star detected sky position of the star (degrees) - * @param[in] goal desired sky position on the chip (degrees) - * @param[out] offsets (dRA * cos(dec), dDEC) in degrees + * @brief compute (dRA*cos(dec), dDEC) offset to move star onto aim point + * @details Returns (star - goal) as a true on-sky angular offset in degrees. + * Applying this offset to the telescope pointing moves the star + * onto the aim point. * */ void Math::calculate_acquisition_offsets( World star, World goal, std::pair &offsets ) { double dra = star.ra - goal.ra; - - // Wrap RA difference into [-180, +180] degrees. - // while ( dra > 180.0 ) dra -= 360.0; while ( dra < -180.0 ) dra += 360.0; - // Project onto the sky: multiply by cos(dec) so the RA offset is a - // true angular separation rather than a coordinate difference. - // const double cosdec = std::cos( goal.dec * M_PI / 180.0 ); - const double ddec = star.dec - goal.dec; - - offsets = { dra * cosdec, ddec }; + offsets = { dra * cosdec, star.dec - goal.dec }; } /***** Slicecam::Math::calculate_acquisition_offsets *************************/ diff --git a/slicecamd/slicecam_math.h b/slicecamd/slicecam_math.h index 6c81c010..483400f0 100644 --- a/slicecamd/slicecam_math.h +++ b/slicecamd/slicecam_math.h @@ -31,7 +31,7 @@ namespace Slicecam { /** * @brief compute the centroid of the brightest source near an aim point */ - static long calculate_centroid( float* image, + static long calculate_centroid( const float* image, long cols, long rows, Rect background, Point aimpoint, diff --git a/slicecamd/slicecamd.cpp b/slicecamd/slicecamd.cpp index 694a31f8..27df8f71 100644 --- a/slicecamd/slicecamd.cpp +++ b/slicecamd/slicecamd.cpp @@ -147,6 +147,7 @@ int main(int argc, char **argv) { // takes a list of subscription topics // if ( slicecamd.interface.init_pubsub( { Topic::SLITD, + Topic::ACAMD, Topic::TCSD }) == ERROR ) { logwrite(function, "ERROR initializing publisher-subscriber handler"); slicecamd.exit_cleanly(); From 2758c533a42895ccc4ebb035dd26e9bd59c1d893 Mon Sep 17 00:00:00 2001 From: David Hale Date: Mon, 23 Mar 2026 17:10:59 -0700 Subject: [PATCH 15/16] fixed bug in do_acquire. works now. --- acamd/acam_interface.cpp | 7 ++-- acamd/acam_interface.h | 2 +- slicecamd/slicecam_camera.cpp | 10 ++++- slicecamd/slicecam_camera.h | 1 + slicecamd/slicecam_interface.cpp | 68 +++++++++++++++++++------------- slicecamd/slicecam_interface.h | 1 + slicecamd/slicecam_math.cpp | 5 ++- slicecamd/slicecam_math.h | 3 +- slicecamd/slicecamd.cpp | 3 -- 9 files changed, 60 insertions(+), 40 deletions(-) diff --git a/acamd/acam_interface.cpp b/acamd/acam_interface.cpp index f1ff6fe4..ea2d0a89 100644 --- a/acamd/acam_interface.cpp +++ b/acamd/acam_interface.cpp @@ -1433,7 +1433,7 @@ namespace Acam { * */ void Interface::publish_snapshot() { - this->publish_status(); + this->publish_status(true); nlohmann::json jmessage_out; jmessage_out[Key::SOURCE] = Topic::ACAMD; @@ -1467,7 +1467,7 @@ namespace Acam { * @details This publishes a JSON message containing important telemetry. * */ - void Interface::publish_status() { + void Interface::publish_status(bool force) { const std::string acquire_mode = this->target.acquire_mode_string(); const bool is_acquired = this->target.is_acquired.load(); const int nacquired = this->target.nacquired; @@ -1477,7 +1477,8 @@ namespace Acam { // only will publish if there was a change in any one of these // - if ( acquire_mode == this->last_status.acquire_mode && + if ( !force && + acquire_mode == this->last_status.acquire_mode && is_acquired == this->last_status.is_acquired && nacquired == this->last_status.nacquired && attempts == this->last_status.attempts ) return; diff --git a/acamd/acam_interface.h b/acamd/acam_interface.h index 9e3b5bb6..8c4b317e 100644 --- a/acamd/acam_interface.h +++ b/acamd/acam_interface.h @@ -655,7 +655,7 @@ namespace Acam { long bin( std::string args, std::string &retstring ); void publish_snapshot(); - void publish_status(); + void publish_status(bool force=false); void request_snapshot(); bool wait_for_snapshots(); long handle_json_message( std::string message_in ); diff --git a/slicecamd/slicecam_camera.cpp b/slicecamd/slicecam_camera.cpp index 797625a2..2c6963ca 100644 --- a/slicecamd/slicecam_camera.cpp +++ b/slicecamd/slicecam_camera.cpp @@ -997,7 +997,8 @@ namespace Slicecam { auto it = this->andor.find(which); if (it==this->andor.end() || it->second==nullptr) return {}; const auto &cam = it->second; - if (cam->is_emulated()) return this->read_from_file(which); +// if (cam->is_emulated()) { return this->read_from_file(which); +// logwrite("Slicecam::Camera::get_image", "[DEBUG] PROBLEM: is_emulated=false"); const float* buf = cam->get_avg_data(); if (buf==nullptr) return {}; const long npix = cam->camera_info.axes[0]*cam->camera_info.axes[1]; @@ -1006,10 +1007,15 @@ namespace Slicecam { std::vector Camera::read_from_file(const std::string &extname) { + return{}; + } + std::vector Camera::read_from_file(const std::string &extname, long &ncols, long &nrows) { const char* function = "Slicecam::Camera::read_image"; try { - std::unique_ptr pInfile(new CCfits::FITS(fitsinfo.fits_name, CCfits::Read, true)); + std::unique_ptr pInfile(new CCfits::FITS(fitsinfo.fits_name, CCfits::Read, false)); CCfits::ExtHDU& ext = pInfile->extension(extname); + ncols = ext.axis(0); + nrows = ext.axis(1); std::valarray tmp; ext.read(tmp); return std::vector(std::begin(tmp), std::end(tmp)); diff --git a/slicecamd/slicecam_camera.h b/slicecamd/slicecam_camera.h index ddd0a744..f608fe2b 100644 --- a/slicecamd/slicecam_camera.h +++ b/slicecamd/slicecam_camera.h @@ -68,6 +68,7 @@ namespace Slicecam { long write_frame( std::string source_file, std::string &outfile, const bool _tcs_online ); std::vector get_image(const std::string &which); std::vector read_from_file(const std::string &which); + std::vector read_from_file(const std::string &which, long &ncols, long &nrows); long bin( const int hbin, const int vbin ); long set_fan( std::string which, int mode ); long imflip( std::string args, std::string &retstring ); diff --git a/slicecamd/slicecam_interface.cpp b/slicecamd/slicecam_interface.cpp index f10c45f5..2d8714c5 100644 --- a/slicecamd/slicecam_interface.cpp +++ b/slicecamd/slicecam_interface.cpp @@ -154,10 +154,12 @@ namespace Slicecam { return; } auto* cam = it->second.get(); - const long ncols = cam->camera_info.axes[0]; - const long nrows = cam->camera_info.axes[1]; + long ncols = cam->camera_info.axes[0]; + long nrows = cam->camera_info.axes[1]; - const std::vector img_data = this->camera.get_image(which); + const std::vector img_data = cam->is_emulated() + ? this->camera.read_from_file(which, ncols, nrows) + : this->camera.get_image(which); if (img_data.empty()) { logwrite(function, "no image data for slicecam '"+which+"'"); @@ -167,31 +169,8 @@ namespace Slicecam { // find the star centroid near the aim point // Point centroid; - { - std::ostringstream oss; - oss << "[DEBUG] ncols=" << ncols << " nrows=" << nrows << " img_data=" << std::hex << std::uppercase << img_data.data(); - logwrite(function, oss.str()); - oss.str(""); oss << "[DEBUG] pix="; - for (int i=0; i<5; i++) oss << " " << img_data[i]; - logwrite(function, oss.str()); - } - -{ -const int ax = static_cast( this->fineacquire_state.aimpoint.x ) - 1; // 0-based -const int ay = static_cast( this->fineacquire_state.aimpoint.y ) - 1; -std::ostringstream oss; -oss << "[DEBUG] aimpoint=(" << ax+1 << "," << ay+1 << ") pixels around aimpoint:"; -for ( int dy = -2; dy <= 2; dy++ ) { - for ( int dx = -2; dx <= 2; dx++ ) { - const int x = ax + dx; - const int y = ay + dy; - if ( x >= 0 && x < ncols && y >= 0 && y < nrows ) - oss << " (" << x+1 << "," << y+1 << ")=" << img_data.data()[y*ncols+x]; - } -} -logwrite( function, oss.str() ); -} - if ( Math::calculate_centroid( img_data.data(), ncols, nrows, + + if ( Math::calculate_centroid( img_data, ncols, nrows, this->fineacquire_state.bg_region, this->fineacquire_state.aimpoint, centroid) != NO_ERROR ) { @@ -778,6 +757,9 @@ logwrite( function, oss.str() ); long error = this->tcs_init( "", retstring ); std::this_thread::sleep_for(std::chrono::milliseconds(500)); + // open connection to acamd + error |= this->acamd_init(); + if ( this->camera.open( which, args ) == NO_ERROR ) { // open the camera error |= this->framegrab( "start", retstring ); // start frame grabbing if open succeeds std::thread( &Slicecam::GUIManager::push_gui_settings, &gui_manager ).detach(); // force display refresh @@ -958,6 +940,36 @@ logwrite( function, oss.str() ); /***** Slicecam::Interface::tcs_init ****************************************/ + /***** Slicecam::Interface::acamd_init **************************************/ + /** + * @brief initialize connection to acamd + * @return ERROR | NO_ERROR + * + */ + long Interface::acamd_init() { + const char* function = "Slicecam::Interface::acam_init"; + + // If not connected to acamd then try to connect to the daemon. + std::string retstring; + if (this->acamd.is_connected(retstring) != NO_ERROR) { + logwrite(function, "ERROR no response from acamd"); + return NO_ERROR; + } + + // Not connected, try to connect + if ( retstring.find("false") != std::string::npos ) { + logwrite( function, "connecting to acamd" ); + if (this->acamd.connect() != NO_ERROR) { + logwrite( function, "ERROR unable to connect to acamd" ); + return NO_ERROR; + } + logwrite( function, "connected to acamd" ); + } + return NO_ERROR; + } + /***** Slicecam::Interface::acamd_init **************************************/ + + /***** Slicecam::Interface::saveframes **************************************/ /** * @brief set/get number of frame grabs to save during target acquisition diff --git a/slicecamd/slicecam_interface.h b/slicecamd/slicecam_interface.h index 16736589..4f4b5164 100644 --- a/slicecamd/slicecam_interface.h +++ b/slicecamd/slicecam_interface.h @@ -252,6 +252,7 @@ namespace Slicecam { void close(); long close( std::string args, std::string &retstring ); long tcs_init( std::string args, std::string &retstring ); /// initialize connection to TCS + long acamd_init(); long saveframes( std::string args, std::string &retstring ); void alert_framegrabbing_stopped(const int &waitms); long framegrab( std::string args ); /// wrapper to control Andor frame grabbing diff --git a/slicecamd/slicecam_math.cpp b/slicecamd/slicecam_math.cpp index cb9943c4..a87d9e61 100644 --- a/slicecamd/slicecam_math.cpp +++ b/slicecamd/slicecam_math.cpp @@ -13,6 +13,7 @@ #include #include #include +#include namespace Slicecam { @@ -191,12 +192,12 @@ namespace Slicecam { * Internally everything is 0-based. * */ - long Math::calculate_centroid( const float* image, + long Math::calculate_centroid( const std::vector &image, long ncols, long nrows, Rect background, Point aimpoint, Point ¢roid ) { - if ( !image || ncols <= 0 || nrows <= 0 ) return ERROR; + if ( image.empty() || ncols <= 0 || nrows <= 0 ) return ERROR; // Convert 1-based inclusive ROI to 0-based, clamped const long bx1 = std::max( 0L, background.x1 - 1 ); diff --git a/slicecamd/slicecam_math.h b/slicecamd/slicecam_math.h index 483400f0..a9a9e76e 100644 --- a/slicecamd/slicecam_math.h +++ b/slicecamd/slicecam_math.h @@ -31,7 +31,7 @@ namespace Slicecam { /** * @brief compute the centroid of the brightest source near an aim point */ - static long calculate_centroid( const float* image, + static long calculate_centroid( const std::vector &image, long cols, long rows, Rect background, Point aimpoint, @@ -46,6 +46,7 @@ namespace Slicecam { */ static void calculate_acquisition_offsets( World star, World goal, std::pair &offsets ); + }; /***** Slicecam::Math *******************************************************/ diff --git a/slicecamd/slicecamd.cpp b/slicecamd/slicecamd.cpp index 27df8f71..82279803 100644 --- a/slicecamd/slicecamd.cpp +++ b/slicecamd/slicecamd.cpp @@ -153,9 +153,6 @@ int main(int argc, char **argv) { slicecamd.exit_cleanly(); } - std::this_thread::sleep_for( std::chrono::milliseconds(100) ); - slicecamd.interface.publish_snapshot(); - std::this_thread::sleep_for( std::chrono::milliseconds(100) ); slicecamd.interface.request_snapshot(); From 69a532737b15873b33d6f18a2a4bd549081b33fc Mon Sep 17 00:00:00 2001 From: David Hale Date: Tue, 24 Mar 2026 12:27:57 -0700 Subject: [PATCH 16/16] added FINE_ACQUIRE* params to slicecam.cfg.in, removes some obsolete code, and adds comments and other minor cleanup --- Config/slicecamd.cfg.in | 19 ++ acamd/acam_interface.cpp | 324 +++++++++++-------------------- slicecamd/slicecam_interface.cpp | 229 +++++++++++++++------- slicecamd/slicecam_interface.h | 23 ++- slicecamd/slicecam_math.h | 25 ++- 5 files changed, 324 insertions(+), 296 deletions(-) diff --git a/Config/slicecamd.cfg.in b/Config/slicecamd.cfg.in index 53873240..e3abb098 100644 --- a/Config/slicecamd.cfg.in +++ b/Config/slicecamd.cfg.in @@ -58,6 +58,25 @@ PUSH_GUI_SETTINGS=/home/developer/Software/GuiderGUI/push_settings_slicev.sh # PUSH_GUI_IMAGE=/home/developer/Software/GuiderGUI/push_image.sh +# FINE_ACQUIRE_AIMPOINT=( ) +# camera and location of aimpoint for fine acquisition +# which camera must be { L R } +# x-coordinate (col) +# y-coordinate (row) +# aimpoint x,y may be fractional +# +FINE_ACQUIRE_AIMPOINT=(L 150.0 115.5) + +# FINE_ACQUIRE_BACKGROUND=( ) +# defines the bounds of the region for background coorection +# for fine acquisition centroiding +# x-coordinate lower left +# x-coordinate lower right +# y-coordinate upper left +# y-coordinate upper right +# +FINE_ACQUIRE_BACKGROUND=(80 165 30 210) + # SkySimulator options: # SKYSIM_IMAGE_SIZE= where is integer # Sets the keyword argument "IMAGE_SIZE=" diff --git a/acamd/acam_interface.cpp b/acamd/acam_interface.cpp index ea2d0a89..f19fbfcf 100644 --- a/acamd/acam_interface.cpp +++ b/acamd/acam_interface.cpp @@ -26,8 +26,8 @@ namespace Acam { * */ long Camera::emulator( std::string args, std::string &retstring ) { - std::string function = "Acam::Camera::emulator"; - std::stringstream message; + const char* function = "Acam::Camera::emulator"; + std::ostringstream message; // Help // @@ -81,8 +81,8 @@ namespace Acam { * */ long Camera::open( int sn ) { - std::string function = "Acam::Camera::open"; - std::stringstream message; + const char* function = "Acam::Camera::open"; + std::ostringstream message; long error=NO_ERROR; // Opens the Andor and initializes SDK @@ -178,8 +178,8 @@ namespace Acam { * */ long Camera::imflip( std::string args, std::string &retstring ) { - std::string function = "Acam::Camera::imflip"; - std::stringstream message; + const char* function = "Acam::Camera::imflip"; + std::ostringstream message; long error = NO_ERROR; // Help @@ -262,8 +262,8 @@ namespace Acam { * */ long Camera::imrot( std::string args, std::string &retstring ) { - std::string function = "Acam::Camera::imrot"; - std::stringstream message; + const char* function = "Acam::Camera::imrot"; + std::ostringstream message; long error = NO_ERROR; // Help @@ -366,7 +366,7 @@ namespace Acam { * @return ERROR | NO_ERROR */ long Camera::set_fan( int mode ) { - const std::string function="Acam::Camera::set_fan"; + const char* function="Acam::Camera::set_fan"; // Andor must be connected // @@ -392,8 +392,8 @@ namespace Acam { * */ long Camera::gain( std::string args, std::string &retstring ) { - std::string function = "Acam::Camera::gain"; - std::stringstream message; + const char* function = "Acam::Camera::gain"; + std::ostringstream message; long error = NO_ERROR; int gain = -999; @@ -532,7 +532,6 @@ namespace Acam { * */ int Camera::gain() { - std::string function = "Acam::Camera::gain"; std::string svalue; int ivalue=0; this->gain( "", svalue ); @@ -555,8 +554,8 @@ namespace Acam { * */ long Camera::speed( std::string args, std::string &retstring ) { - std::string function = "Acam::Camera::speed"; - std::stringstream message; + const char* function = "Acam::Camera::speed"; + std::ostringstream message; long error = NO_ERROR; float hori=-1, vert=-1; @@ -656,8 +655,8 @@ namespace Acam { * */ long Camera::temperature( std::string args, std::string &retstring ) { - std::string function = "Acam::Camera::temperature"; - std::stringstream message; + const char* function = "Acam::Camera::temperature"; + std::ostringstream message; long error = NO_ERROR; int temp = 999; @@ -780,8 +779,8 @@ namespace Acam { * */ long Camera::write_frame( std::string source_file, std::string &outfile, const bool _tcs_online ) { - std::string function = "Acam::Camera::write_frame"; - std::stringstream message; + const char* function = "Acam::Camera::write_frame"; + std::ostringstream message; long error = NO_ERROR; // Nothing to do if not Andor image data @@ -850,13 +849,7 @@ namespace Acam { * */ long Interface::test_image( ) { - std::string function = "Acam::Interface::test_image"; - std::stringstream message; - long error = NO_ERROR; - - error = this->camera.andor.test(); - - return error; + return this->camera.andor.test(); } /***** Acam::Camera::test_image *********************************************/ @@ -870,7 +863,7 @@ namespace Acam { * */ long Astrometry::initialize_python() { - std::string function = "Acam::Astrometry::initialize_python"; + const char* function = "Acam::Astrometry::initialize_python"; if ( ! py_instance.is_initialized() ) { logwrite( function, "ERROR could not initialize Python" ); @@ -895,7 +888,7 @@ namespace Acam { this->pQualityModule = PyImport_Import( pModuleNameQuality ); if ( this->pAstrometryModule == nullptr || this->pQualityModule == nullptr ) { - std::stringstream message; + std::ostringstream message; message << "ERROR could not import Python module(s):"; if ( this->pAstrometryModule == nullptr ) message << " " << PYTHON_ASTROMETRY_MODULE; if ( this->pQualityModule == nullptr ) message << " " << PYTHON_IMAGEQUALITY_MODULE; @@ -928,8 +921,8 @@ namespace Acam { * */ long Astrometry::image_quality( ) { - std::string function = "Acam::Astrometry::image_quality"; - std::stringstream message; + const char* function = "Acam::Astrometry::image_quality"; + std::ostringstream message; if ( !this->python_initialized ) { logwrite( function, "ERROR Python is not initialized" ); @@ -1079,8 +1072,8 @@ namespace Acam { * */ long Astrometry::solve( std::string imagename_in, std::vector solverargs_in ) { - std::string function = "Acam::Astrometry::solve"; - std::stringstream message; + const char* function = "Acam::Astrometry::solve"; + std::ostringstream message; if ( !this->python_initialized ) { logwrite( function, "ERROR Python is not initialized" ); @@ -1343,8 +1336,8 @@ namespace Acam { * */ long Interface::bin( std::string args, std::string &retstring ) { - std::string function = "Acam::Interface::bin"; - std::stringstream message; + const char* function = "Acam::Interface::bin"; + std::ostringstream message; long error = NO_ERROR; // Help @@ -1433,12 +1426,14 @@ namespace Acam { * */ void Interface::publish_snapshot() { + // force-publish status this->publish_status(true); + nlohmann::json jmessage_out; jmessage_out[Key::SOURCE] = Topic::ACAMD; int ccdtemp=99; - this->camera.andor.get_temperature( ccdtemp ); // temp is int + this->camera.andor.get_temperature( ccdtemp ); // temp is int jmessage_out[Key::Acamd::TANDOR] = ( this->isopen("camera") ? static_cast(ccdtemp) : // but the database wants floats NAN ); @@ -1465,6 +1460,7 @@ namespace Acam { /** * @brief publishes my acam-related (important) status on change * @details This publishes a JSON message containing important telemetry. + * @param[in] force optional (default=false) forces publish irrespective of change * */ void Interface::publish_status(bool force) { @@ -1472,10 +1468,8 @@ namespace Acam { const bool is_acquired = this->target.is_acquired.load(); const int nacquired = this->target.nacquired; const int attempts = this->target.attempts; - const double seeing = this->astrometry.get_seeing(); - const double background = this->astrometry.get_background(); - // only will publish if there was a change in any one of these + // unless forced, only publish if there was a change in any one of these // if ( !force && acquire_mode == this->last_status.acquire_mode && @@ -1491,7 +1485,7 @@ namespace Acam { // assemble the telemetry into a json message // nlohmann::json jmessage_out; - jmessage_out[Key::SOURCE] = Topic::ACAMD; + jmessage_out[Key::SOURCE] = Topic::ACAMD; jmessage_out[Key::Acamd::ACQUIRE_MODE] = this->target.acquire_mode_string(); jmessage_out[Key::Acamd::IS_ACQUIRED] = this->target.is_acquired.load(); jmessage_out[Key::Acamd::NACQUIRED] = this->target.nacquired; @@ -1513,7 +1507,9 @@ namespace Acam { /***** Acam::Interface::request_snapshot ************************************/ /** - * @brief sends request for snapshot + * @brief publises request for snapshot + * @details publishing Topic::SNAPSHOT induces subscribers to publish a + * snapshot of their telemetry * */ void Interface::request_snapshot() { @@ -1539,6 +1535,8 @@ namespace Acam { /***** Acam::Interface::wait_for_snapshots **********************************/ /** * @brief wait for everyone to publish their snaphots + * @details When forcing subscribers to publish their telemetry, + * this waits until they have done so. * */ bool Interface::wait_for_snapshots() { @@ -1563,7 +1561,7 @@ namespace Acam { if (all_received) return true; if (std::chrono::steady_clock::now() - start_time > timeout) { - std::stringstream message; + std::ostringstream message; message << "ERROR timeout waiting for telemetry from:"; for ( const auto &[topic,status] : snapshot_status ) { if (!status) message << " " << topic; @@ -1580,27 +1578,26 @@ namespace Acam { /***** Acam::Interface::handletopic_snapshot ********************************/ /** - * @brief publishes snapshot of my telemetry + * @brief what to do when the topic is Topic::ACAMD * @details This publishes a JSON message containing a snapshot of my - * telemetry info when the subscriber receives the "_snapshot" - * topic and the payload contains my daemon name. + * 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 my name is in the jmessage then publish my snapshot - // - if ( jmessage_in.contains( Acam::DAEMON_NAME ) ) { - this->publish_snapshot(); - } - else - if ( jmessage_in.contains( "test" ) ) { - logwrite( "Acamd::Interface::handletopic_snapshot", jmessage_in.dump() ); - } + if ( jmessage_in.contains( Topic::ACAMD ) ) this->publish_snapshot(); } /***** Acam::Interface::handletopic_snapshot ********************************/ + /***** Acam::Interface::handletopic_tcsd ************************************/ + /** + * @brief what to do when the topic is Topic::TCSD + * @details This receives tcs telemetry + * @param[in] jmessage_in subscribed-received JSON message + * + */ void Interface::handletopic_tcsd( const nlohmann::json &jmessage ) { { std::lock_guard lock(snapshot_mtx); @@ -1630,8 +1627,16 @@ namespace Acam { this->database.add_key_val( "focus", telem.telfocus ); this->database.add_key_val( "AIRMASS", telem.airmass ); } + /***** Acam::Interface::handletopic_tcsd ************************************/ + /***** Acam::Interface::handletopic_targetinfo ******************************/ + /** + * @brief what to do when the topic is Topic::TARGETINFO + * @details This receives target info + * @param[in] jmessage_in subscribed-received JSON message + * + */ void Interface::handletopic_targetinfo( const nlohmann::json &jmessage ) { { std::lock_guard lock(snapshot_mtx); @@ -1643,11 +1648,13 @@ namespace Acam { this->database.add_from_json( jmessage, "RA" ); this->database.add_from_json( jmessage, "DECL" ); } + /***** Acam::Interface::handletopic_targetinfo ******************************/ /***** Acam::Interface::handletopic_slitd ***********************************/ /** - * @brief handles topic subscription to slitd + * @brief what to do when the topic is Topic::SLITD + * @details This receives slitd telemetry * @param[in] jmessage incoming json message * */ @@ -1662,97 +1669,6 @@ namespace Acam { /***** Acam::Interface::handletopic_slitd ***********************************/ - /***** Acam::Interface::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 Interface::handle_json_message( std::string message_in ) { - const std::string function="Acam::Interface::handle_json_message"; - std::stringstream message; - - // nothing to do if the message is empty - // - 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 - // - if ( messagetype == "tcsinfo" ) { - this->database.add_from_json( jmessage, "CASANGLE" ); - this->database.add_from_json( jmessage, "TELRA", "RAtel" ); - this->database.add_from_json( jmessage, "TELDEC", "DECLtel" ); - this->database.add_from_json( jmessage, "AZ" ); - this->database.add_from_json( jmessage, "TELFOCUS", "focus" ); - this->database.add_from_json( jmessage, "AIRMASS" ); - } - else - if ( messagetype == "targetinfo" ) { - this->database.add_from_json( jmessage, "OBS_ID" ); - this->database.add_from_json( jmessage, "NAME" ); - this->database.add_from_json( jmessage, "POINTMODE" ); - this->database.add_from_json( jmessage, "RA" ); - this->database.add_from_json( jmessage, "DECL" ); - } - else - if ( messagetype == "slitinfo" ) { - float slitw, slito; - Common::extract_telemetry_value( message_in, "SLITW", slitw ); - this->camera.fitsinfo.fitskeys.addkey( "SLITW", slitw, "slit width in arcsec" ); - Common::extract_telemetry_value( message_in, "SLITO", slito ); - this->camera.fitsinfo.fitskeys.addkey( "SLITO", slito, "slit offset in arcsec" ); - } - 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; - } - /***** Acam::Interface::handle_json_message *********************************/ - - /***** Acam::Interface::initialize_python_objects ***************************/ /** * @brief provides interface to initialize Python objects in the class @@ -1777,8 +1693,8 @@ namespace Acam { * */ long Interface::configure_interface( Config &config ) { - std::string function = "Acam::Interface::configure_interface"; - std::stringstream message; + const char* function = "Acam::Interface::configure_interface"; + std::ostringstream message; int applied=0; long error = NO_ERROR; @@ -2075,8 +1991,8 @@ namespace Acam { * */ long Interface::open( std::string args, std::string &retstring ) { - std::string function = "Acam::Interface::open"; - std::stringstream message; + const char* function = "Acam::Interface::open"; + std::ostringstream message; long error = NO_ERROR; std::vector arglist; std::string component, camarg; @@ -2168,15 +2084,14 @@ namespace Acam { // If serial number not specified as an arg then open the s/n specified // in the config file. // - int sn; + int sn=-1; if ( camarg.empty() ) sn = this->camera.andor.camera_info.serial_number; else { try { sn = std::stoi( camarg ); } catch( const std::exception &e ) { - message.str(""); message << "ERROR parsing serial number from \"" << camarg << "\": " << e.what(); - logwrite( function, message.str() ); + logwrite(function, "ERROR parsing serial number from '"+camarg+"': "+std::string(e.what())); error = ERROR; } } @@ -2224,8 +2139,8 @@ namespace Acam { * */ long Interface::isopen( std::string component, bool &state, std::string &retstring ) { - std::string function = "Acam::Interface::isopen"; - std::stringstream message; + const char* function = "Acam::Interface::isopen"; + std::ostringstream message; // Help // @@ -2305,8 +2220,8 @@ namespace Acam { this->close("",dontcare); } long Interface::close( std::string component, std::string &help ) { - std::string function = "Acam::Interface::close"; - std::stringstream message; + const char* function = "Acam::Interface::close"; + std::ostringstream message; long error = NO_ERROR; // Help @@ -2375,8 +2290,8 @@ namespace Acam { * */ long Interface::tcs_init( std::string args, std::string &retstring ) { - std::string function = "Acam::Interface::tcs_init"; - std::stringstream message; + const char* function = "Acam::Interface::tcs_init"; + std::ostringstream message; long error = NO_ERROR; // If shutting down then stop the focus monitoring thread first @@ -2465,8 +2380,8 @@ namespace Acam { * */ long Interface::framegrab_fix( std::string args, std::string &retstring ) { - std::string function = "Acam::Interface::framegrab_fix"; - std::stringstream message; + const char* function = "Acam::Interface::framegrab_fix"; + std::ostringstream message; // Help // @@ -2523,7 +2438,7 @@ namespace Acam { * */ long Interface::saveframes( std::string args, std::string &retstring ) { - const std::string function = "Acam::Interface::saveframes"; + const char* function = "Acam::Interface::saveframes"; // Help // @@ -2576,7 +2491,7 @@ namespace Acam { * */ long Interface::skipframes( std::string args, std::string &retstring ) { - const std::string function = "Acam::Interface::skipframes"; + const char* function = "Acam::Interface::skipframes"; // Help // @@ -2622,8 +2537,8 @@ namespace Acam { * */ long Interface::framegrab( std::string args, std::string &retstring ) { - std::string function = "Acam::Interface::framegrab"; - std::stringstream message; + const char* function = "Acam::Interface::framegrab"; + std::ostringstream message; long error = NO_ERROR; std::string _imagename = this->imagename; @@ -2728,8 +2643,8 @@ namespace Acam { * */ void Interface::dothread_framegrab( Acam::Interface &iface, const std::string whattodo, std::string sourcefile ) { - std::string function = "Acam::Interface::dothread_framegrab"; - std::stringstream message; + const char* function = "Acam::Interface::dothread_framegrab"; + std::ostringstream message; long error = NO_ERROR; if ( iface.is_framegrab_running.load(std::memory_order_acquire) ) { @@ -2906,8 +2821,8 @@ namespace Acam { * */ long Interface::guider_settings_control( std::string args, std::string &retstring ) { - std::string function = "Acam::Interface::guider_settings_control"; - std::stringstream message; + const char* function = "Acam::Interface::guider_settings_control"; + std::ostringstream message; // Help // @@ -3150,8 +3065,8 @@ namespace Acam { * */ long Interface::acquire( std::string args, std::string &retstring ) { - std::string function = "Acam::Interface::acquire"; - std::stringstream message; + const char* function = "Acam::Interface::acquire"; + std::ostringstream message; // Help // @@ -3390,8 +3305,8 @@ logwrite( function, message.str() ); * */ long Target::acquire( Acam::TargetAcquisitionModes requested_mode ) { - std::string function = "Acam::Target::acquire"; - std::stringstream message; + const char* function = "Acam::Target::acquire"; + std::ostringstream message; // reset guide offset filtering parameters // @@ -3489,8 +3404,8 @@ logwrite( function, message.str() ); * */ long Target::do_acquire() { - std::string function = "Acam::Target::do_acquire"; - std::stringstream message; + const char* function = "Acam::Target::do_acquire"; + std::ostringstream message; // Do nothing, return immediately if no acquisition mode selected // or if stop_acquisition is set. @@ -3919,8 +3834,8 @@ logwrite( function, message.str() ); * */ void Interface::dothread_set_filter( Acam::Interface &iface, std::string filter_req ) { - std::string function = "Acam::Interface::dothread_set_filter"; - std::stringstream message; + const char* function = "Acam::Interface::dothread_set_filter"; + std::ostringstream message; // get current filter, used to determine if it changed // @@ -3970,9 +3885,9 @@ logwrite( function, message.str() ); * */ void Interface::dothread_set_focus( Acam::Interface &iface, double focus_req ) { - std::string function = "Acam::Interface::dothread_set_focus"; - std::stringstream message; /***** + const char* function = "Acam::Interface::dothread_set_focus"; + std::ostringstream message; // get current focus, used to determine if it changed // double focus_og; @@ -4027,8 +3942,8 @@ logwrite( function, message.str() ); * */ void Interface::dothread_fpoffset( Acam::Interface &iface ) { - std::string function = "Acam::Interface::dothread_fpoffset"; - std::stringstream message; + const char* function = "Acam::Interface::dothread_fpoffset"; + std::ostringstream message; message.str(""); message << "calling fpoffsets.compute_offset() from thread: PyGILState=" << PyGILState_Check(); logwrite( function, message.str() ); @@ -4054,8 +3969,8 @@ logwrite( function, message.str() ); */ void Interface::dothread_monitor_focus( Acam::Interface &iface ) { /***** - std::string function = "Acam::Interface::dothread_monitor_focus"; - std::stringstream message; + const char* function = "Acam::Interface::dothread_monitor_focus"; + std::ostringstream message; if ( iface.monitor_focus_state.load(std::memory_order_seq_cst) == Acam::FOCUS_MONITOR_RUNNING ) { logwrite( function, "thread already running" ); @@ -4143,8 +4058,8 @@ logwrite( function, message.str() ); * */ long Interface::shutdown( std::string args, std::string &retstring ) { - std::string function = "Acam::Interface::shutdown"; - std::stringstream message; + const char* function = "Acam::Interface::shutdown"; + std::ostringstream message; // Help // @@ -4202,8 +4117,8 @@ logwrite( function, message.str() ); * */ long Interface::test( std::string args, std::string &retstring ) { - std::string function = "Acam::Interface::test"; - std::stringstream message; + const char* function = "Acam::Interface::test"; + std::ostringstream message; std::vector tokens; long error = NO_ERROR; @@ -4789,8 +4704,8 @@ logwrite( function, message.str() ); * */ long Interface::exptime( const std::string args, std::string &retstring ) { - std::string function = "Acam::Interface::exptime"; - std::stringstream message; + const char* function = "Acam::Interface::exptime"; + std::ostringstream message; long error=NO_ERROR; if ( args == "?" || args == "help" ) { @@ -4867,8 +4782,8 @@ logwrite( function, message.str() ); * */ long Interface::fan_mode( std::string args, std::string &retstring ) { - std::string function = "Acam::Interface::fan_mode"; - std::stringstream message; + const char* function = "Acam::Interface::fan_mode"; + std::ostringstream message; long error = NO_ERROR; // Help @@ -4940,11 +4855,7 @@ logwrite( function, message.str() ); * */ long Interface::image_quality( std::string args, std::string &retstring ) { - std::string function = "Acam::Interface::image_quality"; - std::stringstream message; - // Help - // if ( args == "?" ) { retstring = ACAMD_QUALITY; retstring.append( "\n" ); @@ -4976,8 +4887,8 @@ logwrite( function, message.str() ); * */ long Interface::solve( std::string args, std::string &retstring ) { - std::string function = "Acam::Interface::solve"; - std::stringstream message; + const char* function = "Acam::Interface::solve"; + std::ostringstream message; long error = NO_ERROR; std::string _imagename; std::string _wcsname; @@ -5106,11 +5017,8 @@ logwrite( function, message.str() ); * */ long Interface::collect_header_info() { - std::string function = "Acam::Interface::collect_header_info"; - std::stringstream message; - - // request external telemetry, results in struct telem. - // + // force subscribers to publish now, then wait + // esults in struct telem. this->request_snapshot(); this->wait_for_snapshots(); @@ -5254,8 +5162,8 @@ logwrite( function, message.str() ); * */ long Interface::target_coords( const std::string args, std::string &retstring ) { - std::string function = "Acam::Interface::target_coords"; - std::stringstream message; + const char* function = "Acam::Interface::target_coords"; + std::ostringstream message; double _ra=NAN, _dec=NAN, _angle=NAN; std::string _name; @@ -5388,8 +5296,8 @@ logwrite( function, message.str() ); * */ long Interface::offset_cal( const std::string args, std::string &retstring ) { - std::string function = "Acam::Interface::offset_cal"; - std::stringstream message; + const char* function = "Acam::Interface::offset_cal"; + std::ostringstream message; // Help // @@ -5461,7 +5369,7 @@ logwrite( function, message.str() ); // Form and send the acquire command. // This will change the target.acquire_mode to TARGET_ACQUIRE while it's acquiring. // - std::stringstream cmd; + std::ostringstream cmd; cmd << std::fixed << std::setprecision(6) << acam_ra << " " << acam_dec << " " << acam_angle << " acam"; error = this->acquire( cmd.str(), retstring ); @@ -5615,8 +5523,8 @@ logwrite( function, message.str() ); * */ long Interface::put_on_slit( const std::string args, std::string &retstring ) { - std::string function = "Acam::Interface::put_on_slit"; - std::stringstream message; + const char* function = "Acam::Interface::put_on_slit"; + std::ostringstream message; long error = NO_ERROR; // Help @@ -5730,7 +5638,7 @@ logwrite( function, message.str() ); return; } - std::stringstream fn; + std::ostringstream fn; fn << path << "/" << basename << "_" << std::setfill('0') << std::setw(5) << npreserve << ".fits"; // increment until a unique file is found so that it never overwrites diff --git a/slicecamd/slicecam_interface.cpp b/slicecamd/slicecam_interface.cpp index 2d8714c5..fca6d53f 100644 --- a/slicecamd/slicecam_interface.cpp +++ b/slicecamd/slicecam_interface.cpp @@ -26,15 +26,18 @@ namespace Slicecam { * */ long Interface::fineacquire(std::string args, std::string &retstring) { - const std::string function("Slicecam::Interface::fineacquire"); - std::stringstream message; + const char* function = "Slicecam::Interface::fineacquire"; + std::ostringstream message; // Help if ( args == "?" || args == "help" ) { retstring = SLICECAMD_FINEACQUIRE; - retstring.append( " stop | start { L | R } | [ status ]\n" ); + retstring.append( " stop | start [ { L | R } ] | [ status ]\n" ); retstring.append( " start or stop fine target acquisition.\n" ); - retstring.append( " L | R specifies which camera\n" ); + retstring.append( " aimpoint is optional and uses configuration by default, but\n" ); + retstring.append( " if specified must contain both L or R to specify which camera,\n" ); + retstring.append( " and aimpoint , coordinates, which may be fractional pixels.\n" ); + retstring.append( " No argument (or optional 'status') returns status.\n" ); return HELP; } @@ -66,7 +69,7 @@ namespace Slicecam { // not empty, stop or start is an error if (action != "start" || tokens.size() < 2) { - logwrite(function, "ERROR expected stop | start { L | R }"); + logwrite(function, "ERROR expected stop | start [ { L | R } ]"); retstring="invalid_argument"; return ERROR; } @@ -94,14 +97,34 @@ namespace Slicecam { return ERROR; } - const std::string which = tokens.at(1); - if (which != "L" && which != "R") { - logwrite(function, "ERROR expected stop | start { L | R }"); - retstring="invalid_argument"; + // 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"; return ERROR; } - - this->fineacquire_state.which = which; + else + // override the default camera and aimpoint if provided + if (tokens.size() > 1 && tokens.size() == 4) { + try { + const std::string which = tokens.at(1); + double x = std::stod(tokens.at(2)); + double y = std::stod(tokens.at(3)); + if ( (which != "L" && which != "R") || + std::isnan(x) || std::isnan(y) || x<0 || y<0) { + logwrite(function, "ERROR expected stop | start [ { L | R } ]"); + retstring="invalid_argument"; + return ERROR; + } + this->fineacquire_state.which = which; + this->fineacquire_state.aimpoint = { x, y }; + } + catch (const std::exception &e) { + logwrite(function, "ERROR expected stop | start [ { L | R } ] : "+std::string(e.what())); + retstring="invalid_argument"; + return ERROR; + } + } // start the state machine this->fineacquire_state.reset(); @@ -131,7 +154,7 @@ namespace Slicecam { * */ void Interface::do_fineacquire() { - const std::string function = "Slicecam::Interface::do_fineacquire"; + const char* function = "Slicecam::Interface::do_fineacquire"; // skip frames if we are waiting for the telescope to settle after a move // @@ -281,8 +304,8 @@ namespace Slicecam { * */ long Interface::bin( std::string args, std::string &retstring ) { - std::string function = "Slicecam::Interface::bin"; - std::stringstream message; + const char* function = "Slicecam::Interface::bin"; + std::ostringstream message; long error = NO_ERROR; // Help @@ -368,22 +391,24 @@ namespace Slicecam { /***** Slicecam::Interface::bin *********************************************/ + /***** Slicecam::Interface::handletopic_snapshot ****************************/ + /** + * @brief what to do when the topic is Topic::SLICECAMD + * @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 ) { - // If my name is in the jmessage then publish my snapshot - // - if ( jmessage.contains( Slicecam::DAEMON_NAME ) ) { - this->publish_snapshot(); - } - else - if ( jmessage.contains( "test" ) ) { - logwrite( "Slicecam::Interface::handletopic_snapshot", jmessage.dump() ); - } + if ( jmessage.contains(Topic::SLICECAMD) ) this->publish_snapshot(); } + /***** Slicecam::Interface::handletopic_snapshot ****************************/ /***** Slicecam::Interface::handletopic_acamd *******************************/ /** - * @brief handles Topic::ACAMD telemetry + * @brief what to do when the topic is Topic::ACAMD * @param[in] jmessage subscribed-received JSON message * */ @@ -400,6 +425,13 @@ namespace Slicecam { /***** Slicecam::Interface::handletopic_acamd *******************************/ + /***** Slicecam::Interface::handletopic_slitd *******************************/ + /** + * @brief what to do when the topic is Topic::SLITD + * @details This receives tcs telemetry + * @param[in] jmessage_in subscribed-received JSON message + * + */ void Interface::handletopic_slitd( const nlohmann::json &jmessage ) { { std::lock_guard lock(snapshot_mtx); @@ -411,8 +443,16 @@ namespace Slicecam { 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); } + /***** Slicecam::Interface::handletopic_slitd *******************************/ + /***** Slicecam::Interface::handletopic_tcsd ********************************/ + /** + * @brief what to do when the topic is Topic::TCSD + * @details This receives tcs telemetry + * @param[in] jmessage_in subscribed-received JSON message + * + */ void Interface::handletopic_tcsd( const nlohmann::json &jmessage ) { { std::lock_guard lock(snapshot_mtx); @@ -433,21 +473,24 @@ namespace Slicecam { Common::extract_telemetry_value( jmessage, "TELFOCUS", telem.telfocus ); Common::extract_telemetry_value( jmessage, "AIRMASS", telem.airmass ); } + /***** Slicecam::Interface::handletopic_tcsd ********************************/ /***** Slicecam::Interface::publish_status **********************************/ /** * @brief publishes my important status on change * @details This publishes a JSON message containing important telemetry. + * @param[in] force optional (default=false) forces publish irrespective of change * */ - void Interface::publish_status() { + void Interface::publish_status(bool force) { const bool is_fineacquire_running_now = this->is_fineacquire_running.load(); const bool is_fineacquire_locked_now = this->is_fineacquire_locked.load(); - // only publish if there was a change + // unless forced, only publish if there was a change // - if ( is_fineacquire_running_now == this->last_status.is_fineacquire_running && + if ( !force && + is_fineacquire_running_now == this->last_status.is_fineacquire_running && is_fineacquire_locked_now == this->last_status.is_fineacquire_locked) return; this->last_status.is_fineacquire_running = is_fineacquire_running_now; @@ -477,6 +520,9 @@ namespace Slicecam { * */ void Interface::publish_snapshot() { + // force-publish status + this->publish_status(true); + nlohmann::json jmessage_out; jmessage_out[Key::SOURCE] = Topic::SLICECAMD; @@ -525,6 +571,8 @@ namespace Slicecam { /***** Slicecam::Interface::wait_for_snapshots ******************************/ /** * @brief wait for everyone to publish their snaphots + * @details When forcing subscribers to publish their telemetry, + * this waits until they have done so. * */ bool Interface::wait_for_snapshots() { @@ -549,7 +597,7 @@ namespace Slicecam { if (all_received) return true; if (std::chrono::steady_clock::now() - start_time > timeout) { - std::stringstream message; + std::ostringstream message; message << "ERROR timeout waiting for telemetry from:"; for ( const auto &[topic,status] : snapshot_status ) { if (!status) message << " " << topic; @@ -573,8 +621,8 @@ namespace Slicecam { * */ long Interface::configure_interface( Config &config ) { - std::string function = "Slicecam::Interface::configure_interface"; - std::stringstream message; + const char* function = "Slicecam::Interface::configure_interface"; + std::ostringstream message; int applied=0; long error = NO_ERROR; @@ -661,21 +709,21 @@ namespace Slicecam { logwrite( function, message.str() ); applied++; } - + else if ( starts_with( config.param[entry], "PUSH_GUI_SETTINGS" ) ) { this->gui_manager.set_push_settings( config.arg[entry] ); message.str(""); message << "SLICECAMD:config:" << config.param[entry] << "=" << config.arg[entry]; logwrite( function, message.str() ); applied++; } - + else if ( starts_with( config.param[entry], "PUSH_GUI_IMAGE" ) ) { this->gui_manager.set_push_image( config.arg[entry] ); message.str(""); message << "SLICECAMD:config:" << config.param[entry] << "=" << config.arg[entry]; logwrite( function, message.str() ); applied++; } - + else if ( starts_with( config.param[entry], "TCSD_PORT" ) ) { int port; try { @@ -691,7 +739,7 @@ namespace Slicecam { logwrite( function, message.str() ); applied++; } - + else if ( config.param[entry] == "SKYSIM_IMAGE_SIZE" ) { try { this->camera.set_simsize( std::stoi( config.arg[entry] ) ); @@ -705,8 +753,38 @@ namespace Slicecam { logwrite( function, message.str() ); applied++; } + else + if ( config.param[entry] == "FINE_ACQUIRE_AIMPOINT=" ) { + std::string which; + double x,y; + std::istringstream iss(config.arg[entry]); + if (!(iss >> which >> x >> y)) { + logwrite(function, "ERROR invalid FINE_ACQUIRE_AIMPOINT='"+config.arg[entry]+"' expected "); + return ERROR; + } + this->fineacquire_state.which = which; + this->fineacquire_state.aimpoint = { x, y }; + } + else + if ( config.param[entry] == "FINE_ACQUIRE_BACKGROUND=" ) { + long x1, x2, y1, y2; + std::istringstream iss(config.arg[entry]); + if (!(iss >> x1 >> x2 >> y1 >> y2)) { + logwrite(function, "ERROR invalid FINE_ACQUIRE_BACKGROUND='"+config.arg[entry]+"' expected "); + return ERROR; + } + this->fineacquire_state.bg_region = { x1, x2, y1, y2 }; + } + + } + // FINE_ACQUIRE parameters must have been configured properly + // + if (!this->fineacquire_state.is_valid()) { + logwrite(function, "ERROR bad or missing FINE_ACQUIRE configuration"); + return ERROR; } + message.str(""); message << "applied " << applied << " configuration lines to the slicecam interface"; logwrite(function, message.str()); @@ -724,8 +802,6 @@ namespace Slicecam { * */ long Interface::open( std::string args, std::string &retstring ) { - std::string function = "Slicecam::Interface::open"; - std::stringstream message; if ( args == "?" || args == "help" ) { retstring = SLICECAMD_OPEN; @@ -785,8 +861,8 @@ namespace Slicecam { * */ long Interface::isopen( std::string which, bool &state, std::string &retstring ) { - std::string function = "Slicecam::Interface::isopen"; - std::stringstream message; + const char* function = "Slicecam::Interface::isopen"; + std::ostringstream message; // Help // @@ -850,19 +926,22 @@ namespace Slicecam { /***** Slicecam::Interface::close *******************************************/ /** - * @brief closes slicecams - * @param[in] args optionally request help - * @param[out] retstring contains return string for help - * @return ERROR | NO_ERROR | HELP + * @brief closes slicecams, internal use * */ void Interface::close() { std::string dontcare; this->close("",dontcare); } + /***** Slicecam::Interface::close *******************************************/ + /** + * @brief closes slicecams + * @param[in] args optionally request help + * @param[out] retstring contains return string for help + * @return ERROR | NO_ERROR | HELP + * + */ long Interface::close( std::string args, std::string &retstring ) { - std::string function = "Slicecam::Interface::close"; - std::stringstream message; long error = NO_ERROR; // Help @@ -895,8 +974,8 @@ namespace Slicecam { * */ long Interface::tcs_init( std::string args, std::string &retstring ) { - std::string function = "Slicecam::Interface::tcs_init"; - std::stringstream message; + const char* function = "Slicecam::Interface::tcs_init"; + std::ostringstream message; // Send command to tcs daemon client. If help was requested then that // request is passed on here to tcsd.init() so this could return HELP. @@ -979,7 +1058,7 @@ namespace Slicecam { * */ long Interface::saveframes( std::string args, std::string &retstring ) { - const std::string function = "Slicecam::Interface::saveframes"; + const char* function = "Slicecam::Interface::saveframes"; // Help // @@ -1032,8 +1111,8 @@ namespace Slicecam { * */ long Interface::framegrab_fix( std::string args, std::string &retstring ) { - std::string function = "Slicecam::Interface::framegrab_fix"; - std::stringstream message; + const char* function = "Slicecam::Interface::framegrab_fix"; + std::ostringstream message; // Help // @@ -1115,8 +1194,8 @@ namespace Slicecam { * */ long Interface::framegrab( std::string args, std::string &retstring ) { - std::string function = "Slicecam::Interface::framegrab"; - std::stringstream message; + const char* function = "Slicecam::Interface::framegrab"; + std::ostringstream message; std::string _imagename = this->imagename; // Help @@ -1222,8 +1301,8 @@ namespace Slicecam { * */ void Interface::dothread_framegrab( const std::string whattodo, const std::string sourcefile ) { - std::string function = "Slicecam::Interface::dothread_framegrab"; - std::stringstream message; + const char* function = "Slicecam::Interface::dothread_framegrab"; + std::ostringstream message; long error = NO_ERROR; // For any whattodo that will take an image, when running the Andor emulator, @@ -1377,7 +1456,7 @@ namespace Slicecam { return; } - std::stringstream fn; + std::ostringstream fn; fn << path << "/" << basename << "_" << std::setfill('0') << std::setw(5) << npreserve << ".fits"; // increment until a unique file is found so that it never overwrites @@ -1417,8 +1496,8 @@ namespace Slicecam { * */ long Interface::gui_settings_control( std::string args, std::string &retstring ) { - std::string function = "Slicecam::Interface::gui_settings_control"; - std::stringstream message; + const char* function = "Slicecam::Interface::gui_settings_control"; + std::ostringstream message; auto info = this->camera.andor.begin()->second->camera_info; // Help @@ -1613,8 +1692,8 @@ namespace Slicecam { * */ void Interface::dothread_fpoffset( Slicecam::Interface &iface ) { - std::string function = "Slicecam::Interface::dothread_fpoffset"; - std::stringstream message; + const char* function = "Slicecam::Interface::dothread_fpoffset"; + std::ostringstream message; message.str(""); message << "calling fpoffsets.compute_offset() from thread: PyGILState=" << PyGILState_Check(); logwrite( function, message.str() ); @@ -1642,7 +1721,7 @@ namespace Slicecam { * */ long Interface::offset_acam_goal(const std::pair &offsets, std::optional fineacquire) { - const std::string function("Slicecam::Interface::offset_acam_goal"); + const char* function = "Slicecam::Interface::offset_acam_goal"; auto [ra_off, dec_off] = offsets; // local copy @@ -1659,7 +1738,7 @@ namespace Slicecam { if ( is_guiding ) { // Send to acamd if guiding // - std::stringstream cmd; + std::ostringstream cmd; cmd << ACAMD_OFFSETGOAL << " " << std::fixed << std::setprecision(6) << ra_off << " " << dec_off; // add fineguiding arg when used for fine acquisition mode @@ -1709,8 +1788,8 @@ namespace Slicecam { * */ long Interface::put_on_slit( std::string args, std::string &retstring ) { - std::string function = "Slicecam::Interface::put_on_slit"; - std::stringstream message; + const char* function = "Slicecam::Interface::put_on_slit"; + std::ostringstream message; // Help // @@ -1777,8 +1856,8 @@ namespace Slicecam { * */ long Interface::shutdown( std::string args, std::string &retstring ) { - std::string function = "Slicecam::Interface::shutdown"; - std::stringstream message; + const char* function = "Slicecam::Interface::shutdown"; + std::ostringstream message; // Help // @@ -1814,7 +1893,7 @@ namespace Slicecam { * */ long Interface::shutter(const std::string args, std::string &retstring) { - const std::string function("Slicecam::Interface::shutter"); + const char* function("Slicecam::Interface::shutter"); // Help if ( args == "?" || args == "help" ) { @@ -1870,8 +1949,8 @@ namespace Slicecam { * */ long Interface::test( std::string args, std::string &retstring ) { - std::string function = "Slicecam::Interface::test"; - std::stringstream message; + const char* function = "Slicecam::Interface::test"; + std::ostringstream message; std::vector tokens; long error = NO_ERROR; @@ -2179,8 +2258,8 @@ namespace Slicecam { * */ long Interface::exptime( std::string args, std::string &retstring ) { - std::string function = "Slicecam::Interface::exptime"; - std::stringstream message; + const char* function = "Slicecam::Interface::exptime"; + std::ostringstream message; if ( args == "?" || args == "help" ) { retstring = SLICECAMD_EXPTIME; @@ -2256,8 +2335,8 @@ namespace Slicecam { * */ long Interface::fan_mode( std::string args, std::string &retstring ) { - std::string function = "Slicecam::Interface::fan_mode"; - std::stringstream message; + const char* function = "Slicecam::Interface::fan_mode"; + std::ostringstream message; long error = NO_ERROR; // Help @@ -2351,8 +2430,8 @@ namespace Slicecam { * */ long Interface::gain( std::string args, std::string &retstring ) { - std::string function = "Slicecam::Interface::gain"; - std::stringstream message; + const char* function = "Slicecam::Interface::gain"; + std::ostringstream message; long error = NO_ERROR; int gain = -999; @@ -2459,8 +2538,8 @@ namespace Slicecam { * */ long Interface::collect_header_info( std::unique_ptr &slicecam ) { - std::string function = "Slicecam::Interface::collect_header_info"; - std::stringstream message; + const char* function = "Slicecam::Interface::collect_header_info"; + std::ostringstream message; std::string cam = slicecam->camera_info.camera_name; diff --git a/slicecamd/slicecam_interface.h b/slicecamd/slicecam_interface.h index 4f4b5164..0854d1a2 100644 --- a/slicecamd/slicecam_interface.h +++ b/slicecamd/slicecam_interface.h @@ -80,17 +80,20 @@ namespace Slicecam { * after a commanded move */ struct FineAcqState { - std::string which = "L"; - Point aimpoint = { 150.0, 115.5 }; ///< 1-based pixel aim point - Rect bg_region = { 80, 165, 30, 210 }; ///< 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 - 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 + std::string which; + Point aimpoint; ///< 1-based pixel aim point + 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 + 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; } + bool is_valid() const noexcept { + return !which.empty() && aimpoint.is_valid() && bg_region.is_valid(); + } }; /***** Slicecam::FineAcqState ***********************************************/ @@ -235,7 +238,7 @@ namespace Slicecam { void handletopic_acamd( const nlohmann::json &jmessage ); void handletopic_slitd( const nlohmann::json &jmessage ); void handletopic_tcsd( const nlohmann::json &jmessage ); - void publish_status(); + void publish_status(bool force=false); void publish_snapshot(); void request_snapshot(); bool wait_for_snapshots(); diff --git a/slicecamd/slicecam_math.h b/slicecamd/slicecam_math.h index a9a9e76e..74fb6cdb 100644 --- a/slicecamd/slicecam_math.h +++ b/slicecamd/slicecam_math.h @@ -17,9 +17,28 @@ namespace Slicecam { - struct Point { double x = 0.0; double y = 0.0; }; ///< pixel coordinate - struct Rect { long x1 = 1; long x2 = 1; long y1 = 1; long y2 = 1; }; ///< rectangular region - struct World { double ra = 0.0; double dec = 0.0; }; ///< sky coordinates + struct Point { ///< pixel coordinate + double x = 0.0; double y = 0.0; + bool is_valid() const noexcept { + return !std::isnan(x) && !std::isnan(y) && + x >= 0.0 && y >= 0.0; + } + }; + + struct Rect { ///< rectangular region + long x1 = 1; long x2 = 1; long y1 = 1; long y2 = 1; + bool is_valid() const noexcept { + return x1>0 && x2>0 && y1>0 && y2>0 && x1 != x2 && y1 != y2; + } + }; + + struct World { ///< sky coordinates + double ra = 0.0; double dec = 0.0; + bool is_valid() const noexcept { + return !std::isnan(ra) && !std::isnan(dec) && + ra >= 0.0 && dec >= 0.0; + } + }; /***** Slicecam::Math *******************************************************/ /**