diff --git a/Config/camerad.cfg.in b/Config/camerad.cfg.in index fe3e69bb..e90faefd 100644 --- a/Config/camerad.cfg.in +++ b/Config/camerad.cfg.in @@ -15,6 +15,13 @@ ASYNCGROUP=239.1.1.234 # or set to NONE if not using ASYNC messaging DAEMON=no # run as a daemon? {yes,no} PUBLISHER_PORT="tcp://127.0.0.1:@CAMERAD_PUB_PORT@" # my zeromq pub port +# Message pub/sub +# PUB_ENDPOINT=tcp://127.0.0.1: +# SUB_ENDPOINT=tcp://127.0.0.1: +# +PUB_ENDPOINT="tcp://127.0.0.1:@MESSAGE_BROKER_SUB_PORT@" +SUB_ENDPOINT="tcp://127.0.0.1:@MESSAGE_BROKER_PUB_PORT@" + # ----------------------------------------------------------------------------- # The following are for simulated ARC controllers ARCSIM_NUMDEV=0 @@ -136,6 +143,17 @@ CONSTKEY_EXT=(SPECTPART WHOLE) # fixed only because we're writing single-channe # USERKEYS_PERSIST=yes +# ACTIVATE_COMMANDS=(CHAN CMD [, CMD, CMD ...]) +# commands sent to activate channel after de-activation +# CHAN is space-delimited from a comma-delimited list of one or more commands +# and must have been first configured by CONTROLER= +# commands that contain a space must be enclosed by double quotes +# +ACTIVATE_COMMANDS=(I PON, ERS 1000 1000, EPG 500, CLR) +ACTIVATE_COMMANDS=(R PON, ERS 1000 1000, EPG 500, CLR) +ACTIVATE_COMMANDS=(G PON, ERS 1000 1000, EPG 500, CLR) +ACTIVATE_COMMANDS=(U PON, CLR) + # ----------------------------------------------------------------------------- # TELEM_PROVIDER=( ) # diff --git a/Config/sequencerd.cfg.in b/Config/sequencerd.cfg.in index 88fe2a0d..0507565c 100644 --- a/Config/sequencerd.cfg.in +++ b/Config/sequencerd.cfg.in @@ -107,7 +107,7 @@ CALIB_DOOR__SHUTDOWN=close # Virtual Slit Mode slit offset positions # units are arcseconds # -VIRTUAL_SLITW_ACQUIRE=0.5 # slit width during acquire +VIRTUAL_SLITW_ACQUIRE=0.4 # slit width during acquire VIRTUAL_SLITO_ACQUIRE=-3.0 # slit offset for acquiring target VIRTUAL_SLITO_EXPOSE=3.0 # slit offset for science exposure @@ -155,33 +155,40 @@ TCS_PREAUTH_TIME=10 # seconds before end of exposure to notify # ACAM Target acquisition # -ACQUIRE_TIMEOUT=90 # seconds before ACAM acquisition sequence aborts on failure to acquire (REQUIRED!) +ACQUIRE_TIMEOUT=120 # seconds before ACAM acquisition sequence aborts on failure to acquire (REQUIRED!) ACQUIRE_RETRYS=5 # max number of retrys before acquisition fails (optional, can be left blank to disable) ACQUIRE_OFFSET_THRESHOLD=0.5 # computed offset below this threshold (in arcsec) defines successful acquisition ACQUIRE_MIN_REPEAT=2 # minimum number of sequential successful acquires ACQUIRE_TCS_MAX_OFFSET=60 # the maximum allowable offset sent to the TCS, in arcsec +ACQ_AUTOMATIC_MODE=1 # 1=legacy, 2=semi-auto, 3=auto +ACQ_FINE_TUNE_CMD=ngps_acq # command to run after guiding for final fine tune +ACQ_FINE_TUNE_LOG=1 # log fine tune output to /data//logs/ngps_acq_.log (0/1) +ACQ_OFFSET_SETTLE=3 # seconds to wait after automatic offset # Calibration Settings -# CAL_TARGET=(name caldoor calcover lampthar lampfear lampbluc lampredc lolamp hilamp mod1 mod2 ... mod6) +# CAL_TARGET=(name caldoor calcover U G R I lampthar lampfear lampbluc lampredc lolamp hilamp mod1 mod2 ... mod6) # # where name must be "DEFAULT" or start with "CAL_" # caldoor = open | close # calcover = open | close -# lamp* = on | off -# mod* = on | off -# for a total of 15 required parameters +# U,G,R,I = on | off # indicates which channels to enable/disable +# lamp* = on | off # lamp power +# mod* = on | off # lamp modulator +# for a total of 19 required parameters # name=SCIENCE defines science target operation # -# name door cover thar fear bluc redc llmp hlmp mod1 mod2 mod3 mod4 mod5 mod6 -CAL_TARGET=(CAL_THAR open close on off off off off off off off off off off on ) -CAL_TARGET=(CAL_FEAR open close off on off off off off on off off off off off) -CAL_TARGET=(CAL_REDCONT open close off off off on off off off off off on off off) -CAL_TARGET=(CAL_BLUCONT open close off off on off off off off off off off on off) -CAL_TARGET=(CAL_ETALON open close off off off on off off off off on off off off) -CAL_TARGET=(CAL_DOME close open off off off off off on off off off off off off) -CAL_TARGET=(CAL_BIAS close close off off off off off off off off off off off off) -CAL_TARGET=(CAL_DARK close close off off off off off off off off off off off off) -CAL_TARGET=(SCIENCE close open off off off off off off off off off off off off) +# name door cover U G R I thar fear bluc redc llmp hlmp mod1 mod2 mod3 mod4 mod5 mod6 +CAL_TARGET=(CAL_THAR open close on on on on on on on on off off off off off off off on ) +CAL_TARGET=(CAL_FEAR open close on on on on on on on on off off on off off off off off) +CAL_TARGET=(CAL_THAR_UG open close on on off off on on on on off off off off off off off on ) +CAL_TARGET=(CAL_FEAR_UG open close on on off off on on on on off off on off off off off off) +CAL_TARGET=(CAL_CONTR open close on on on on on on on on off off off off off on off off) +CAL_TARGET=(CAL_CONTB open close on on on on on on on on off off off off off off on off) +CAL_TARGET=(CAL_DOME close open on on on on off off off off off on off off off off off off) +CAL_TARGET=(CAL_DOME_UG close open on on off off off off off off off on off off off off off off) +CAL_TARGET=(CAL_BIAS close close on on on on off off off off off off off off off off off off) +CAL_TARGET=(CAL_DARK close close on on on on off off off off off off off off off off off off) +CAL_TARGET=(SCIENCE close open on on on on off off off off off off off off off off off off) # miscellaneous # diff --git a/Config/slicecamd.cfg.in b/Config/slicecamd.cfg.in index 53873240..ad197d77 100644 --- a/Config/slicecamd.cfg.in +++ b/Config/slicecamd.cfg.in @@ -22,6 +22,9 @@ SUB_ENDPOINT="tcp://127.0.0.1:@MESSAGE_BROKER_PUB_PORT@" # TCSD_PORT=@TCSD_BLK_PORT@ ACAMD_PORT=@ACAMD_BLK_PORT@ +# AUTOACQ_ARGS defines defaults for slicecamd in-process ngps_acq. +# This line intentionally includes all supported ngps_acq options. +AUTOACQ_ARGS="--frame-mode stream --input /tmp/slicecam.fits --goal-x 150.0 --goal-y 115.5 --bg-x1 80 --bg-x2 165 --bg-y1 30 --bg-y2 210 --pixel-origin 1 --max-dist 40 --snr 3 --min-adj 4 --centroid-hw 4 --centroid-sigma 1.2 --loop 1 --cadence-sec 4 --prec-arcsec 0.4 --goal-arcsec 0.3 --gain 1.0 --dry-run 0 --tcs-set-units 0 --verbose 1 --debug 1 --use-putonslit 1 --adaptive 1 --adaptive-bright 40000 --adaptive-bright-goal 10000" # ANDOR=( [emulate] ) # For each slice camera specify: diff --git a/DSP b/DSP index 6ac8315d..4f4c6801 160000 --- a/DSP +++ b/DSP @@ -1 +1 @@ -Subproject commit 6ac8315ddd249da7eac405a7914a7504c1ae84f6 +Subproject commit 4f4c6801322f52fb43347e8218552cb47829342a diff --git a/acamd/acam_interface.cpp b/acamd/acam_interface.cpp index b61f0427..738c43e1 100644 --- a/acamd/acam_interface.cpp +++ b/acamd/acam_interface.cpp @@ -1453,6 +1453,11 @@ namespace Acam { this->motion.get_current_coverpos() : "not_connected" ); + std::string mode = this->target.acquire_mode_string(); + jmessage_out["ACAM_ACQUIRE_MODE"] = mode; + jmessage_out["ACAM_GUIDING"] = ( mode == "guiding" ); + jmessage_out["ACAM_ACQUIRING"] = ( mode == "acquiring" ); + try { this->publisher->publish( jmessage_out ); } @@ -5522,12 +5527,8 @@ 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); + // Keep GUIDE mode; just reset filtering so the new goal takes effect quickly. + this->target.reset_offset_params(); } return NO_ERROR; diff --git a/camerad/CMakeLists.txt b/camerad/CMakeLists.txt index 037051be..f4fcc0a0 100644 --- a/camerad/CMakeLists.txt +++ b/camerad/CMakeLists.txt @@ -99,6 +99,11 @@ target_include_directories(${INTERFACE_TARGET} PUBLIC ${INTERFACE_INCLUDES}) find_library(CCFITS_LIB CCfits NAMES libCCfits PATHS /usr/local/lib) find_library(CFITS_LIB cfitsio NAMES libcfitsio PATHS /usr/local/lib) +# ZeroMQ +# +find_library( ZMQPP_LIB zmqpp NAMES libzmqpp PATHS /usr/local/lib ) +find_library( ZMQ_LIB zmq NAMES libzmq PATHS /usr/local/lib ) + find_package(Threads) add_library(camera STATIC @@ -124,6 +129,8 @@ target_link_libraries(camerad ${CMAKE_THREAD_LIBS_INIT} ${CCFITS_LIB} ${CFITS_LIB} + ${ZMQPP_LIB} + ${ZMQ_LIB} ) # ---------------------------------------------------------------------------- diff --git a/camerad/astrocam.cpp b/camerad/astrocam.cpp index 594dbf67..2675ccdb 100644 --- a/camerad/astrocam.cpp +++ b/camerad/astrocam.cpp @@ -12,10 +12,44 @@ */ #include "camerad.h" +#include "message_keys.h" + extern Camera::Server server; namespace AstroCam { + /**** AstroCam::Interface::publish_snapshot *********************************/ + /** + * @brief publish a snapshot of my telemetry + * @param[out] retstring optional pointer to buffer for return string + * + */ + void Interface::publish_snapshot(std::string *retstring) { + const std::string function("AstroCam::Interface::publish_snapshot"); + nlohmann::json jmessage_out; + + // build JSON message with my telemetry + jmessage_out[Key::SOURCE] = "camerad"; + jmessage_out[Key::Camerad::READY] = this->can_expose.load(); + + // publish JSON message + try { + this->publisher->publish(jmessage_out); + } + catch (const std::exception &e) { + logwrite(function, "ERROR: "+std::string(e.what())); + return; + } + + // if a retstring buffer was supplied then return the JSON message + if (retstring) { + *retstring=jmessage_out.dump(); + retstring->append(JEOF); + } + } + /**** AstroCam::Interface::publish_snapshot *********************************/ + + long NewAstroCam::new_expose( std::string nseq_in ) { logwrite( "NewAstroCam::new_expose", nseq_in ); return( NO_ERROR ); @@ -159,25 +193,25 @@ namespace AstroCam { return; } - server.controller[devnum].in_frametransfer = false; + server.controller.at(devnum).in_frametransfer = false; server.exposure_pending( devnum, false ); // this also does the notify - server.controller[devnum].in_readout = true; + server.controller.at(devnum).in_readout = true; server.state_monitor_condition.notify_all(); // Trigger the readout waveforms here. // try { - server.controller[devnum].pArcDev->readout( expbuf, + server.controller.at(devnum).pArcDev->readout( expbuf, devnum, - server.controller[devnum].info.axes[_ROW_], - server.controller[devnum].info.axes[_COL_], + server.controller.at(devnum).info.axes[_ROW_], + server.controller.at(devnum).info.axes[_COL_], server.camera.abortstate, - server.controller[devnum].pCallback + server.controller.at(devnum).pCallback ); } catch ( const std::exception &e ) { // arc::gen3::CArcDevice::readout may throw an exception - message.str(""); message << "ERROR starting readout for " << server.controller[devnum].devname - << " channel " << server.controller[devnum].channel << ": " << e.what(); + message.str(""); message << "ERROR starting readout for " << server.controller.at(devnum).devname + << " channel " << server.controller.at(devnum).channel << ": " << e.what(); std::thread( std::ref(AstroCam::Interface::handle_queue), message.str() ).detach(); return; } @@ -187,52 +221,6 @@ namespace AstroCam { /***** AstroCam::Callback::ftCallback ***************************************/ - /***** AstroCam::Interface::Interface ***************************************/ - /** - * @brief AstroCam Interface class constructor - * - */ - Interface::Interface() { - this->state_monitor_thread_running = false; - this->modeselected = false; - this->pci_cmd_num.store(0); - this->nexp.store(1); - this->numdev = 0; - this->nframes = 1; - this->nfpseq = 1; - this->useframes = true; - this->framethreadcount = 0; - - this->pFits.resize( NUM_EXPBUF ); // pre-allocate FITS_file object pointers for each exposure buffer - this->fitsinfo.resize( NUM_EXPBUF ); // pre-allocate Camera Information object pointers for each exposure buffer - - this->writes_pending.resize( NUM_EXPBUF ); // pre-allocate writes_pending vector for each exposure buffer - - // Initialize STL map of Readout Amplifiers - // Indexed by amplifier name. - // The number is the argument for the Arc command to set this amplifier in the firmware. - // - // Format here is: { AMP_NAME, { ENUM_TYPE, ARC_ARG } } - // where AMP_NAME is the name of the readout amplifier, the index for this map - // ENUM_TYPE is an enum of type ReadoutType - // ARC_ARG is the ARC argument for the SOS command to select this readout source - // - this->readout_source.insert( { "U1", { U1, 0x5f5531 } } ); // "_U1" - this->readout_source.insert( { "L1", { L1, 0x5f4c31 } } ); // "_L1" - this->readout_source.insert( { "U2", { U2, 0x5f5532 } } ); // "_U2" - this->readout_source.insert( { "L2", { L2, 0x5f4c32 } } ); // "_L2" - this->readout_source.insert( { "SPLIT1", { SPLIT1, 0x5f5f31 } } ); // "__1" - this->readout_source.insert( { "SPLIT2", { SPLIT2, 0x5f5f32 } } ); // "__2" - this->readout_source.insert( { "QUAD", { QUAD, 0x414c4c } } ); // "ALL" - this->readout_source.insert( { "FT2", { FT2, 0x465432 } } ); // "FT2" -- frame transfer from 1->2, read split2 - this->readout_source.insert( { "FT1", { FT1, 0x465431 } } ); // "FT1" -- frame transfer from 2->1, read split1 -// this->readout_source.insert( { "hawaii1", { HAWAII_1CH, 0xffffff } } ); ///< TODO @todo implement HxRG 1 channel deinterlacing -// this->readout_source.insert( { "hawaii32", { HAWAII_32CH, 0xffffff } } ); ///< TODO @todo implement HxRG 32 channel deinterlacing -// this->readout_source.insert( { "hawaii32lr", { HAWAII_32CH_LR, 0xffffff } } ); ///< TODO @todo implement HxRG 32 channel alternate left/right deinterlacing - } - /***** AstroCam::Interface::Interface ***************************************/ - - /***** AstroCam::Interface::interface ***************************************/ /** * @brief returns the interface @@ -253,7 +241,7 @@ namespace AstroCam { void Interface::state_monitor_thread(Interface& interface) { std::string function = "AstroCam::Interface::state_monitor_thread"; std::stringstream message; - std::vector selectdev; + std::vector selectdev; // notify that the thread is running // @@ -272,11 +260,11 @@ namespace AstroCam { while ( interface.is_camera_idle() ) { selectdev.clear(); message.str(""); message << "enabling detector idling for channel(s)"; - for ( const auto &dev : interface.devnums ) { + for ( const auto &dev : interface.active_devnums ) { logwrite(function, std::to_string(dev)); - if ( interface.controller[dev].connected ) { + if ( interface.controller.at(dev).connected ) { selectdev.push_back(dev); - message << " " << interface.controller[dev].channel; + message << " " << interface.controller.at(dev).channel; } } if ( selectdev.size() > 0 ) { @@ -308,30 +296,30 @@ namespace AstroCam { std::string function = "AstroCam::Interface::make_image_keywords"; std::stringstream message; - auto chan = this->controller[dev].channel; + auto chan = this->controller.at(dev).channel; - auto rows = this->controller[dev].info.axes[_ROW_]; - auto cols = this->controller[dev].info.axes[_COL_]; - auto osrows = this->controller[dev].osrows; - auto oscols = this->controller[dev].oscols; - auto skiprows = this->controller[dev].skiprows; - auto skipcols = this->controller[dev].skipcols; + auto rows = this->controller.at(dev).info.axes[_ROW_]; + auto cols = this->controller.at(dev).info.axes[_COL_]; + auto osrows = this->controller.at(dev).osrows; + auto oscols = this->controller.at(dev).oscols; + auto skiprows = this->controller.at(dev).skiprows; + auto skipcols = this->controller.at(dev).skipcols; int binspec, binspat; - this->controller[dev].physical_to_logical(this->controller[dev].info.binning[_ROW_], - this->controller[dev].info.binning[_COL_], + this->controller.at(dev).physical_to_logical(this->controller.at(dev).info.binning[_ROW_], + this->controller.at(dev).info.binning[_COL_], binspat, binspec); - this->controller[dev].info.systemkeys.add_key( "AMP_ID", this->controller[dev].info.readout_name, "readout amplifier", EXT, chan ); - this->controller[dev].info.systemkeys.add_key( "FT", this->controller[dev].have_ft, "frame transfer used", EXT, chan ); + this->controller.at(dev).info.systemkeys.add_key( "AMP_ID", this->controller.at(dev).info.readout_name, "readout amplifier", EXT, chan ); + this->controller.at(dev).info.systemkeys.add_key( "FT", this->controller.at(dev).have_ft, "frame transfer used", EXT, chan ); - this->controller[dev].info.systemkeys.add_key( "IMG_ROWS", this->controller[dev].info.axes[_ROW_], "image rows", EXT, chan ); - this->controller[dev].info.systemkeys.add_key( "IMG_COLS", this->controller[dev].info.axes[_COL_]-oscols, "image cols", EXT, chan ); + this->controller.at(dev).info.systemkeys.add_key( "IMG_ROWS", this->controller.at(dev).info.axes[_ROW_], "image rows", EXT, chan ); + this->controller.at(dev).info.systemkeys.add_key( "IMG_COLS", this->controller.at(dev).info.axes[_COL_]-oscols, "image cols", EXT, chan ); - this->controller[dev].info.systemkeys.add_key( "OS_ROWS", osrows, "overscan rows", EXT, chan ); - this->controller[dev].info.systemkeys.add_key( "OS_COLS", oscols, "overscan cols", EXT, chan ); - this->controller[dev].info.systemkeys.add_key( "SKIPROWS", skiprows, "skipped rows", EXT, chan ); - this->controller[dev].info.systemkeys.add_key( "SKIPCOLS", skipcols, "skipped cols", EXT, chan ); + this->controller.at(dev).info.systemkeys.add_key( "OS_ROWS", osrows, "overscan rows", EXT, chan ); + this->controller.at(dev).info.systemkeys.add_key( "OS_COLS", oscols, "overscan cols", EXT, chan ); + this->controller.at(dev).info.systemkeys.add_key( "SKIPROWS", skiprows, "skipped rows", EXT, chan ); + this->controller.at(dev).info.systemkeys.add_key( "SKIPCOLS", skipcols, "skipped cols", EXT, chan ); int L=0, B=0; switch ( this->controller[ dev ].info.readout_type ) { @@ -351,57 +339,57 @@ namespace AstroCam { // << " ltv2=" << ltv2 << " ltv1=" << ltv1; //logwrite(function,message.str() ); - this->controller[dev].info.systemkeys.add_key( "LTV2", ltv2, "", EXT, chan ); - this->controller[dev].info.systemkeys.add_key( "LTV1", ltv1, "", EXT, chan ); - this->controller[dev].info.systemkeys.add_key( "CRPIX1A", ltv1+1, "", EXT, chan ); - this->controller[dev].info.systemkeys.add_key( "CRPIX2A", ltv2+1, "", EXT, chan ); + this->controller.at(dev).info.systemkeys.add_key( "LTV2", ltv2, "", EXT, chan ); + this->controller.at(dev).info.systemkeys.add_key( "LTV1", ltv1, "", EXT, chan ); + this->controller.at(dev).info.systemkeys.add_key( "CRPIX1A", ltv1+1, "", EXT, chan ); + this->controller.at(dev).info.systemkeys.add_key( "CRPIX2A", ltv2+1, "", EXT, chan ); - this->controller[dev].info.systemkeys.add_key( "BINSPEC", binspec, "binning in spectral direction", EXT, chan ); - this->controller[dev].info.systemkeys.add_key( "BINSPAT", binspat, "binning in spatial direction", EXT, chan ); + this->controller.at(dev).info.systemkeys.add_key( "BINSPEC", binspec, "binning in spectral direction", EXT, chan ); + this->controller.at(dev).info.systemkeys.add_key( "BINSPAT", binspat, "binning in spatial direction", EXT, chan ); - this->controller[dev].info.systemkeys.add_key( "CDELT1A", - this->controller[dev].info.dispersion*binspec, + this->controller.at(dev).info.systemkeys.add_key( "CDELT1A", + this->controller.at(dev).info.dispersion*binspec, "Dispersion in Angstrom/pixel", EXT, chan ); - this->controller[dev].info.systemkeys.add_key( "CRVAL1A", - this->controller[dev].info.minwavel, + this->controller.at(dev).info.systemkeys.add_key( "CRVAL1A", + this->controller.at(dev).info.minwavel, "Reference value in Angstrom", EXT, chan ); // These keys are for proper mosaic display. // Adjust GAPY to taste. // int GAPY=20; - int crval2 = ( this->controller[dev].info.axes[_ROW_] / binspat + GAPY ) * dev; + int crval2 = ( this->controller.at(dev).info.axes[_ROW_] / binspat + GAPY ) * dev; - this->controller[dev].info.systemkeys.add_key( "CRPIX1", 0, "", EXT, chan ); - this->controller[dev].info.systemkeys.add_key( "CRPIX2", 0, "", EXT, chan ); - this->controller[dev].info.systemkeys.add_key( "CRVAL1", 0, "", EXT, chan ); - this->controller[dev].info.systemkeys.add_key( "CRVAL2", crval2, "", EXT, chan ); + this->controller.at(dev).info.systemkeys.add_key( "CRPIX1", 0, "", EXT, chan ); + this->controller.at(dev).info.systemkeys.add_key( "CRPIX2", 0, "", EXT, chan ); + this->controller.at(dev).info.systemkeys.add_key( "CRVAL1", 0, "", EXT, chan ); + this->controller.at(dev).info.systemkeys.add_key( "CRVAL2", crval2, "", EXT, chan ); // Add ___SEC keywords to the extension header for this channel // std::stringstream sec; /* 01-24-2025 *** - sec.str(""); sec << "[" << this->controller[dev].info.region_of_interest[0] << ":" << this->controller[dev].info.region_of_interest[1] - << "," << this->controller[dev].info.region_of_interest[2] << ":" << this->controller[dev].info.region_of_interest[3] << "]"; - this->controller[dev].info.systemkeys.add_key( "CCDSEC", sec.str(), "physical format of CCD", EXT, chan ); + sec.str(""); sec << "[" << this->controller.at(dev).info.region_of_interest[0] << ":" << this->controller.at(dev).info.region_of_interest[1] + << "," << this->controller.at(dev).info.region_of_interest[2] << ":" << this->controller.at(dev).info.region_of_interest[3] << "]"; + this->controller.at(dev).info.systemkeys.add_key( "CCDSEC", sec.str(), "physical format of CCD", EXT, chan ); - sec.str(""); sec << "[" << this->controller[dev].info.region_of_interest[0] + skipcols << ":" << cols - << "," << this->controller[dev].info.region_of_interest[2] + skiprows << ":" << rows << "]"; - this->controller[dev].info.systemkeys.add_key( "DATASEC", sec.str(), "section containing the CCD data", EXT, chan ); + sec.str(""); sec << "[" << this->controller.at(dev).info.region_of_interest[0] + skipcols << ":" << cols + << "," << this->controller.at(dev).info.region_of_interest[2] + skiprows << ":" << rows << "]"; + this->controller.at(dev).info.systemkeys.add_key( "DATASEC", sec.str(), "section containing the CCD data", EXT, chan ); sec.str(""); sec << '[' << cols << ":" << cols+oscols - << "," << this->controller[dev].info.region_of_interest[2] + skiprows << ":" << rows+osrows << "]"; - this->controller[dev].info.systemkeys.add_key( "BIASSEC", sec.str(), "overscan section", EXT, chan ); + << "," << this->controller.at(dev).info.region_of_interest[2] + skiprows << ":" << rows+osrows << "]"; + this->controller.at(dev).info.systemkeys.add_key( "BIASSEC", sec.str(), "overscan section", EXT, chan ); *** */ sec.str(""); sec << "[" << oscols+1-2 // -2 is KLUDGE FACTOR << ":" << cols-2 // -2 is KLUDGE FACTOR - << "," << this->controller[dev].info.region_of_interest[2] + skiprows << ":" << rows << "]"; - this->controller[dev].info.systemkeys.add_key( "DATASEC", sec.str(), "section containing the CCD data", EXT, chan ); + << "," << this->controller.at(dev).info.region_of_interest[2] + skiprows << ":" << rows << "]"; + this->controller.at(dev).info.systemkeys.add_key( "DATASEC", sec.str(), "section containing the CCD data", EXT, chan ); - sec.str(""); sec << "[" << this->controller[dev].info.region_of_interest[0] << ":" << oscols-2 // -2 is KLUDGE FACTOR - << "," << this->controller[dev].info.region_of_interest[2] + skiprows << ":" << rows << "]"; - this->controller[dev].info.systemkeys.add_key( "BIASSEC", sec.str(), "overscan section", EXT, chan ); + sec.str(""); sec << "[" << this->controller.at(dev).info.region_of_interest[0] << ":" << oscols-2 // -2 is KLUDGE FACTOR + << "," << this->controller.at(dev).info.region_of_interest[2] + skiprows << ":" << rows << "]"; + this->controller.at(dev).info.systemkeys.add_key( "BIASSEC", sec.str(), "overscan section", EXT, chan ); return; } @@ -428,8 +416,8 @@ namespace AstroCam { // Parse the three values from the args string try { int dev = devnum_from_chan(tokens.at(0)); - this->controller[dev].info.dispersion = std::stod(tokens.at(1)); - this->controller[dev].info.minwavel = std::stod(tokens.at(2)); + this->controller.at(dev).info.dispersion = std::stod(tokens.at(1)); + this->controller.at(dev).info.minwavel = std::stod(tokens.at(2)); } catch(const std::exception &e) { logwrite(function, "ERROR parsing SPEC_INFO config: "+std::string(e.what())); @@ -473,8 +461,8 @@ namespace AstroCam { } if (spec==spat) throw std::runtime_error("PHYSSPAT/PHYSSPEC must be unique"); - this->controller[dev].spat_axis = (spat=="ROW" ? Controller::ROW : Controller::COL); - this->controller[dev].spec_axis = (spec=="ROW" ? Controller::ROW : Controller::COL); + this->controller.at(dev).spat_axis = (spat=="ROW" ? Controller::ROW : Controller::COL); + this->controller.at(dev).spec_axis = (spec=="ROW" ? Controller::ROW : Controller::COL); } catch(const std::exception &e) { logwrite(function, "ERROR parsing DETECTOR_GEOMETRY config: "+std::string(e.what())); @@ -518,55 +506,44 @@ namespace AstroCam { * */ long Interface::parse_controller_config( std::string args ) { - std::string function = "AstroCam::Interface::parse_controller_config"; - std::stringstream message; - std::vector tokens; + const std::string function("AstroCam::Interface::parse_controller_config"); + std::ostringstream message; logwrite( function, args ); - int dev, readout_type=-1; - uint32_t readout_arg=0xBAD; - std::string chan, id, firm, amp; - bool ft, readout_valid=false; + std::istringstream iss(args); - Tokenize( args, tokens, " " ); + int readout_type=-1; + uint32_t readout_arg=0xBAD; + bool readout_valid=false; - if ( tokens.size() != 6 ) { - message.str(""); message << "ERROR: bad value \"" << args << "\". expected { PCIDEV CHAN ID FT FIRMWARE READOUT }"; - logwrite( function, message.str() ); - return( ERROR ); + int dev; + std::string chan, id, firm, amp, ft; + bool have_ft; + + if (!(iss >> dev + >> chan + >> id + >> ft + >> firm + >> amp)) { + logwrite(function, "ERROR bad config. expected { PCIDEV CHAN ID FT FIRMWARE READOUT }"); + return ERROR; } - try { - dev = std::stoi( tokens.at(0) ); - chan = tokens.at(1); - id = tokens.at(2); - firm = tokens.at(4); - amp = tokens.at(5); - if ( tokens.at(3) == "yes" ) ft = true; - else - if ( tokens.at(3) == "no" ) ft = false; - else { - message.str(""); message << "unrecognized value for FT: " << tokens.at(2) << ". Expected { yes | no }"; - this->camera.log_error( function, message.str() ); - return( ERROR ); - } - } - catch (std::invalid_argument &) { - this->camera.log_error( function, "invalid number: unable to convert to integer" ); - return(ERROR); - } - catch (std::out_of_range &) { - this->camera.log_error( function, "value out of integer range" ); - return(ERROR); + if (ft=="yes") have_ft=true; + else if (ft=="no") have_ft=false; + else { + logwrite(function, "ERROR. FT expected { yes | no }"); + return ERROR; } // Check the PCIDEV number is in expected range // if ( dev < 0 || dev > 3 ) { - message.str(""); message << "ERROR: bad PCIDEV " << dev << ". Expected {0,1,2,3}"; + message << "ERROR: bad PCIDEV " << dev << ". Expected {0,1,2,3}"; this->camera.log_error( function, message.str() ); - return( ERROR ); + return ERROR; } // Check that READOUT has a match in the list of known readout amps. @@ -595,11 +572,15 @@ namespace AstroCam { // // The first four come from the config file, the rest are defaults // - this->controller[dev].devnum = dev; // device number - this->controller[dev].channel = chan; // spectrographic channel - this->controller[dev].ccd_id = id; // CCD identifier - this->controller[dev].have_ft = ft; // frame transfer supported? - this->controller[dev].firmware = firm; // firmware file + + // create a local reference indexed by dev + Controller &con = this->controller[dev]; + + con.devnum = dev; // device number + con.channel = chan; // spectrographic channel + con.ccd_id = id; // CCD identifier + con.have_ft = have_ft; // frame transfer supported? + con.firmware = firm; // firmware file /* arc::gen3::CArcDevice* pArcDev = NULL; // create a generic CArcDevice pointer @@ -609,39 +590,45 @@ namespace AstroCam { this->controller[dev].pArcDev = pArcDev; // set the pointer to this object in the public vector this->controller[dev].pCallback = pCB; // set the pointer to this object in the public vector */ - this->controller[dev].pArcDev = ( new arc::gen3::CArcPCI() ); // set the pointer to this object in the public vector - this->controller[dev].pCallback = ( new Callback() ); // set the pointer to this object in the public vector - this->controller[dev].devname = ""; // device name - this->controller[dev].connected = false; // not yet connected - this->controller[dev].is_imsize_set = false; // need to set image_size - this->controller[dev].firmwareloaded = false; // no firmware loaded - this->controller[dev].inactive = false; // assume active unless shown otherwise - - this->controller[dev].info.readout_name = amp; - this->controller[dev].info.readout_type = readout_type; - this->controller[dev].readout_arg = readout_arg; + con.pArcDev = ( new arc::gen3::CArcPCI() ); // set the pointer to this object in the public vector + con.pCallback = ( new Callback() ); // set the pointer to this object in the public vector + con.devname = ""; // device name + con.connected = false; // not yet connected + con.is_imsize_set = false; // need to set image_size + con.firmwareloaded = false; // no firmware loaded + + con.info.readout_name = amp; + con.info.readout_type = readout_type; + con.readout_arg = readout_arg; this->exposure_pending( dev, false ); - this->controller[dev].in_readout = false; - this->controller[dev].in_frametransfer = false; + con.in_readout = false; + con.in_frametransfer = false; this->state_monitor_condition.notify_all(); + // configured by config file, can never be reversed unless it is removed from the config file + con.configured = true; + + // configured and active. this state can be reversed on command or failure to connect + // active alone isn't connected, but if not connected then it's not active + con.active = true; + // Header keys specific to this controller are stored in the controller's extension // - this->controller[dev].info.systemkeys.add_key( "CCD_ID", id, "CCD identifier parse", EXT, chan ); - this->controller[dev].info.systemkeys.add_key( "FT", ft, "frame transfer used", EXT, chan ); - this->controller[dev].info.systemkeys.add_key( "AMP_ID", amp, "CCD readout amplifier ID", EXT, chan ); - this->controller[dev].info.systemkeys.add_key( "SPEC_ID", chan, "spectrograph channel", EXT, chan ); - this->controller[dev].info.systemkeys.add_key( "DEV_ID", dev, "detector controller PCI device ID", EXT, chan ); + con.info.systemkeys.add_key( "CCD_ID", id, "CCD identifier parse", EXT, chan ); + con.info.systemkeys.add_key( "FT", ft, "frame transfer used", EXT, chan ); + con.info.systemkeys.add_key( "AMP_ID", amp, "CCD readout amplifier ID", EXT, chan ); + con.info.systemkeys.add_key( "SPEC_ID", chan, "spectrograph channel", EXT, chan ); + con.info.systemkeys.add_key( "DEV_ID", dev, "detector controller PCI device ID", EXT, chan ); // FITS_file* pFits = new FITS_file(); // create a pointer to a FITS_file class object -// this->controller[dev].pFits = pFits; // set the pointer to this object in the public vector +// this->controller.at(dev).pFits = pFits; // set the pointer to this object in the public vector #ifdef LOGLEVEL_DEBUG message.str(""); message << "[DEBUG] pointers for dev " << dev << ": " - << " pArcDev=" << std::hex << std::uppercase << this->controller[dev].pArcDev - << " pCB=" << std::hex << std::uppercase << this->controller[dev].pCallback; + << " pArcDev=" << std::hex << std::uppercase << this->controller.at(dev).pArcDev + << " pCB=" << std::hex << std::uppercase << this->controller.at(dev).pCallback; logwrite(function, message.str()); #endif return( NO_ERROR ); @@ -649,6 +636,59 @@ namespace AstroCam { /***** AstroCam::Interface::parse_controller_config *************************/ + /***** AstroCam::Interface::parse_activate_commands *************************/ + /** + * @brief parses the ACTIVATE_COMMANDS keywords from config file + * @details This gets the list of native commands needed to send when + * (de)activating a controller channel. + * @param[in] args expected format is "CHAN CMD [, CMD, CMD, ...]" + * + */ + long Interface::parse_activate_commands(std::string args) { + const std::string function("AstroCam::Interface::parse_activate_commands"); + logwrite(function, args); + + std::istringstream iss(args); + + // get the channel + std::string chan; + if (!std::getline(iss, chan, ' ')) { + logwrite(function, "ERROR bad config. expected , , ..."); + return ERROR; + } + + // get device number for that channel + int dev; + try { + dev = devnum_from_chan(chan); + } + catch(const std::exception &e) { + logwrite(function, "ERROR: "+std::string(e.what())); + return ERROR; + } + + // get the list of commands + std::string cmdlist; + if (!std::getline(iss, cmdlist)) { + logwrite(function, "ERROR bad config. expected , , ..."); + return ERROR; + } + + // get a pointer to this configured controller + auto pcontroller = this->get_controller(dev); + if (!pcontroller) { + logwrite(function, "ERROR bad controller for channel "+chan); + return ERROR; + } + + // tokenize inserts each command into a vector element + Tokenize(cmdlist, pcontroller->activate_commands, ","); + + return NO_ERROR; + } + /***** AstroCam::Interface::parse_activestate_commands **********************/ + + /***** AstroCam::Interface::devnum_from_chan ********************************/ /** * @brief return the devnum associated with a channel name @@ -660,7 +700,7 @@ namespace AstroCam { int Interface::devnum_from_chan( const std::string &chan ) { int devnum=-1; for ( const auto &con : this->controller ) { - if ( con.second.inactive ) continue; // skip controllers flagged as inactive + if ( !con.second.configured ) continue; // skip controllers not configured if ( con.second.channel == chan ) { // check to see if it matches a configured channel. devnum = con.second.devnum; break; @@ -745,11 +785,14 @@ namespace AstroCam { // for ( const auto &con : this->controller ) { #ifdef LOGLEVEL_DEBUG - message.str(""); message << "[DEBUG] con.first=" << con.first << " con.second.channel=" << con.second.channel - << " .devnum=" << con.second.devnum << " .inactive=" << (con.second.inactive?"T":"F"); + message.str(""); message << "[DEBUG] con.first=" << con.first + << " con.second.channel=" << con.second.channel + << " .devnum=" << con.second.devnum + << " .configured=" << (con.second.configured?"T":"F") + << " .active=" << (con.second.active?"T":"F"); logwrite( function, message.str() ); #endif - if ( con.second.inactive ) continue; // skip controllers flagged as inactive + if (!con.second.configured) continue; // skip controllers not configured if ( con.second.channel == tryme ) { // check to see if it matches a configured channel. dev = con.second.devnum; chan = tryme; @@ -784,7 +827,7 @@ namespace AstroCam { std::string function = "AstroCam::Interface::do_abort"; std::stringstream message; // int this_expbuf = this->get_expbuf(); - for ( const auto &dev : this->devnums ) { + for ( const auto &dev : this->active_devnums ) { this->exposure_pending( dev, false ); for ( int buf=0; buf < NUM_EXPBUF; ++buf ) this->write_pending( buf, dev, false ); } @@ -838,7 +881,7 @@ namespace AstroCam { std::vector pending = this->exposure_pending_list(); message.str(""); message << "ERROR: cannot change binning while exposure is pending for chan"; message << ( pending.size() > 1 ? "s " : " " ); - for ( const auto &dev : pending ) message << this->controller[dev].channel << " "; + for ( const auto &dev : pending ) message << this->controller.at(dev).channel << " "; this->camera.async.enqueue_and_log( "CAMERAD", function, message.str() ); retstring="exposure_in_progress"; return(ERROR); @@ -877,8 +920,10 @@ namespace AstroCam { // This uses the existing image size parameters and the new binning. // The requested overscans are sent here, which can be modified by binning. // - for ( const auto &dev : this->devnums ) { - Controller* pcontroller = &this->controller[dev]; + for ( const auto &dev : this->active_devnums ) { + auto pcontroller = this->get_active_controller(dev); + if (!pcontroller) continue; + // determine which physical axis corresponds to the requested logical axis int physical_axis; if (logical_axis == "spec") { @@ -925,10 +970,10 @@ namespace AstroCam { // return binning for the requested logical axis if (this->numdev>0) { - int dev = this->devnums[0]; - int physical_axis = (logical_axis=="spec") ? this->controller[dev].spec_physical_axis() : - this->controller[dev].spat_physical_axis(); - message.str(""); message << this->controller[dev].info.binning[physical_axis]; + int dev = this->active_devnums[0]; + int physical_axis = (logical_axis=="spec") ? this->controller.at(dev).spec_physical_axis() : + this->controller.at(dev).spat_physical_axis(); + message.str(""); message << this->controller.at(dev).info.binning[physical_axis]; if ( error == NO_ERROR ) retstring = message.str(); } } @@ -957,7 +1002,7 @@ namespace AstroCam { * connect to all detected devices. * * If devices_in is specified (and not empty) then it must contain a space-delimited - * list of device numbers to open. A public vector devnums will hold these device + * list of device numbers to open. A public vector connected_devnums will hold these device * numbers. This vector will be updated here to represent only the devices that * are actually connected. * @@ -966,6 +1011,8 @@ namespace AstroCam { * user wishes to connect to only the device(s) available then the user must * call with the specific device(s). In other words, it's all (requested) or nothing. * + * This will override the controller active flag; all opened devices become active. + * */ long Interface::do_connect_controller( const std::string devices_in, std::string &retstring ) { std::string function = "AstroCam::Interface::do_connect_controller"; @@ -1027,25 +1074,25 @@ namespace AstroCam { // Look at the requested device(s) to open, which are in the // space-delimited string devices_in. The devices to open - // are stored in a public vector "devnums". + // are stored in a public vector "connected_devnums". // // If no string is given then use vector of configured devices. The configured_devnums // vector contains a list of devices defined in the config file with the // keyword CONTROLLER=( ). // - if ( devices_in.empty() ) this->devnums = this->configured_devnums; + if ( devices_in.empty() ) this->connected_devnums = this->configured_devnums; else { - // Otherwise, tokenize the device list string and build devnums from the tokens + // Otherwise, tokenize the device list string and build connected_devnums from the tokens // - this->devnums.clear(); // empty devnums vector since it's being built here + this->connected_devnums.clear(); // empty connected_devnums vector since it's being built here std::vector tokens; Tokenize(devices_in, tokens, " "); for ( const auto &n : tokens ) { // For each token in the devices_in string, try { int dev = std::stoi( n ); // convert to int - if ( std::find( this->devnums.begin(), this->devnums.end(), dev ) == this->devnums.end() ) { // If it's not already in the vector, - this->devnums.push_back( dev ); // then push into devnums vector. + if ( std::find( this->connected_devnums.begin(), this->connected_devnums.end(), dev ) == this->connected_devnums.end() ) { // If it's not already in the vector, + this->connected_devnums.push_back( dev ); // then push into connected_devnums vector. } } catch (const std::exception &e) { @@ -1062,130 +1109,109 @@ namespace AstroCam { } } - // For each requested dev in devnums, if there is a matching controller in the config file, + // For each requested dev in connected_devnums, if there is a matching controller in the config file, // then get the devname and store it in the controller map. // - for ( const auto &dev : this->devnums ) { + for ( const auto &dev : this->connected_devnums ) { if ( this->controller.find( dev ) != this->controller.end() ) { - this->controller[ dev ].devname = devNames[dev]; + this->controller.at( dev ).devname = devNames[dev]; } } - // The size of devnums at this point is the number of devices that will + // The size of connected_devnums at this point is the number of devices that will // be _requested_ to be opened. This should match the number of opened // devices at the end of this function. // - size_t requested_device_count = this->devnums.size(); + size_t requested_device_count = this->connected_devnums.size(); - // Open only the devices specified by the devnums vector + // Open only the devices specified by the connected_devnums vector // - for ( size_t i = 0; i < this->devnums.size(); ) { - int dev = this->devnums[i]; + for ( size_t i = 0; i < this->connected_devnums.size(); ) { + int dev = this->connected_devnums[i]; auto dev_found = this->controller.find( dev ); if ( dev_found == this->controller.end() ) { message.str(""); message << "ERROR: devnum " << dev << " not found in controller definition. check config file"; logwrite( function, message.str() ); - this->controller[dev].inactive=true; // flag the non-connected controller as inactive + this->controller.at(dev).configured=false; // flag as no longer configured + this->controller.at(dev).active=false; // flag as no longer active this->do_disconnect_controller(dev); retstring="unknown_device"; error = ERROR; break; } - else this->controller[dev].inactive=false; + else this->controller.at(dev).configured=true; try { // Open the PCI device if not already open // (otherwise just reset and test connection) // - if ( ! this->controller[dev].connected ) { - message.str(""); message << "opening " << this->controller[dev].devname; + if ( ! this->controller.at(dev).connected ) { + message.str(""); message << "opening " << this->controller.at(dev).devname; logwrite(function, message.str()); - this->controller[dev].pArcDev->open(dev); + this->controller.at(dev).pArcDev->open(dev); } else { - message.str(""); message << this->controller[dev].devname << " already open"; + message.str(""); message << this->controller.at(dev).devname << " already open"; logwrite(function, message.str()); } // Reset the PCI device // - message.str(""); message << "resetting " << this->controller[dev].devname; + message.str(""); message << "resetting " << this->controller.at(dev).devname; logwrite(function, message.str()); try { - this->controller[dev].pArcDev->reset(); + this->controller.at(dev).pArcDev->reset(); } catch (const std::exception &e) { - message.str(""); message << "ERROR resetting " << this->controller[dev].devname << ": " << e.what(); + message.str(""); message << "ERROR resetting " << this->controller.at(dev).devname << ": " << e.what(); logwrite(function, message.str()); error = ERROR; } // Is Controller Connected? (tested with a TDL command) // - this->controller[dev].connected = this->controller[dev].pArcDev->isControllerConnected(); - message.str(""); message << this->controller[dev].devname << (this->controller[dev].connected ? "" : " not" ) << " connected to ARC controller" - << (this->controller[dev].connected ? " for channel " : "" ) - << (this->controller[dev].connected ? this->controller[dev].channel : "" ); + this->controller.at(dev).connected = this->controller.at(dev).pArcDev->isControllerConnected(); + message.str(""); message << this->controller.at(dev).devname << (this->controller.at(dev).connected ? "" : " not" ) << " connected to ARC controller" + << (this->controller.at(dev).connected ? " for channel " : "" ) + << (this->controller.at(dev).connected ? this->controller.at(dev).channel : "" ); logwrite(function, message.str()); - // If not connected then this should remove it from the devnums list - // - if ( !this->controller[dev].connected ) this->do_disconnect_controller(dev); - -/****** YOU CAN'T DO THIS - // Now that controller is open, update it with the current image size - // that has been stored in the class. Create an arg string in the same - // format as that found in the config file. - // - std::stringstream args; - std::string retstring; - args << dev << " " - << this->controller[dev].detrows << " " - << this->controller[dev].detcols << " " - << this->controller[dev].osrows << " " - << this->controller[dev].oscols << " " - << this->camera_info.binning[_ROW_] << " " - << this->camera_info.binning[_COL_]; - - // If image_size fails then close only this controller, - // which allows operating without this one if needed. - // - if ( this->image_size( args.str(), retstring ) != NO_ERROR ) { // set IMAGE_SIZE here after opening - message.str(""); message << "ERROR setting image size for " << this->controller[dev].devname << ": " << retstring; - this->camera.async.enqueue_and_log( function, message.str() ); - this->controller[dev].inactive=true; // flag the non-connected controller as inactive + // If connected then it is active + if ( this->controller.at(dev).connected ) { + this->controller.at(dev).active=true; + } + // otherwise disconnect, which removes it from the connected_devnums list and clears active + else { this->do_disconnect_controller(dev); - error = ERROR; } - ******/ } catch ( const std::exception &e ) { // arc::gen3::CArcPCI::open and reset may throw exceptions - message.str(""); message << "ERROR opening " << this->controller[dev].devname - << " channel " << this->controller[dev].channel << ": " << e.what(); + message.str(""); message << "ERROR opening " << this->controller.at(dev).devname + << " channel " << this->controller.at(dev).channel << ": " << e.what(); this->camera.async.enqueue_and_log( function, message.str() ); - this->controller[dev].inactive=true; // flag the non-connected controller as inactive this->do_disconnect_controller(dev); retstring="exception"; error = ERROR; } - // A call to do_disconnect_controller() can modify the size of devnums, + // A call to do_disconnect_controller() can modify the size of connected_devnums, // so only if the loop index i is still valid with respect to the current - // size of devnums should it be incremented. + // size of connected_devnums should it be incremented. // - if ( i < devnums.size() ) ++i; + if ( i < connected_devnums.size() ) ++i; } // Log the list of connected devices // message.str(""); message << "connected devices { "; - for (const auto &devcheck : this->devnums) { message << devcheck << " "; } message << "}"; + for (const auto &devcheck : this->connected_devnums) { message << devcheck << " "; } message << "}"; logwrite(function, message.str()); - // check the size of the devnums now, against the size requested + // if the size of the connected_devnums now is not the size requested + // then close them all // - if ( this->devnums.size() != requested_device_count ) { - message.str(""); message << "ERROR: " << this->devnums.size() <<" connected device(s) but " + if ( this->connected_devnums.size() != requested_device_count ) { + message.str(""); message << "ERROR: " << this->connected_devnums.size() <<" connected device(s) but " << requested_device_count << " requested"; logwrite( function, message.str() ); @@ -1201,6 +1227,10 @@ namespace AstroCam { error = ERROR; } + // all connected devnums are active devnums at this stage + // + this->active_devnums = this->connected_devnums; + // Start a thread to monitor the state of things (if not already running) // if ( !this->state_monitor_thread_running.load() ) { @@ -1223,6 +1253,8 @@ namespace AstroCam { error = ERROR; } + this->publish_snapshot(); + return( error ); } /***** AstroCam::Interface::do_connect_controller ***************************/ @@ -1233,6 +1265,8 @@ namespace AstroCam { * @brief closes the connection to the specified PCI/e device * @return ERROR or NO_ERROR * + * This will override the controller active flag; all closed devices are inactive. + * * This function is overloaded * */ @@ -1245,7 +1279,7 @@ namespace AstroCam { return ERROR; } - // close indicated PCI device and remove dev from devnums + // close indicated PCI device and remove dev from connected_devnums // try { if ( this->controller.at(dev).pArcDev == nullptr ) { @@ -1257,12 +1291,11 @@ namespace AstroCam { logwrite(function, message.str()); this->controller.at(dev).pArcDev->close(); // throws nothing, no error handling this->controller.at(dev).connected=false; - // remove dev from devnums - // - auto it = std::find( this->devnums.begin(), this->devnums.end(), dev ); - if ( it != this->devnums.end() ) { - this->devnums.erase(it); - } + this->controller.at(dev).active=false; + + // remove dev from connected and active devnums + remove_dev(dev, this->connected_devnums); + remove_dev(dev, this->active_devnums); } catch ( std::out_of_range &e ) { message.str(""); message << "dev " << dev << " not found: " << e.what(); @@ -1282,6 +1315,8 @@ namespace AstroCam { * * no error handling. can only fail if the camera is busy. * + * This will override the controller active flag; all closed devices are inactive. + * * This function is overloaded * */ @@ -1295,17 +1330,19 @@ namespace AstroCam { return( ERROR ); } - // close all of the PCI devices + // close all of the PCI devices regardless of active status // for ( auto &con : this->controller ) { message.str(""); message << "closing " << con.second.devname; logwrite(function, message.str()); if ( con.second.pArcDev != nullptr ) con.second.pArcDev->close(); // throws nothing con.second.connected=false; + con.second.active=false; } - this->devnums.clear(); // no devices open - this->numdev = 0; // no devices open + this->connected_devnums.clear(); // no devices open + this->active_devnums.clear(); // no devices open + this->numdev = 0; // no devices open return error; } /***** AstroCam::Interface::do_disconnect_controller ************************/ @@ -1322,21 +1359,21 @@ namespace AstroCam { std::string function = "AstroCam::Interface::is_connected"; std::stringstream message; - size_t ndev = this->devnums.size(); /// number of connected devices - size_t nopen=0; /// number of open devices (should be equal to ndev if all are open) + size_t ndev = this->connected_devnums.size(); /// number of connected devices + size_t nopen=0; /// number of open devices (should be equal to ndev if all are open) // look through all connected devices // - for ( const auto &dev : this->devnums ) { + for ( const auto &dev : this->connected_devnums ) { if ( this->controller.find( dev ) != this->controller.end() ) - if ( this->controller[dev].connected ) nopen++; + if ( this->controller.at(dev).connected ) nopen++; #ifdef LOGLEVEL_DEBUG - message.str(""); message << "[DEBUG] " << this->controller[dev].devname << " is " << ( this->controller[dev].connected ? "connected" : "disconnected" ); + message.str(""); message << "[DEBUG] " << this->controller.at(dev).devname << " is " << ( this->controller.at(dev).connected ? "connected" : "disconnected" ); logwrite( function, message.str() ); #endif } - // If all devices in (non-empty) devnums are connected then return true, + // If all devices in (non-empty) connected_devnums are connected then return true, // otherwise return false. // if ( ndev !=0 && ndev == nopen ) { @@ -1368,6 +1405,12 @@ namespace AstroCam { // which will get built up from parse_controller_config() below. // this->configured_devnums.clear(); + this->active_devnums.clear(); + this->connected_devnums.clear(); + + // initialize the controller map + // + this->controller.clear(); // loop through the entries in the configuration file, stored in config class // @@ -1390,6 +1433,14 @@ namespace AstroCam { } else + // ACTIVATE_COMMANDS + if (this->config.param[entry]=="ACTIVATE_COMMANDS") { + if (this->parse_activate_commands(this->config.arg[entry]) != ERROR) { + numapplied++; + } + } + else + if ( this->config.param[entry].find( "IMDIR" ) == 0 ) { this->camera.imdir( config.arg[entry] ); numapplied++; @@ -1542,10 +1593,10 @@ namespace AstroCam { return this->do_native( dev, cmdstr, dontcare ); } else { - // didn't find a dev in args so build vector of all open controllers - std::vector selectdev; - for ( const auto &dev : this->devnums ) { - if ( this->controller[dev].connected ) selectdev.push_back( dev ); + // didn't find a dev in args so build vector of all active controllers + std::vector selectdev; + for ( const auto &dev : this->active_devnums ) { + if ( this->controller.at(dev).connected ) selectdev.push_back( dev ); } // this will send the native command to all controllers in that vector return this->do_native( selectdev, args, retstring ); @@ -1562,11 +1613,12 @@ namespace AstroCam { * @return NO_ERROR on success, ERROR on error * */ - long Interface::do_native(std::vector selectdev, std::string cmdstr) { - // Use the erase-remove idiom to remove disconnected devices from selectdev + long Interface::do_native(std::vector selectdev, std::string cmdstr) { + // Use the erase-remove idiom to remove disconnected/inactive devices from selectdev // selectdev.erase( std::remove_if( selectdev.begin(), selectdev.end(), - [this](uint32_t dev) { return !this->controller[dev].connected; } ), + [this](int dev) { return !this->controller.at(dev).connected || + !this->controller.at(dev).active; } ), selectdev.end() ); std::string retstring; @@ -1585,8 +1637,8 @@ namespace AstroCam { * */ long Interface::do_native( int dev, std::string cmdstr, std::string &retstring ) { - std::vector selectdev; - if ( this->controller[dev].connected ) selectdev.push_back( dev ); + std::vector selectdev; + if ( this->controller.at(dev).active ) selectdev.push_back( dev ); return this->do_native( selectdev, cmdstr, retstring ); } /***** AstroCam::Interface::do_native ***************************************/ @@ -1601,7 +1653,7 @@ namespace AstroCam { * @return NO_ERROR | ERROR | HELP * */ - long Interface::do_native( std::vector selectdev, std::string cmdstr, std::string &retstring ) { + long Interface::do_native( std::vector selectdev, std::string cmdstr, std::string &retstring ) { std::string function = "AstroCam::Interface::do_native"; std::stringstream message; std::vector tokens; @@ -1641,6 +1693,15 @@ namespace AstroCam { return( ERROR ); } + // purge selectdev of any inactive devnums to prevent sending this command + // to an inactive controller + // + for (const auto &dev : selectdev) { + if (!this->controller.at(dev).active) { + remove_dev(dev, selectdev); + } + } + std::vector cmd; // this vector will contain the cmd and any arguments uint32_t c0, c1, c2; @@ -1711,7 +1772,7 @@ namespace AstroCam { { // start local scope for this stuff std::vector threads; // local scope vector stores all of the threads created here for ( const auto &dev : selectdev ) { // spawn a thread for each device in selectdev -// std::thread thr( std::ref(AstroCam::Interface::dothread_native), std::ref(this->controller[dev]), cmd ); +// std::thread thr( std::ref(AstroCam::Interface::dothread_native), std::ref(this->controller.at(dev)), cmd ); std::thread thr( &AstroCam::Interface::dothread_native, std::ref(*this), dev, cmd ); threads.push_back(std::move(thr)); // push the thread into a vector } @@ -1734,7 +1795,7 @@ namespace AstroCam { // std::uint32_t check_retval; try { - check_retval = this->controller[selectdev.at(0)].retval; // save the first one in the controller vector + check_retval = this->controller.at(selectdev.at(0)).retval; // save the first one in the controller vector } catch(std::out_of_range &) { logwrite(function, "ERROR: no device found. Is the controller connected?"); @@ -1743,7 +1804,7 @@ namespace AstroCam { } bool allsame = true; - for ( const auto &dev : selectdev ) { if (this->controller[dev].retval != check_retval) { allsame = false; } } + for ( const auto &dev : selectdev ) { if (this->controller.at(dev).retval != check_retval) { allsame = false; } } // If all the return values are equal then return only one value... // @@ -1756,8 +1817,8 @@ namespace AstroCam { else { std::stringstream rs; for ( const auto &dev : selectdev ) { - this->retval_to_string( this->controller[dev].retval, retstring ); // this sets retstring = to_string( retval ) - rs << std::dec << this->controller[dev].devnum << ":" << retstring << " "; // build up a stringstream of each controller's reply + this->retval_to_string( this->controller.at(dev).retval, retstring ); // this sets retstring = to_string( retval ) + rs << std::dec << this->controller.at(dev).devnum << ":" << retstring << " "; // build up a stringstream of each controller's reply } retstring = rs.str(); // re-use retstring to contain all of the replies } @@ -1769,15 +1830,15 @@ namespace AstroCam { /*** for ( const auto &dev : selectdev ) { // any command that doesn't return DON sets error flag - if ( this->controller[dev].retval != 0x00444F4E ) { + if ( this->controller.at(dev).retval != 0x00444F4E ) { error = ERROR; } // std::string retvalstring; -// this->retval_to_string( this->controller[dev].retval, retvalstring ); -// message.str(""); message << this->controller[dev].devname << " \"" << cmdstr << "\"" +// this->retval_to_string( this->controller.at(dev).retval, retvalstring ); +// message.str(""); message << this->controller.at(dev).devname << " \"" << cmdstr << "\"" // << " returns " << retvalstring -// << " (0x" << std::hex << std::uppercase << this->controller[dev].retval << ")"; +// << " (0x" << std::hex << std::uppercase << this->controller.at(dev).retval << ")"; // logwrite(function, message.str()); } ***/ @@ -2242,12 +2303,12 @@ namespace AstroCam { std::stringstream message; uint32_t command; - std::lock_guard lock(this->controller[dev].pcimtx); + std::lock_guard lock(this->controller.at(dev).pcimtx); ++this->pci_cmd_num; message << "sending command (" << std::dec << this->pci_cmd_num << ") to chan " - << this->controller[dev].channel << " dev " << dev << ":" + << this->controller.at(dev).channel << " dev " << dev << ":" << std::setfill('0') << std::setw(2) << std::hex << std::uppercase; for (const auto &arg : cmd) message << " 0x" << arg; logwrite(function, message.str()); @@ -2259,46 +2320,46 @@ namespace AstroCam { // ARC_API now uses an initialized_list object for the TIM_ID, command, and arguments. // The list object must be instantiated with a fixed size at compile time. // - if (cmd.size() == 1) this->controller[dev].retval = this->controller[dev].pArcDev->command( { TIM_ID, cmd.at(0) } ); + if (cmd.size() == 1) this->controller.at(dev).retval = this->controller.at(dev).pArcDev->command( { TIM_ID, cmd.at(0) } ); else - if (cmd.size() == 2) this->controller[dev].retval = this->controller[dev].pArcDev->command( { TIM_ID, cmd.at(0), cmd.at(1) } ); + if (cmd.size() == 2) this->controller.at(dev).retval = this->controller.at(dev).pArcDev->command( { TIM_ID, cmd.at(0), cmd.at(1) } ); else - if (cmd.size() == 3) this->controller[dev].retval = this->controller[dev].pArcDev->command( { TIM_ID, cmd.at(0), cmd.at(1), cmd.at(2) } ); + if (cmd.size() == 3) this->controller.at(dev).retval = this->controller.at(dev).pArcDev->command( { TIM_ID, cmd.at(0), cmd.at(1), cmd.at(2) } ); else - if (cmd.size() == 4) this->controller[dev].retval = this->controller[dev].pArcDev->command( { TIM_ID, cmd.at(0), cmd.at(1), cmd.at(2), cmd.at(3) } ); + if (cmd.size() == 4) this->controller.at(dev).retval = this->controller.at(dev).pArcDev->command( { TIM_ID, cmd.at(0), cmd.at(1), cmd.at(2), cmd.at(3) } ); else - if (cmd.size() == 5) this->controller[dev].retval = this->controller[dev].pArcDev->command( { TIM_ID, cmd.at(0), cmd.at(1), cmd.at(2), cmd.at(3), cmd.at(4) } ); + if (cmd.size() == 5) this->controller.at(dev).retval = this->controller.at(dev).pArcDev->command( { TIM_ID, cmd.at(0), cmd.at(1), cmd.at(2), cmd.at(3), cmd.at(4) } ); else { message.str(""); message << "ERROR: invalid number of command arguments: " << cmd.size() << " (expecting 1,2,3,4,5)"; logwrite(function, message.str()); - this->controller[dev].retval = 0x455252; + this->controller.at(dev).retval = 0x455252; } } catch(const std::runtime_error &e) { message.str(""); message << "ERROR sending (" << this->pci_cmd_num << ") 0x" << std::setfill('0') << std::setw(2) << std::hex << std::uppercase - << command << " to " << this->controller[dev].devname << ": " << e.what(); + << command << " to " << this->controller.at(dev).devname << ": " << e.what(); logwrite(function, message.str()); - this->controller[dev].retval = 0x455252; + this->controller.at(dev).retval = 0x455252; return; } catch(std::out_of_range &) { // impossible logwrite(function, "ERROR: indexing command argument ("+std::to_string(this->pci_cmd_num)+")"); - this->controller[dev].retval = 0x455252; + this->controller.at(dev).retval = 0x455252; return; } catch(...) { message.str(""); message << "ERROR sending (" << std::dec << this->pci_cmd_num << ") 0x" << std::setfill('0') << std::setw(2) << std::hex << std::uppercase - << command << " to " << this->controller[dev].devname << ": unknown"; + << command << " to " << this->controller.at(dev).devname << ": unknown"; logwrite(function, message.str()); - this->controller[dev].retval = 0x455252; + this->controller.at(dev).retval = 0x455252; return; } std::string retvalstring; - this->retval_to_string( this->controller[dev].retval, retvalstring ); - message.str(""); message << this->controller[dev].devname << std::dec << " (" << this->pci_cmd_num << ")" + this->retval_to_string( this->controller.at(dev).retval, retvalstring ); + message.str(""); message << this->controller.at(dev).devname << std::dec << " (" << this->pci_cmd_num << ")" << " returns " << retvalstring; logwrite( function, message.str() ); @@ -2396,10 +2457,10 @@ namespace AstroCam { logwrite(function, message.str()); return(ERROR); } - for (const auto &dev : this->devnums) { // spawn a thread for each device in devnums + for (const auto &dev : this->connected_devnums) { // spawn a thread for each device in connected_devnums try { - int rows = this->controller[dev].rows; - int cols = this->controller[dev].cols; + int rows = this->controller.at(dev).rows; + int cols = this->controller.at(dev).cols; this->nfpseq = parse_val(tokens.at(1)); // requested nframes is nframes/sequence this->nframes = this->nfpseq * this->nsequences; // number of frames is (frames/sequence) x (sequences) @@ -2460,7 +2521,7 @@ namespace AstroCam { } catch( std::out_of_range & ) { message.str(""); message << "ERROR: unable to find device " << dev << " in list: { "; - for ( const auto &check : this->devnums ) message << check << " "; + for ( const auto &check : this->connected_devnums ) message << check << " "; message << "}"; logwrite( function, message.str() ); return( ERROR ); @@ -2491,6 +2552,8 @@ namespace AstroCam { // Log this message once only // if ( interface.exposure_pending() ) { + interface.can_expose.store(false); + interface.publish_snapshot(); interface.camera.async.enqueue_and_log( function, "NOTICE:exposure pending" ); interface.camera.async.enqueue( "CAMERAD:READY:false" ); } @@ -2522,6 +2585,8 @@ namespace AstroCam { interface.do_expose(interface.nexp); } else { + interface.can_expose.store(true); + interface.publish_snapshot(); interface.camera.async.enqueue_and_log( function, "NOTICE:ready for next exposure" ); interface.camera.async.enqueue( "CAMERAD:READY:true" ); } @@ -2544,16 +2609,16 @@ namespace AstroCam { std::string _start_time; long error; - if (this->devnums.empty()) { - logwrite(function, "ERROR no connected controllers"); + if (this->active_devnums.empty()) { + logwrite(function, "ERROR no active controllers"); return ERROR; } - for (const auto &dev : this->devnums) { + for (const auto &dev : this->active_devnums) { std::string naughtylist; - if (!this->controller[dev].is_imsize_set) { + if (!this->controller.at(dev).is_imsize_set) { if (!naughtylist.empty()) naughtylist += ' '; - naughtylist += this->controller[dev].channel; + naughtylist += this->controller.at(dev).channel; } if (!naughtylist.empty()) { logwrite(function, "ERROR image_size not set for channel(s): "+naughtylist); @@ -2571,7 +2636,7 @@ namespace AstroCam { std::vector pending = this->exposure_pending_list(); message.str(""); message << "ERROR: cannot start new exposure while exposure is pending for chan"; message << ( pending.size() > 1 ? "s " : " " ); - for ( const auto &dev : pending ) message << this->controller[dev].channel << " "; + for ( const auto &dev : pending ) message << this->controller.at(dev).channel << " "; this->camera.async.enqueue_and_log( "CAMERAD", function, message.str() ); return(ERROR); } @@ -2616,7 +2681,7 @@ namespace AstroCam { // check readout type // - for ( const auto &dev : this->devnums ) { + for ( const auto &dev : this->active_devnums ) { if ( this->controller[ dev ].info.readout_name.empty() ) { message.str(""); message << "ERROR: readout undefined"; this->camera.async.enqueue_and_log( "CAMERAD", function, message.str() ); @@ -2654,7 +2719,7 @@ namespace AstroCam { // Each thread gets the exposure buffer number for the current exposure, // and a reference to "this" Interface object. // - for ( const auto &dev : this->devnums ) this->write_pending( this_expbuf, dev, true ); + for ( const auto &dev : this->active_devnums ) this->write_pending( this_expbuf, dev, true ); std::thread( std::ref(AstroCam::Interface::FITS_handler), this_expbuf, std::ref(*this) ).detach(); { @@ -2663,23 +2728,23 @@ namespace AstroCam { // If it IS in frame transfer then only clear the CCD if the cameras are idle. // std::string retstr; - for ( const auto &dev : this->devnums ) { + for ( const auto &dev : this->active_devnums ) { if ( this->is_camera_idle( dev ) ) { error = this->do_native( dev, "CLR", retstr ); // send the clear command here to this dev if ( error != NO_ERROR ) { - message.str(""); message << "ERROR clearing chan " << this->controller[dev].channel << " CCD: " << retstr; + message.str(""); message << "ERROR clearing chan " << this->controller.at(dev).channel << " CCD: " << retstr; logwrite( function, message.str() ); return( error ); } - message.str(""); message << "cleared chan " << this->controller[dev].channel << " CCD"; + message.str(""); message << "cleared chan " << this->controller.at(dev).channel << " CCD"; logwrite( function, message.str() ); } #ifdef LOGLEVEL_DEBUG else { - message.str(""); message << "[DEBUG] chan " << this->controller[dev].channel << " CCD was *not* cleared:" + message.str(""); message << "[DEBUG] chan " << this->controller.at(dev).channel << " CCD was *not* cleared:" << " exposure_pending=" << this->exposure_pending() - << " in_readout=" << this->controller[dev].in_readout - << " in_frametransfer=" << this->controller[dev].in_frametransfer; + << " in_readout=" << this->controller.at(dev).in_readout + << " in_frametransfer=" << this->controller.at(dev).in_frametransfer; logwrite( function, message.str() ); } #endif @@ -2730,22 +2795,22 @@ namespace AstroCam { // and spawn a thread to monitor it, which will provide a notification // when ready for the next exposure. // - for ( const auto &dev : this->devnums ) this->exposure_pending( dev, true ); + for ( const auto &dev : this->active_devnums ) this->exposure_pending( dev, true ); this->state_monitor_condition.notify_all(); std::thread( std::ref(AstroCam::Interface::dothread_monitor_exposure_pending), std::ref(*this) ).detach(); - // prepare the camera info class object for each controller + // prepare the camera info class object for each active controller // - for (const auto &dev : this->devnums) { // spawn a thread for each device in devnums + for (const auto &dev : this->active_devnums) { // spawn a thread for each device in active_devnums try { // Initialize a frame counter for each device. // - this->controller[dev].init_framecount(); + this->controller.at(dev).init_framecount(); // Allocate workspace memory for deinterlacing (each dev has its own workbuf) // - if ( ( error = this->controller[dev].alloc_workbuf( ) ) != NO_ERROR ) { + if ( ( error = this->controller.at(dev).alloc_workbuf( ) ) != NO_ERROR ) { this->camera.async.enqueue_and_log( "CAMERAD", function, "ERROR: allocating memory for deinterlacing" ); return( error ); } @@ -2754,16 +2819,16 @@ namespace AstroCam { // then set the filename for this specific dev // Assemble the FITS filename. // If naming type = "time" then this will use this->fitstime so that must be set first. - // If there are multiple devices in the devnums then force the fitsname to include the dev number + // If there are multiple devices in the active_devnums then force the fitsname to include the dev number // in order to make it unique for each device. // - if ( this->devnums.size() > 1 ) { + if ( this->active_devnums.size() > 1 ) { devstr = std::to_string( dev ); // passing a non-empty devstr will put that in the fitsname } else { devstr = ""; } - if ( ( error = this->camera.get_fitsname( devstr, this->controller[dev].info.fits_name ) ) != NO_ERROR ) { + if ( ( error = this->camera.get_fitsname( devstr, this->controller.at(dev).info.fits_name ) ) != NO_ERROR ) { this->camera.async.enqueue_and_log( "CAMERAD", function, "ERROR: assembling fitsname" ); return( error ); } @@ -2772,15 +2837,15 @@ namespace AstroCam { #ifdef LOGLEVEL_DEBUG message.str(""); message << "[DEBUG] pointers for dev " << dev << ": " - << " pArcDev=" << std::hex << this->controller[dev].pArcDev - << " pCB=" << std::hex << this->controller[dev].pCallback; -// << " pFits=" << std::hex << this->controller[dev].pFits; + << " pArcDev=" << std::hex << this->controller.at(dev).pArcDev + << " pCB=" << std::hex << this->controller.at(dev).pCallback; +// << " pFits=" << std::hex << this->controller.at(dev).pFits; logwrite(function, message.str()); #endif } catch(std::out_of_range &) { message.str(""); message << "ERROR: unable to find device " << dev << " in list: { "; - for (const auto &check : this->devnums) message << check << " "; + for (const auto &check : this->active_devnums) message << check << " "; message << "}"; this->camera.async.enqueue_and_log( "CAMERAD", function, message.str() ); return(ERROR); @@ -2812,7 +2877,7 @@ namespace AstroCam { this->camera_info.systemkeys.add_key("CDELT2A", 0.25*this->camera_info.binspat, "Spatial scale in arcsec/pixel", EXT, "all"); this->camera_info.systemkeys.add_key("CRVAL2A", 0.0, "Reference value in arcsec", EXT, "all"); - for (const auto &dev : this->devnums) { // spawn a thread for each device in devnums + for (const auto &dev : this->active_devnums) { // spawn a thread for each device in active_devnums this->make_image_keywords(dev); @@ -2820,27 +2885,27 @@ namespace AstroCam { // copy the info class from controller[dev] to controller[dev].expinfo[expbuf] // - this->controller[dev].expinfo[this_expbuf] = this->controller[dev].info; + this->controller.at(dev).expinfo[this_expbuf] = this->controller.at(dev).info; // copy the info class from controller[dev] to controller[dev].expinfo[expbuf] // create handy references to the Common::Header objects for expinfo // - auto &_systemkeys = this->controller[dev].expinfo[this_expbuf].systemkeys; - auto &_telemkeys = this->controller[dev].expinfo[this_expbuf].telemkeys; - auto &_userkeys = this->controller[dev].expinfo[this_expbuf].userkeys; + auto &_systemkeys = this->controller.at(dev).expinfo[this_expbuf].systemkeys; + auto &_telemkeys = this->controller.at(dev).expinfo[this_expbuf].telemkeys; + auto &_userkeys = this->controller.at(dev).expinfo[this_expbuf].userkeys; // store BOI in this local _systemkeys so that it's overwritten each exposure // int nboi=0; int lastrowread=0; int stop=0; - for ( const auto &[nskip,nread] : this->controller[dev].info.interest_bands ) { + for ( const auto &[nskip,nread] : this->controller.at(dev).info.interest_bands ) { nboi++; std::string boikey = "BOI"+std::to_string(nboi); lastrowread += nskip; stop = lastrowread+nread; std::string boival = std::to_string(lastrowread)+":"+std::to_string(stop); - _systemkeys.add_key( boikey, boival, "band of interest "+std::to_string(nboi), EXT, this->controller[dev].channel ); + _systemkeys.add_key( boikey, boival, "band of interest "+std::to_string(nboi), EXT, this->controller.at(dev).channel ); lastrowread = stop; } @@ -2861,7 +2926,7 @@ namespace AstroCam { // to reference keywords to be written to all extensions and only the // extension for this channel // - auto channel = this->controller[dev].channel; + auto channel = this->controller.at(dev).channel; std::vector channels = { "all", channel }; // Loop through both "channels" and merge the Header objects from camera_info @@ -2880,10 +2945,10 @@ namespace AstroCam { this->fitsinfo[this_expbuf]->telemkeys.primary() = _telemkeys.primary(); this->fitsinfo[this_expbuf]->userkeys.primary() = _userkeys.primary(); - this->controller[dev].expinfo[this_expbuf].fits_name="not_needed"; + this->controller.at(dev).expinfo[this_expbuf].fits_name="not_needed"; std::string hash; - md5_file( this->controller[dev].firmware, hash ); // compute the md5 hash + md5_file( this->controller.at(dev).firmware, hash ); // compute the md5 hash // erase the per-exposure keyword databases. // @@ -2905,13 +2970,13 @@ namespace AstroCam { // std::thread( std::ref(AstroCam::Interface::dothread_read), std::ref(this->camera), - std::ref(this->controller[dev]), + std::ref(this->controller.at(dev)), this_expbuf ).detach(); } catch(std::out_of_range &) { message.str(""); message << "ERROR: unable to find device " << dev << " in list: { "; - for (const auto &check : this->devnums) message << check << " "; + for (const auto &check : this->active_devnums) message << check << " "; message << "}"; this->camera.async.enqueue_and_log( "CAMERAD", function, message.str() ); return(ERROR); @@ -2930,36 +2995,36 @@ namespace AstroCam { logwrite( function, "[DEBUG] expose is done now!" ); - for (const auto &dev : this->devnums) { + for (const auto &dev : this->active_devnums) { message.str(""); message << std::dec - << "** dev=" << dev << " type_set=" << this->controller[dev].info.type_set << " frame_type=" << this->controller[dev].info.frame_type - << " detector_pixels[]=" << this->controller[dev].info.detector_pixels[0] << " " << this->controller[dev].info.detector_pixels[1] - << " section_size=" << this->controller[dev].info.section_size << " image_memory=" << this->controller[dev].info.image_memory - << " readout_name=" << this->controller[dev].info.readout_name - << " readout_name2=" << this->controller[dev].expinfo[this_expbuf].readout_name - << " readout_type=" << this->controller[dev].info.readout_type - << " axes[]=" << this->controller[dev].info.axes[0] << " " << this->controller[dev].info.axes[1] << " " << this->controller[dev].info.axes[2] - << " cubedepth=" << this->controller[dev].info.cubedepth << " fitscubed=" << this->controller[dev].info.fitscubed - << " phys binning=" << this->controller[dev].info.binning[0] << " " << this->controller[dev].info.binning[1] - << " axis_pixels[]=" << this->controller[dev].info.axis_pixels[0] << " " << this->controller[dev].info.axis_pixels[1] - << " ismex=" << this->controller[dev].info.ismex << " extension=" << this->controller[dev].info.extension; + << "** dev=" << dev << " type_set=" << this->controller.at(dev).info.type_set << " frame_type=" << this->controller.at(dev).info.frame_type + << " detector_pixels[]=" << this->controller.at(dev).info.detector_pixels[0] << " " << this->controller.at(dev).info.detector_pixels[1] + << " section_size=" << this->controller.at(dev).info.section_size << " image_memory=" << this->controller.at(dev).info.image_memory + << " readout_name=" << this->controller.at(dev).info.readout_name + << " readout_name2=" << this->controller.at(dev).expinfo[this_expbuf].readout_name + << " readout_type=" << this->controller.at(dev).info.readout_type + << " axes[]=" << this->controller.at(dev).info.axes[0] << " " << this->controller.at(dev).info.axes[1] << " " << this->controller.at(dev).info.axes[2] + << " cubedepth=" << this->controller.at(dev).info.cubedepth << " fitscubed=" << this->controller.at(dev).info.fitscubed + << " phys binning=" << this->controller.at(dev).info.binning[0] << " " << this->controller.at(dev).info.binning[1] + << " axis_pixels[]=" << this->controller.at(dev).info.axis_pixels[0] << " " << this->controller.at(dev).info.axis_pixels[1] + << " ismex=" << this->controller.at(dev).info.ismex << " extension=" << this->controller.at(dev).info.extension; logwrite( function, message.str() ); } - for (const auto &dev : this->devnums) { + for (const auto &dev : this->active_devnums) { for ( int ii=0; iicontroller[dev].expinfo.at(ii).detector_pixels[1] - << " section_size=" << this->controller[dev].expinfo.at(ii).section_size << " image_memory=" << this->controller[dev].expinfo.at(ii).image_memory - << " readout_name=" << this->controller[dev].expinfo.at(ii).readout_name << " readout_type=" << this->controller[dev].expinfo.at(ii).readout_type - << " axes[]=" << this->controller[dev].expinfo.at(ii).axes[0] << " " << this->controller[dev].expinfo.at(ii).axes[1] << " " << this->controller[dev].expinfo.at(ii).axes[2] - << " cubedepth=" << this->controller[dev].expinfo.at(ii).cubedepth << " fitscubed=" << this->controller[dev].expinfo.at(ii).fitscubed - << " phys binning=" << this->controller[dev].expinfo.at(ii).binning[0] << " " << this->controller[dev].expinfo.at(ii).binning[1] - << " axis_pixels[]=" << this->controller[dev].expinfo.at(ii).axis_pixels[0] << " " << this->controller[dev].expinfo.at(ii).axis_pixels[1] - << " ismex=" << this->controller[dev].expinfo.at(ii).ismex << " extension=" << this->controller[dev].expinfo.at(ii).extension; + << "** dev=" << dev << " expbuf=" << ii << " type_set=" << this->controller.at(dev).expinfo.at(ii).type_set << " frame_type=" << this->controller.at(dev).expinfo.at(ii).frame_type + << " detector_pixels[]=" << this->controller.at(dev).expinfo.at(ii).detector_pixels[0] << " " << this->controller.at(dev).expinfo.at(ii).detector_pixels[1] + << " section_size=" << this->controller.at(dev).expinfo.at(ii).section_size << " image_memory=" << this->controller.at(dev).expinfo.at(ii).image_memory + << " readout_name=" << this->controller.at(dev).expinfo.at(ii).readout_name << " readout_type=" << this->controller.at(dev).expinfo.at(ii).readout_type + << " axes[]=" << this->controller.at(dev).expinfo.at(ii).axes[0] << " " << this->controller.at(dev).expinfo.at(ii).axes[1] << " " << this->controller.at(dev).expinfo.at(ii).axes[2] + << " cubedepth=" << this->controller.at(dev).expinfo.at(ii).cubedepth << " fitscubed=" << this->controller.at(dev).expinfo.at(ii).fitscubed + << " phys binning=" << this->controller.at(dev).expinfo.at(ii).binning[0] << " " << this->controller.at(dev).expinfo.at(ii).binning[1] + << " axis_pixels[]=" << this->controller.at(dev).expinfo.at(ii).axis_pixels[0] << " " << this->controller.at(dev).expinfo.at(ii).axis_pixels[1] + << " ismex=" << this->controller.at(dev).expinfo.at(ii).ismex << " extension=" << this->controller.at(dev).expinfo.at(ii).extension; logwrite( function, message.str() ); } } @@ -3357,10 +3422,10 @@ namespace AstroCam { // to load each controller with the specified file. // for ( const auto &con : this->controller ) { - if ( con.second.inactive ) continue; // skip controllers flagged as inactive - // But only use it if the device is open + if (!con.second.configured) continue; // skip controllers not configured + // But only use it if the device is open and active // - if ( con.second.connected ) { + if ( con.second.connected && con.second.active ) { std::stringstream lodfilestream; lodfilestream << con.second.devnum << " " << con.second.firmware; @@ -3399,7 +3464,7 @@ namespace AstroCam { std::string function = "AstroCam::Interface::do_load_firmware"; std::stringstream message; std::vector tokens; - std::vector selectdev; + std::vector selectdev; struct stat st; long error = ERROR; @@ -3441,10 +3506,10 @@ namespace AstroCam { } // If there's only one token then it's the lodfile and load - // into all controllers in the devnums. + // into all controllers in the active_devnums. // if (tokens.size() == 1) { - for (const auto &dev : this->devnums) { + for (const auto &dev : this->active_devnums) { selectdev.push_back( dev ); // build selectdev vector from all connected controllers } } @@ -3455,7 +3520,7 @@ namespace AstroCam { // if (tokens.size() > 1) { for (uint32_t n = 0; n < (tokens.size()-1); n++) { // tokens.size() - 1 because the last token must be the filename - selectdev.push_back( (uint32_t)parse_val( tokens.at(n) ) ); + selectdev.push_back( (int)parse_val( tokens.at(n) ) ); } timlodfile = tokens.at( tokens.size() - 1 ); // the last token must be the filename } @@ -3474,8 +3539,8 @@ namespace AstroCam { for (const auto &dev : selectdev) { // spawn a thread for each device in the selectdev list if ( firstdev == -1 ) firstdev = dev; // save the first device from the list of connected controllers try { - if ( this->controller[dev].connected ) { // but only if connected - std::thread thr( std::ref(AstroCam::Interface::dothread_load), std::ref(this->controller[dev]), timlodfile ); + if ( this->controller.at(dev).connected ) { // but only if connected + std::thread thr( std::ref(AstroCam::Interface::dothread_load), std::ref(this->controller.at(dev)), timlodfile ); threads.push_back ( std::move(thr) ); // push the thread into the local vector } } @@ -3517,7 +3582,7 @@ namespace AstroCam { check_retval = this->controller[firstdev].retval; // save the first one in the controller vector bool allsame = true; - for ( const auto &dev : selectdev ) { if ( this->controller[dev].retval != check_retval ) { allsame = false; } } + for ( const auto &dev : selectdev ) { if ( this->controller.at(dev).retval != check_retval ) { allsame = false; } } // If all the return values are equal then report only NO_ERROR (if "DON") or ERROR (anything else) // @@ -3534,8 +3599,8 @@ namespace AstroCam { std::stringstream rss; std::string rs; for (const auto &dev : selectdev) { - this->retval_to_string( this->controller[dev].retval, rs ); // convert the retval to string (DON, ERR, etc.) - rss << this->controller[dev].devnum << ":" << rs << " "; + this->retval_to_string( this->controller.at(dev).retval, rs ); // convert the retval to string (DON, ERR, etc.) + rss << this->controller.at(dev).devnum << ":" << rs << " "; } retstring = rss.str(); error = ERROR; @@ -3545,8 +3610,8 @@ namespace AstroCam { /*** logwrite( function, "NOTICE: firmware loaded" ); for ( const auto &dev : selectdev ) { - for ( auto it = this->controller[dev].extkeys.keydb.begin(); - it != this->controller[dev].extkeys.keydb.end(); it++ ) { + for ( auto it = this->controller.at(dev).extkeys.keydb.begin(); + it != this->controller.at(dev).extkeys.keydb.end(); it++ ) { message.str(""); message << "NOTICE: dev=" << dev << "key=" << it->second.keyword << " val=" << it->second.keyvalue; logwrite( function, message.str() ); } @@ -3554,7 +3619,7 @@ for ( const auto &dev : selectdev ) { ***/ for (const auto &dev: selectdev) { - std::string init=this->controller[dev].channel+" init"; + std::string init=this->controller.at(dev).channel+" init"; std::string retstring; this->image_size(init, retstring); } @@ -3661,7 +3726,8 @@ for ( const auto &dev : selectdev ) { retstring.append( " Specify from { " ); message.str(""); for ( const auto &con : this->controller ) { - if ( con.second.inactive ) continue; // skip controllers flagged as inactive + // skip unconfigured and inactive controllers + if (!con.second.configured || !con.second.active) continue; message << con.second.channel << " "; } message << "}\n"; @@ -3669,7 +3735,8 @@ for ( const auto &dev : selectdev ) { retstring.append( " or from { " ); message.str(""); for ( const auto &con : this->controller ) { - if ( con.second.inactive ) continue; // skip controllers flagged as inactive + // skip unconfigured and inactive controllers + if (!con.second.configured || !con.second.active) continue; message << con.second.devnum << " "; } message << "}\n"; @@ -3791,7 +3858,8 @@ for ( const auto &dev : selectdev ) { retstring.append( " Specify from { " ); message.str(""); for ( const auto &con : this->controller ) { - if ( con.second.inactive ) continue; // skip controllers flagged as inactive + // skip unconfigured and inactive controllers + if (!con.second.configured || !con.second.active) continue; message << con.second.channel << " "; } message << "}\n"; @@ -3799,7 +3867,8 @@ for ( const auto &dev : selectdev ) { retstring.append( " or from { " ); message.str(""); for ( const auto &con : this->controller ) { - if ( con.second.inactive ) continue; // skip controllers flagged as inactive + // skip unconfigured and inactive controllers + if (!con.second.configured || !con.second.active) continue; message << con.second.devnum << " "; } message << "}\n"; @@ -3872,7 +3941,7 @@ for ( const auto &dev : selectdev ) { // In any case, set or not, get the current type // - retstring = this->controller[dev].info.readout_name; + retstring = this->controller.at(dev).info.readout_name; return( NO_ERROR ); } @@ -3911,7 +3980,8 @@ for ( const auto &dev : selectdev ) { retstring.append( " Specify from { " ); message.str(""); for ( const auto &con : this->controller ) { - if ( con.second.inactive ) continue; // skip controllers flagged as inactive + // skip unconfigured and inactive controllers + if (!con.second.configured || !con.second.active) continue; message << con.second.channel << " "; } message << "}\n"; @@ -3919,7 +3989,8 @@ for ( const auto &dev : selectdev ) { retstring.append( " or from { " ); message.str(""); for ( const auto &con : this->controller ) { - if ( con.second.inactive ) continue; // skip controllers flagged as inactive + // skip unconfigured and inactive controllers + if (!con.second.configured || !con.second.active) continue; message << con.second.devnum << " "; } message << "}\n"; @@ -3957,13 +4028,10 @@ for ( const auto &dev : selectdev ) { std::string chan; if ( this->extract_dev_chan( args, dev, chan, retstring ) != NO_ERROR ) return ERROR; - Controller* pcontroller = &this->controller[dev]; + Controller* pcontroller = this->get_active_controller(dev); - // don't continue if that controller is not connected now - // - if ( !pcontroller->connected ) { - logwrite( function, "ERROR controller channel "+chan+" not connected" ); - retstring="not_connected"; + if (!pcontroller) { + logwrite(function, "ERROR: controller not available for channel "+chan); return ERROR; } @@ -4168,11 +4236,11 @@ for ( const auto &dev : selectdev ) { // and fpbcount to index the frameinfo STL map on that devnum, // and assign the pointer to that buffer to a local variable. // - void* imbuf = this->controller[devnum].frameinfo[fpbcount].buf; + void* imbuf = this->controller.at(devnum).frameinfo[fpbcount].buf; - message << this->controller[devnum].devname << " received exposure " - << this->controller[devnum].frameinfo[fpbcount].framenum << " into image buffer " - << std::hex << this->controller[devnum].frameinfo[fpbcount].buf; + message << this->controller.at(devnum).devname << " received exposure " + << this->controller.at(devnum).frameinfo[fpbcount].framenum << " into image buffer " + << std::hex << this->controller.at(devnum).frameinfo[fpbcount].buf; logwrite(function, message.str()); // Call the class' deinterlace and write functions. @@ -4182,12 +4250,12 @@ for ( const auto &dev : selectdev ) { // that buffer is already known. // try { - switch (this->controller[devnum].info.datatype) { + switch (this->controller.at(devnum).info.datatype) { case USHORT_IMG: { - this->controller[devnum].deinterlace( expbuf, (uint16_t *)imbuf ); -message.str(""); message << this->controller[devnum].devname << " exposure buffer " << expbuf << " deinterlaced " << std::hex << imbuf; + this->controller.at(devnum).deinterlace( expbuf, (uint16_t *)imbuf ); +message.str(""); message << this->controller.at(devnum).devname << " exposure buffer " << expbuf << " deinterlaced " << std::hex << imbuf; logwrite(function, message.str()); -message.str(""); message << "about to write section size " << this->controller[devnum].expinfo[expbuf].section_size ; // << " to file \"" << this->pFits[expbuf]->fits_name << "\""; +message.str(""); message << "about to write section size " << this->controller.at(devnum).expinfo[expbuf].section_size ; // << " to file \"" << this->pFits[expbuf]->fits_name << "\""; logwrite(function, message.str()); // Call write_image(), @@ -4198,27 +4266,27 @@ logwrite(function, message.str()); this->pFits[ expbuf ]->extension++; -message.str(""); message << this->controller[devnum].devname << " exposure buffer " << expbuf << " wrote " << std::hex << this->controller[devnum].workbuf; +message.str(""); message << this->controller.at(devnum).devname << " exposure buffer " << expbuf << " wrote " << std::hex << this->controller.at(devnum).workbuf; logwrite(function, message.str()); -// error = this->controller[devnum].write( ); 10/30/23 BOB -- the write is above. .write() called ->write_image(), skip that extra function +// error = this->controller.at(devnum).write( ); 10/30/23 BOB -- the write is above. .write() called ->write_image(), skip that extra function break; } /******* case SHORT_IMG: { - this->controller[devnum].deinterlace( expbuf, (int16_t *)imbuf ); - error = this->controller[devnum].write( ); + this->controller.at(devnum).deinterlace( expbuf, (int16_t *)imbuf ); + error = this->controller.at(devnum).write( ); break; } case FLOAT_IMG: { - this->controller[devnum].deinterlace( expbuf, (uint32_t *)imbuf ); - error = this->controller[devnum].write( ); + this->controller.at(devnum).deinterlace( expbuf, (uint32_t *)imbuf ); + error = this->controller.at(devnum).write( ); break; } ********/ default: message.str(""); - message << "ERROR: unknown datatype: " << this->controller[devnum].info.datatype; + message << "ERROR: unknown datatype: " << this->controller.at(devnum).info.datatype; logwrite(function, message.str()); error = ERROR; break; @@ -4226,16 +4294,16 @@ logwrite(function, message.str()); // A frame has been written for this device, // so increment the framecounter for devnum. // - if (error == NO_ERROR) this->controller[devnum].increment_framecount(); + if (error == NO_ERROR) this->controller.at(devnum).increment_framecount(); #ifdef LOGLEVEL_DEBUG message.str(""); message << "[DEBUG] framecount(" << devnum << ")=" - << this->controller[devnum].get_framecount() << " written"; + << this->controller.at(devnum).get_framecount() << " written"; logwrite( function, message.str() ); #endif } catch (std::out_of_range &) { message.str(""); message << "ERROR: unable to find device " << devnum << " in list: { "; - for (const auto &check : this->devnums) message << check << " "; + for (const auto &check : this->active_devnums) message << check << " "; message << "}"; logwrite(function, message.str()); error = ERROR; @@ -4245,9 +4313,9 @@ logwrite(function, message.str()); message.str(""); message << "[DEBUG] completed " << (error != NO_ERROR ? "with error. " : "ok. ") << "devnum=" << devnum << " " << "fpbcount=" << fpbcount << " " - << this->controller[devnum].devname << " received exposure " - << this->controller[devnum].frameinfo[fpbcount].framenum << " into buffer " - << std::hex << std::uppercase << this->controller[devnum].frameinfo[fpbcount].buf; + << this->controller.at(devnum).devname << " received exposure " + << this->controller.at(devnum).frameinfo[fpbcount].framenum << " into buffer " + << std::hex << std::uppercase << this->controller.at(devnum).frameinfo[fpbcount].buf; logwrite(function, message.str()); #endif return( error ); @@ -4285,6 +4353,7 @@ logwrite(function, message.str()); if ( this->in_readout() ) { message.str(""); message << "ERROR: cannot change exposure time while reading out chan "; for ( const auto &con : this->controller ) { + if (!con.second.active) continue; // skip inactive controllers if ( con.second.in_readout || con.second.in_frametransfer ) message << con.second.channel << " "; } this->camera.async.enqueue_and_log( "CAMERAD", function, message.str() ); @@ -4578,16 +4647,16 @@ logwrite(function, message.str()); // Set shutterenable the same for all devices // - if ( error==NO_ERROR && this->camera.ext_shutter ) for ( const auto &dev : this->devnums ) { - this->controller[dev].info.shutterenable = this->camera.shutter.is_enabled; + if ( error==NO_ERROR && this->camera.ext_shutter ) for ( const auto &dev : this->active_devnums ) { + this->controller.at(dev).info.shutterenable = this->camera.shutter.is_enabled; } } // For external shutter (i.e. triggered by Leach controller) // read the shutterenable state back from the controller class. // - if ( this->camera.ext_shutter ) for ( const auto &dev : this->devnums ) { - this->camera.shutter.is_enabled = this->controller[dev].info.shutterenable; + if ( this->camera.ext_shutter ) for ( const auto &dev : this->active_devnums ) { + this->camera.shutter.is_enabled = this->controller.at(dev).info.shutterenable; break; // just need one since they're all the same } // otherwise shutterenable state is whatever is in the camera_info class @@ -4652,7 +4721,8 @@ logwrite(function, message.str()); retstring.append( " Specify from { " ); message.str(""); for ( const auto &con : this->controller ) { - if ( con.second.inactive ) continue; // skip controllers flagged as inactive + // skip unconfigured and inactive controllers + if (!con.second.configured || !con.second.active) continue; message << con.second.channel << " "; } message << "}\n"; @@ -4660,7 +4730,8 @@ logwrite(function, message.str()); retstring.append( " or from { " ); message.str(""); for ( const auto &con : this->controller ) { - if ( con.second.inactive ) continue; // skip controllers flagged as inactive + // skip unconfigured and inactive controllers + if (!con.second.configured || !con.second.active) continue; message << con.second.devnum << " "; } message << "}\n"; @@ -4673,8 +4744,8 @@ logwrite(function, message.str()); // if (args=="all") { bool all_true = true; - for ( const auto &dev : this->devnums ) { - if ( !this->controller[dev].have_ft ) { + for ( const auto &dev : this->active_devnums ) { + if ( !this->controller.at(dev).have_ft ) { all_true=false; break; } @@ -4705,14 +4776,14 @@ logwrite(function, message.str()); // If a state was provided then set it // if ( ! retstring.empty() ) { - this->controller[dev].have_ft = ( retstring == "yes" ? true : false ); + this->controller.at(dev).have_ft = ( retstring == "yes" ? true : false ); // add keyword to the extension for this channel -//TCB this->controller[dev].info.systemkeys.add_key( "FT", this->controller[dev].have_ft, "frame transfer used", EXT, chan ); +//TCB this->controller.at(dev).info.systemkeys.add_key( "FT", this->controller.at(dev).have_ft, "frame transfer used", EXT, chan ); } // In any case, return the current state // - retstring = ( this->controller[dev].have_ft ? "yes" : "no" ); + retstring = ( this->controller.at(dev).have_ft ? "yes" : "no" ); return( NO_ERROR ); } @@ -4751,7 +4822,8 @@ logwrite(function, message.str()); retstring.append( " Specify from { " ); message.str(""); for ( const auto &con : this->controller ) { - if ( con.second.inactive ) continue; // skip controllers flagged as inactive + // skip unconfigured and inactive controllers + if (!con.second.configured || !con.second.active) continue; message << con.second.channel << " "; } message << "}\n"; @@ -4759,7 +4831,8 @@ logwrite(function, message.str()); retstring.append( " or from { " ); message.str(""); for ( const auto &con : this->controller ) { - if ( con.second.inactive ) continue; // skip controllers flagged as inactive + // skip unconfigured and inactive controllers + if (!con.second.configured || !con.second.active) continue; message << con.second.devnum << " "; } message << "}\n"; @@ -4789,7 +4862,17 @@ logwrite(function, message.str()); std::vector tokens; Tokenize( retstring, tokens, " " ); - Controller* pcontroller = &this->controller[dev]; + // Just need to get a configured controller here, + // it doesn't need to be active or connected at this stage. + // This allows setting up image size prior to connecting, which is done + // when the config file is read. + // + Controller* pcontroller = this->get_controller(dev); + + if (!pcontroller) { + logwrite(function, "ERROR: controller not available for channel "+chan); + return ERROR; + } int spat=-1, spec=-1, osspat=-1, osspec=-1, binspat=-1, binspec=-1; // start by loading the values in the class @@ -4887,7 +4970,7 @@ logwrite(function, message.str()); pcontroller->skipcols = cols % bincols; pcontroller->skiprows = rows % binrows; -// message.str(""); message << "[DEBUG] skipcols=" << this->controller[dev].skipcols << " skiprows=" << this->controller[dev].skiprows; +// message.str(""); message << "[DEBUG] skipcols=" << this->controller.at(dev).skipcols << " skiprows=" << this->controller.at(dev).skiprows; // logwrite( function, message.str() ); cols -= pcontroller->skipcols; @@ -4923,8 +5006,8 @@ logwrite(function, message.str()); // message.str(""); // message << "[DEBUG] new binned values before set_axes() to re-calculate:" -// << " detector_pixels[" << _COL_ << "]=" << this->controller[dev].info.detector_pixels[_COL_] -// << " detector_pixels[" << _ROW_ << "]=" << this->controller[dev].info.detector_pixels[_ROW_]; +// << " detector_pixels[" << _COL_ << "]=" << this->controller.at(dev).info.detector_pixels[_COL_] +// << " detector_pixels[" << _ROW_ << "]=" << this->controller.at(dev).info.detector_pixels[_ROW_]; // logwrite(function, message.str()); // *** This is where the binned-image dimensions are re-calculated *** @@ -5068,7 +5151,8 @@ logwrite(function, message.str()); retstring.append( " Specify from { " ); message.str(""); for ( const auto &con : this->controller ) { - if ( con.second.inactive ) continue; // skip controllers flagged as inactive + // skip unconfigured and inactive controllers + if (!con.second.configured || !con.second.active) continue; message << con.second.channel << " "; } message << "}\n"; @@ -5076,7 +5160,8 @@ logwrite(function, message.str()); retstring.append( " or from { " ); message.str(""); for ( const auto &con : this->controller ) { - if ( con.second.inactive ) continue; // skip controllers flagged as inactive + // skip unconfigured and inactive controllers + if (!con.second.configured || !con.second.active) continue; message << con.second.devnum << " "; } message << "}\n"; @@ -5125,8 +5210,8 @@ logwrite(function, message.str()); return( ERROR ); } -// message.str(""); message << "[DEBUG] " << this->controller[dev].devname -// << " chan " << this->controller[dev].channel << " rows:" << setrows << " cols:" << setcols; +// message.str(""); message << "[DEBUG] " << this->controller.at(dev).devname +// << " chan " << this->controller.at(dev).channel << " rows:" << setrows << " cols:" << setcols; // logwrite( function, message.str() ); // Write the geometry to the selected controllers @@ -5157,13 +5242,13 @@ logwrite(function, message.str()); // cmd.str(""); cmd << "RDM 0x400001 "; if ( this->do_native( dev, cmd.str(), getcols ) != NO_ERROR ) return ERROR; - this->controller[dev].cols = (uint32_t)parse_val( getcols.substr( getcols.find(":")+1 ) ); + this->controller.at(dev).cols = (uint32_t)parse_val( getcols.substr( getcols.find(":")+1 ) ); cmd.str(""); cmd << "RDM 0x400002 "; if ( this->do_native( dev, cmd.str(), getrows ) != NO_ERROR ) return ERROR; - this->controller[dev].rows = (uint32_t)parse_val( getrows.substr( getrows.find(":")+1 ) ); + this->controller.at(dev).rows = (uint32_t)parse_val( getrows.substr( getrows.find(":")+1 ) ); - rs << this->controller[dev].rows << " " << this->controller[dev].cols; + rs << this->controller.at(dev).rows << " " << this->controller.at(dev).cols; retstring = rs.str(); // Form the return string from the read-back rows cols @@ -5248,36 +5333,36 @@ logwrite(function, message.str()); // // When useframes is false, fpbcount=0, fcount=0, framenum=0 // - if ( ! server.controller[devnum].have_ft ) { + if ( ! server.controller.at(devnum).have_ft ) { server.exposure_pending( devnum, false ); // this also does the notify server.state_monitor_condition.notify_all(); #ifdef LOGLEVEL_DEBUG - message.str(""); message << "[DEBUG] dev " << devnum << " chan " << server.controller[devnum].channel << " exposure_pending=false"; + message.str(""); message << "[DEBUG] dev " << devnum << " chan " << server.controller.at(devnum).channel << " exposure_pending=false"; server.camera.async.enqueue_and_log( "CAMERAD", function, message.str() ); #endif } - server.controller[devnum].in_readout = false; + server.controller.at(devnum).in_readout = false; server.state_monitor_condition.notify_all(); #ifdef LOGLEVEL_DEBUG - message.str(""); message << "[DEBUG] dev " << devnum << " chan " << server.controller[devnum].channel << " in_readout=false"; + message.str(""); message << "[DEBUG] dev " << devnum << " chan " << server.controller.at(devnum).channel << " in_readout=false"; server.camera.async.enqueue_and_log( "CAMERAD", function, message.str() ); #endif - server.controller[devnum].frameinfo[fpbcount].tid = fpbcount; // create this index in the .frameinfo[] map - server.controller[devnum].frameinfo[fpbcount].buf = buffer; + server.controller.at(devnum).frameinfo[fpbcount].tid = fpbcount; // create this index in the .frameinfo[] map + server.controller.at(devnum).frameinfo[fpbcount].buf = buffer; /*** - if ( server.controller[devnum].frameinfo.count( fpbcount ) == 0 ) { // searches .frameinfo[] map for an index of fpbcount (none) - server.controller[devnum].frameinfo[ fpbcount ].tid = fpbcount; // create this index in the .frameinfo[] map - server.controller[devnum].frameinfo[ fpbcount ].buf = buffer; + if ( server.controller.at(devnum).frameinfo.count( fpbcount ) == 0 ) { // searches .frameinfo[] map for an index of fpbcount (none) + server.controller.at(devnum).frameinfo[ fpbcount ].tid = fpbcount; // create this index in the .frameinfo[] map + server.controller.at(devnum).frameinfo[ fpbcount ].buf = buffer; // If useframes is false then set framenum=0 because it doesn't mean anything, // otherwise set it to the fcount received from the API. // - server.controller[devnum].frameinfo[ fpbcount ].framenum = server.useframes ? fcount : 0; + server.controller.at(devnum).frameinfo[ fpbcount ].framenum = server.useframes ? fcount : 0; } else { // already have this fpbcount in .frameinfo[] map message.str(""); message << "ERROR: frame buffer overrun! Try allocating a larger buffer." - << " chan " << server.controller[devnum].channel; + << " chan " << server.controller.at(devnum).channel; logwrite( function, message.str() ); server.frameinfo_mutex.unlock(); return; @@ -5293,7 +5378,7 @@ logwrite(function, message.str()); double start_time = get_clock_time(); do { int this_frame = fcount; // the current frame - int last_frame = server.controller[devnum].get_framecount(); // the last frame that has been written by this device + int last_frame = server.controller.at(devnum).get_framecount(); // the last frame that has been written by this device int next_frame = last_frame + 1; // the next frame in line if (this_frame != next_frame) { // if the current frame is NOT the next in line then keep waiting usleep(5); @@ -5323,7 +5408,7 @@ logwrite(function, message.str()); message.str(""); message << "[DEBUG] calling server.write_frame for devnum=" << devnum << " fpbcount=" << fpbcount; logwrite(function, message.str()); #endif - error = server.write_frame( expbuf, devnum, server.controller[devnum].channel, fpbcount ); + error = server.write_frame( expbuf, devnum, server.controller.at(devnum).channel, fpbcount ); } else { logwrite(function, "aborted!"); @@ -5338,10 +5423,10 @@ logwrite(function, message.str()); // Erase it from the STL map so it's not seen again. // server.frameinfo_mutex.lock(); // protect access to frameinfo structure -// server.controller[devnum].frameinfo.erase( fpbcount ); +// server.controller.at(devnum).frameinfo.erase( fpbcount ); /*** 10/30/23 BOB - server.controller[devnum].close_file( server.camera.writekeys_when ); + server.controller.at(devnum).close_file( server.camera.writekeys_when ); ***/ server.frameinfo_mutex.unlock(); @@ -5404,7 +5489,7 @@ logwrite(function, message.str()); message.str(""); message << "NOTICE:exposure buffer " << expbuf << " waiting for frames from "; std::vector pending = interface.writes_pending[ expbuf ]; - for ( const auto &dev : pending ) message << interface.controller[dev].channel << " "; + for ( const auto &dev : pending ) message << interface.controller.at(dev).channel << " "; logwrite( function, message.str() ); // wait() will repeatedly call this lambda function before actually entering @@ -5430,6 +5515,176 @@ logwrite(function, message.str()); /***** AstroCam::Interface::FITS_handler ************************************/ + /***** AstroCam::Interface::camera_active_state *****************************/ + /** + * @brief set/get camera active state + * @details De-activating a configured channel turns off the biases and + * flagging it for non-use. This allows keeping a controller in + * a sort of standby condition, without having to reload waveforms + * and reconfigure. + * @param[in] args space-delimited list of one or more channel names {U G R I} + * @param[out] retstring activated|deactivated|error + * @param[in] cmd AstroCam::ActiveState:: {Activate|DeActivate|Query} + * @return ERROR|NO_ERROR + * + */ + long Interface::camera_active_state(const std::string &args, std::string &retstring, + AstroCam::ActiveState cmd) { + const std::string function("AstroCam::Interface::camera_active_state"); + std::vector _devnums; // local list of devnum(s) associated with chan(s) + std::string chan; // current channel + std::istringstream iss(args); + + // get channel name(s) from args and + // convert to a vector of devnum(s) + // + while (iss >> chan) { + // validate device number for that channel + int dev; + try { + dev = devnum_from_chan(chan); + } + // exceptions are not fatal, just don't add dev to the vector + catch(const std::exception &e) { + logwrite(function, "channel "+chan+": "+std::string(e.what())); + continue; + } + // push it into a vector + _devnums.push_back(dev); + } + + long error = NO_ERROR; + + retstring.clear(); + + // activate/deactivate each dev + // + for (const auto &dev : _devnums) { + // get pointer to the Controller object for this device + // it only needs to exist and be connected + auto pcontroller = this->get_controller(dev); + + // unavailable channels are not fatal, they just don't get used + if (!pcontroller) { + logwrite(function, "channel "+pcontroller->channel+" not configured"); + continue; + } + if (!pcontroller->configured || !pcontroller->connected) { + logwrite(function, "channel "+pcontroller->channel+" not connected"); + continue; + } + + // set or get active state as specified by cmd + switch (cmd) { + + // first set active flag, then send activation commands + case AstroCam::ActiveState::Activate: + if (pcontroller->active) break; // nothing to do if already activated + pcontroller->active = true; + // add this devnum to the active_devnums list + add_dev(dev, this->active_devnums); + // send the activation commands + for (const auto &cmd : pcontroller->activate_commands) { + error |= this->do_native(dev, std::string(cmd), retstring); + std::this_thread::sleep_for(std::chrono::milliseconds(500)); + } + break; + + // first turn off power, then clear active flag + case AstroCam::ActiveState::DeActivate: + if (!pcontroller->active) break; // nothing to do if already deactivated + if ( (error=this->do_native(dev, std::string("POF"), retstring))==NO_ERROR ) { + pcontroller->active = false; + // remove this devnum from the active_devnums list + remove_dev(dev, this->active_devnums); + } + break; + + // do nothing + case AstroCam::ActiveState::Query: + default: + break; + } + + // build up return string + retstring += (pcontroller->channel+":"+(pcontroller->active ? "activated " : "deactivated ")); + } + + return error; + } + /***** AstroCam::Interface::camera_active_state *****************************/ + + + /***** AstroCam::Interface::get_controller **********************************/ + /** + * @brief helper function returns pointer to element of Controller map + * @details This will return a pointer if the requested device has been + * configured. + * @param[in] dev integer device number for map indexing + * @return pointer to Controller | nullptr + * + */ + Interface::Controller* Interface::get_controller(const int dev) { + const std::string function("AstroCam::Interface::get_controller"); + + auto it = this->controller.find(dev); + + if (it==this->controller.end()) { + logwrite(function, "controller for dev "+std::to_string(dev)+" not found"); + return nullptr; + } + + return &it->second; + } + /***** AstroCam::Interface::get_controller **********************************/ + + + /***** AstroCam::Interface::get_active_controller ***************************/ + /** + * @brief helper function returns pointer to element of Controller map + * @details This will return a pointer only if the requested device is + * active. It must be configured, connected, and active. + * @param[in] dev integer device number for map indexing + * @return pointer to Controller | nullptr + * + */ + Interface::Controller* Interface::get_active_controller(const int dev) { + const std::string function("AstroCam::Interface::get_active_controller"); + std::ostringstream oss; + + auto it = this->controller.find(dev); + + if (it==this->controller.end()) { + oss << "controller for dev " << dev << " not found"; + logwrite(function, oss.str()); + return nullptr; + } + + Controller &con = it->second; + + if (!con.configured) { + oss << "controller for dev " << dev << " not configured"; + logwrite(function, oss.str()); + return nullptr; + } + + if (!con.connected) { + oss << "controller for dev " << dev << " not connected"; + logwrite(function, oss.str()); + return nullptr; + } + + if (!con.active) { + oss << "controller for dev " << dev << " not active"; + logwrite(function, oss.str()); + return nullptr; + } + + return &con; + } + /***** AstroCam::Interface::get_active_controller ***************************/ + + /***** AstroCam::Interface::add_framethread *********************************/ /** * @brief call on thread creation to increment framethreadcount @@ -5481,34 +5736,6 @@ logwrite(function, message.str()); /***** AstroCam::Interface::init_framethread_count **************************/ - /***** AstroCam::Interface::Controller::Controller **************************/ - /** - * @brief class constructor - * - */ - Interface::Controller::Controller() { - this->workbuf = NULL; - this->workbuf_size = 0; - this->bufsize = 0; - this->rows=0; - this->cols=0; - this->devnum = 0; - this->framecount = 0; - this->pArcDev = NULL; - this->pCallback = NULL; - this->connected = false; - this->is_imsize_set = false; - this->firmwareloaded = false; - this->firmware = ""; - this->info.readout_name = ""; - this->info.readout_type = -1; - this->readout_arg = 0xBAD; - this->expinfo.resize( NUM_EXPBUF ); // vector of Camera::Information, one for each exposure buffer - this->info.exposure_unit = "msec"; // chaning unit not currently supported in ARC - } - /***** AstroCam::Interface::Controller::Controller **************************/ - - /***** AstroCam::Interface::Controller::logical_to_physical *****************/ /** * @brief translates logical (spat,spec) to physical (rows,cols) @@ -5820,8 +6047,10 @@ logwrite(function, message.str()); retstring.append( " shdelay ? | | test\n" ); retstring.append( " shutter ? | init | open | close | get | time | expose \n" ); retstring.append( " telem ? | collect | test | calibd | flexured | focusd | tcsd\n" ); + retstring.append( " canexpose\n" ); retstring.append( " isreadout\n" ); retstring.append( " pixelcount\n" ); + retstring.append( " devnums\n" ); return HELP; } @@ -5856,8 +6085,8 @@ logwrite(function, message.str()); } std::string msg; this->camera.set_fitstime( get_timestamp( ) ); // must set camera.fitstime first - if ( this->devnums.size() > 1 ) { - for (const auto &dev : this->devnums) { + if ( this->active_devnums.size() > 1 ) { + for (const auto &dev : this->active_devnums) { this->camera.get_fitsname( std::to_string(dev), msg ); // get the fitsname (by reference) this->camera.async.enqueue( msg ); // queue the fitsname logwrite( function, msg ); // log ths fitsname @@ -6174,22 +6403,26 @@ logwrite(function, message.str()); logwrite( function, message.str() ); retstring.append( message.str() ); retstring.append( "\n" ); + message.str(""); message << "can_expose=" << ( this->can_expose.load() ? "true" : "false" ); + logwrite( function, message.str() ); + retstring.append( message.str() ); retstring.append( "\n" ); + // this shows which channels have an exposure pending { std::vector pending = this->exposure_pending_list(); message.str(""); message << "exposures pending: "; - for ( const auto &dev : pending ) message << this->controller[dev].channel << " "; + for ( const auto &dev : pending ) message << this->controller.at(dev).channel << " "; logwrite( function, message.str() ); } retstring.append( message.str() ); retstring.append( "\n" ); message.str(""); message << "in readout: "; - for ( const auto &dev : this->devnums ) if ( this->controller[dev].in_readout ) message << this->controller[dev].channel << " "; + for ( const auto &dev : this->active_devnums ) if ( this->controller.at(dev).in_readout ) message << this->controller.at(dev).channel << " "; logwrite( function, message.str() ); retstring.append( message.str() ); retstring.append( "\n" ); message.str(""); message << "in frametransfer: "; - for ( const auto &dev : this->devnums ) if ( this->controller[dev].in_frametransfer ) message << this->controller[dev].channel << " "; + for ( const auto &dev : this->active_devnums ) if ( this->controller.at(dev).in_frametransfer ) message << this->controller.at(dev).channel << " "; logwrite( function, message.str() ); retstring.append( message.str() ); retstring.append( "\n" ); @@ -6225,27 +6458,27 @@ logwrite(function, message.str()); retstring.append( " Initiate the frame transfer waveforms on the indicated device.\n" ); retstring.append( " Supply dev# or chan from { " ); message.str(""); - for ( const auto &dd : this->devnums ) { + for ( const auto &dd : this->active_devnums ) { message << dd << " " << this->controller[dd].channel << " "; } - if ( this->devnums.empty() ) message << "no_devices_open "; + if ( this->active_devnums.empty() ) message << "no_active_devices "; message << "}"; retstring.append( message.str() ); return HELP; } - // must have at least one device open + // must have at least one active device open // - if ( this->devnums.empty() ) { + if ( this->active_devnums.empty() ) { logwrite( function, "ERROR: no open devices" ); retstring="no_devices"; return( ERROR ); } - // check if arg is a channel by comparing to all the defined channels in the devnums + // check if arg is a channel by comparing to all the defined channels in the active_devnums // int dev=-1; - for ( const auto &dd : this->devnums ) { + for ( const auto &dd : this->active_devnums ) { if ( this->controller[dd].channel == tokens[1] ) { dev = dd; break; @@ -6271,13 +6504,13 @@ logwrite(function, message.str()); // initiate the frame transfer waveforms // - this->controller[dev].pArcDev->frame_transfer( 0, - this->controller[dev].devnum, - this->controller[dev].info.axes[_ROW_], - this->controller[dev].info.axes[_COL_], - this->controller[dev].pCallback + this->controller.at(dev).pArcDev->frame_transfer( 0, + this->controller.at(dev).devnum, + this->controller.at(dev).info.axes[_ROW_], + this->controller.at(dev).info.axes[_COL_], + this->controller.at(dev).pCallback ); - retstring=this->controller[dev].channel; + retstring=this->controller.at(dev).channel; return( NO_ERROR ); } else @@ -6289,8 +6522,10 @@ logwrite(function, message.str()); if ( testname == "controller" ) { for ( auto &con : this->controller ) { - if ( con.second.inactive ) continue; // skip controllers flagged as inactive - message.str(""); message << "controller[" << con.second.devnum << "] connected:" << ( con.second.connected ? "T" : "F" ) + if (!con.second.configured) continue; // skip controllers not configured + message.str(""); message << "controller[" << con.second.devnum << "]" + << " connected:" << ( con.second.connected ? "T" : "F" ) + << " active:" << ( con.second.active ? "T" : "F" ) << " bufsize:" << con.second.get_bufsize() << " rows:" << con.second.rows << " cols:" << con.second.cols << " in_readout:" << ( con.second.in_readout ? "T" : "F" ) @@ -6388,6 +6623,16 @@ logwrite(function, message.str()); } else // ---------------------------------------------------- + // isready + // ---------------------------------------------------- + // am I ready for an exposure? + if (testname=="canexpose") { + retstring=(this->can_expose?"yes":"no"); + logwrite(function, retstring); + return NO_ERROR; + } + else + // ---------------------------------------------------- // isreadout // ---------------------------------------------------- // call ARC API isReadout() function directly @@ -6397,7 +6642,7 @@ logwrite(function, message.str()); retstring.clear(); try { for ( auto &con : this->controller ) { - if ( con.second.pArcDev != nullptr && con.second.connected ) { + if ( con.second.pArcDev != nullptr && con.second.connected && con.second.active ) { bool isreadout = con.second.pArcDev->isReadout(); error=NO_ERROR; retstring += (isreadout ? "T " : "F "); @@ -6423,7 +6668,7 @@ logwrite(function, message.str()); retstring="no_controllers"; try { for ( auto &con : this->controller ) { - if ( con.second.pArcDev != nullptr && con.second.connected ) { + if ( con.second.pArcDev != nullptr && con.second.connected && con.second.active ) { uint32_t pixelcount = con.second.pArcDev->getPixelCount(); error=NO_ERROR; retstring = std::to_string(pixelcount); @@ -6437,6 +6682,24 @@ logwrite(function, message.str()); return ERROR; } } + else + // ---------------------------------------------------- + // devnums + // ---------------------------------------------------- + // print the *_devnums vectors + // + if ( testname == "devnums" ) { + std::ostringstream oss; + oss << "configured="; + for (const auto &dev : this->configured_devnums) oss << dev << " "; + oss << " active="; + for (const auto &dev : this->active_devnums) oss << dev << " "; + oss << " connected="; + for (const auto &dev : this->connected_devnums) oss << dev << " "; + logwrite(function, oss.str()); + retstring=oss.str(); + return NO_ERROR; + } else { // ---------------------------------------------------- // invalid test name diff --git a/camerad/astrocam.h b/camerad/astrocam.h index 93e8cfe8..05999020 100644 --- a/camerad/astrocam.h +++ b/camerad/astrocam.h @@ -19,6 +19,9 @@ #include #include #include +#include +#include +#include #include "utilities.h" #include "common.h" @@ -46,6 +49,12 @@ namespace AstroCam { const int NUM_EXPBUF = 3; // number of exposure buffers + enum class ActiveState { + Activate, + DeActivate, + Query + }; + /** * ENUM list for each readout type */ @@ -556,6 +565,7 @@ namespace AstroCam { */ class Interface : public Camera::InterfaceBase { private: + zmqpp::context context; // int bufsize; int FITS_STRING_KEY; int FITS_DOUBLE_KEY; @@ -590,7 +600,8 @@ namespace AstroCam { int num_deinter_thr; //!< number of threads that can de-interlace an image int numdev; //!< total number of Arc devices detected in system std::vector configured_devnums; //!< vector of configured Arc devices (from camerad.cfg file) - std::vector devnums; //!< vector of all opened and connected devices + std::vector active_devnums; //!< vector of active Arc devices + std::vector connected_devnums; //!< vector of all open and connected devices std::mutex epend_mutex; std::vector exposures_pending; //!< vector of devnums that have a pending exposure (which needs to be stored) @@ -600,8 +611,54 @@ namespace AstroCam { void retval_to_string( std::uint32_t check_retval, std::string& retstring ); + inline void remove_dev(const int dev, std::vector &vec) { + auto it = std::find(vec.begin(), vec.end(), dev); + if ( it != vec.end() ) vec.erase(it); + } + + inline void add_dev(const int dev, std::vector &vec) { + auto it = std::find(vec.begin(), vec.end(), dev); + if ( it == vec.end() ) vec.push_back(dev); + } + public: - Interface(); + Interface() + : context(), + pci_cmd_num(0), + nexp(1), + nfpseq(1), + nframes(1), + numdev(0), + is_subscriber_thread_running(false), + should_subscriber_thread_run(false), + framethreadcount(0), + state_monitor_thread_running(false), + can_expose(true), // am I ready for the next exposure? + modeselected(false), + useframes(true) { + this->pFits.resize( NUM_EXPBUF ); // pre-allocate FITS_file object pointers for each exposure buffer + this->fitsinfo.resize( NUM_EXPBUF ); // pre-allocate Camera Information object pointers for each exposure buffer + this->writes_pending.resize( NUM_EXPBUF ); // pre-allocate writes_pending vector for each exposure buffer + + // Initialize STL map of Readout Amplifiers + // Indexed by amplifier name. + // The number is the argument for the Arc command to set this amplifier in the firmware. + // + // Format here is: { AMP_NAME, { ENUM_TYPE, ARC_ARG } } + // where AMP_NAME is the name of the readout amplifier, the index for this map + // ENUM_TYPE is an enum of type ReadoutType + // ARC_ARG is the ARC argument for the SOS command to select this readout source + // + this->readout_source.insert( { "U1", { U1, 0x5f5531 } } ); // "_U1" + this->readout_source.insert( { "L1", { L1, 0x5f4c31 } } ); // "_L1" + this->readout_source.insert( { "U2", { U2, 0x5f5532 } } ); // "_U2" + this->readout_source.insert( { "L2", { L2, 0x5f4c32 } } ); // "_L2" + this->readout_source.insert( { "SPLIT1", { SPLIT1, 0x5f5f31 } } ); // "__1" + this->readout_source.insert( { "SPLIT2", { SPLIT2, 0x5f5f32 } } ); // "__2" + this->readout_source.insert( { "QUAD", { QUAD, 0x414c4c } } ); // "ALL" + this->readout_source.insert( { "FT2", { FT2, 0x465432 } } ); // "FT2" -- frame transfer from 1->2, read split2 + this->readout_source.insert( { "FT1", { FT1, 0x465431 } } ); // "FT1" -- frame transfer from 2->1, read split1 + }; // Class Objects // @@ -609,6 +666,28 @@ namespace AstroCam { Camera::Camera camera; /// instantiate a Camera object Camera::Information camera_info; /// this is the main camera_info object + std::unique_ptr publisher; ///< publisher object + std::string publisher_address; ///< publish socket endpoint + std::string publisher_topic; ///< my default topic for publishing + std::unique_ptr subscriber; ///< subscriber object + std::string subscriber_address; ///< subscribe socket endpoint + std::vector subscriber_topics; ///< list of topics I subscribe to + std::atomic is_subscriber_thread_running; ///< is my subscriber thread running? + std::atomic should_subscriber_thread_run; ///< should my subscriber thread run? + std::unordered_map> topic_handlers; + ///< maps a handler function to each topic + + long init_pubsub(const std::initializer_list &topics={}) { + if (!subscriber) { + subscriber = std::make_unique(context, Common::PubSub::Mode::SUB); + } + return Common::PubSubHandler::init_pubsub(context, *this, topics); + } + void start_subscriber_thread() { Common::PubSubHandler::start_subscriber_thread(*this); } + void stop_subscriber_thread() { Common::PubSubHandler::stop_subscriber_thread(*this); } + void publish_snapshot(std::string* retstring=nullptr); + // vector of pointers to Camera Information containers, one for each exposure number // std::vector> fitsinfo; @@ -650,8 +729,8 @@ std::vector> fitsinfo; */ inline bool is_camera_idle( int dev ) { int num=0; - num += ( this->controller[dev].in_readout ? 1 : 0 ); - num += ( this->controller[dev].in_frametransfer ? 1 : 0 ); + num += ( this->controller.at(dev).in_readout ? 1 : 0 ); + num += ( this->controller.at(dev).in_frametransfer ? 1 : 0 ); std::lock_guard lock( this->epend_mutex ); num += this->exposures_pending.size(); return ( num>0 ? false : true ); @@ -659,9 +738,9 @@ std::vector> fitsinfo; inline bool is_camera_idle() { int num=0; - for ( auto dev : this->devnums ) { - num += ( this->controller[dev].in_readout ? 1 : 0 ); - num += ( this->controller[dev].in_frametransfer ? 1 : 0 ); + for ( auto dev : this->connected_devnums ) { + num += ( this->controller.at(dev).in_readout ? 1 : 0 ); + num += ( this->controller.at(dev).in_frametransfer ? 1 : 0 ); } std::lock_guard lock( this->epend_mutex ); num += this->exposures_pending.size(); @@ -670,7 +749,7 @@ std::vector> fitsinfo; inline bool in_readout() const { int num=0; - for ( auto dev : this->devnums ) { + for ( auto dev : this->connected_devnums ) { num += ( this->controller.at(dev).in_readout ? 1 : 0 ); num += ( this->controller.at(dev).in_frametransfer ? 1 : 0 ); } @@ -679,7 +758,7 @@ std::vector> fitsinfo; inline bool in_frametransfer() const { int num=0; - for ( auto dev : this->devnums ) { + for ( auto dev : this->connected_devnums ) { num += ( this->controller.at(dev).in_frametransfer ? 1 : 0 ); } return( num==0 ? false : true ); @@ -706,6 +785,7 @@ std::vector> fitsinfo; * exposure pending stuff * */ + std::atomic can_expose; std::condition_variable exposure_condition; std::mutex exposure_lock; static void dothread_monitor_exposure_pending( Interface &interface ); @@ -850,7 +930,28 @@ std::vector> fitsinfo; long workbuf_size; public: - Controller(); //!< class constructor + Controller() + : bufsize(0), + framecount(0), + workbuf_size(0), + info(), + workbuf(nullptr), + cols(0), + rows(0), + pArcDev(nullptr), + pCallback(nullptr), + connected(false), + configured(false), + active(false), + is_imsize_set(false), + firmwareloaded(false) + { + info.readout_type = -1; + readout_arg = 0xBAD; + expinfo.resize( NUM_EXPBUF ); // vector of Camera::Information, one for each exposure buffer + info.exposure_unit = "msec"; // chaning unit not currently supported in ARC + } + ~Controller() { }; //!< no deconstructor Camera::Information info; //!< camera info object for this controller @@ -910,7 +1011,8 @@ std::vector> fitsinfo; arc::gen3::CArcDevice* pArcDev; //!< arc::CController object pointer -- things pointed to by this are in the ARC API Callback* pCallback; //!< Callback class object must be pointer because the API functions are virtual bool connected; //!< true if controller connected (requires successful TDL command) - bool inactive; //!< set true to skip future use of controllers when unable to connect + bool configured; //!< set false to skip future use of controllers when unable to connect + bool active; //!< used to disable an otherwise-configured controller bool is_imsize_set; //!< has image_size been called after controller connected? bool firmwareloaded; //!< true if firmware is loaded, false otherwise std::string firmware; //!< name of firmware (.lod) file @@ -926,6 +1028,8 @@ std::vector> fitsinfo; std::atomic in_readout; //!< Is the controller currently reading out/transmitting pixels? std::atomic in_frametransfer; //!< Is the controller currently performing a frame transfer? + std::vector activate_commands; + // Functions // inline uint32_t get_bufsize() { return this->bufsize; }; @@ -978,12 +1082,16 @@ std::vector> fitsinfo; // Functions // + long camera_active_state(const std::string &args, std::string &retstring, AstroCam::ActiveState cmd); + Controller* get_controller(const int dev); + Controller* get_active_controller(const int dev); void exposure_progress(); void make_image_keywords( int dev ); long handle_json_message( std::string message_in ); long parse_spec_info( std::string args ); long parse_det_geometry( std::string args ); long parse_controller_config( std::string args ); + long parse_activate_commands(std::string args); int devnum_from_chan( const std::string &chan ); long extract_dev_chan( std::string args, int &dev, std::string &chan, std::string &retstring ); long test(std::string args, std::string &retstring); ///< test routines @@ -1044,8 +1152,8 @@ std::vector> fitsinfo; */ long do_native(std::string cmdstr); ///< selected or all open controllers long do_native(std::string cmdstr, std::string &retstring); ///< selected or all open controllers, return reply - long do_native(std::vector selectdev, std::string cmdstr); ///< specified by vector - long do_native(std::vector selectdev, std::string cmdstr, std::string &retstring); ///< specified by vector + long do_native(std::vector selectdev, std::string cmdstr); ///< specified by vector + long do_native(std::vector selectdev, std::string cmdstr, std::string &retstring); ///< specified by vector long do_native(int dev, std::string cmdstr, std::string &retstring); ///< specified by devnum long write_frame( int expbuf, int devnum, const std::string chan, int fpbcount ); diff --git a/camerad/camerad.cpp b/camerad/camerad.cpp index 23dda522..d995bf9a 100644 --- a/camerad/camerad.cpp +++ b/camerad/camerad.cpp @@ -178,6 +178,14 @@ int main(int argc, char **argv) { server.exit_cleanly(); } + if (server.init_pubsub()==ERROR) { + logwrite(function, "ERROR initializing publisher-subscriber handler"); + server.exit_cleanly(); + } + + std::this_thread::sleep_for(std::chrono::milliseconds(100)); + server.publish_snapshot(); + // This will pre-thread N_THREADS threads. // The 0th thread is reserved for the blocking port, and the rest are for the non-blocking port. // Each thread gets a socket object. All of the socket objects are stored in a vector container. @@ -598,6 +606,18 @@ void doit(Network::TcpSocket &sock) { } #ifdef ASTROCAM else + if ( cmd == CAMERAD_ACTIVATE ) { + ret=server.camera_active_state(args, retstring, AstroCam::ActiveState::Activate); + } + else + if ( cmd == CAMERAD_DEACTIVATE ) { + ret=server.camera_active_state(args, retstring, AstroCam::ActiveState::DeActivate); + } + else + if ( cmd == CAMERAD_ISACTIVE ) { + ret=server.camera_active_state(args, retstring, AstroCam::ActiveState::Query); + } + else if ( cmd == CAMERAD_MODEXPTIME ) { ret = server.modify_exptime(args, retstring); } @@ -769,6 +789,21 @@ void doit(Network::TcpSocket &sock) { if ( cmd == CAMERAD_TEST ) { ret = server.test(args, retstring); } + else + if ( cmd == SNAPSHOT || cmd == TELEMREQUEST ) { + if ( args=="?" || args=="help" ) { + retstring=TELEMREQUEST+"\n"; + retstring.append( " Returns a serialized JSON message containing telemetry\n" ); + retstring.append( " information, terminated with \"EOF\\n\".\n" ); + ret=HELP; + } + else { + server.publish_snapshot( &retstring ); + if (retstring.empty()) retstring="(empty)"; + ret = JSON; + } + } + // Unknown commands generate an error // else { diff --git a/camerad/camerad.h b/camerad/camerad.h index f63fc4b8..d38dcde8 100644 --- a/camerad/camerad.h +++ b/camerad/camerad.h @@ -223,6 +223,23 @@ namespace Camera { applied++; } + // PUB_ENDPOINT + // + if (config.param[entry]=="PUB_ENDPOINT") { + this->publisher_address=config.arg[entry]; + this->publisher_topic=DAEMON_NAME; + this->camera.async.enqueue_and_log("CAMERAD", function, "CAMERAD:config:"+config.param[entry]+"="+config.arg[entry]); + applied++; + } + + // SUB_ENDPOINT + // + if (config.param[entry]=="SUB_ENDPOINT") { + this->subscriber_address=config.arg[entry]; + this->camera.async.enqueue_and_log("CAMERAD", function, "CAMERAD:config:"+config.param[entry]+"="+config.arg[entry]); + applied++; + } + // USERKEYS_PERSIST: should userkeys persist or be cleared after each exposure // if ( config.param[entry] == "USERKEYS_PERSIST" ) { diff --git a/common/camerad_commands.h b/common/camerad_commands.h index 2411bdc7..97ad2ce3 100644 --- a/common/camerad_commands.h +++ b/common/camerad_commands.h @@ -9,6 +9,7 @@ #pragma once const std::string CAMERAD_ABORT = "abort"; +const std::string CAMERAD_ACTIVATE = "activate"; const std::string CAMERAD_AUTODIR = "autodir"; const std::string CAMERAD_BASENAME = "basename"; const std::string CAMERAD_BIAS = "bias"; @@ -17,6 +18,7 @@ const std::string CAMERAD_BOI = "boi"; const std::string CAMERAD_BUFFER = "buffer"; const std::string CAMERAD_CLOSE = "close"; const std::string CAMERAD_CONFIG = "config"; +const std::string CAMERAD_DEACTIVATE = "deactivate"; const std::string CAMERAD_ECHO = "echo"; const std::string CAMERAD_EXPOSE = "expose"; const std::string CAMERAD_EXPTIME = "exptime"; @@ -28,6 +30,7 @@ const std::string CAMERAD_IMDIR = "imdir"; const std::string CAMERAD_IMNUM = "imnum"; const std::string CAMERAD_IMSIZE = "imsize"; const std::string CAMERAD_INTERFACE = "interface"; +const std::string CAMERAD_ISACTIVE = "isactive"; const std::string CAMERAD_ISOPEN = "isopen"; const std::string CAMERAD_KEY = "key"; const std::string CAMERAD_LOAD = "load"; @@ -48,6 +51,7 @@ const std::string CAMERAD_USEFRAMES = "useframes"; const std::string CAMERAD_WRITEKEYS = "writekeys"; const std::vector CAMERAD_SYNTAX = { CAMERAD_ABORT, + CAMERAD_ACTIVATE, CAMERAD_AUTODIR, CAMERAD_BASENAME, CAMERAD_BIAS, @@ -56,6 +60,7 @@ const std::vector CAMERAD_SYNTAX = { CAMERAD_BUFFER+" ? | | [ | ]", CAMERAD_CLOSE, CAMERAD_CONFIG, + CAMERAD_DEACTIVATE, CAMERAD_ECHO, CAMERAD_EXPOSE, CAMERAD_EXPTIME+" [ ]", @@ -67,6 +72,7 @@ const std::vector CAMERAD_SYNTAX = { CAMERAD_IMNUM, CAMERAD_IMSIZE+" ? | | [ ]", CAMERAD_INTERFACE, + CAMERAD_ISACTIVE, CAMERAD_ISOPEN, CAMERAD_KEY, CAMERAD_LOAD, diff --git a/common/message_keys.h b/common/message_keys.h new file mode 100644 index 00000000..46156854 --- /dev/null +++ b/common/message_keys.h @@ -0,0 +1,39 @@ +/** + * @file message_keys.h + * @brief contains keys for JSON messages + * @author David Hale + * + */ +#pragma once + +#include + +namespace Topic { + inline const std::string SNAPSHOT = "_snapshot"; + 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 SEQ_PROGRESS = "seq_progress"; +} + +namespace Key { + + inline const std::string SOURCE = "source"; + + namespace Camerad { + inline const std::string READY = "ready"; + } + + namespace SeqProgress { + inline const std::string ONTARGET = "ontarget"; + inline const std::string FINE_TUNE_ACTIVE = "fine_tune_active"; + inline const std::string OFFSET_ACTIVE = "offset_active"; + inline const std::string OFFSET_SETTLE = "offset_settle"; + inline const std::string OFFSET_RA = "offset_ra"; + inline const std::string OFFSET_DEC = "offset_dec"; + inline const std::string OBSID = "obsid"; + inline const std::string TARGET_STATE = "target_state"; + inline const std::string EVENT = "event"; + } +} diff --git a/common/ngps_acq_embed.h b/common/ngps_acq_embed.h new file mode 100644 index 00000000..530a1581 --- /dev/null +++ b/common/ngps_acq_embed.h @@ -0,0 +1,39 @@ +/** + * @file ngps_acq_embed.h + * @brief in-process API for NGPS auto-acquire logic + */ + +#pragma once + +#include + +#ifdef __cplusplus +extern "C" { +#endif + +typedef struct ngps_acq_hooks { + void *user; + + int (*tcs_set_native_units)( void *user, int dry_run, int verbose ); + int (*tcs_move_arcsec)( void *user, double dra_arcsec, double ddec_arcsec, + int dry_run, int verbose ); + int (*scam_putonslit_deg)( void *user, + double slit_ra_deg, double slit_dec_deg, + double cross_ra_deg, double cross_dec_deg, + int dry_run, int verbose ); + int (*acam_query_state)( void *user, char *state, size_t state_sz, int verbose ); + int (*scam_framegrab_one)( void *user, const char *outpath, int verbose ); + int (*scam_set_exptime)( void *user, double exptime_sec, int dry_run, int verbose ); + int (*scam_set_avgframes)( void *user, int avgframes, int dry_run, int verbose ); + int (*is_stop_requested)( void *user ); + void (*log_message)( void *user, const char *line ); +} ngps_acq_hooks_t; + +void ngps_acq_set_hooks( const ngps_acq_hooks_t *hooks ); +void ngps_acq_clear_hooks( void ); +void ngps_acq_request_stop( int stop_requested ); +int ngps_acq_run_from_argv( int argc, char **argv ); + +#ifdef __cplusplus +} +#endif diff --git a/common/sequencerd_commands.h b/common/sequencerd_commands.h index cf38c57d..5797ba02 100644 --- a/common/sequencerd_commands.h +++ b/common/sequencerd_commands.h @@ -8,6 +8,7 @@ #ifndef SEQEUNCERD_COMMANDS_H #define SEQEUNCERD_COMMANDS_H const std::string SEQUENCERD_ABORT = "abort"; +const std::string SEQUENCERD_ACQMODE = "acqmode"; const std::string SEQUENCERD_CONFIG = "config"; const std::string SEQUENCERD_DOTYPE = "do"; const std::string SEQUENCERD_EXIT = "exit"; @@ -47,6 +48,7 @@ const std::vector SEQUENCERD_SYNTAX = { SEQUENCERD_TCS+" ...", "", SEQUENCERD_ABORT, + SEQUENCERD_ACQMODE+" [ ? | ]", SEQUENCERD_CONFIG, SEQUENCERD_DOTYPE+" [ one | all ]", SEQUENCERD_EXIT, diff --git a/common/slicecamd_commands.h b/common/slicecamd_commands.h index 1ddad325..08b17dec 100644 --- a/common/slicecamd_commands.h +++ b/common/slicecamd_commands.h @@ -10,6 +10,7 @@ #pragma once const std::string SLICECAMD_AVGFRAMES= "avgframes"; ///< set/get camera binning +const std::string SLICECAMD_AUTOACQ = "autoacq"; ///< run/monitor in-process auto-acquire logic const std::string SLICECAMD_BIN = "bin"; ///< set/get camera binning const std::string SLICECAMD_CLOSE = "close"; ///< *** close connection to all devices const std::string SLICECAMD_CONFIG = "config"; ///< reload configuration, apply what can be applied @@ -69,6 +70,7 @@ const std::vector SLICECAMD_SYNTAX = { SLICECAMD_SPEED+" [ ? | ]", SLICECAMD_TEMP+" [ ? | ]", " OTHER:", + SLICECAMD_AUTOACQ+" [ ? | start [--log-file ] [] | stop | status ]", SLICECAMD_PUTONSLIT+" [ ? | ]", SLICECAMD_SAVEFRAMES+" [ ? | ]", SLICECAMD_SHUTDOWN+" [ ? ]", diff --git a/run/seq-progress b/run/seq-progress new file mode 100755 index 00000000..4c7e1746 --- /dev/null +++ b/run/seq-progress @@ -0,0 +1,10 @@ +#!/bin/bash +# +# Launch the sequencer progress popup GUI +# + +SCRIPT_DIR="$(cd -- "$( dirname -- "${BASH_SOURCE[0]}" )" &> /dev/null && pwd)" +export NGPS_ROOT="$(cd -- "${SCRIPT_DIR}/.." && pwd)" + +CONFIG="${NGPS_ROOT}/Config/sequencerd.cfg" +exec "${NGPS_ROOT}/bin/seq_progress_gui" --config "${CONFIG}" --poll-ms 10000 diff --git a/sequencerd/sequence.cpp b/sequencerd/sequence.cpp index f321c6a7..f86f11c0 100644 --- a/sequencerd/sequence.cpp +++ b/sequencerd/sequence.cpp @@ -13,6 +13,8 @@ #include "sequence.h" +#include + namespace Sequencer { constexpr long CAMERA_PROLOG_TIMEOUT = 6000; ///< timeout msec to send camera prolog command @@ -40,6 +42,57 @@ namespace Sequencer { /***** Sequencer::Sequence::handletopic_snapshot ***************************/ + /***** Sequencer::Sequence::handletopic_slicecam_autoacq ********************/ + /** + * @brief handles slicecamd autoacq status updates + * @param[in] jmessage_in subscribed-received JSON message + * + */ + void Sequence::handletopic_slicecam_autoacq( const nlohmann::json &jmessage_in ) { + if ( !jmessage_in.contains("state") ) return; + if ( jmessage_in.contains("source") && + jmessage_in.at("source").is_string() && + jmessage_in.at("source").get() != "slicecamd" ) return; + + std::string state = jmessage_in.value( "state", "" ); + std::string message = jmessage_in.value( "message", "" ); + uint64_t run_id = jmessage_in.value( "run_id", static_cast(0) ); + + bool should_notify = false; + bool should_publish = false; + { + std::lock_guard lock( this->fine_tune_mtx ); + + // Ignore stale messages when waiting on a specific run_id. + if ( this->fine_tune_run_id != 0 && run_id != 0 && run_id != this->fine_tune_run_id ) return; + + if ( state == "running" ) { + this->fine_tune_active.store( true, std::memory_order_release ); + this->fine_tune_done = false; + this->fine_tune_success = false; + this->fine_tune_message = message; + should_publish = true; + } + else if ( state == "success" || state == "failed" || state == "aborted" ) { + this->fine_tune_active.store( false, std::memory_order_release ); + this->fine_tune_done = true; + this->fine_tune_success = ( state == "success" ); + this->fine_tune_message = message; + should_notify = true; + should_publish = true; + } + else if ( state == "idle" ) { + this->fine_tune_active.store( false, std::memory_order_release ); + should_publish = true; + } + } + + if ( should_publish ) this->publish_progress(); + if ( should_notify ) this->fine_tune_cv.notify_all(); + } + /***** Sequencer::Sequence::handletopic_slicecam_autoacq ********************/ + + /***** Sequencer::Sequence::publish_snapshot *******************************/ /** * @brief publishes snapshot of my telemetry @@ -55,6 +108,7 @@ namespace Sequencer { this->publish_seqstate(); this->publish_waitstate(); this->publish_daemonstate(); + this->publish_progress(); } /***** Sequencer::Sequence::publish_snapshot *******************************/ @@ -177,6 +231,65 @@ namespace Sequencer { /***** Sequencer::Sequence::publish_threadstate *****************************/ + /***** Sequencer::Sequence::publish_progress ********************************/ + /** + * @brief publishes progress information with topic "seq_progress" + * @details publishes fine-grained progress tracking for seq-progress GUI + * + */ + void Sequence::publish_progress() { + nlohmann::json jmessage_out; + jmessage_out["source"] = Sequencer::DAEMON_NAME; + + // Track fine-tune status from slicecamd autoacq state + jmessage_out["fine_tune_active"] = this->fine_tune_active.load(std::memory_order_acquire); + + // Track offset status + jmessage_out["offset_active"] = this->offset_active.load(); + + // offset_settle is true when we're waiting after applying offset + // (determined by caller context - set before calling publish_progress) + jmessage_out["offset_settle"] = this->offset_active.load(); // same as offset_active for simplicity + + // ontarget status + jmessage_out["ontarget"] = this->is_ontarget.load(); + + // Current target info + jmessage_out["obsid"] = this->target.obsid; + jmessage_out["target_state"] = this->target.state; + + // Target offset values (arcsec) + jmessage_out["offset_ra"] = this->target.offset_ra; + jmessage_out["offset_dec"] = this->target.offset_dec; + + // Number of exposures for this target + jmessage_out["nexp"] = this->target.nexp; + + // Current acquisition mode + jmessage_out["acqmode"] = this->acq_automatic_mode; + + // Explicit USER gate action for GUI button labeling + std::string user_gate_action = "NONE"; + switch ( this->user_gate_action.load() ) { + case USER_GATE_ACQUIRE: user_gate_action = "ACQUIRE"; break; + case USER_GATE_EXPOSE: user_gate_action = "EXPOSE"; break; + case USER_GATE_OFFSET_EXPOSE:user_gate_action = "OFFSET_EXPOSE"; break; + default: user_gate_action = "NONE"; break; + } + jmessage_out["user_gate_action"] = user_gate_action; + + try { + this->publisher->publish( jmessage_out, "seq_progress" ); + } + catch ( const std::exception &e ) { + logwrite( "Sequencer::Sequence::publish_progress", + "ERROR publishing message: "+std::string(e.what()) ); + return; + } + } + /***** Sequencer::Sequence::publish_progress ********************************/ + + /***** Sequencer::Sequence::broadcast_daemonstate ***************************/ /** * @brief publishes daemonstate and can control seqstate @@ -306,6 +419,36 @@ namespace Sequencer { } } + // -------------------------------------------------------------- + // Republish EXPTIME messages to ZMQ for seq-progress GUI + // Parse EXPTIME:remaining total percent and publish to seq_progress + // Rate-limited: only publish when percent changes to reduce message spam + // -------------------------------------------------------------- + // + if ( statstr.compare( 0, 8, "EXPTIME:" ) == 0 ) { // async message tag EXPTIME: + // Parse "EXPTIME:remaining total percent" + std::string vals = statstr.substr(8); // Skip "EXPTIME:" + std::istringstream iss(vals); + int remaining, total, percent; + if (iss >> remaining >> total >> percent) { + static int last_published_percent = -1; + // Only publish when percentage changes (rate limiting) + if (percent != last_published_percent) { + nlohmann::json jmessage; + jmessage["source"] = Sequencer::DAEMON_NAME; + jmessage["exptime_remaining_ms"] = remaining; + jmessage["exptime_total_ms"] = total; + jmessage["exptime_percent"] = percent; + try { + seq.publisher->publish( jmessage, "seq_progress" ); + last_published_percent = percent; + } catch (...) { + // Ignore publish errors + } + } + } + } + // ------------------------------------------------------------------ // Set READOUT flag and clear EXPOSE flag when pixels start coming in // ------------------------------------------------------------------ @@ -318,12 +461,31 @@ namespace Sequencer { // --------------------------------------------- // clear READOUT flag on the end-of-frame signal + // Parse and publish frame count for seq-progress GUI // --------------------------------------------- // if ( statstr.compare( 0, 10, "FRAMECOUNT" ) == 0 ) { // async message tag FRAMECOUNT if ( seq.wait_state_manager.is_set( Sequencer::SEQ_WAIT_READOUT ) ) { seq.wait_state_manager.clear( Sequencer::SEQ_WAIT_READOUT ); } + // Parse frame number from "FRAMECOUNT_: rows=X cols=Y" + size_t colon_pos = statstr.find(':'); + if (colon_pos != std::string::npos) { + size_t space_pos = statstr.find(' ', colon_pos); + try { + std::string frame_str = statstr.substr(colon_pos + 1, + space_pos == std::string::npos ? std::string::npos : space_pos - colon_pos - 1); + int framenum = std::stoi(frame_str); + // Publish frame count to seq_progress topic + nlohmann::json jmessage; + jmessage["source"] = Sequencer::DAEMON_NAME; + jmessage["current_frame"] = framenum; + jmessage["nexp"] = seq.target.nexp; + seq.publisher->publish(jmessage, "seq_progress"); + } catch (...) { + // Ignore parse errors + } + } } // --------------------- @@ -463,6 +625,7 @@ namespace Sequencer { // message.str(""); message << "TARGETSTATE:" << this->target.state << " TARGET:" << this->target.name << " OBSID:" << this->target.obsid; this->async.enqueue( message.str() ); + this->publish_progress(); // Publish new target info (obsid, target_state) #ifdef LOGLEVEL_DEBUG logwrite( function, "[DEBUG] target found, starting threads" ); #endif @@ -560,40 +723,331 @@ namespace Sequencer { * 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 then handle acquisition automation // 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 ); + std::stringstream mode_msg; + mode_msg << "NOTICE: acquisition automation mode " << this->acq_automatic_mode; + this->async.enqueue_and_log( function, mode_msg.str() ); + } - this->async.enqueue_and_log( function, "NOTICE: waiting for USER to send \"continue\" signal" ); + auto wait_for_user = [&](const std::string ¬ice, UserGateAction action) -> bool { + this->is_usercontinue.store(false); + ScopedState wait_state( wait_state_manager, Sequencer::SEQ_WAIT_USER ); + this->user_gate_action.store( action ); + this->publish_progress(); + this->async.enqueue_and_log( function, "NOTICE: "+notice ); + 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!" ); + this->user_gate_action.store( USER_GATE_NONE ); + this->publish_progress(); + if ( this->cancel_flag.load() ) return false; + this->is_usercontinue.store(false); + return true; + }; + + auto wait_for_guiding = [&]() -> long { + ScopedState wait_state( wait_state_manager, Sequencer::SEQ_WAIT_GUIDE ); + this->async.enqueue_and_log( function, "NOTICE: waiting for ACAM guiding" ); + auto start_time = std::chrono::steady_clock::now(); + const bool use_timeout = ( this->acquisition_timeout > 0 ); + const auto timeout = std::chrono::duration( this->acquisition_timeout ); + while ( !this->cancel_flag.load() ) { + std::string reply; + if ( this->acamd.command( ACAMD_ACQUIRE, reply ) != NO_ERROR ) { + logwrite( function, "ERROR reading ACAM acquire state" ); + return ERROR; + } + if ( reply.find( "guiding" ) != std::string::npos ) return NO_ERROR; + if ( reply.find( "stopped" ) != std::string::npos ) return ERROR; + if ( use_timeout && std::chrono::steady_clock::now() > ( start_time + timeout ) ) return TIMEOUT; + std::this_thread::sleep_for( std::chrono::milliseconds(500) ); + } + return ERROR; + }; + + auto run_fine_tune = [&]() -> long { + if ( this->acq_fine_tune_cmd.empty() ) return NO_ERROR; + std::string cmd_to_run = this->acq_fine_tune_cmd; + std::string acq_logfile; + if ( this->acq_fine_tune_log ) { + std::string datedir = get_latest_datedir( "/data" ); + if ( !datedir.empty() ) { + std::stringstream logfilename; + logfilename << "/data/" << datedir << "/logs/ngps_acq_" << datedir << ".log"; + acq_logfile = logfilename.str(); + } + else { + acq_logfile = "/tmp/ngps_acq.log"; + this->async.enqueue_and_log( function, "NOTICE: /data datedir not found, logging fine tune output to "+acq_logfile ); + } + } - 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() ); } ); - } + auto append_fine_tune_log = [&]( const std::string &line ) { + if ( !this->acq_fine_tune_log || acq_logfile.empty() ) return; + std::ofstream logfile( acq_logfile, std::ios::app ); + if ( logfile.is_open() ) { + logfile << "=== SEQUENCER: " << line << " ===" << std::endl; + } + }; - 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->acq_fine_tune_log && !acq_logfile.empty() ) { + this->async.enqueue_and_log( function, "NOTICE: logging fine tune output to "+acq_logfile ); + } - if ( this->cancel_flag.load() ) { - this->async.enqueue_and_log( function, "NOTICE: sequence cancelled" ); - return; + this->async.enqueue_and_log( function, "NOTICE: starting slicecamd autoacq: "+cmd_to_run ); + + { + std::lock_guard lock( this->fine_tune_mtx ); + this->fine_tune_done = false; + this->fine_tune_success = false; + this->fine_tune_run_id = 0; + this->fine_tune_message.clear(); + } + + std::string start_reply; + std::string start_cmd = SLICECAMD_AUTOACQ + " start"; + if ( !acq_logfile.empty() ) start_cmd += " --log-file " + acq_logfile; + if ( !cmd_to_run.empty() ) start_cmd += " " + cmd_to_run; + this->fine_tune_active.store( true, std::memory_order_release ); + this->publish_progress(); + + if ( this->slicecamd.command( start_cmd, start_reply ) != NO_ERROR ) { + this->fine_tune_active.store( false, std::memory_order_release ); + this->publish_progress(); + this->async.enqueue_and_log( function, "ERROR failed to start slicecamd autoacq" ); + append_fine_tune_log( "failed to start slicecamd autoacq" ); + return ERROR; + } + + auto runpos = start_reply.find( "run_id=" ); + if ( runpos != std::string::npos ) { + try { + std::string runid_str = start_reply.substr( runpos + 7 ); + std::vector tokens; + Tokenize( runid_str, tokens, " " ); + if ( !tokens.empty() ) { + std::lock_guard lock( this->fine_tune_mtx ); + this->fine_tune_run_id = std::stoull( tokens.at(0) ); + } + } + catch ( const std::exception & ) { } + } + + bool sent_stop = false; + bool timed_out = false; + auto stop_deadline = std::chrono::steady_clock::time_point::min(); + auto next_status_poll = std::chrono::steady_clock::now(); + const bool use_timeout = ( this->acquisition_timeout > 0 ); + const auto fine_tune_timeout = std::chrono::duration( this->acquisition_timeout ); + const auto fine_tune_start = std::chrono::steady_clock::now(); + while ( true ) { + { + std::unique_lock lock( this->fine_tune_mtx ); + if ( this->fine_tune_cv.wait_for( lock, std::chrono::milliseconds(200), + [this](){ return this->fine_tune_done || this->cancel_flag.load(); } ) ) { + if ( this->fine_tune_done ) break; + } + } + + // Fallback in case status topic messages are delayed or dropped. + if ( std::chrono::steady_clock::now() >= next_status_poll ) { + next_status_poll = std::chrono::steady_clock::now() + std::chrono::seconds(1); + std::string status_reply; + if ( this->slicecamd.command( SLICECAMD_AUTOACQ + " status", status_reply ) == NO_ERROR ) { + auto pos = status_reply.find( "state=" ); + if ( pos != std::string::npos ) { + std::string statestr = status_reply.substr( pos + 6 ); + std::vector tokens; + Tokenize( statestr, tokens, " " ); + if ( !tokens.empty() ) { + if ( tokens.at(0) == "success" || tokens.at(0) == "failed" || tokens.at(0) == "aborted" ) { + std::lock_guard lock( this->fine_tune_mtx ); + this->fine_tune_done = true; + if ( timed_out ) { + this->fine_tune_success = false; + this->fine_tune_message = "fine tune timeout"; + } + else { + this->fine_tune_success = ( tokens.at(0) == "success" ); + this->fine_tune_message = "fine tune " + tokens.at(0); + } + break; + } + } + } + } + } + + if ( use_timeout && !timed_out && + std::chrono::steady_clock::now() > ( fine_tune_start + fine_tune_timeout ) ) { + timed_out = true; + this->async.enqueue_and_log( function, + "ERROR: fine tune timed out after " + +std::to_string(this->acquisition_timeout) + +" s; stopping slicecamd autoacq" ); + { + std::lock_guard lock( this->fine_tune_mtx ); + this->fine_tune_success = false; + this->fine_tune_message = "fine tune timeout"; + } + if ( !sent_stop ) { + this->slicecamd.command( SLICECAMD_AUTOACQ + " stop" ); + sent_stop = true; + stop_deadline = std::chrono::steady_clock::now() + std::chrono::seconds(5); + } + } + + if ( timed_out && stop_deadline != std::chrono::steady_clock::time_point::min() && + std::chrono::steady_clock::now() > stop_deadline ) { + std::lock_guard lock( this->fine_tune_mtx ); + this->fine_tune_done = true; + this->fine_tune_success = false; + if ( this->fine_tune_message.empty() ) this->fine_tune_message = "fine tune timeout"; + break; + } + + if ( this->cancel_flag.load() ) { + if ( !sent_stop ) { + this->async.enqueue_and_log( function, "NOTICE: abort requested; stopping slicecamd autoacq" ); + this->slicecamd.command( SLICECAMD_AUTOACQ + " stop" ); + sent_stop = true; + stop_deadline = std::chrono::steady_clock::now() + std::chrono::seconds(5); + } + else + if ( stop_deadline != std::chrono::steady_clock::time_point::min() && + std::chrono::steady_clock::now() > stop_deadline ) { + break; + } + } + } + + bool success = false; + std::string fine_tune_message; + { + std::lock_guard lock( this->fine_tune_mtx ); + success = this->fine_tune_success; + fine_tune_message = this->fine_tune_message; + this->fine_tune_run_id = 0; + } + + this->fine_tune_active.store( false, std::memory_order_release ); + this->publish_progress(); + + if ( success ) { + this->async.enqueue_and_log( function, "NOTICE: fine tune complete" ); + append_fine_tune_log( "fine tune complete" ); + return NO_ERROR; + } + + if ( fine_tune_message.empty() ) fine_tune_message = "fine tune failed"; + this->async.enqueue_and_log( function, "ERROR: "+fine_tune_message ); + append_fine_tune_log( fine_tune_message ); + return ERROR; + }; + + if ( this->acq_automatic_mode == 1 ) { + if ( !wait_for_user( "waiting for USER to send \"continue\" signal", USER_GATE_EXPOSE ) ) { + this->async.enqueue_and_log( function, "NOTICE: sequence cancelled" ); + return; + } } + else { + if ( this->acq_automatic_mode == 2 ) { + if ( !wait_for_user( "waiting for USER to send \"continue\" signal to start acquisition", USER_GATE_ACQUIRE ) ) { + this->async.enqueue_and_log( function, "NOTICE: sequence cancelled" ); + return; + } + } - this->is_usercontinue.store(false); + this->async.enqueue_and_log( function, "NOTICE: starting acquisition" ); + std::thread( &Sequencer::Sequence::dothread_acquisition, this ).detach(); + + long acqerr = wait_for_guiding(); + if ( acqerr != NO_ERROR ) { + std::string reason = ( acqerr == TIMEOUT ? "timeout" : "error" ); + this->async.enqueue_and_log( function, "WARNING: failed to reach guiding state ("+reason+"); falling back to manual continue" ); + UserGateAction gate_action = ( this->target.offset_ra != 0.0 || this->target.offset_dec != 0.0 ) + ? USER_GATE_OFFSET_EXPOSE : USER_GATE_EXPOSE; + if ( !wait_for_user( "waiting for USER to send \"continue\" signal to apply offset and expose (guiding failed)", gate_action ) ) { + this->async.enqueue_and_log( function, "NOTICE: sequence cancelled" ); + return; + } + // Apply offset if target has one, even though guiding failed + if ( this->target.offset_ra != 0.0 || this->target.offset_dec != 0.0 ) { + // Zero TCS offsets first so observer sees only the target offset + this->async.enqueue_and_log( function, "NOTICE: zeroing TCS offsets before applying target offset" ); + error |= this->tcsd.command( TCSD_NATIVE + " z" ); + this->async.enqueue_and_log( function, "NOTICE: applying target offset" ); + this->offset_active.store(true); + this->publish_progress(); + error |= this->target_offset(); + this->offset_active.store(false); + if ( error != NO_ERROR ) { + this->thread_error_manager.set( THR_ACQUISITION ); + this->publish_progress(); + return; + } + if ( this->acq_offset_settle > 0 ) { + this->async.enqueue_and_log( function, "NOTICE: waiting for offset settle time" ); + std::this_thread::sleep_for( std::chrono::duration( this->acq_offset_settle ) ); + } + this->publish_progress(); + } + } + else { + bool fine_tune_ok = ( run_fine_tune() == NO_ERROR ); + if ( !fine_tune_ok ) { + this->async.enqueue_and_log( function, "WARNING: fine tune failed; waiting for USER continue to apply offset and expose" ); + UserGateAction gate_action = ( this->target.offset_ra != 0.0 || this->target.offset_dec != 0.0 ) + ? USER_GATE_OFFSET_EXPOSE : USER_GATE_EXPOSE; + if ( !wait_for_user( "waiting for USER to send \"continue\" signal to apply offset and expose (fine tune failed)", gate_action ) ) { + this->async.enqueue_and_log( function, "NOTICE: sequence cancelled" ); + return; + } + } + + // acqmode 2: wait for user before offset (only if fine-tune succeeded) + if ( fine_tune_ok && this->acq_automatic_mode == 2 ) { + UserGateAction gate_action = ( this->target.offset_ra != 0.0 || this->target.offset_dec != 0.0 ) + ? USER_GATE_OFFSET_EXPOSE : USER_GATE_EXPOSE; + if ( !wait_for_user( "waiting for USER to send \"continue\" signal to apply offset and expose", gate_action ) ) { + this->async.enqueue_and_log( function, "NOTICE: sequence cancelled" ); + return; + } + } - this->async.enqueue_and_log( function, "NOTICE: received USER continue signal!" ); + // Apply offset for both acqmode 2 and 3, regardless of fine-tune success + // If target has offset defined, apply it before exposing + if ( this->target.offset_ra != 0.0 || this->target.offset_dec != 0.0 ) { + // Zero TCS offsets first so observer sees only the target offset + this->async.enqueue_and_log( function, "NOTICE: zeroing TCS offsets before applying target offset" ); + error |= this->tcsd.command( TCSD_NATIVE + " z" ); + std::string mode_str = (this->acq_automatic_mode == 3 && fine_tune_ok) ? "automatically " : ""; + this->async.enqueue_and_log( function, "NOTICE: applying target offset " + mode_str ); + this->offset_active.store(true); + this->publish_progress(); // Publish offset_active=true + error |= this->target_offset(); + this->offset_active.store(false); + if ( error != NO_ERROR ) { + this->thread_error_manager.set( THR_ACQUISITION ); + this->publish_progress(); // Publish with offset error state + return; + } + if ( this->acq_offset_settle > 0 ) { + this->async.enqueue_and_log( function, "NOTICE: waiting for offset settle time" ); + std::this_thread::sleep_for( std::chrono::duration( this->acq_offset_settle ) ); + } + this->publish_progress(); // Publish offset complete + } + } + } // Ensure slit offset is in "expose" position // @@ -682,6 +1136,7 @@ namespace Sequencer { // message.str(""); message << "TARGETSTATE:" << this->target.state << " TARGET:" << this->target.name << " OBSID:" << this->target.obsid; this->async.enqueue( message.str() ); + this->publish_progress(); // Publish target completion state // Check the "dotype" -- // If this was "do one" then do_once is set and get out now. @@ -791,12 +1246,7 @@ namespace Sequencer { modestr = "EXPOSE"; break; case Sequencer::VSM_ACQUIRE: - // uses virtual-mode width and offset for acquire, - // but only for new targets - if ( this->target.ra_hms == this->last_ra_hms && - this->target.dec_dms == this->last_dec_dms ) { - return NO_ERROR; - } + // uses virtual-mode width and offset for acquire slitcmd << this->slitwidthacquire << " " << this->slitoffsetacquire; modestr = "ACQUIRE"; break; @@ -2126,19 +2576,16 @@ namespace Sequencer { // Get the calibration target map. // This contains a map of all the required settings, indexed by target name. + // get_info() throws exception if not found, so no need to check for null. // - auto calinfo = this->caltarget.get_info(name); - if (!calinfo) { - logwrite( function, "ERROR unrecognized calibration target: "+name ); - throw std::runtime_error("unrecognized calibration target: "+name); - } + const auto &calinfo = this->caltarget.get_info(name); // set the calib door and cover // std::stringstream cmd; cmd.str(""); cmd << CALIBD_SET - << " door=" << ( calinfo->caldoor ? "open" : "close" ) - << " cover=" << ( calinfo->calcover ? "open" : "close" ); + << " door=" << ( calinfo.caldoor ? "open" : "close" ) + << " cover=" << ( calinfo.calcover ? "open" : "close" ); logwrite( function, "calib: "+cmd.str() ); if ( !this->cancel_flag.load() && @@ -2149,7 +2596,7 @@ namespace Sequencer { // set the internal calibration lamps // - for ( const auto &[lamp,state] : calinfo->lamp ) { + for ( const auto &[lamp,state] : calinfo.lamp ) { if ( this->cancel_flag.load() ) break; cmd.str(""); cmd << lamp << " " << (state?"on":"off"); message.str(""); message << "power " << cmd.str(); @@ -2176,7 +2623,7 @@ namespace Sequencer { // set the lamp modulators // - for ( const auto &[mod,state] : calinfo->lampmod ) { + for ( const auto &[mod,state] : calinfo.lampmod ) { if ( this->cancel_flag.load() ) break; cmd.str(""); cmd << CALIBD_LAMPMOD << " " << mod << " " << (state?1:0) << " 1000"; if ( this->calibd.command( cmd.str() ) != NO_ERROR ) { @@ -2222,6 +2669,15 @@ namespace Sequencer { this->cancel_flag.store(true); this->cv.notify_all(); + // request slicecamd to stop autoacq if it is running + // + if ( this->fine_tune_active.load(std::memory_order_acquire) ) { + logwrite( function, "NOTICE: stopping slicecamd autoacq" ); + this->slicecamd.command( SLICECAMD_AUTOACQ + " stop" ); + this->fine_tune_active.store( false, std::memory_order_release ); + this->publish_progress(); + } + // drop into do-one to prevent auto increment to next target // this->do_once.store(true); @@ -3394,6 +3850,26 @@ 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 ) { + // 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; + } + error = this->tcsd.command( TCSD_ZERO_OFFSETS ); std::stringstream cmd; @@ -4553,13 +5029,16 @@ namespace Sequencer { else // --------------------------------------------------------- - // clearlasttarget -- clear the last target name, allowing repointing - // to the same target (otherwise move_to_target won't - // repoint the telescope if the name is the same) + // clearlasttarget -- clear the last target name and coordinates, allowing + // full re-acquisition of the same target (otherwise + // move_to_target and acquisition will skip if coords match) // --------------------------------------------------------- // if ( testname == "clearlasttarget" ) { this->last_target=""; + this->last_ra_hms=""; + this->last_dec_dms=""; + this->async.enqueue_and_log( function, "cleared last target coordinates" ); error=NO_ERROR; } else diff --git a/sequencerd/sequence.h b/sequencerd/sequence.h index 8703c1d6..a10681d6 100644 --- a/sequencerd/sequence.h +++ b/sequencerd/sequence.h @@ -15,6 +15,8 @@ #include #include #include +#include +#include #include #include #include @@ -153,6 +155,7 @@ namespace Sequencer { SEQ_WAIT_TCS, ///< set when waiting for tcs // states SEQ_WAIT_ACQUIRE, ///< set when waiting for acquire + SEQ_WAIT_GUIDE, ///< set when waiting for guiding state SEQ_WAIT_EXPOSE, ///< set when waiting for camera exposure SEQ_WAIT_READOUT, ///< set when waiting for camera readout SEQ_WAIT_TCSOP, ///< set when waiting specifically for tcs operator @@ -173,12 +176,24 @@ namespace Sequencer { {SEQ_WAIT_TCS, "TCS"}, // states {SEQ_WAIT_ACQUIRE, "ACQUIRE"}, + {SEQ_WAIT_GUIDE, "GUIDE"}, {SEQ_WAIT_EXPOSE, "EXPOSE"}, {SEQ_WAIT_READOUT, "READOUT"}, {SEQ_WAIT_TCSOP, "TCSOP"}, {SEQ_WAIT_USER, "USER"} }; + /** + * @enum UserGateAction + * @brief explicit action represented by a USER wait gate + */ + enum UserGateAction : int { + USER_GATE_NONE = 0, + USER_GATE_ACQUIRE, + USER_GATE_EXPOSE, + USER_GATE_OFFSET_EXPOSE + }; + /** * @enum ThreadStatusBits * @brief assigns each thread a bit in a threadstate word @@ -286,6 +301,9 @@ 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 fine_tune_active{false}; ///< fine tune running state reported by slicecamd autoacq + std::atomic offset_active{false}; ///< tracks offset operation in progress + std::atomic user_gate_action{USER_GATE_NONE}; ///< explicit USER gate action for GUI labeling /** @brief safely runs function in a detached thread using lambda to catch exceptions */ @@ -312,6 +330,10 @@ namespace Sequencer { arm_readout_flag(false), acquisition_timeout(0), acquisition_max_retrys(-1), + acq_automatic_mode(1), + acq_fine_tune_cmd("ngps_acq"), + acq_fine_tune_log(false), + acq_offset_settle(0), tcs_offsetrate_ra(45), tcs_offsetrate_dec(45), tcs_settle_timeout(10), @@ -335,7 +357,9 @@ namespace Sequencer { topic_handlers = { { "_snapshot", std::function( - [this](const nlohmann::json &msg) { handletopic_snapshot(msg); } ) } + [this](const nlohmann::json &msg) { handletopic_snapshot(msg); } ) }, + { "slicecam_autoacq", std::function( + [this](const nlohmann::json &msg) { handletopic_slicecam_autoacq(msg); } ) } }; } @@ -369,6 +393,10 @@ namespace Sequencer { double acquisition_timeout; ///< timeout for target acquisition (in sec) set by configuration parameter ACAM_ACQUIRE_TIMEOUT int acquisition_max_retrys; ///< max number of acquisition loop attempts + int acq_automatic_mode; ///< acquisition automation mode (1=legacy, 2=semi-auto, 3=auto) + std::string acq_fine_tune_cmd; ///< optional fine-tune args override passed to slicecamd autoacq + bool acq_fine_tune_log; ///< log fine-tune output to /data//logs/ngps_acq_.log + double acq_offset_settle; ///< seconds to wait after automatic offset double tcs_offsetrate_ra; ///< TCS offset rate RA ("MRATE") in arcsec per second double tcs_offsetrate_dec; ///< TCS offset rate DEC ("MRATE") in arcsec per second double tcs_settle_timeout; ///< timeout for telescope to settle (in sec) set by configuration parameter TCS_SETTLE_TIMEOUT @@ -382,6 +410,12 @@ namespace Sequencer { std::mutex wait_mtx; std::condition_variable cv; std::mutex cv_mutex; + std::mutex fine_tune_mtx; + std::condition_variable fine_tune_cv; + bool fine_tune_done{false}; + bool fine_tune_success{false}; + uint64_t fine_tune_run_id{0}; + std::string fine_tune_message; std::mutex monitor_mtx; std::map sequence_states; @@ -448,12 +482,14 @@ namespace Sequencer { void stop_subscriber_thread() { Common::PubSubHandler::stop_subscriber_thread(*this); } void handletopic_snapshot( const nlohmann::json &jmessage ); + void handletopic_slicecam_autoacq( const nlohmann::json &jmessage ); void publish_snapshot(); void publish_snapshot(std::string &retstring); void publish_seqstate(); void publish_waitstate(); void publish_daemonstate(); void publish_threadstate(); + void publish_progress(); std::unique_ptr publisher; ///< publisher object std::string publisher_address; ///< publish socket endpoint diff --git a/sequencerd/sequencer_interface.cpp b/sequencerd/sequencer_interface.cpp index e85740d9..810feb63 100644 --- a/sequencerd/sequencer_interface.cpp +++ b/sequencerd/sequencer_interface.cpp @@ -689,7 +689,7 @@ namespace Sequencer { // if ( ( this->ra_hms.empty() && ! this->dec_dms.empty() ) || ( ! this->ra_hms.empty() && this->dec_dms.empty() ) ) { - message.str(""); message << "ERROR cannot have only RA or only DEC empty. both must be empty or filled"; + message << "ERROR cannot have only RA or only DEC empty. both must be empty or filled"; status = message.str(); logwrite( function, message.str() ); return ERROR; @@ -700,7 +700,7 @@ namespace Sequencer { if ( ! this->ra_hms.empty() ) { double _rah = radec_to_decimal( this->ra_hms ); // convert RA from HH:MM:SS.s to decimal hours if ( _rah < 0 ) { - message.str(""); message << "ERROR cannot have negative RA " << this->ra_hms; + message << "ERROR cannot have negative RA " << this->ra_hms; status = message.str(); logwrite( function, message.str() ); return ERROR; @@ -712,7 +712,7 @@ namespace Sequencer { if ( ! this->dec_dms.empty() ) { double _dec = radec_to_decimal( this->dec_dms ); // convert DEC from DD:MM:SS.s to decimal degrees if ( _dec < -90.0 || _dec > 90.0 ) { - message.str(""); message << "ERROR declination " << this->dec_dms << " outside range {-90:+90}"; + message << "ERROR declination " << this->dec_dms << " outside range {-90:+90}"; status = message.str(); logwrite( function, message.str() ); return ERROR; @@ -727,14 +727,18 @@ namespace Sequencer { else { if ( ! caseCompareString( this->pointmode, Acam::POINTMODE_ACAM ) && ! caseCompareString( this->pointmode, Acam::POINTMODE_SLIT ) ) { - message.str(""); message << "ERROR invalid pointmode \"" << this->pointmode << "\": must be { " - << Acam::POINTMODE_ACAM << " " << Acam::POINTMODE_SLIT << " }"; + message << "ERROR invalid pointmode \"" << this->pointmode << "\": must be { " + << Acam::POINTMODE_ACAM << " " << Acam::POINTMODE_SLIT << " }"; status = message.str(); logwrite( function, message.str() ); return ERROR; } } + // number of exposures must be >= 1 + // + if (this->nexp <= 0) this->nexp=1; + return NO_ERROR; } /***** Sequencer::TargetInfo::target_qc_check *******************************/ @@ -939,15 +943,26 @@ namespace Sequencer { */ long CalibrationTarget::configure( const std::string &args ) { const std::string function("Sequencer::CalibrationTarget::configure"); - std::stringstream message; std::vector tokens; + // helpers + auto on_off = [](const std::string &s) { + if (s=="on") return true; + if (s=="off") return false; + throw std::runtime_error("expected on|off but got '"+s+"'"); + }; + auto open_close = [](const std::string &s) { + if (s=="open") return true; + if (s=="close") return false; + throw std::runtime_error("expected open|close but got '"+s+"'"); + }; + auto size = Tokenize( args, tokens, " \t" ); - // there must be 15 args. see cfg file for complete description - if ( size != 15 ) { - message << "ERROR expected 15 but received " << size << " parameters"; - logwrite( function, message.str() ); + // there must be 19 args. see cfg file for complete description + if ( size != 19 ) { + logwrite(function, "ERROR bad config file. expected 19 but received " + +std::to_string(size)+" parameters"); return ERROR; } @@ -956,64 +971,43 @@ namespace Sequencer { std::string name(tokens[0]); if ( name.empty() || ( name != "SCIENCE" && name.compare(0, 4, "CAL_") !=0 ) ) { - message << "ERROR invalid calibration target name \"" << name << "\": must be \"SCIENCE\" or start with \"CAL_\" "; + logwrite(function, "ERROR invalid calibration target name '"+name + +"': must be 'SCIENCE' or start with 'CAL_' "); return ERROR; } - this->calmap[name].name = name; - // token[1] = caldoor - if ( tokens[1].empty() || - ( tokens[1].find("open")==std::string::npos && tokens[1].find("close") ) ) { - message << "ERROR invalid caldoor \"" << tokens[1] << "\": expected {open|close}"; - return ERROR; - } - this->calmap[name].caldoor = (tokens.at(1).find("open")==0); + // create map and get a reference to use for the remaining values + calinfo_t &info = this->calmap[name]; + info.name = name; - // token[2] = calcover - if ( tokens[2].empty() || - ( tokens[2].find("open")==std::string::npos && tokens[2].find("close") ) ) { - message << "ERROR invalid calcover \"" << tokens[2] << "\": expected {open|close}"; - return ERROR; - } - this->calmap[name].calcover = (tokens.at(2).find("open")==0); + try { + // tokens 1-2 are caldoor and calcover + info.caldoor = open_close(tokens.at(1)); + info.calcover = open_close(tokens.at(2)); - // tokens[3:6] = LAMPTHAR, LAMPFEAR, LAMPBLUC, LAMPREDC - int n=3; // incremental token counter used for the following groups - for ( const auto &lamp : this->lampnames ) { - if ( tokens[n].empty() || - ( tokens[n].find("on")==std::string::npos && tokens[n].find("off")==std::string::npos ) ) { - message << "ERROR invalid state \"" << tokens[n] << "\" for " << lamp << ": expected {on|off}"; - logwrite( function, message.str() ); - return ERROR; + // tokens 3-6 are the channel active states, indexed by channel name + for (size_t i=0; i < 4; i++) { + info.channel_active[chans.at(i)] = on_off(tokens.at(3+i)); } - this->calmap[name].lamp[lamp] = (tokens[n].find("on")==0); - n++; - } - // tokens[7:8] = domelamps - // i indexes domelampnames vector {0,1} - // j indexes domelamp map {1,2} - for ( int i=0,j=1; j<=2; i++,j++ ) { - if ( tokens[n].empty() || - ( tokens[n].find("on")==std::string::npos && tokens[n].find("off")==std::string::npos ) ) { - message << "ERROR invalid state \"" << tokens[n] << "\" for " << domelampnames[i] << ": expected {on|off}"; - logwrite( function, message.str() ); - return ERROR; + // tokens 7-10 are lamp states LAMPTHAR, LAMPFEAR, LAMPBLUC, LAMPREDC + for (size_t i=0; i < 4; i++) { + info.lamp[lampnames.at(i)] = on_off(tokens.at(7+i)); } - this->calmap[name].domelamp[j] = (tokens[n].find("on")==0); - n++; - } - // tokens[0:14] = lampmod{1:6} - for ( int i=1; i<=6; i++ ) { - if ( tokens[n].empty() || - ( tokens[n].find("on")==std::string::npos && tokens[n].find("off")==std::string::npos ) ) { - message << "ERROR invalid state \"" << tokens[n] << "\" for lampmod" << n << ": expected {on|off}"; - logwrite( function, message.str() ); - return ERROR; + // tokens 11-12 are dome lamps + for (size_t i=0; i < 2; i++) { + info.domelamp[i] = on_off(tokens.at(11+i)); + } + + // tokens 13-19 + for (size_t i=0; i<6; i++) { + info.lampmod[i] = on_off(tokens.at(13+i)); } - this->calmap[name].lampmod[i] = (tokens[n].find("on")==0); - n++; + } + catch (const std::exception &e) { + logwrite(function, "ERROR: "+std::string(e.what())); + return ERROR; } return NO_ERROR; diff --git a/sequencerd/sequencer_interface.h b/sequencerd/sequencer_interface.h index a90dcced..f4569e7c 100644 --- a/sequencerd/sequencer_interface.h +++ b/sequencerd/sequencer_interface.h @@ -132,12 +132,14 @@ namespace Sequencer { class CalibrationTarget { public: CalibrationTarget() : + chans { "U", "G", "R", "I" }, lampnames { "LAMPTHAR", "LAMPFEAR", "LAMPBLUC", "LAMPREDC" }, domelampnames { "LOLAMP", "HILAMP" } { } ///< struct holds all calibration parameters not in the target database typedef struct { std::string name; // calibration target name + std::map channel_active; // true=on bool caldoor; // true=open bool calcover; // true=open std::map lamp; // true=on @@ -152,14 +154,17 @@ namespace Sequencer { const std::unordered_map &getmap() const { return calmap; }; ///< returns just the map contents for specified targetname key - const calinfo_t* get_info( const std::string &_name ) const { + const calinfo_t &get_info( const std::string &_name ) const { auto it = calmap.find(_name); - if ( it != calmap.end() ) return &it->second; - return nullptr; + if ( it == calmap.end() ) { + throw std::runtime_error("calinfo not found for: "+_name); + } + return it->second; } private: std::unordered_map calmap; + std::vector chans; std::vector lampnames; std::vector domelampnames; }; diff --git a/sequencerd/sequencer_server.cpp b/sequencerd/sequencer_server.cpp index 06514cef..23ca221e 100644 --- a/sequencerd/sequencer_server.cpp +++ b/sequencerd/sequencer_server.cpp @@ -392,6 +392,74 @@ namespace Sequencer { applied++; } + // ACQ_AUTOMATIC_MODE + if (config.param[entry] == "ACQ_AUTOMATIC_MODE") { + int mode=1; + try { + mode = std::stoi( config.arg[entry] ); + if ( mode < 1 || mode > 3 ) { + message.str(""); message << "ERROR: ACQ_AUTOMATIC_MODE " << mode << " out of range {1:3}"; + this->sequence.async.enqueue_and_log( function, message.str() ); + return ERROR; + } + } + catch (const std::exception &e) { + message.str(""); message << "ERROR parsing ACQ_AUTOMATIC_MODE: " << e.what(); + this->sequence.async.enqueue_and_log( function, message.str() ); + return ERROR; + } + this->sequence.acq_automatic_mode = mode; + message.str(""); message << "SEQUENCERD:config:" << config.param[entry] << "=" << config.arg[entry]; + this->sequence.async.enqueue_and_log( function, message.str() ); + applied++; + } + + // ACQ_FINE_TUNE_CMD + if (config.param[entry] == "ACQ_FINE_TUNE_CMD") { + this->sequence.acq_fine_tune_cmd = config.arg[entry]; + message.str(""); message << "SEQUENCERD:config:" << config.param[entry] << "=" << config.arg[entry]; + this->sequence.async.enqueue_and_log( function, message.str() ); + applied++; + } + + // ACQ_FINE_TUNE_LOG + if (config.param[entry] == "ACQ_FINE_TUNE_LOG") { + try { + int val = std::stoi( config.arg[entry] ); + this->sequence.acq_fine_tune_log = ( val != 0 ); + } + catch (const std::exception &e) { + message.str(""); message << "ERROR parsing ACQ_FINE_TUNE_LOG: " << e.what(); + this->sequence.async.enqueue_and_log( function, message.str() ); + return ERROR; + } + message.str(""); message << "SEQUENCERD:config:" << config.param[entry] << "=" << config.arg[entry]; + this->sequence.async.enqueue_and_log( function, message.str() ); + applied++; + } + + // ACQ_OFFSET_SETTLE + if (config.param[entry] == "ACQ_OFFSET_SETTLE") { + double settle=0; + try { + settle = std::stod( config.arg[entry] ); + if ( settle < 0 ) { + message.str(""); message << "ERROR: ACQ_OFFSET_SETTLE " << settle << " out of range (>=0)"; + this->sequence.async.enqueue_and_log( function, message.str() ); + return ERROR; + } + } + catch (const std::exception &e) { + message.str(""); message << "ERROR parsing ACQ_OFFSET_SETTLE: " << e.what(); + this->sequence.async.enqueue_and_log( function, message.str() ); + return ERROR; + } + this->sequence.acq_offset_settle = settle; + message.str(""); message << "SEQUENCERD:config:" << config.param[entry] << "=" << config.arg[entry]; + this->sequence.async.enqueue_and_log( function, message.str() ); + applied++; + } + // TCS_WHICH -- which TCS to connect to, defults to real if not specified if ( config.param[entry] == "TCS_WHICH" ) { if ( config.arg[entry] != "sim" && config.arg[entry] != "real" ) { @@ -1443,6 +1511,42 @@ namespace Sequencer { } else + // Set/Get acquisition automation mode + // + if ( cmd == SEQUENCERD_ACQMODE ) { + if ( args.empty() || args == "?" || args == "help" ) { + retstring = SEQUENCERD_ACQMODE + " [ ]\n"; + retstring.append( " Set or get acquisition automation mode.\n" ); + retstring.append( " = { 1 (legacy), 2 (semi-auto), 3 (auto) }\n" ); + if ( args.empty() ) { + retstring.append( " current mode = " + std::to_string(this->sequence.acq_automatic_mode) + "\n" ); + } + ret = HELP; + } + else { + try { + int mode = std::stoi( args ); + if ( mode < 1 || mode > 3 ) { + this->sequence.async.enqueue_and_log( function, "ERROR: acqmode out of range {1:3}" ); + ret = ERROR; + } + else { + this->sequence.acq_automatic_mode = mode; + message.str(""); message << "NOTICE: acqmode set to " << mode; + this->sequence.async.enqueue_and_log( function, message.str() ); + this->sequence.publish_progress(); // push updated acqmode to seq-progress GUI + retstring = std::to_string( mode ); + ret = NO_ERROR; + } + } + catch ( const std::exception & ) { + this->sequence.async.enqueue_and_log( function, "ERROR: invalid acqmode argument" ); + ret = ERROR; + } + } + } + else + // // if ( cmd == SEQUENCERD_GETONETARGET ) { diff --git a/sequencerd/sequencerd.cpp b/sequencerd/sequencerd.cpp index a447ca09..2e86fc50 100644 --- a/sequencerd/sequencerd.cpp +++ b/sequencerd/sequencerd.cpp @@ -129,7 +129,7 @@ int main(int argc, char **argv) { // initialize the pub/sub handler // - if ( sequencerd.sequence.init_pubsub() == ERROR ) { + if ( sequencerd.sequence.init_pubsub( {"camerad", "slicecam_autoacq"} ) == ERROR ) { logwrite(function, "ERROR initializing publisher-subscriber handler"); sequencerd.exit_cleanly(); } diff --git a/slicecamd/CMakeLists.txt b/slicecamd/CMakeLists.txt index ecfcfcd0..12616a95 100644 --- a/slicecamd/CMakeLists.txt +++ b/slicecamd/CMakeLists.txt @@ -7,6 +7,7 @@ cmake_minimum_required( VERSION 3.12 ) set( SLICECAMD_DIR ${PROJECT_BASE_DIR}/slicecamd ) +set( AUTOACQ_SRC ${SLICECAMD_DIR}/ngps_acq.c ) set( CMAKE_CXX_STANDARD 17 ) @@ -25,6 +26,7 @@ find_library( ZMQ_LIB zmq NAMES libzmq PATHS /usr/local/lib ) # find_library( CCFITS_LIB CCfits NAMES libCCfits PATHS /usr/local/lib ) find_library( CFITS_LIB cfitsio NAMES libcfitsio PATHS /usr/local/lib ) +find_library( WCS_LIB wcs NAMES libwcs PATHS /usr/local/lib /opt/homebrew/lib ) include_directories( ${PROJECT_BASE_DIR}/utils ) include_directories( ${PROJECT_BASE_DIR}/Andor ) @@ -38,8 +40,11 @@ add_executable(slicecamd ${SLICECAMD_DIR}/slicecam_server.cpp ${SLICECAMD_DIR}/slicecam_interface.cpp ${SLICECAMD_DIR}/slicecam_fits.cpp + ${AUTOACQ_SRC} ${PYTHON_DEV} ) +set_source_files_properties( ${AUTOACQ_SRC} + PROPERTIES COMPILE_FLAGS "-x c -std=gnu11" ) target_link_libraries(slicecamd andor @@ -55,10 +60,12 @@ target_link_libraries(slicecamd ${PYTHON_LIB} ${CCFITS_LIB} ${CFITS_LIB} + ${WCS_LIB} ${CMAKE_THREAD_LIBS_INIT} ) target_compile_options(slicecamd PRIVATE -g -Wall -O1 -Wno-variadic-macros -ggdb) +target_compile_definitions(slicecamd PRIVATE NGPS_ACQ_EMBEDDED=1) # -- External Definitions ------------------------------------------------------ # diff --git a/slicecamd/ngps_acq.c b/slicecamd/ngps_acq.c new file mode 100644 index 00000000..e647d3ba --- /dev/null +++ b/slicecamd/ngps_acq.c @@ -0,0 +1,2535 @@ +// ngps_acq.c +// NGPS acquisition / fine-acquire helper for the slice-viewing camera. +// +// Goals: +// - robust source detection + centroiding under poor seeing +// - robust statistics before issuing any TCS offset +// - reject duplicate frames and frames taken during/just after telescope motion +// - optional "manual" frame acquisition via: scam framegrab one +// +// Build (typical on NGPS machines): +// gcc -O3 -Wall -Wextra -std=c11 -o ngps_acq ngps_acq.c -lcfitsio -lwcs -lm +// +// Example (stream mode, file updated by camera): +// ./ngps_acq --input /tmp/slicecam.fits --goal-x 512 --goal-y 512 --loop 1 \ +// --bg-x1 50 --bg-x2 950 --bg-y1 30 --bg-y2 980 --debug 1 +// +// Example (manual framegrab mode, synchronous): +// ./ngps_acq --frame-mode framegrab --framegrab-out /tmp/ngps_acq.fits \ +// --goal-x 512 --goal-y 512 --loop 1 +// +// Notes: +// - TCS command assumed: tcs native pt +// - We set native units (dra/ddec arcsec) once per run by default. +// - Commanded offsets are computed as (star - goal) (arcsec), i.e. the move +// that should place the star on the goal pixel. If your TCS sign convention +// is opposite, set --tcs-sign -1. + +#define _GNU_SOURCE +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "fitsio.h" +#include "ngps_acq_embed.h" + +#ifdef __has_include +# if __has_include() +# include +# include +# else +# include +# include +# endif +#else +# include +# include +#endif + +#ifndef PATH_MAX +#define PATH_MAX 4096 +#endif + +// ROI mask bits +#define ROI_X1_SET 0x01 +#define ROI_X2_SET 0x02 +#define ROI_Y1_SET 0x04 +#define ROI_Y2_SET 0x08 + +typedef enum { + FRAME_STREAM = 0, + FRAME_FRAMEGRAB = 1 +} FrameMode; + +typedef struct { + char input[PATH_MAX]; // stream file path + + FrameMode frame_mode; + char framegrab_out[PATH_MAX]; + int framegrab_use_tmp; // if 1: write to framegrab_out then atomically rename + + double goal_x; + double goal_y; + int pixel_origin; // 0 or 1 + + // Candidate search constraints + double max_dist_pix; // circular cut around goal (pix) + double snr_thresh; // detection threshold (sigma) + int min_adjacent; // raw-pixel neighbors above raw threshold + + // Filtering for detection + double filt_sigma_pix; // Gaussian sigma for smoothing (pix) + + // Centroiding + int centroid_halfwin; // window half-width (pix) + double centroid_sigma_pix; // Gaussian window sigma (pix) + int centroid_maxiter; + double centroid_eps_pix; + + // FITS selection + int extnum; // 0=primary, 1=first extension, ... + char extname[32]; // preferred EXTNAME, empty disables + + // Background statistics ROI (inclusive; user origin) + int bg_roi_mask; + long bg_x1, bg_x2, bg_y1, bg_y2; + + // Candidate search ROI (inclusive; user origin) + int search_roi_mask; + long search_x1, search_x2, search_y1, search_y2; + + // Closed-loop / guiding wrapper + int loop; + double cadence_sec; // minimum seconds between accepted samples (stream mode) + int max_samples; // per-move gather (accepted samples) + int min_samples; // minimum before testing scatter + double prec_arcsec; // required robust scatter (MAD->sigma) per axis + double goal_arcsec; // convergence threshold on robust offset magnitude + int max_cycles; // number of move cycles + double gain; // gain applied to commanded move (0..1 recommended) + int adaptive; // if 1: adapt exposure + loop thresholds from measured source counts + double adaptive_faint; // start faint adaptation at/under this metric + double adaptive_faint_goal; // faint-mode target metric + double adaptive_bright; // start bright adaptation at/over this metric + double adaptive_bright_goal; // bright-mode target metric + + // Frame quality & safety + int reject_identical; // reject identical image signatures + int reject_after_move; // reject N new frames after any TCS move + double settle_sec; // optional sleep after move (additional to rejecting frames) + double max_move_arcsec; // safety cap; do not issue moves larger than this + int continue_on_fail; // if 0: exit on failure to build stats; if 1: keep trying + + // Offset conventions + int dra_use_cosdec; // 1: dra = dRA*cos(dec) + int tcs_sign; // multiply commanded offsets by +/-1 + + // TCS options + int tcs_set_units; // if 1: run "tcs native dra 'arcsec'" and "... ddec 'arcsec'" once + + // SCAM daemon option (guiding-friendly moves) + int use_putonslit; // if 1: call putonslit + int wait_guiding; // if 1: wait for ACAM guiding state after putonslit + double guiding_poll_sec; // polling interval for "acam acquire" + double guiding_timeout_sec; // timeout for waiting on guiding (0 = no timeout) + + // Debug + int debug; + char debug_out[PATH_MAX]; + + // General + int dry_run; + int verbose; +} AcqParams; + +typedef struct { + int found; + // peak in pixel coords (user origin) + double peak_x, peak_y; + // centroid (user origin) + double cx, cy; + // quality + double peak_val; + double peak_snr_raw; + double snr_ap; + double src_top10_mean; // mean of top 10% positive residual pixels in centroid window + double bkg; + double sigma; + // debug ROI bounds (0-based inclusive) + long rx1, rx2, ry1, ry2; // stats ROI + long sx1, sx2, sy1, sy2; // search ROI +} Detection; + +typedef struct { + int ok; + Detection det; + + // Pixel offsets star - goal (pix) + double dx_pix; + double dy_pix; + + // WCS + int wcs_ok; + double ra_goal_deg, dec_goal_deg; + double ra_star_deg, dec_star_deg; + + // Commanded offsets (arcsec) for tcs native pt + double dra_cmd_arcsec; + double ddec_cmd_arcsec; + double r_cmd_arcsec; +} FrameResult; + +typedef struct { + time_t mtime; + off_t size; + uint64_t sig; // image signature (subsampled) + int have_sig; + struct timespec t_accept; // time we accepted this frame +} FrameState; + +typedef enum { + ADAPT_MODE_BASELINE = 0, + ADAPT_MODE_FAINT = 1, + ADAPT_MODE_BRIGHT = 2 +} AdaptiveMode; + +typedef struct { + AdaptiveMode mode; + double cadence_sec; + int reject_after_move; + double prec_arcsec; + double goal_arcsec; + int apply_camera; + double exptime_sec; + int avgframes; +} AdaptiveCycleConfig; + +typedef struct { + AdaptiveMode mode; + int have_metric; + double metric_ewma; + int have_last_camera; + double last_exptime_sec; + int last_avgframes; +} AdaptiveRuntime; + +static volatile sig_atomic_t g_stop = 0; +static ngps_acq_hooks_t g_hooks; +static int g_hooks_initialized = 0; +static void on_sigint(int sig) { (void)sig; g_stop = 1; } + +static int stop_requested(void) { + if (g_stop) return 1; + if (g_hooks_initialized && g_hooks.is_stop_requested) { + return g_hooks.is_stop_requested(g_hooks.user) ? 1 : 0; + } + return 0; +} + +static void acq_vlogf(const char* fmt, va_list ap) { + va_list copy; + va_copy(copy, ap); + vfprintf(stderr, fmt, copy); + va_end(copy); + + if (g_hooks_initialized && g_hooks.log_message) { + char buffer[2048]; + va_copy(copy, ap); + vsnprintf(buffer, sizeof(buffer), fmt, copy); + va_end(copy); + g_hooks.log_message(g_hooks.user, buffer); + } +} + +static void acq_logf(const char* fmt, ...) { + va_list ap; + va_start(ap, fmt); + acq_vlogf(fmt, ap); + va_end(ap); +} + +static void die(const char* msg) { + acq_logf("FATAL: %s\n", msg); + exit(4); +} + +static void sleep_seconds(double sec) { + if (sec <= 0) return; + struct timespec ts; + ts.tv_sec = (time_t)floor(sec); + ts.tv_nsec = (long)((sec - (double)ts.tv_sec) * 1e9); + while (nanosleep(&ts, &ts) == -1 && errno == EINTR) {} +} + +static double now_monotonic_sec(void) { + struct timespec ts; + clock_gettime(CLOCK_MONOTONIC, &ts); + return (double)ts.tv_sec + 1e-9*(double)ts.tv_nsec; +} + +static int cmp_float(const void* a, const void* b) { + float fa = *(const float*)a; + float fb = *(const float*)b; + return (fa < fb) ? -1 : (fa > fb) ? 1 : 0; +} + +static int cmp_double(const void* a, const void* b) { + double da = *(const double*)a; + double db = *(const double*)b; + return (da < db) ? -1 : (da > db) ? 1 : 0; +} + +static double wrap_dra_deg(double dra) { + while (dra > 180.0) dra -= 360.0; + while (dra < -180.0) dra += 360.0; + return dra; +} + +// Convert a user-specified ROI into clamped 0-based inclusive bounds. +// If mask is 0, returns full-frame bounds. +static void compute_roi_0based(long nx, long ny, int pixel_origin, + int mask, long ux1_in, long ux2_in, long uy1_in, long uy2_in, + long* x1, long* x2, long* y1, long* y2) +{ + long ux1 = (pixel_origin == 0) ? 0 : 1; + long ux2 = (pixel_origin == 0) ? (nx - 1) : nx; + long uy1 = (pixel_origin == 0) ? 0 : 1; + long uy2 = (pixel_origin == 0) ? (ny - 1) : ny; + + if (mask & ROI_X1_SET) ux1 = ux1_in; + if (mask & ROI_X2_SET) ux2 = ux2_in; + if (mask & ROI_Y1_SET) uy1 = uy1_in; + if (mask & ROI_Y2_SET) uy2 = uy2_in; + + long ax1 = ux1, ax2 = ux2, ay1 = uy1, ay2 = uy2; + if (pixel_origin == 1) { ax1--; ax2--; ay1--; ay2--; } + + if (ax2 < ax1) { long t=ax1; ax1=ax2; ax2=t; } + if (ay2 < ay1) { long t=ay1; ay1=ay2; ay2=t; } + + if (ax1 < 0) ax1 = 0; + if (ay1 < 0) ay1 = 0; + if (ax2 > nx-1) ax2 = nx-1; + if (ay2 > ny-1) ay2 = ny-1; + + if (ax2 < ax1) { ax1=0; ax2=nx-1; } + if (ay2 < ay1) { ay1=0; ay2=ny-1; } + + *x1=ax1; *x2=ax2; *y1=ay1; *y2=ay2; +} + +// Subsample pixels in ROI into a float array. +static float* roi_subsample(const float* img, long nx, long ny, + long x1, long x2, long y1, long y2, + long target, long* n_out) +{ + if (x1 < 0) x1 = 0; + if (y1 < 0) y1 = 0; + if (x2 > nx-1) x2 = nx-1; + if (y2 > ny-1) y2 = ny-1; + + long wx = x2 - x1 + 1; + long wy = y2 - y1 + 1; + if (wx <= 0 || wy <= 0) { *n_out = 0; return NULL; } + + long Nroi = wx * wy; + long stride = (Nroi > target) ? (Nroi / target) : 1; + if (stride < 1) stride = 1; + + long ns = (Nroi + stride - 1) / stride; + float* sample = (float*)malloc((size_t)ns * sizeof(float)); + if (!sample) die("malloc sample failed"); + + long k = 0; + long idx = 0; + for (long y = y1; y <= y2; y++) { + long row0 = y * nx; + for (long x = x1; x <= x2; x++, idx++) { + if ((idx % stride) == 0) sample[k++] = img[row0 + x]; + } + } + + *n_out = k; + return sample; +} + +// SExtractor-like background + sigma estimation within ROI: +// - initial median + MAD +// - iterative sigma-clipping around median +// - background via mode = 2.5*median - 1.5*mean (unless skewed, then median) +static void bg_sigma_sextractor_like(const float* img, long nx, long ny, + long x1, long x2, long y1, long y2, + double* bkg_out, double* sigma_out) +{ + *bkg_out = 0.0; + *sigma_out = 1.0; + + long ns = 0; + float* sample = roi_subsample(img, nx, ny, x1, x2, y1, y2, 200000, &ns); + if (!sample || ns < 64) { + if (sample) free(sample); + return; + } + + qsort(sample, (size_t)ns, sizeof(float), cmp_float); + double median = (ns % 2) ? sample[ns/2] : 0.5*(sample[ns/2 - 1] + sample[ns/2]); + + // MAD + float* dev = (float*)malloc((size_t)ns * sizeof(float)); + if (!dev) die("malloc dev failed"); + for (long i = 0; i < ns; i++) dev[i] = (float)fabs((double)sample[i] - median); + qsort(dev, (size_t)ns, sizeof(float), cmp_float); + double mad = (ns % 2) ? dev[ns/2] : 0.5*(dev[ns/2 - 1] + dev[ns/2]); + free(dev); + + double sigma = 1.4826 * mad; + if (!isfinite(sigma) || sigma <= 0) sigma = 1.0; + + // Iterative clip + const double clip = 3.0; + double mean = median; + double sigma_prev = sigma; + for (int it = 0; it < 8; it++) { + double lo = median - clip * sigma; + double hi = median + clip * sigma; + + double sum = 0.0, sum2 = 0.0; + long n = 0; + for (long i = 0; i < ns; i++) { + double v = sample[i]; + if (v < lo || v > hi) continue; + sum += v; + sum2 += v*v; + n++; + } + if (n < 32) break; + + mean = sum / (double)n; + double var = (sum2 / (double)n) - mean*mean; + if (var < 0) var = 0; + sigma = sqrt(var); + if (!isfinite(sigma) || sigma <= 0) sigma = sigma_prev; + + double rel = fabs(sigma - sigma_prev) / (sigma_prev > 0 ? sigma_prev : 1.0); + sigma_prev = sigma; + if (rel < 0.01) break; + } + + double mode = 2.5*median - 1.5*mean; + double bkg = mode; + if (sigma > 0 && (mean - median)/sigma > 0.3) bkg = median; + if (!isfinite(bkg)) bkg = median; + if (!isfinite(sigma) || sigma <= 0) sigma = 1.0; + + *bkg_out = bkg; + *sigma_out = sigma; + + free(sample); +} + +static double median_of_doubles(double* a, int n) +{ + if (n <= 0) return 0.0; + qsort(a, (size_t)n, sizeof(double), cmp_double); + return (n % 2) ? a[n/2] : 0.5*(a[n/2 - 1] + a[n/2]); +} + +static double mad_sigma_of_doubles(const double* a_in, int n, double med) +{ + if (n <= 1) return 0.0; + double* d = (double*)malloc((size_t)n * sizeof(double)); + if (!d) die("malloc mad failed"); + for (int i = 0; i < n; i++) d[i] = fabs(a_in[i] - med); + qsort(d, (size_t)n, sizeof(double), cmp_double); + double mad = (n % 2) ? d[n/2] : 0.5*(d[n/2 - 1] + d[n/2]); + free(d); + return 1.4826 * mad; +} + +// Gaussian kernel (1D) normalized to sum=1. +static double* make_gaussian_kernel(double sigma, int* radius_out) +{ + if (sigma <= 0.2) sigma = 0.2; + int r = (int)ceil(3.0*sigma); + if (r < 1) r = 1; + int len = 2*r + 1; + double* k = (double*)malloc((size_t)len * sizeof(double)); + if (!k) die("malloc kernel failed"); + + double sum = 0.0; + for (int i = -r; i <= r; i++) { + double x = (double)i / sigma; + double v = exp(-0.5 * x * x); + k[i+r] = v; + sum += v; + } + if (sum <= 0) sum = 1.0; + for (int i = 0; i < len; i++) k[i] /= sum; + + *radius_out = r; + return k; +} + +static double kernel_sum_sq(const double* k, int radius) +{ + int len = 2*radius + 1; + double s2 = 0.0; + for (int i = 0; i < len; i++) s2 += k[i]*k[i]; + return s2; +} + +// Separable convolution on a patch (w*h). Border handling: clamp. +static void convolve_separable(const float* in, float* tmp, float* out, + int w, int h, const double* k, int r) +{ + // horizontal + for (int y = 0; y < h; y++) { + const float* row = in + y*w; + float* trow = tmp + y*w; + for (int x = 0; x < w; x++) { + double acc = 0.0; + for (int dx = -r; dx <= r; dx++) { + int xx = x + dx; + if (xx < 0) xx = 0; + if (xx >= w) xx = w-1; + acc += (double)row[xx] * k[dx+r]; + } + trow[x] = (float)acc; + } + } + + // vertical + for (int y = 0; y < h; y++) { + float* orow = out + y*w; + for (int x = 0; x < w; x++) { + double acc = 0.0; + for (int dy = -r; dy <= r; dy++) { + int yy = y + dy; + if (yy < 0) yy = 0; + if (yy >= h) yy = h-1; + acc += (double)tmp[yy*w + x] * k[dy+r]; + } + orow[x] = (float)acc; + } + } +} + +// Simple debug drawing (PPM) +static void set_px(uint8_t* rgb, int w, int h, int x, int y, uint8_t r, uint8_t g, uint8_t b) +{ + if (x < 0 || y < 0 || x >= w || y >= h) return; + size_t idx = (size_t)(y*w + x) * 3; + rgb[idx+0] = r; + rgb[idx+1] = g; + rgb[idx+2] = b; +} + +static void draw_plus(uint8_t* rgb, int w, int h, int x, int y, int rad, uint8_t r, uint8_t g, uint8_t b) +{ + for (int dx = -rad; dx <= rad; dx++) set_px(rgb, w, h, x+dx, y, r,g,b); + for (int dy = -rad; dy <= rad; dy++) set_px(rgb, w, h, x, y+dy, r,g,b); +} + +static void draw_x(uint8_t* rgb, int w, int h, int x, int y, int rad, uint8_t r, uint8_t g, uint8_t b) +{ + for (int d = -rad; d <= rad; d++) { + set_px(rgb, w, h, x+d, y+d, r,g,b); + set_px(rgb, w, h, x+d, y-d, r,g,b); + } +} + +static void draw_circle(uint8_t* rgb, int w, int h, int xc, int yc, int rad, uint8_t r, uint8_t g, uint8_t b) +{ + int x = rad; + int y = 0; + int err = 0; + while (x >= y) { + set_px(rgb,w,h, xc + x, yc + y, r,g,b); + set_px(rgb,w,h, xc + y, yc + x, r,g,b); + set_px(rgb,w,h, xc - y, yc + x, r,g,b); + set_px(rgb,w,h, xc - x, yc + y, r,g,b); + set_px(rgb,w,h, xc - x, yc - y, r,g,b); + set_px(rgb,w,h, xc - y, yc - x, r,g,b); + set_px(rgb,w,h, xc + y, yc - x, r,g,b); + set_px(rgb,w,h, xc + x, yc - y, r,g,b); + y++; + if (err <= 0) { + err += 2*y + 1; + } else { + x--; + err += 2*(y - x) + 1; + } + } +} + +static void draw_line(uint8_t* rgb, int w, int h, int x0, int y0, int x1, int y1, uint8_t r, uint8_t g, uint8_t b) +{ + int dx = abs(x1 - x0), sx = (x0 < x1) ? 1 : -1; + int dy = -abs(y1 - y0), sy = (y0 < y1) ? 1 : -1; + int err = dx + dy; + while (1) { + set_px(rgb,w,h,x0,y0,r,g,b); + if (x0 == x1 && y0 == y1) break; + int e2 = 2*err; + if (e2 >= dy) { err += dy; x0 += sx; } + if (e2 <= dx) { err += dx; y0 += sy; } + } +} + +static void draw_arrow(uint8_t* rgb, int w, int h, int x0, int y0, int x1, int y1, uint8_t r, uint8_t g, uint8_t b) +{ + draw_line(rgb,w,h,x0,y0,x1,y1,r,g,b); + double ang = atan2((double)(y1 - y0), (double)(x1 - x0)); + double a1 = ang + M_PI/8.0; + double a2 = ang - M_PI/8.0; + int L = 10; + int hx1 = x1 - (int)lround(L * cos(a1)); + int hy1 = y1 - (int)lround(L * sin(a1)); + int hx2 = x1 - (int)lround(L * cos(a2)); + int hy2 = y1 - (int)lround(L * sin(a2)); + draw_line(rgb,w,h,x1,y1,hx1,hy1,r,g,b); + draw_line(rgb,w,h,x1,y1,hx2,hy2,r,g,b); +} + +static int write_debug_ppm(const char* outpath, + const float* img, long nx, + long rx1, long rx2, long ry1, long ry2, + double bkg, double sigma, double snr_thresh, + const Detection* det, + const AcqParams* p) +{ + int w = (int)(rx2 - rx1 + 1); + int h = (int)(ry2 - ry1 + 1); + if (w <= 0 || h <= 0) return 1; + + uint8_t* rgb = (uint8_t*)malloc((size_t)w * (size_t)h * 3); + if (!rgb) return 2; + + double vmin = bkg - 2.0*sigma; + double vmax = bkg + 6.0*sigma; + double inv = (vmax > vmin) ? (1.0/(vmax - vmin)) : 1.0; + + for (int yy = 0; yy < h; yy++) { + long y = ry1 + yy; + for (int xx = 0; xx < w; xx++) { + long x = rx1 + xx; + double v = img[y*nx + x]; + double t = (v - vmin) * inv; + if (t < 0) t = 0; + if (t > 1) t = 1; + uint8_t g = (uint8_t)lround(255.0 * t); + size_t idx = (size_t)(yy*w + xx) * 3; + rgb[idx+0] = g; + rgb[idx+1] = g; + rgb[idx+2] = g; + + double snr = (sigma > 0) ? ((v - bkg) / sigma) : 0; + if (snr >= snr_thresh) { + // color above-threshold pixels (cyan-ish) + rgb[idx+0] = (uint8_t)((int)rgb[idx+0] / 2); + rgb[idx+1] = 255; + rgb[idx+2] = 255; + } + } + } + + if (det && det->found) { + int px0 = (p->pixel_origin == 0) ? (int)lround(det->peak_x) : (int)lround(det->peak_x - 1.0); + int py0 = (p->pixel_origin == 0) ? (int)lround(det->peak_y) : (int)lround(det->peak_y - 1.0); + int cx0 = (p->pixel_origin == 0) ? (int)lround(det->cx) : (int)lround(det->cx - 1.0); + int cy0 = (p->pixel_origin == 0) ? (int)lround(det->cy) : (int)lround(det->cy - 1.0); + + int gx0 = (p->pixel_origin == 0) ? (int)lround(p->goal_x) : (int)lround(p->goal_x - 1.0); + int gy0 = (p->pixel_origin == 0) ? (int)lround(p->goal_y) : (int)lround(p->goal_y - 1.0); + + // shift into ROI image coordinates + int px = px0 - (int)rx1; + int py = py0 - (int)ry1; + int cx = cx0 - (int)rx1; + int cy = cy0 - (int)ry1; + int gx = gx0 - (int)rx1; + int gy = gy0 - (int)ry1; + + draw_circle(rgb, w, h, px, py, 10, 255, 50, 50); + draw_plus(rgb, w, h, cx, cy, 6, 50, 255, 50); + draw_x(rgb, w, h, gx, gy, 8, 255, 255, 0); + draw_arrow(rgb, w, h, cx, cy, gx, gy, 255, 200, 0); + } + + FILE* f = fopen(outpath, "wb"); + if (!f) { free(rgb); return 3; } + fprintf(f, "P6\n%d %d\n255\n", w, h); + fwrite(rgb, 1, (size_t)w*(size_t)h*3, f); + fclose(f); + free(rgb); + return 0; +} + +// --- FITS I/O helpers --- +static int move_to_image_hdu_by_extname(fitsfile* fptr, const char* want_extname, int* out_hdu_index, int* status) +{ + if (!want_extname || want_extname[0] == '\0') return 1; + + int nhdus = 0; + if (fits_get_num_hdus(fptr, &nhdus, status)) return 2; + + for (int hdu = 1; hdu <= nhdus; hdu++) { + int hdutype = 0; + if (fits_movabs_hdu(fptr, hdu, &hdutype, status)) return 3; + if (hdutype != IMAGE_HDU) continue; + + char extname[FLEN_VALUE] = {0}; + int keystat = 0; + if (fits_read_key(fptr, TSTRING, "EXTNAME", extname, NULL, &keystat)) { + extname[0] = '\0'; + } + + if (extname[0] && strcasecmp(extname, want_extname) == 0) { + if (out_hdu_index) *out_hdu_index = hdu; + return 0; + } + } + + return 4; +} + +static int read_fits_image_and_header(const char* path, const AcqParams* p, + float** img_out, long* nx_out, long* ny_out, + char** header_out, int* nkeys_out, + int* used_hdu_out, char* used_extname_out, size_t used_extname_sz, + double* exptime_sec_out) +{ + fitsfile* fptr = NULL; + int status = 0; + + if (fits_open_file(&fptr, path, READONLY, &status)) return status; + + int used_hdu = 1; + char used_extname[FLEN_VALUE] = {0}; + + // Prefer EXTNAME + if (p->extname[0]) { + int found_hdu = 0; + int rc = move_to_image_hdu_by_extname(fptr, p->extname, &found_hdu, &status); + if (rc == 0) { + used_hdu = found_hdu; + } else { + if (p->verbose) acq_logf( "WARNING: EXTNAME='%s' not found; falling back to extnum=%d\n", p->extname, p->extnum); + status = 0; + int hdutype = 0; + if (fits_movabs_hdu(fptr, p->extnum + 1, &hdutype, &status)) { + fits_close_file(fptr, &status); + return status; + } + used_hdu = p->extnum + 1; + } + } else { + int hdutype = 0; + if (fits_movabs_hdu(fptr, p->extnum + 1, &hdutype, &status)) { + fits_close_file(fptr, &status); + return status; + } + used_hdu = p->extnum + 1; + } + + // record EXTNAME + { + int keystat = 0; + if (fits_read_key(fptr, TSTRING, "EXTNAME", used_extname, NULL, &keystat)) { + used_extname[0] = '\0'; + } + } + + // record EXPTIME (fallback to 1s if unavailable) + double exptime_sec = 1.0; + { + int keystat = 0; + double exptmp = 0.0; + if (!fits_read_key(fptr, TDOUBLE, "EXPTIME", &exptmp, NULL, &keystat) && + isfinite(exptmp) && exptmp > 0.0) { + exptime_sec = exptmp; + } + } + + int bitpix = 0, naxis = 0; + long naxes[3] = {0,0,0}; + if (fits_get_img_param(fptr, 3, &bitpix, &naxis, naxes, &status)) { + fits_close_file(fptr, &status); + return status; + } + if (naxis < 2) { + fits_close_file(fptr, &status); + return BAD_NAXIS; + } + + long nx = naxes[0]; + long ny = naxes[1]; + + float* img = (float*)malloc((size_t)nx * (size_t)ny * sizeof(float)); + if (!img) { + fits_close_file(fptr, &status); + return MEMORY_ALLOCATION; + } + + long fpixel[3] = {1,1,1}; + long nelem = nx * ny; + int anynul = 0; + if (fits_read_pix(fptr, TFLOAT, fpixel, nelem, NULL, img, &anynul, &status)) { + free(img); + fits_close_file(fptr, &status); + return status; + } + + char* header = NULL; + int nkeys = 0; + int hstatus = 0; + if (fits_hdr2str(fptr, 1, NULL, 0, &header, &nkeys, &hstatus)) { + header = NULL; + nkeys = 0; + } + + fits_close_file(fptr, &status); + + *img_out = img; + *nx_out = nx; + *ny_out = ny; + *header_out = header; + *nkeys_out = nkeys; + if (used_hdu_out) *used_hdu_out = used_hdu; + if (used_extname_out && used_extname_sz > 0) { + snprintf(used_extname_out, used_extname_sz, "%s", used_extname); + } + if (exptime_sec_out) *exptime_sec_out = exptime_sec; + + return 0; +} + +// --- WCS helpers --- +static int init_wcs_from_header(const char* header, int nkeys, struct wcsprm** wcs_out, int* nwcs_out) +{ + if (!header || nkeys <= 0) return 1; + + int relax = WCSHDR_all; + int ctrl = 2; + int nrej = 0, nwcs = 0; + struct wcsprm* wcs = NULL; + + int stat = wcspih((char*)header, nkeys, relax, ctrl, &nrej, &nwcs, &wcs); + if (stat || nwcs < 1 || !wcs) return 2; + + if (wcsset(&wcs[0])) { + wcsvfree(&nwcs, &wcs); + return 3; + } + + *wcs_out = wcs; + *nwcs_out = nwcs; + return 0; +} + +// Pixel -> (RA,Dec) degrees using WCSLIB. Inputs are FITS 1-based pixels. +static int pix2world_wcs(const struct wcsprm* wcs0, double pix_x, double pix_y, + double* ra_deg, double* dec_deg) +{ + if (!wcs0) return 1; + + double pixcrd[2] = {pix_x, pix_y}; + double imgcrd[2] = {0,0}; + double phi=0, theta=0; + double world[2] = {0,0}; + int stat[2] = {0,0}; + + int rc = wcsp2s((struct wcsprm*)wcs0, 1, 2, pixcrd, imgcrd, &phi, &theta, world, stat); + if (rc) return 2; + + *ra_deg = world[0]; + *dec_deg = world[1]; + return 0; +} + +// --- Frame signature (reject identical) --- +static uint64_t fnv1a64_init(void) { return 1469598103934665603ULL; } +static uint64_t fnv1a64_step(uint64_t h, uint64_t x) { + h ^= x; + return h * 1099511628211ULL; +} + +static uint64_t image_signature_subsample(const float* img, long nx, long ny, + long x1, long x2, long y1, long y2) +{ + if (!img || nx <= 0 || ny <= 0) return 0; + if (x1 < 0) x1 = 0; + if (y1 < 0) y1 = 0; + if (x2 > nx-1) x2 = nx-1; + if (y2 > ny-1) y2 = ny-1; + if (x2 < x1 || y2 < y1) return 0; + + long wx = x2 - x1 + 1; + long wy = y2 - y1 + 1; + long N = wx * wy; + + // Aim for ~50k samples max. + long target = 50000; + long stride = (N > target) ? (N / target) : 1; + if (stride < 1) stride = 1; + + uint64_t h = fnv1a64_init(); + long idx = 0; + for (long y = y1; y <= y2; y++) { + long row0 = y * nx; + for (long x = x1; x <= x2; x++, idx++) { + if ((idx % stride) != 0) continue; + // quantize float to int32 with mild scaling to be stable + float v = img[row0 + x]; + int32_t q = (int32_t)lrintf(v * 8.0f); + h = fnv1a64_step(h, (uint64_t)(uint32_t)q); + } + } + // Mix in ROI bounds to reduce accidental collisions + h = fnv1a64_step(h, (uint64_t)(uint32_t)x1); + h = fnv1a64_step(h, (uint64_t)(uint32_t)y1); + h = fnv1a64_step(h, (uint64_t)(uint32_t)x2); + h = fnv1a64_step(h, (uint64_t)(uint32_t)y2); + return h; +} + +// --- TCS helpers --- +static int tcs_set_native_units(int dry_run, int verbose) +{ + if (g_hooks_initialized && g_hooks.tcs_set_native_units) { + return g_hooks.tcs_set_native_units(g_hooks.user, dry_run, verbose); + } + + const char* cmd1 = "tcs native dra 'arcsec'"; + const char* cmd2 = "tcs native ddec 'arcsec'"; + if (verbose || dry_run) { + acq_logf( "TCS CMD: %s\n", cmd1); + acq_logf( "TCS CMD: %s\n", cmd2); + } + if (dry_run) return 0; + int rc1 = system(cmd1); + int rc2 = system(cmd2); + if (rc1 != 0 || rc2 != 0) { + acq_logf( "WARNING: TCS unit command failed rc1=%d rc2=%d\n", rc1, rc2); + return 1; + } + return 0; +} + +static int tcs_move_arcsec(double dra_arcsec, double ddec_arcsec, int dry_run, int verbose) +{ + if (g_hooks_initialized && g_hooks.tcs_move_arcsec) { + return g_hooks.tcs_move_arcsec(g_hooks.user, dra_arcsec, ddec_arcsec, dry_run, verbose); + } + + char cmd[512]; + snprintf(cmd, sizeof(cmd), "tcs native pt %.3f %.3f", dra_arcsec, ddec_arcsec); + if (verbose || dry_run) acq_logf( "TCS CMD: %s\n", cmd); + if (dry_run) return 0; + + int rc = system(cmd); + if (rc != 0) { + acq_logf( "WARNING: TCS command returned %d\n", rc); + return 1; + } + return 0; +} + +// --- SCAM daemon helper (guiding-friendly move) --- +static double wrap_ra_deg(double ra) +{ + // keep in [0,360) + while (ra < 0.0) ra += 360.0; + while (ra >= 360.0) ra -= 360.0; + return ra; +} + +// Convert desired PT offsets (arcsec) into a synthetic "crosshair" RA/Dec given a "slit" RA/Dec. +// This preserves robust median offset logic while still invoking putonslit (which computes PT internally). +static void slit_cross_from_offsets(const AcqParams* p, + double slit_ra_deg, double slit_dec_deg, + double dra_arcsec, double ddec_arcsec, + double* cross_ra_deg, double* cross_dec_deg) +{ + double cosdec = cos(slit_dec_deg * M_PI / 180.0); + double ddec_deg = ddec_arcsec / 3600.0; + + double dra_deg = dra_arcsec / 3600.0; + if (p->dra_use_cosdec) { + // dra_arcsec was computed as dRA*cos(dec)*3600, so invert the cos(dec) here. + double denom = (fabs(cosdec) > 1e-12) ? cosdec : (cosdec >= 0 ? 1e-12 : -1e-12); + dra_deg /= denom; + } + + *cross_dec_deg = slit_dec_deg + ddec_deg; + *cross_ra_deg = wrap_ra_deg(slit_ra_deg + dra_deg); +} + +// Hard-coded interface: call via the SCAM daemon. +// Intentionally NOT user-adjustable. +static int scam_putonslit_deg(double slit_ra_deg, double slit_dec_deg, + double cross_ra_deg, double cross_dec_deg, + int dry_run, int verbose) +{ + if (g_hooks_initialized && g_hooks.scam_putonslit_deg) { + return g_hooks.scam_putonslit_deg(g_hooks.user, + slit_ra_deg, slit_dec_deg, + cross_ra_deg, cross_dec_deg, + dry_run, verbose); + } + + char cmd[1024]; + // putonslit (all decimal degrees) + snprintf(cmd, sizeof(cmd), "scam putonslit %.10f %.10f %.10f %.10f", + slit_ra_deg, slit_dec_deg, cross_ra_deg, cross_dec_deg); + + if (verbose || dry_run) acq_logf( "SCAM CMD: %s\n", cmd); + if (dry_run) return 0; + + int rc = system(cmd); + if (rc != 0) { + acq_logf( "WARNING: putonslit command returned %d\n", rc); + return 1; + } + return 0; +} + +// --- ACAM guiding state helpers --- +static int acam_query_state(char* state, size_t state_sz, int verbose) +{ + if (g_hooks_initialized && g_hooks.acam_query_state) { + return g_hooks.acam_query_state(g_hooks.user, state, state_sz, verbose); + } + + if (!state || state_sz == 0) return 2; + state[0] = '\0'; + + FILE* fp = popen("acam acquire", "r"); + if (!fp) { + if (verbose) acq_logf( "WARNING: failed to run 'acam acquire': %s\n", strerror(errno)); + return 1; + } + + char buf[256]; + int found = 0; + while (fgets(buf, sizeof(buf), fp)) { + if (strcasestr(buf, "guiding")) { + snprintf(state, state_sz, "guiding"); + found = 1; + } else if (strcasestr(buf, "acquiring")) { + snprintf(state, state_sz, "acquiring"); + found = 1; + } + } + + int rc = pclose(fp); + if (rc != 0 && verbose) { + acq_logf( "WARNING: 'acam acquire' returned %d\n", rc); + } + + return found ? 0 : 2; +} + +static int wait_for_guiding(const AcqParams* p) +{ + if (!p->wait_guiding || p->dry_run) return 0; + + const double poll_sec = (p->guiding_poll_sec > 0) ? p->guiding_poll_sec : 1.0; + const double timeout_sec = p->guiding_timeout_sec; + + double t0 = now_monotonic_sec(); + char last_state[32] = {0}; + int warned = 0; + + for (;;) { + if (stop_requested()) return 2; + + char state[32] = {0}; + int rc = acam_query_state(state, sizeof(state), p->verbose); + if (rc == 0) { + if (strcmp(state, last_state) != 0) { + if (p->verbose) acq_logf( "ACAM state: %s\n", state); + snprintf(last_state, sizeof(last_state), "%s", state); + } + if (!strcmp(state, "guiding")) return 0; + } else { + if (!warned) { + acq_logf( "WARNING: could not determine ACAM state; will keep polling.\n"); + warned = 1; + } + } + + if (timeout_sec > 0) { + double dt = now_monotonic_sec() - t0; + if (dt >= timeout_sec) { + acq_logf( "WARNING: timed out waiting for guiding (%.1fs).\n", timeout_sec); + return 1; + } + } + + sleep_seconds(poll_sec); + } +} + + +// --- Camera command helpers --- +static int scam_framegrab_one(const char* outpath, int verbose) +{ + if (g_hooks_initialized && g_hooks.scam_framegrab_one) { + return g_hooks.scam_framegrab_one(g_hooks.user, outpath, verbose); + } + + char cmd[PATH_MAX + 128]; + // We assume scam writes the file atomically enough; if not, stream stability check handles it. + snprintf(cmd, sizeof(cmd), "scam framegrab one %s", outpath); + if (verbose) acq_logf( "CAM CMD: %s\n", cmd); + int rc = system(cmd); + if (rc != 0) { + acq_logf( "WARNING: framegrab command failed (rc=%d)\n", rc); + return 1; + } + return 0; +} + +static int scam_set_exptime(double exptime_sec, int dry_run, int verbose) +{ + if (g_hooks_initialized && g_hooks.scam_set_exptime) { + return g_hooks.scam_set_exptime(g_hooks.user, exptime_sec, dry_run, verbose); + } + + if (!isfinite(exptime_sec) || exptime_sec <= 0) return 1; + char cmd[256]; + snprintf(cmd, sizeof(cmd), "scam exptime %.3f", exptime_sec); + if (verbose || dry_run) acq_logf( "SCAM CMD: %s\n", cmd); + if (dry_run) return 0; + int rc = system(cmd); + if (rc != 0) { + acq_logf( "WARNING: scam exptime command failed (rc=%d)\n", rc); + return 1; + } + return 0; +} + +static int scam_set_avgframes(int avgframes, int dry_run, int verbose) +{ + if (g_hooks_initialized && g_hooks.scam_set_avgframes) { + return g_hooks.scam_set_avgframes(g_hooks.user, avgframes, dry_run, verbose); + } + + if (avgframes < 1) avgframes = 1; + char cmd[256]; + snprintf(cmd, sizeof(cmd), "scam avgframes %d", avgframes); + if (verbose || dry_run) acq_logf( "SCAM CMD: %s\n", cmd); + if (dry_run) return 0; + int rc = system(cmd); + if (rc != 0) { + acq_logf( "WARNING: scam avgframes command failed (rc=%d)\n", rc); + return 1; + } + return 0; +} + +static const char* adaptive_mode_name(AdaptiveMode m) +{ + if (m == ADAPT_MODE_FAINT) return "faint"; + if (m == ADAPT_MODE_BRIGHT) return "bright"; + return "baseline"; +} + +static AdaptiveMode adaptive_next_mode(AdaptiveMode current, double metric, const AcqParams* p) +{ + if (!isfinite(metric) || metric <= 0) return current; + + // Hysteresis prevents flapping when source counts jitter near thresholds. + const double faint_enter = p->adaptive_faint; + const double faint_exit = p->adaptive_faint * 1.15; + const double bright_enter = p->adaptive_bright; + const double bright_exit = p->adaptive_bright * 0.85; + + if (current == ADAPT_MODE_FAINT) { + if (metric >= bright_enter) return ADAPT_MODE_BRIGHT; + return (metric >= faint_exit) ? ADAPT_MODE_BASELINE : ADAPT_MODE_FAINT; + } + if (current == ADAPT_MODE_BRIGHT) { + if (metric <= faint_enter) return ADAPT_MODE_FAINT; + return (metric <= bright_exit) ? ADAPT_MODE_BASELINE : ADAPT_MODE_BRIGHT; + } + + if (metric <= faint_enter) return ADAPT_MODE_FAINT; + if (metric >= bright_enter) return ADAPT_MODE_BRIGHT; + return ADAPT_MODE_BASELINE; +} + +static int adaptive_bright_avg_target(double metric, double goal_metric) +{ + double ratio = (goal_metric > 0) ? (metric / goal_metric) : 1.0; + if (!isfinite(ratio)) ratio = 1.0; + if (ratio > 8.0) return 5; + if (ratio > 4.0) return 4; + if (ratio > 2.0) return 3; + if (ratio > 1.2) return 2; + return 1; +} + +static void adaptive_build_cycle_config(const AcqParams* p, const AdaptiveRuntime* rt, + AdaptiveMode mode, double metric, AdaptiveCycleConfig* cfg) +{ + memset(cfg, 0, sizeof(*cfg)); + cfg->mode = mode; + cfg->cadence_sec = p->cadence_sec; + cfg->reject_after_move = p->reject_after_move; + cfg->prec_arcsec = p->prec_arcsec; + cfg->goal_arcsec = p->goal_arcsec; + + double cur_exp = (rt && rt->have_last_camera) ? rt->last_exptime_sec : 1.0; + int cur_avg = (rt && rt->have_last_camera) ? rt->last_avgframes : 1; + if (!isfinite(cur_exp) || cur_exp <= 0) cur_exp = 1.0; + if (cur_exp < 0.1) cur_exp = 0.1; + if (cur_exp > 15.0) cur_exp = 15.0; + if (cur_avg < 1) cur_avg = 1; + if (cur_avg > 5) cur_avg = 5; + cfg->exptime_sec = cur_exp; + cfg->avgframes = cur_avg; + + if (mode == ADAPT_MODE_BASELINE) return; + + double goal_metric = (mode == ADAPT_MODE_FAINT) ? p->adaptive_faint_goal : p->adaptive_bright_goal; + if (!isfinite(goal_metric) || goal_metric <= 0) { + goal_metric = (mode == ADAPT_MODE_FAINT) ? p->adaptive_faint : p->adaptive_bright; + } + + double factor = 1.0; + if (isfinite(metric) && metric > 0 && isfinite(goal_metric) && goal_metric > 0) { + double lo = goal_metric * 0.85; + double hi = goal_metric * 1.15; + if (metric < lo || metric > hi) { + factor = sqrt(goal_metric / metric); + if (!isfinite(factor) || factor <= 0) factor = 1.0; + if (factor < 0.5) factor = 0.5; + if (factor > 2.0) factor = 2.0; + } + } + + double target_exp = cur_exp * factor; + if (target_exp < 0.1) target_exp = 0.1; + if (target_exp > 15.0) target_exp = 15.0; + + int target_avg = cur_avg; + if (mode == ADAPT_MODE_BRIGHT) { + int desired = adaptive_bright_avg_target(metric, goal_metric); + if (target_avg < desired) target_avg++; + else if (target_avg > desired) target_avg--; + } else if (mode == ADAPT_MODE_FAINT) { + if (target_avg > 1) target_avg--; + else if (target_avg < 1) target_avg++; + cfg->reject_after_move = (p->reject_after_move > 1) ? 1 : p->reject_after_move; + cfg->prec_arcsec = fmax(p->prec_arcsec, 0.20); + cfg->goal_arcsec = fmax(p->goal_arcsec, 0.30); + } + + cfg->exptime_sec = target_exp; + cfg->avgframes = target_avg; + cfg->apply_camera = (fabs(target_exp - cur_exp) >= 0.02 || target_avg != cur_avg); + + double min_cadence = cfg->exptime_sec * (double)cfg->avgframes + 0.20; + cfg->cadence_sec = fmax(cfg->cadence_sec, min_cadence); +} + +static int adaptive_apply_camera(const AcqParams* p, AdaptiveRuntime* rt, const AdaptiveCycleConfig* cfg) +{ + if (!p->adaptive) return 0; + if (!cfg->apply_camera) return 0; + + int rc = 0; + if (scam_set_exptime(cfg->exptime_sec, p->dry_run, p->verbose)) rc = 1; + if (scam_set_avgframes(cfg->avgframes, p->dry_run, p->verbose)) rc = 1; + rt->have_last_camera = 1; + rt->last_exptime_sec = cfg->exptime_sec; + rt->last_avgframes = cfg->avgframes; + return rc; +} + +static void adaptive_update_metric_and_mode(const AcqParams* p, AdaptiveRuntime* rt, double cycle_metric) +{ + if (!p->adaptive) return; + if (!isfinite(cycle_metric) || cycle_metric <= 0) return; + + if (!rt->have_metric) { + rt->metric_ewma = cycle_metric; + rt->have_metric = 1; + } else { + const double alpha = 0.35; + double lprev = log(fmax(rt->metric_ewma, 1e-6)); + double lnow = log(fmax(cycle_metric, 1e-6)); + rt->metric_ewma = exp((1.0 - alpha) * lprev + alpha * lnow); + } + + rt->mode = adaptive_next_mode(rt->mode, rt->metric_ewma, p); +} + +static void adaptive_finish_cycle(const AcqParams* p, AdaptiveRuntime* rt, + const double* metric_samp, int n) +{ + if (!p->adaptive || n <= 0) return; + + double mtmp[n]; + for (int i = 0; i < n; i++) mtmp[i] = metric_samp[i]; + double cycle_metric = median_of_doubles(mtmp, n); + + AdaptiveMode prev = rt->mode; + adaptive_update_metric_and_mode(p, rt, cycle_metric); + + if (p->verbose) { + if (prev != rt->mode) { + acq_logf( "Adaptive transition: %s -> %s cycle_metric=%.1f ewma=%.1f\n", + adaptive_mode_name(prev), adaptive_mode_name(rt->mode), + cycle_metric, rt->metric_ewma); + } else { + acq_logf( "Adaptive hold: mode=%s cycle_metric=%.1f ewma=%.1f\n", + adaptive_mode_name(rt->mode), cycle_metric, rt->metric_ewma); + } + } +} + +static double source_top_fraction_mean(const float* img, long nx, long ny, + const Detection* d, const AcqParams* p, + double frac) +{ + if (!img || !d || !p || nx <= 0 || ny <= 0) return 0.0; + if (!d->found) return 0.0; + if (!isfinite(frac) || frac <= 0 || frac > 1) frac = 0.10; + + double cx0 = (p->pixel_origin == 0) ? d->cx : (d->cx - 1.0); + double cy0 = (p->pixel_origin == 0) ? d->cy : (d->cy - 1.0); + + long xlo = (long)floor(cx0) - p->centroid_halfwin; + long xhi = (long)floor(cx0) + p->centroid_halfwin; + long ylo = (long)floor(cy0) - p->centroid_halfwin; + long yhi = (long)floor(cy0) + p->centroid_halfwin; + + if (xlo < d->sx1) xlo = d->sx1; + if (xhi > d->sx2) xhi = d->sx2; + if (ylo < d->sy1) ylo = d->sy1; + if (yhi > d->sy2) yhi = d->sy2; + + if (xhi < xlo || yhi < ylo) return 0.0; + + long maxn = (xhi - xlo + 1) * (yhi - ylo + 1); + if (maxn <= 0) return 0.0; + + float* vals = (float*)malloc((size_t)maxn * sizeof(float)); + if (!vals) return 0.0; + + long n = 0; + for (long y = ylo; y <= yhi; y++) { + long row0 = y * nx; + for (long x = xlo; x <= xhi; x++) { + double v = (double)img[row0 + x] - d->bkg; + if (v > 0) vals[n++] = (float)v; + } + } + + if (n <= 0) { + free(vals); + return 0.0; + } + + qsort(vals, (size_t)n, sizeof(float), cmp_float); + + long k = (long)ceil(frac * (double)n); + if (k < 1) k = 1; + if (k > n) k = n; + + long start = n - k; + double sum = 0.0; + for (long i = start; i < n; i++) sum += (double)vals[i]; + + free(vals); + return sum / (double)k; +} + +// --- Detection + centroiding --- +static Detection detect_star_near_goal(const float* img, long nx, long ny, const AcqParams* p) +{ + Detection d; + memset(&d, 0, sizeof(d)); + + // Stats ROI + compute_roi_0based(nx, ny, p->pixel_origin, + p->bg_roi_mask, p->bg_x1, p->bg_x2, p->bg_y1, p->bg_y2, + &d.rx1, &d.rx2, &d.ry1, &d.ry2); + + // Search ROI (defaults to stats ROI) + if (p->search_roi_mask == 0) { + d.sx1 = d.rx1; d.sx2 = d.rx2; d.sy1 = d.ry1; d.sy2 = d.ry2; + } else { + compute_roi_0based(nx, ny, p->pixel_origin, + p->search_roi_mask, p->search_x1, p->search_x2, p->search_y1, p->search_y2, + &d.sx1, &d.sx2, &d.sy1, &d.sy2); + } + + // Background and sigma from stats ROI + bg_sigma_sextractor_like(img, nx, ny, d.rx1, d.rx2, d.ry1, d.ry2, &d.bkg, &d.sigma); + if (!isfinite(d.sigma) || d.sigma <= 0) { + d.found = 0; + return d; + } + + // Goal (0-based) + double goal_x0 = (p->pixel_origin == 0) ? p->goal_x : (p->goal_x - 1.0); + double goal_y0 = (p->pixel_origin == 0) ? p->goal_y : (p->goal_y - 1.0); + + // Build detection patch (search ROI) and background-subtract + int w = (int)(d.sx2 - d.sx1 + 1); + int h = (int)(d.sy2 - d.sy1 + 1); + if (w <= 3 || h <= 3) { + d.found = 0; + return d; + } + + float* patch = (float*)malloc((size_t)w*(size_t)h*sizeof(float)); + float* tmp = (float*)malloc((size_t)w*(size_t)h*sizeof(float)); + float* filt = (float*)malloc((size_t)w*(size_t)h*sizeof(float)); + if (!patch || !tmp || !filt) die("malloc patch/tmp/filt failed"); + + for (int yy = 0; yy < h; yy++) { + long y = d.sy1 + yy; + long row0 = y * nx; + float* prow = patch + (size_t)yy*(size_t)w; + for (int xx = 0; xx < w; xx++) { + long x = d.sx1 + xx; + double v = (double)img[row0 + x] - d.bkg; + // keep negatives; filter uses them too + prow[xx] = (float)v; + } + } + + // Filter for detection + int kr = 0; + double* k = make_gaussian_kernel(p->filt_sigma_pix, &kr); + convolve_separable(patch, tmp, filt, w, h, k, kr); + double sumsq1d = kernel_sum_sq(k, kr); + free(k); + + // Detection threshold in filtered image + // For separable 2D kernel, sigma_filt = sigma_raw * sqrt(sum(K^2)) = sigma_raw * sumsq1d + double sigma_filt = d.sigma * sumsq1d; + if (!isfinite(sigma_filt) || sigma_filt <= 0) sigma_filt = d.sigma; + double thr_filt = p->snr_thresh * sigma_filt; + + // Raw threshold for adjacency check + double thr_raw = p->snr_thresh * d.sigma; + + // Search best local maximum + double best_val = -1e300; + int best_x = -1, best_y = -1; + double best_snr_raw = 0; + + for (int yy = 1; yy < h-1; yy++) { + for (int xx = 1; xx < w-1; xx++) { + float v = filt[(size_t)yy*(size_t)w + (size_t)xx]; + if ((double)v < thr_filt) continue; + + // local max in filtered + if (v < filt[(size_t)yy*(size_t)w + (size_t)(xx-1)]) continue; + if (v < filt[(size_t)yy*(size_t)w + (size_t)(xx+1)]) continue; + if (v < filt[(size_t)(yy-1)*(size_t)w + (size_t)xx]) continue; + if (v < filt[(size_t)(yy+1)*(size_t)w + (size_t)xx]) continue; + + long x0 = d.sx1 + xx; + long y0 = d.sy1 + yy; + + // within max distance from goal + double dxg = (double)x0 - goal_x0; + double dyg = (double)y0 - goal_y0; + if (hypot(dxg, dyg) > p->max_dist_pix) continue; + + // adjacency check in raw residual image (patch) + int nadj = 0; + for (int dy = -1; dy <= 1; dy++) { + for (int dx = -1; dx <= 1; dx++) { + if (dx == 0 && dy == 0) continue; + float rv = patch[(size_t)(yy+dy)*(size_t)w + (size_t)(xx+dx)]; + if ((double)rv > thr_raw) nadj++; + } + } + if (nadj < p->min_adjacent) continue; + + // best peak by raw peak value at that location (not filtered) + float rawv = patch[(size_t)yy*(size_t)w + (size_t)xx]; + double snr_here = (d.sigma > 0) ? ((double)rawv / d.sigma) : 0; + if ((double)rawv > best_val) { + best_val = (double)rawv; + best_x = (int)x0; + best_y = (int)y0; + best_snr_raw = snr_here; + } + } + } + + if (best_x < 0) { + free(patch); free(tmp); free(filt); + d.found = 0; + return d; + } + + // Iterative Gaussian-windowed centroid on raw residuals (img - bkg) + double cx = (double)best_x; + double cy = (double)best_y; + + int hw = p->centroid_halfwin; + double s2 = p->centroid_sigma_pix * p->centroid_sigma_pix; + if (s2 <= 0.1) s2 = 0.1; + + double sumI = 0, sumX = 0, sumY = 0; + for (int it = 0; it < p->centroid_maxiter; it++) { + long xlo = (long)floor(cx) - hw; + long xhi = (long)floor(cx) + hw; + long ylo = (long)floor(cy) - hw; + long yhi = (long)floor(cy) + hw; + + // clamp to search ROI + if (xlo < d.sx1) xlo = d.sx1; + if (xhi > d.sx2) xhi = d.sx2; + if (ylo < d.sy1) ylo = d.sy1; + if (yhi > d.sy2) yhi = d.sy2; + + sumI = sumX = sumY = 0.0; + for (long y = ylo; y <= yhi; y++) { + long row0 = y * nx; + for (long x = xlo; x <= xhi; x++) { + double I = (double)img[row0 + x] - d.bkg; + if (I <= 0) continue; + double dx = (double)x - cx; + double dy = (double)y - cy; + double wgt = exp(-0.5*(dx*dx + dy*dy)/s2); + double ww = wgt * I; + sumI += ww; + sumX += ww * (double)x; + sumY += ww * (double)y; + } + } + + if (sumI <= 0) break; + + double nxp = sumX / sumI; + double nyp = sumY / sumI; + + double shift = hypot(nxp - cx, nyp - cy); + cx = nxp; + cy = nyp; + + if (shift < p->centroid_eps_pix) break; + } + + if (sumI <= 0 || !isfinite(cx) || !isfinite(cy)) { + free(patch); free(tmp); free(filt); + d.found = 0; + return d; + } + + // aperture-like SNR within centroid window + long xlo = (long)floor(cx) - hw; + long xhi = (long)floor(cx) + hw; + long ylo = (long)floor(cy) - hw; + long yhi = (long)floor(cy) + hw; + if (xlo < d.sx1) xlo = d.sx1; + if (xhi > d.sx2) xhi = d.sx2; + if (ylo < d.sy1) ylo = d.sy1; + if (yhi > d.sy2) yhi = d.sy2; + + double sig_sum = 0.0; + long npix = 0; + for (long y = ylo; y <= yhi; y++) { + long row0 = y * nx; + for (long x = xlo; x <= xhi; x++) { + double I = (double)img[row0 + x] - d.bkg; + if (I <= 0) continue; + sig_sum += I; + npix++; + } + } + double noise = d.sigma * sqrt((double)(npix > 1 ? npix : 1)); + double snr_ap = (noise > 0) ? (sig_sum / noise) : 0.0; + + d.found = 1; + d.peak_val = best_val; + d.peak_snr_raw = best_snr_raw; + d.snr_ap = snr_ap; + + d.peak_x = (p->pixel_origin == 0) ? (double)best_x : (double)best_x + 1.0; + d.peak_y = (p->pixel_origin == 0) ? (double)best_y : (double)best_y + 1.0; + d.cx = (p->pixel_origin == 0) ? cx : cx + 1.0; + d.cy = (p->pixel_origin == 0) ? cy : cy + 1.0; + d.src_top10_mean = source_top_fraction_mean(img, nx, ny, &d, p, 0.10); + + free(patch); free(tmp); free(filt); + return d; +} + +// Compute a full FrameResult from an already-loaded FITS image and header. +static FrameResult solve_frame(const float* img, long nx, long ny, const char* header, int nkeys, const AcqParams* p) +{ + FrameResult r; + memset(&r, 0, sizeof(r)); + + r.det = detect_star_near_goal(img, nx, ny, p); + if (!r.det.found) { + r.ok = 0; + return r; + } + + r.dx_pix = r.det.cx - p->goal_x; + r.dy_pix = r.det.cy - p->goal_y; + + // WCS + struct wcsprm* wcs = NULL; + int nwcs = 0; + int wcs_stat = init_wcs_from_header(header, nkeys, &wcs, &nwcs); + if (wcs_stat != 0) { + r.wcs_ok = 0; + r.ok = 0; + return r; + } + + // Convert pixels to FITS 1-based for wcsp2s + double goal_x1 = (p->pixel_origin == 0) ? (p->goal_x + 1.0) : p->goal_x; + double goal_y1 = (p->pixel_origin == 0) ? (p->goal_y + 1.0) : p->goal_y; + double star_x1 = (p->pixel_origin == 0) ? (r.det.cx + 1.0) : r.det.cx; + double star_y1 = (p->pixel_origin == 0) ? (r.det.cy + 1.0) : r.det.cy; + + if (pix2world_wcs(&wcs[0], goal_x1, goal_y1, &r.ra_goal_deg, &r.dec_goal_deg) || + pix2world_wcs(&wcs[0], star_x1, star_y1, &r.ra_star_deg, &r.dec_star_deg)) { + r.wcs_ok = 0; + r.ok = 0; + wcsvfree(&nwcs, &wcs); + return r; + } + + wcsvfree(&nwcs, &wcs); + + r.wcs_ok = 1; + + // Commanded offsets: (star - goal) + double dra_deg = wrap_dra_deg(r.ra_star_deg - r.ra_goal_deg); + double ddec_deg = (r.dec_star_deg - r.dec_goal_deg); + + double cosdec = cos(r.dec_goal_deg * M_PI / 180.0); + double dra_arcsec = dra_deg * 3600.0; + if (p->dra_use_cosdec) dra_arcsec *= cosdec; + double ddec_arcsec = ddec_deg * 3600.0; + + dra_arcsec *= (double)p->tcs_sign; + ddec_arcsec *= (double)p->tcs_sign; + + r.dra_cmd_arcsec = dra_arcsec; + r.ddec_cmd_arcsec = ddec_arcsec; + r.r_cmd_arcsec = hypot(dra_arcsec, ddec_arcsec); + + // Final accept criteria for "good sample" + // - windowed SNR must be >= threshold (it is typically more stable than raw peak SNR) + if (r.det.snr_ap < p->snr_thresh) { + r.ok = 0; + return r; + } + + r.ok = 1; + return r; +} + +// --- Frame acquisition / gating --- +static int stat_file(const char* path, struct stat* st) +{ + if (stat(path, st) != 0) return 1; + if (st->st_size <= 0) return 2; + return 0; +} + +static int wait_for_stream_update(const char* path, FrameState* fs, double settle_check_sec, double cadence_sec, int verbose) +{ + // Wait until file mtime/size changes from last accepted, then is stable across settle_check_sec. + // Also enforce a minimum time between accepted frames (cadence_sec). + struct stat st1, st2; + for (;;) { + if (stop_requested()) return 2; + + if (stat_file(path, &st1) != 0) { + sleep_seconds(0.1); + continue; + } + + // Newness check + if (fs->mtime == st1.st_mtime && fs->size == st1.st_size) { + sleep_seconds(0.1); + continue; + } + + // Stability check (not writing) + sleep_seconds(settle_check_sec); + if (stat_file(path, &st2) != 0) { + sleep_seconds(0.1); + continue; + } + if (st2.st_mtime != st1.st_mtime || st2.st_size != st1.st_size) { + // still changing + sleep_seconds(0.05); + continue; + } + + // Cadence check + if (cadence_sec > 0) { + double tnow = now_monotonic_sec(); + double tlast = (double)fs->t_accept.tv_sec + 1e-9*(double)fs->t_accept.tv_nsec; + if (tlast > 0 && (tnow - tlast) < cadence_sec) { + sleep_seconds(0.05); + continue; + } + } + + // accept this as a candidate to read + if (verbose) { + acq_logf( "Frame candidate: mtime=%ld size=%ld\n", (long)st2.st_mtime, (long)st2.st_size); + } + fs->mtime = st2.st_mtime; + fs->size = st2.st_size; + clock_gettime(CLOCK_MONOTONIC, &fs->t_accept); + return 0; + } +} + +// Read next frame into memory (img/header) according to frame_mode. +// Returns 0 on success. +static int acquire_next_frame(const AcqParams* p, FrameState* fs, + double cadence_sec, + float** img_out, long* nx_out, long* ny_out, + char** header_out, int* nkeys_out, + double* exptime_sec_out) +{ + const char* path = NULL; + + if (p->frame_mode == FRAME_FRAMEGRAB) { + // Acquire synchronously via scam + if (scam_framegrab_one(p->framegrab_out, p->verbose)) return 2; + // Give filesystem a tiny moment; and then read when stable + // (framegrab should generally be complete when system() returns, but be safe) + (void)wait_for_stream_update(p->framegrab_out, fs, 0.05, 0.0, 0); + path = p->framegrab_out; + } else { + // Stream mode: wait until file updates + if (wait_for_stream_update(p->input, fs, 0.05, cadence_sec, 0)) return 3; + path = p->input; + } + + int used_hdu = 0; + char used_extname[64] = {0}; + double exptime_sec = 1.0; + int st = read_fits_image_and_header(path, p, img_out, nx_out, ny_out, header_out, nkeys_out, + &used_hdu, used_extname, sizeof(used_extname), + &exptime_sec); + if (st) { + acq_logf( "ERROR: CFITSIO read failed for %s (status=%d)\n", path, st); + return 4; + } + if (exptime_sec_out) *exptime_sec_out = exptime_sec; + return 0; +} + +// --- CLI --- +static void usage(const char* argv0) +{ + acq_logf( + "Usage: %s --input PATH --goal-x X --goal-y Y [options]\n" + "\n" + "Core options:\n" + " --input PATH Streamed FITS file path (default /tmp/slicecam.fits)\n" + " --goal-x X --goal-y Y Goal pixel coordinates\n" + " --pixel-origin 0|1 Coordinate origin for goal/ROI (default 0)\n" + "\n" + "Frame acquisition:\n" + " --frame-mode stream|framegrab (default stream)\n" + " --framegrab-out PATH Output FITS path for framegrab (default /tmp/ngps_acq.fits)\n" + "\n" + "Detection / centroiding:\n" + " --snr S SNR threshold (sigma) (default 8)\n" + " --max-dist PIX Search radius around goal (default 200)\n" + " --min-adj N Min adjacent raw pixels above threshold (default 4)\n" + " --filt-sigma PIX Detection filter sigma (default 1.2)\n" + " --centroid-hw N Centroid half-window (default 6)\n" + " --centroid-sigma PIX Centroid window sigma (default 2.0)\n" + "\n" + "FITS selection:\n" + " --extname NAME Prefer this EXTNAME (default L). Use 'none' to disable.\n" + " --extnum N Fallback HDU index (0=primary,1=first ext...) (default 1)\n" + "\n" + "ROIs (inclusive bounds; same origin as goal):\n" + " --bg-x1 N --bg-x2 N --bg-y1 N --bg-y2 N Background/stats ROI\n" + " --search-x1 N --search-x2 N --search-y1 N --search-y2 N Search ROI (defaults to bg ROI)\n" + "\n" + "Closed-loop acquisition:\n" + " --loop 0|1 Enable closed-loop mode (default 0)\n" + " --cadence-sec S Minimum seconds between accepted frames (stream) (default 0.0)\n" + " --max-samples N Samples to collect per move (default 10)\n" + " --min-samples N Minimum before evaluating precision (default 3)\n" + " --prec-arcsec A Required robust scatter per axis (default 0.1)\n" + " --goal-arcsec A Converge threshold on |offset| (default 0.1)\n" + " --max-cycles N Move cycles (default 20)\n" + " --gain G Gain applied to move (default 0.7)\n" + " --adaptive 0|1 Adaptive extremes-only mode (default 0)\n" + " --adaptive-faint X Start faint adaptation when metric <= X (default 500)\n" + " --adaptive-faint-goal X Faint-mode target metric (default 500)\n" + " --adaptive-bright Y Start bright adaptation when metric >= Y (default 10000)\n" + " --adaptive-bright-goal Y Bright-mode target metric (default 10000)\n" + " Metric is top-10%% mean source counts (background-subtracted).\n" + " In-range [faint,bright] keeps baseline behavior.\n" + "\n" + "Robustness / safety:\n" + " --reject-identical 0|1 Reject identical frames (default 1)\n" + " --reject-after-move N Reject N new frames after move (default 2)\n" + " --settle-sec S Sleep after move (default 0.0)\n" + " --max-move-arcsec A Do not issue moves larger than this (default 10)\n" + " --continue-on-fail 0|1 If 0: exit on failure; if 1: keep trying (default 0)\n" + "\n" + "TCS conventions:\n" + " --tcs-set-units 0|1 Set native dra/ddec units to arcsec once (default 1)\n" + " --use-putonslit 0|1 Use scam daemon putonslit for moves (default 0)\n" + " --dra-use-cosdec 0|1 dra = dRA*cos(dec) (default 1)\n" + " --tcs-sign +1|-1 Multiply commanded offsets by sign (default +1)\n" + "\n" + "Guiding wait (after putonslit):\n" + " --wait-guiding 0|1 Wait for ACAM guiding state after putonslit (default 1)\n" + " --guiding-poll-sec S Poll period for 'acam acquire' (default 1.0)\n" + " --guiding-timeout-sec S Timeout waiting for guiding; 0=wait forever (default 120)\n" + "\n" + "Debug:\n" + " --debug 0|1 Write overlay PPM (default 0)\n" + " --debug-out PATH PPM path (default ./ngps_acq_debug.ppm)\n" + "\n" + "General:\n" + " --dry-run 0|1 Do not call TCS (default 0)\n" + " --verbose 0|1 Verbose logs (default 1)\n", + argv0 + ); +} + +static void set_defaults(AcqParams* p) +{ + memset(p, 0, sizeof(*p)); + snprintf(p->input, sizeof(p->input), "/tmp/slicecam.fits"); + p->frame_mode = FRAME_STREAM; + snprintf(p->framegrab_out, sizeof(p->framegrab_out), "/tmp/ngps_acq.fits"); + p->framegrab_use_tmp = 0; + + p->goal_x = 0; + p->goal_y = 0; + p->pixel_origin = 0; + + p->max_dist_pix = 200.0; + p->snr_thresh = 8.0; + p->min_adjacent = 4; + + p->filt_sigma_pix = 1.2; + + p->centroid_halfwin = 6; + p->centroid_sigma_pix = 2.0; + p->centroid_maxiter = 12; + p->centroid_eps_pix = 0.01; + + p->extnum = 1; + snprintf(p->extname, sizeof(p->extname), "L"); + + p->bg_roi_mask = 0; + p->search_roi_mask = 0; + + p->loop = 0; + p->cadence_sec = 0.0; + p->max_samples = 10; + p->min_samples = 3; + p->prec_arcsec = 0.1; + p->goal_arcsec = 0.1; + p->max_cycles = 20; + p->gain = 0.7; + p->adaptive = 0; + p->adaptive_faint = 500.0; + p->adaptive_faint_goal = 500.0; + p->adaptive_bright = 10000.0; + p->adaptive_bright_goal = 10000.0; + + p->reject_identical = 1; + p->reject_after_move = 2; + p->settle_sec = 0.0; + p->max_move_arcsec = 10.0; + p->continue_on_fail = 0; + + p->dra_use_cosdec = 1; + p->tcs_sign = +1; + + p->tcs_set_units = 1; + + p->use_putonslit = 0; + p->wait_guiding = 1; + p->guiding_poll_sec = 1.0; + p->guiding_timeout_sec = 120.0; + + p->debug = 0; + snprintf(p->debug_out, sizeof(p->debug_out), "./ngps_acq_debug.ppm"); + + p->dry_run = 0; + p->verbose = 1; +} + +static int parse_args(int argc, char** argv, AcqParams* p) +{ + for (int i = 1; i < argc; i++) { + const char* a = argv[i]; + if (!strcmp(a, "--input") && i+1 < argc) { + snprintf(p->input, sizeof(p->input), "%s", argv[++i]); + } else if (!strcmp(a, "--goal-x") && i+1 < argc) { + p->goal_x = atof(argv[++i]); + } else if (!strcmp(a, "--goal-y") && i+1 < argc) { + p->goal_y = atof(argv[++i]); + } else if (!strcmp(a, "--pixel-origin") && i+1 < argc) { + p->pixel_origin = atoi(argv[++i]); + + } else if (!strcmp(a, "--frame-mode") && i+1 < argc) { + const char* m = argv[++i]; + if (!strcasecmp(m, "stream")) p->frame_mode = FRAME_STREAM; + else if (!strcasecmp(m, "framegrab")) p->frame_mode = FRAME_FRAMEGRAB; + else { acq_logf( "Invalid --frame-mode: %s\n", m); return -1; } + } else if (!strcmp(a, "--framegrab-out") && i+1 < argc) { + snprintf(p->framegrab_out, sizeof(p->framegrab_out), "%s", argv[++i]); + + } else if (!strcmp(a, "--max-dist") && i+1 < argc) { + p->max_dist_pix = atof(argv[++i]); + } else if (!strcmp(a, "--snr") && i+1 < argc) { + p->snr_thresh = atof(argv[++i]); + } else if (!strcmp(a, "--min-adj") && i+1 < argc) { + p->min_adjacent = atoi(argv[++i]); + } else if (!strcmp(a, "--filt-sigma") && i+1 < argc) { + p->filt_sigma_pix = atof(argv[++i]); + } else if (!strcmp(a, "--centroid-hw") && i+1 < argc) { + p->centroid_halfwin = atoi(argv[++i]); + } else if (!strcmp(a, "--centroid-sigma") && i+1 < argc) { + p->centroid_sigma_pix = atof(argv[++i]); + + } else if (!strcmp(a, "--extnum") && i+1 < argc) { + p->extnum = atoi(argv[++i]); + } else if (!strcmp(a, "--extname") && i+1 < argc) { + snprintf(p->extname, sizeof(p->extname), "%s", argv[++i]); + if (!strcasecmp(p->extname, "none")) p->extname[0] = '\0'; + + } else if (!strcmp(a, "--bg-x1") && i+1 < argc) { + p->bg_x1 = atol(argv[++i]); p->bg_roi_mask |= ROI_X1_SET; + } else if (!strcmp(a, "--bg-x2") && i+1 < argc) { + p->bg_x2 = atol(argv[++i]); p->bg_roi_mask |= ROI_X2_SET; + } else if (!strcmp(a, "--bg-y1") && i+1 < argc) { + p->bg_y1 = atol(argv[++i]); p->bg_roi_mask |= ROI_Y1_SET; + } else if (!strcmp(a, "--bg-y2") && i+1 < argc) { + p->bg_y2 = atol(argv[++i]); p->bg_roi_mask |= ROI_Y2_SET; + + } else if (!strcmp(a, "--search-x1") && i+1 < argc) { + p->search_x1 = atol(argv[++i]); p->search_roi_mask |= ROI_X1_SET; + } else if (!strcmp(a, "--search-x2") && i+1 < argc) { + p->search_x2 = atol(argv[++i]); p->search_roi_mask |= ROI_X2_SET; + } else if (!strcmp(a, "--search-y1") && i+1 < argc) { + p->search_y1 = atol(argv[++i]); p->search_roi_mask |= ROI_Y1_SET; + } else if (!strcmp(a, "--search-y2") && i+1 < argc) { + p->search_y2 = atol(argv[++i]); p->search_roi_mask |= ROI_Y2_SET; + + } else if (!strcmp(a, "--loop") && i+1 < argc) { + p->loop = atoi(argv[++i]); + } else if (!strcmp(a, "--cadence-sec") && i+1 < argc) { + p->cadence_sec = atof(argv[++i]); + } else if (!strcmp(a, "--max-samples") && i+1 < argc) { + p->max_samples = atoi(argv[++i]); + } else if (!strcmp(a, "--min-samples") && i+1 < argc) { + p->min_samples = atoi(argv[++i]); + } else if (!strcmp(a, "--prec-arcsec") && i+1 < argc) { + p->prec_arcsec = atof(argv[++i]); + } else if (!strcmp(a, "--goal-arcsec") && i+1 < argc) { + p->goal_arcsec = atof(argv[++i]); + } else if (!strcmp(a, "--max-cycles") && i+1 < argc) { + p->max_cycles = atoi(argv[++i]); + } else if (!strcmp(a, "--gain") && i+1 < argc) { + p->gain = atof(argv[++i]); + } else if (!strcmp(a, "--adaptive") && i+1 < argc) { + p->adaptive = atoi(argv[++i]); + } else if (!strcmp(a, "--adaptive-faint") && i+1 < argc) { + p->adaptive_faint = atof(argv[++i]); + } else if (!strcmp(a, "--adaptive-faint-goal") && i+1 < argc) { + p->adaptive_faint_goal = atof(argv[++i]); + } else if (!strcmp(a, "--adaptive-bright") && i+1 < argc) { + p->adaptive_bright = atof(argv[++i]); + } else if (!strcmp(a, "--adaptive-bright-goal") && i+1 < argc) { + p->adaptive_bright_goal = atof(argv[++i]); + + } else if (!strcmp(a, "--reject-identical") && i+1 < argc) { + p->reject_identical = atoi(argv[++i]); + } else if (!strcmp(a, "--reject-after-move") && i+1 < argc) { + p->reject_after_move = atoi(argv[++i]); + } else if (!strcmp(a, "--settle-sec") && i+1 < argc) { + p->settle_sec = atof(argv[++i]); + } else if (!strcmp(a, "--max-move-arcsec") && i+1 < argc) { + p->max_move_arcsec = atof(argv[++i]); + } else if (!strcmp(a, "--continue-on-fail") && i+1 < argc) { + p->continue_on_fail = atoi(argv[++i]); + + } else if (!strcmp(a, "--dra-use-cosdec") && i+1 < argc) { + p->dra_use_cosdec = atoi(argv[++i]); + } else if (!strcmp(a, "--tcs-sign") && i+1 < argc) { + p->tcs_sign = atoi(argv[++i]); + if (!(p->tcs_sign == 1 || p->tcs_sign == -1)) { acq_logf( "--tcs-sign must be +1 or -1\n"); return -1; } + } else if (!strcmp(a, "--tcs-set-units") && i+1 < argc) { + p->tcs_set_units = atoi(argv[++i]); + + } else if (!strcmp(a, "--use-putonslit") && i+1 < argc) { + p->use_putonslit = atoi(argv[++i]); + + } else if (!strcmp(a, "--wait-guiding") && i+1 < argc) { + p->wait_guiding = atoi(argv[++i]); + } else if (!strcmp(a, "--guiding-poll-sec") && i+1 < argc) { + p->guiding_poll_sec = atof(argv[++i]); + } else if (!strcmp(a, "--guiding-timeout-sec") && i+1 < argc) { + p->guiding_timeout_sec = atof(argv[++i]); + + } else if (!strcmp(a, "--debug") && i+1 < argc) { + p->debug = atoi(argv[++i]); + } else if (!strcmp(a, "--debug-out") && i+1 < argc) { + snprintf(p->debug_out, sizeof(p->debug_out), "%s", argv[++i]); + + } else if (!strcmp(a, "--dry-run") && i+1 < argc) { + p->dry_run = atoi(argv[++i]); + } else if (!strcmp(a, "--verbose") && i+1 < argc) { + p->verbose = atoi(argv[++i]); + + } else if (!strcmp(a, "--help") || !strcmp(a, "-h")) { + return 0; + } else { + acq_logf( "Unknown/invalid arg: %s\n", a); + return -1; + } + } + + if (!(p->pixel_origin == 0 || p->pixel_origin == 1)) { + acq_logf( "--pixel-origin must be 0 or 1\n"); + return -1; + } + if (!isfinite(p->goal_x) || !isfinite(p->goal_y)) { + acq_logf( "You must provide --goal-x and --goal-y\n"); + return -1; + } + if (p->max_samples < 1) p->max_samples = 1; + if (p->min_samples < 1) p->min_samples = 1; + if (p->min_samples > p->max_samples) p->min_samples = p->max_samples; + if (p->gain < 0) p->gain = 0; + if (p->gain > 1.5) p->gain = 1.5; + if (!(p->adaptive == 0 || p->adaptive == 1)) { + acq_logf( "--adaptive must be 0 or 1\n"); + return -1; + } + if (!isfinite(p->adaptive_faint) || p->adaptive_faint <= 0) { + acq_logf( "--adaptive-faint must be > 0\n"); + return -1; + } + if (!isfinite(p->adaptive_bright) || p->adaptive_bright <= p->adaptive_faint) { + acq_logf( "--adaptive-bright must be > --adaptive-faint\n"); + return -1; + } + if (!isfinite(p->adaptive_faint_goal) || p->adaptive_faint_goal <= 0) { + acq_logf( "--adaptive-faint-goal must be > 0\n"); + return -1; + } + if (!isfinite(p->adaptive_bright_goal) || p->adaptive_bright_goal <= 0) { + acq_logf( "--adaptive-bright-goal must be > 0\n"); + return -1; + } + if (p->max_move_arcsec <= 0) p->max_move_arcsec = 10.0; + if (p->reject_after_move < 0) p->reject_after_move = 0; + if (p->cadence_sec < 0) p->cadence_sec = 0; + if (p->filt_sigma_pix <= 0) p->filt_sigma_pix = 1.2; + if (p->centroid_sigma_pix <= 0) p->centroid_sigma_pix = 2.0; + if (p->centroid_halfwin < 2) p->centroid_halfwin = 2; + if (p->guiding_poll_sec <= 0) p->guiding_poll_sec = 1.0; + if (p->guiding_timeout_sec < 0) p->guiding_timeout_sec = 0; + + if (p->use_putonslit) { + // putonslit computes PT internally; no need to set native dra/ddec units. + p->tcs_set_units = 0; + } + + return 1; +} + +// --- One-shot processing --- +static int process_once(const AcqParams* p, FrameState* fs) +{ + float* img = NULL; + long nx=0, ny=0; + char* header = NULL; + int nkeys = 0; + double exptime_sec = 1.0; + + int rc = acquire_next_frame(p, fs, p->cadence_sec, &img, &nx, &ny, &header, &nkeys, &exptime_sec); + if (rc) { + if (img) free(img); + if (header) free(header); + return 4; + } + + (void)exptime_sec; + + // signature for rejecting identical frames + if (p->reject_identical) { + long sx1,sx2,sy1,sy2; + compute_roi_0based(nx, ny, p->pixel_origin, + p->bg_roi_mask, p->bg_x1, p->bg_x2, p->bg_y1, p->bg_y2, + &sx1,&sx2,&sy1,&sy2); + uint64_t sig = image_signature_subsample(img, nx, ny, sx1,sx2,sy1,sy2); + if (fs->have_sig && sig == fs->sig) { + if (p->verbose) acq_logf( "Duplicate frame signature (reject).\n"); + free(img); + if (header) free(header); + return 2; + } + fs->sig = sig; + fs->have_sig = 1; + } + + FrameResult fr = solve_frame(img, nx, ny, header, nkeys, p); + + if (p->debug) { + (void)write_debug_ppm(p->debug_out, img, nx, + fr.det.rx1, fr.det.rx2, fr.det.ry1, fr.det.ry2, + fr.det.bkg, fr.det.sigma, p->snr_thresh, + &fr.det, p); + } + + free(img); + if (header) free(header); + + if (!fr.ok) { + if (p->verbose) acq_logf( "No valid solution (star/WCS/quality).\n"); + return 2; + } + + if (p->verbose) { + acq_logf( "Centroid=(%.3f,%.3f) dx=%.3f dy=%.3f pix SNR_ap=%.2f\n", + fr.det.cx, fr.det.cy, fr.dx_pix, fr.dy_pix, fr.det.snr_ap); + if (p->adaptive) { + acq_logf( "Source metric (top10%% mean count): %.1f\n", fr.det.src_top10_mean); + } + acq_logf( "Command offsets (arcsec): dra=%.3f ddec=%.3f r=%.3f\n", + fr.dra_cmd_arcsec, fr.ddec_cmd_arcsec, fr.r_cmd_arcsec); + } + + // Machine-readable line + printf("NGPS_ACQ_RESULT ok=1 cx=%.6f cy=%.6f dra_arcsec=%.6f ddec_arcsec=%.6f r_arcsec=%.6f snr_ap=%.3f\n", + fr.det.cx, fr.det.cy, fr.dra_cmd_arcsec, fr.ddec_cmd_arcsec, fr.r_cmd_arcsec, fr.det.snr_ap); + + // Safety: do not move without stats in loop mode; in one-shot we do a single move only if --loop=0 + if (!p->loop) { + if (fr.r_cmd_arcsec > p->max_move_arcsec) { + acq_logf( "REFUSE MOVE: |offset|=%.3f"" exceeds --max-move-arcsec=%.3f\n", fr.r_cmd_arcsec, p->max_move_arcsec); + return 3; + } + + double dra = p->gain * fr.dra_cmd_arcsec; + double ddec = p->gain * fr.ddec_cmd_arcsec; + + if (p->use_putonslit) { + if (!fr.wcs_ok) { + acq_logf( "REFUSE MOVE: WCS not available for putonslit\n"); + return 3; + } + double cross_ra=0.0, cross_dec=0.0; + slit_cross_from_offsets(p, fr.ra_goal_deg, fr.dec_goal_deg, dra, ddec, &cross_ra, &cross_dec); + (void)scam_putonslit_deg(fr.ra_goal_deg, fr.dec_goal_deg, cross_ra, cross_dec, p->dry_run, p->verbose); + (void)wait_for_guiding(p); + } else { + (void)tcs_move_arcsec(dra, ddec, p->dry_run, p->verbose); + } + } + + return 0; +} + +// --- Closed-loop acquisition --- +static int run_loop(const AcqParams* p) +{ + FrameState fs; + memset(&fs, 0, sizeof(fs)); + fs.t_accept.tv_sec = 0; + fs.t_accept.tv_nsec = 0; + + if (!p->use_putonslit && p->tcs_set_units) (void)tcs_set_native_units(p->dry_run, p->verbose); + + int skip_after_move = 0; + AdaptiveRuntime adaptive_rt; + memset(&adaptive_rt, 0, sizeof(adaptive_rt)); + adaptive_rt.mode = ADAPT_MODE_BASELINE; + double initial_metric_exptime_sec = 1.0; + int have_initial_metric_exptime = 0; + int initial_metric_scaled_once = 0; + + for (int cycle = 1; cycle <= p->max_cycles && !stop_requested(); cycle++) { + if (p->verbose) acq_logf( "\n=== Cycle %d/%d ===\n", cycle, p->max_cycles); + + AdaptiveCycleConfig cycle_cfg; + adaptive_build_cycle_config(p, &adaptive_rt, ADAPT_MODE_BASELINE, 0.0, &cycle_cfg); + if (p->adaptive) { + double metric_cfg = adaptive_rt.have_metric ? adaptive_rt.metric_ewma : 0.0; + if ( !initial_metric_scaled_once && + adaptive_rt.have_metric && + adaptive_rt.mode != ADAPT_MODE_BASELINE && + have_initial_metric_exptime && + initial_metric_exptime_sec > 0.0 ) { + metric_cfg /= initial_metric_exptime_sec; + initial_metric_scaled_once = 1; + if ( p->verbose ) { + acq_logf( "Adaptive one-shot metric scaling: metric/EXPTIME using initial EXPTIME=%.3fs\n", + initial_metric_exptime_sec ); + } + } + adaptive_build_cycle_config(p, &adaptive_rt, adaptive_rt.mode, metric_cfg, &cycle_cfg); + (void)adaptive_apply_camera(p, &adaptive_rt, &cycle_cfg); + if (p->verbose) { + if (adaptive_rt.have_metric) { + acq_logf( + "Adaptive mode=%s ewma=%.1f exp=%.2fs avg=%d cadence=%.2fs reject_after_move=%d prec=%.3f\" goal=%.3f\"\n", + adaptive_mode_name(adaptive_rt.mode), adaptive_rt.metric_ewma, + cycle_cfg.exptime_sec, cycle_cfg.avgframes, + cycle_cfg.cadence_sec, cycle_cfg.reject_after_move, + cycle_cfg.prec_arcsec, cycle_cfg.goal_arcsec); + } else { + acq_logf( + "Adaptive mode=%s exp=%.2fs avg=%d cadence=%.2fs reject_after_move=%d prec=%.3f\" goal=%.3f\"\n", + adaptive_mode_name(adaptive_rt.mode), cycle_cfg.exptime_sec, cycle_cfg.avgframes, + cycle_cfg.cadence_sec, cycle_cfg.reject_after_move, + cycle_cfg.prec_arcsec, cycle_cfg.goal_arcsec); + } + } + } + + double runtime_cadence_sec = cycle_cfg.cadence_sec; + int runtime_reject_after_move = cycle_cfg.reject_after_move; + double runtime_prec_arcsec = cycle_cfg.prec_arcsec; + double runtime_goal_arcsec = cycle_cfg.goal_arcsec; + + double dra_samp[p->max_samples]; + double ddec_samp[p->max_samples]; + double metric_samp[p->max_samples]; + int n = 0; + + double slit_ra_deg = 0.0, slit_dec_deg = 0.0; + int have_slit = 0; + + int attempts = 0; + int max_attempts = p->max_samples * 10; + int invalid_solution_total = 0; + int faint_rescue_steps = 0; + + while (n < p->max_samples && attempts < max_attempts && !stop_requested()) { + attempts++; + + float* img = NULL; + long nx=0, ny=0; + char* header = NULL; + int nkeys = 0; + double exptime_sec = 1.0; + + int rc = acquire_next_frame(p, &fs, runtime_cadence_sec, &img, &nx, &ny, &header, &nkeys, &exptime_sec); + if (rc) { + if (img) free(img); + if (header) free(header); + acq_logf( "WARNING: failed to acquire frame (rc=%d)\n", rc); + sleep_seconds(0.1); + continue; + } + + // signature-based duplicate reject (compute on stats ROI) + if (p->reject_identical) { + long sx1,sx2,sy1,sy2; + compute_roi_0based(nx, ny, p->pixel_origin, + p->bg_roi_mask, p->bg_x1, p->bg_x2, p->bg_y1, p->bg_y2, + &sx1,&sx2,&sy1,&sy2); + uint64_t sig = image_signature_subsample(img, nx, ny, sx1,sx2,sy1,sy2); + if (fs.have_sig && sig == fs.sig) { + if (p->verbose) acq_logf( "Reject: identical frame signature\n"); + free(img); + if (header) free(header); + continue; + } + fs.sig = sig; + fs.have_sig = 1; + } + + if (skip_after_move > 0) { + skip_after_move--; + if (p->verbose) acq_logf( "Reject: post-move frame (%d remaining)\n", skip_after_move); + free(img); + if (header) free(header); + continue; + } + + FrameResult fr = solve_frame(img, nx, ny, header, nkeys, p); + + if (p->debug) { + (void)write_debug_ppm(p->debug_out, img, nx, + fr.det.rx1, fr.det.rx2, fr.det.ry1, fr.det.ry2, + fr.det.bkg, fr.det.sigma, p->snr_thresh, + &fr.det, p); + } + + free(img); + if (header) free(header); + + if (!fr.ok) { + invalid_solution_total++; + if (p->adaptive && invalid_solution_total >= (6 * (faint_rescue_steps + 1))) { + double cur_exp = adaptive_rt.have_last_camera ? adaptive_rt.last_exptime_sec : exptime_sec; + int cur_avg = adaptive_rt.have_last_camera ? adaptive_rt.last_avgframes : 1; + if (!isfinite(cur_exp) || cur_exp <= 0.0) cur_exp = 1.0; + if (cur_avg < 1) cur_avg = 1; + if (cur_avg > 5) cur_avg = 5; + + static const double rescue_exptime_ladder[] = { 0.1, 0.5, 1.0, 2.0, 3.0, 4.0, 5.0, 6.0, 10.0, 15.0 }; + double target_exp = cur_exp; + for (size_t i = 0; i < sizeof(rescue_exptime_ladder)/sizeof(rescue_exptime_ladder[0]); i++) { + if (rescue_exptime_ladder[i] > cur_exp + 1e-6) { + target_exp = rescue_exptime_ladder[i]; + break; + } + } + + if (target_exp > cur_exp + 1e-6) { + if (p->verbose) { + acq_logf( "Adaptive faint rescue: %d invalid frames; exptime %.3fs -> %.3fs\n", + invalid_solution_total, cur_exp, target_exp); + } + (void)scam_set_exptime(target_exp, p->dry_run, p->verbose); + adaptive_rt.have_last_camera = 1; + adaptive_rt.last_exptime_sec = target_exp; + adaptive_rt.last_avgframes = cur_avg; + runtime_cadence_sec = fmax(runtime_cadence_sec, target_exp * (double)cur_avg + 0.20); + } + else { + if (p->verbose) { + acq_logf( "Adaptive faint rescue: %d invalid frames; exptime cannot increase further (cur=%.3fs)\n", + invalid_solution_total, cur_exp); + } + } + + faint_rescue_steps++; + } + if (p->verbose) acq_logf( "Reject: no valid solution (SNR/WCS/star).\n"); + continue; + } + + if (fr.r_cmd_arcsec > p->max_move_arcsec) { + acq_logf( "Reject: |offset|=%.3f"" exceeds --max-move-arcsec=%.3f\n", fr.r_cmd_arcsec, p->max_move_arcsec); + continue; + } + + if ( !have_initial_metric_exptime && isfinite(exptime_sec) && exptime_sec > 0.0 ) { + initial_metric_exptime_sec = exptime_sec; + have_initial_metric_exptime = 1; + } + + metric_samp[n] = fr.det.src_top10_mean; + dra_samp[n] = fr.dra_cmd_arcsec; + ddec_samp[n] = fr.ddec_cmd_arcsec; + slit_ra_deg = fr.ra_goal_deg; + slit_dec_deg = fr.dec_goal_deg; + have_slit = 1; + n++; + + // Compute robust stats on the fly + double dra_tmp[p->max_samples]; + double ddec_tmp[p->max_samples]; + for (int i = 0; i < n; i++) { dra_tmp[i]=dra_samp[i]; ddec_tmp[i]=ddec_samp[i]; } + double med_dra = median_of_doubles(dra_tmp, n); + double med_ddec = median_of_doubles(ddec_tmp, n); + double sig_dra = mad_sigma_of_doubles(dra_samp, n, med_dra); + double sig_ddec = mad_sigma_of_doubles(ddec_samp, n, med_ddec); + double rmed = hypot(med_dra, med_ddec); + + if (p->verbose) { + if (p->adaptive) { + acq_logf( "Sample %d/%d: dra=%.3f"" ddec=%.3f"" |med|=%.3f"" scatter(MAD)=(%.3f,%.3f)"" metric=%.1f mode=%s\n", + n, p->max_samples, dra_samp[n-1], ddec_samp[n-1], rmed, sig_dra, sig_ddec, + fr.det.src_top10_mean, adaptive_mode_name(adaptive_rt.mode)); + } else { + acq_logf( "Sample %d/%d: dra=%.3f"" ddec=%.3f"" |med|=%.3f"" scatter(MAD)=(%.3f,%.3f)""\n", + n, p->max_samples, dra_samp[n-1], ddec_samp[n-1], rmed, sig_dra, sig_ddec); + } + } + + // If already within goal threshold, finish (no move) + if (n >= p->min_samples && rmed <= runtime_goal_arcsec) { + if (p->verbose) acq_logf( "Converged: |median offset|=%.3f"" <= %.3f""\n", rmed, runtime_goal_arcsec); + return 0; + } + + // If centroiding precision is good enough, we can move now + if (n >= p->min_samples && sig_dra <= runtime_prec_arcsec && sig_ddec <= runtime_prec_arcsec) { + // Issue robust move + double cmd_dra = p->gain * med_dra; + double cmd_ddec = p->gain * med_ddec; + + if (p->verbose) { + acq_logf( "MOVE (robust median): dra=%.3f"" ddec=%.3f"" (gain=%.3f)\n", cmd_dra, cmd_ddec, p->gain); + } + + if (p->use_putonslit) { + if (!have_slit) { + acq_logf( "REFUSE MOVE: missing slit RA/Dec for putonslit\n"); + } else { + double cross_ra=0.0, cross_dec=0.0; + slit_cross_from_offsets(p, slit_ra_deg, slit_dec_deg, cmd_dra, cmd_ddec, &cross_ra, &cross_dec); + (void)scam_putonslit_deg(slit_ra_deg, slit_dec_deg, cross_ra, cross_dec, p->dry_run, p->verbose); + (void)wait_for_guiding(p); + } + } else { + (void)tcs_move_arcsec(cmd_dra, cmd_ddec, p->dry_run, p->verbose); + } + + // After move, reject a few new frames to avoid trailed exposures. + adaptive_finish_cycle(p, &adaptive_rt, metric_samp, n); + if (p->settle_sec > 0) sleep_seconds(p->settle_sec); + skip_after_move = runtime_reject_after_move; + + // proceed to next cycle + goto next_cycle; + } + } + + // If we get here, we did not reach required precision. + if (n >= p->min_samples) { + double dra_tmp[p->max_samples]; + double ddec_tmp[p->max_samples]; + for (int i = 0; i < n; i++) { dra_tmp[i]=dra_samp[i]; ddec_tmp[i]=ddec_samp[i]; } + double med_dra = median_of_doubles(dra_tmp, n); + double med_ddec = median_of_doubles(ddec_tmp, n); + double sig_dra = mad_sigma_of_doubles(dra_samp, n, med_dra); + double sig_ddec = mad_sigma_of_doubles(ddec_samp, n, med_ddec); + double rmed = hypot(med_dra, med_ddec); + + acq_logf( "FAIL: insufficient precision to move safely after %d samples (attempts=%d).\n", n, attempts); + acq_logf( " median(dra,ddec)=(%.3f,%.3f)"" scatter(MAD)=(%.3f,%.3f)"" |med|=%.3f""\n", + med_dra, med_ddec, sig_dra, sig_ddec, rmed); + } else { + acq_logf( "FAIL: too few valid samples (n=%d) after attempts=%d.\n", n, attempts); + } + + adaptive_finish_cycle(p, &adaptive_rt, metric_samp, n); + + if (!p->continue_on_fail) return 2; + +next_cycle: + ; + } + + if (stop_requested()) { + acq_logf( "Stopped by user request.\n"); + return 1; + } + + acq_logf( "FAILED: reached max cycles (%d) without convergence.\n", p->max_cycles); + return 1; +} + +#ifdef __cplusplus +extern "C" { +#endif + +void ngps_acq_set_hooks(const ngps_acq_hooks_t* hooks) { + if (!hooks) { + memset(&g_hooks, 0, sizeof(g_hooks)); + g_hooks_initialized = 0; + return; + } + + g_hooks = *hooks; + g_hooks_initialized = 1; +} + +void ngps_acq_clear_hooks(void) { + memset(&g_hooks, 0, sizeof(g_hooks)); + g_hooks_initialized = 0; +} + +void ngps_acq_request_stop(int stop_requested_flag) { + g_stop = stop_requested_flag ? 1 : 0; +} + +static int ngps_acq_run_internal(int argc, char** argv, int install_signal_handler) +{ + g_stop = 0; + if (install_signal_handler) signal(SIGINT, on_sigint); + + AcqParams p; + set_defaults(&p); + + int pr = parse_args(argc, argv, &p); + if (pr <= 0) { usage(argv[0]); return (pr == 0) ? 0 : 4; } + + if (p.verbose) { + acq_logf( + "NGPS ACQ start:\n" + " mode=%s input=%s framegrab_out=%s\n" + " goal=(%.3f,%.3f) origin=%d max_dist=%.1f snr=%.1f filt_sigma=%.2f\n" + " centroid_hw=%d centroid_sigma=%.2f\n" + " loop=%d cadence=%.2fs max_samples=%d min_samples=%d prec=%.3f\" goal=%.3f\" gain=%.2f\n" + " reject_identical=%d reject_after_move=%d settle=%.2fs max_move=%.2f\"\n" + " use_putonslit=%d wait_guiding=%d guiding_poll=%.2fs guiding_timeout=%.1fs\n" + " tcs_set_units=%d dra_use_cosdec=%d tcs_sign=%d dry_run=%d\n", + (p.frame_mode == FRAME_FRAMEGRAB) ? "framegrab" : "stream", + p.input, p.framegrab_out, + p.goal_x, p.goal_y, p.pixel_origin, p.max_dist_pix, p.snr_thresh, p.filt_sigma_pix, + p.centroid_halfwin, p.centroid_sigma_pix, + p.loop, p.cadence_sec, p.max_samples, p.min_samples, p.prec_arcsec, p.goal_arcsec, p.gain, + p.reject_identical, p.reject_after_move, p.settle_sec, p.max_move_arcsec, + p.use_putonslit, p.wait_guiding, p.guiding_poll_sec, p.guiding_timeout_sec, + p.tcs_set_units, p.dra_use_cosdec, p.tcs_sign, p.dry_run); + if (p.adaptive) { + acq_logf( " adaptive=1 faint-start=%.1f faint-goal=%.1f bright-start=%.1f bright-goal=%.1f\n", + p.adaptive_faint, p.adaptive_faint_goal, p.adaptive_bright, p.adaptive_bright_goal); + } + } + + if (p.loop) { + return run_loop(&p); + } + + // One-shot: acquire one frame and (optionally) move once. + if (p.tcs_set_units) (void)tcs_set_native_units(p.dry_run, p.verbose); + FrameState fs; + memset(&fs, 0, sizeof(fs)); + fs.t_accept.tv_sec = 0; + fs.t_accept.tv_nsec = 0; + return process_once(&p, &fs); +} + +int ngps_acq_run_from_argv(int argc, char** argv) { + return ngps_acq_run_internal(argc, argv, 0); +} + +#ifdef __cplusplus +} +#endif + +#ifndef NGPS_ACQ_EMBEDDED +int main(int argc, char** argv) +{ + return ngps_acq_run_internal(argc, argv, 1); +} +#endif diff --git a/slicecamd/slicecam_interface.cpp b/slicecamd/slicecam_interface.cpp index a35022e5..d4a3e4c3 100644 --- a/slicecamd/slicecam_interface.cpp +++ b/slicecamd/slicecam_interface.cpp @@ -9,6 +9,209 @@ */ #include "slicecam_interface.h" +#include "ngps_acq_embed.h" + +#include +#include +#include +#include +#include +#include +#include +#include + +namespace { + +struct AutoacqRunContext { + Slicecam::Interface *iface; + std::string logfile; + std::mutex log_mtx; +}; + +std::string trim_copy( std::string value ) { + value.erase( value.begin(), + std::find_if( value.begin(), value.end(), + [](unsigned char c){ return !std::isspace(c); } ) ); + rtrim( value ); + return value; +} + +bool split_shell_words( const std::string &input, std::vector &tokens, std::string &error ) { + tokens.clear(); + error.clear(); + + std::string current; + char quote = '\0'; + bool escape = false; + + for ( const char ch : input ) { + if ( escape ) { + current.push_back( ch ); + escape = false; + continue; + } + + if ( ch == '\\' ) { + escape = true; + continue; + } + + if ( quote != '\0' ) { + if ( ch == quote ) quote = '\0'; + else current.push_back( ch ); + continue; + } + + if ( ch == '\'' || ch == '"' ) { + quote = ch; + continue; + } + + if ( std::isspace( static_cast(ch) ) ) { + if ( !current.empty() ) { + tokens.push_back( current ); + current.clear(); + } + continue; + } + + current.push_back( ch ); + } + + if ( escape ) { + error = "unterminated escape in autoacq args"; + return false; + } + + if ( quote != '\0' ) { + error = "unterminated quoted string in autoacq args"; + return false; + } + + if ( !current.empty() ) tokens.push_back( current ); + return true; +} + +bool is_autoacq_program_token( const std::string &token ) { + auto slash = token.find_last_of( "/\\" ); + std::string base = ( slash == std::string::npos ? token : token.substr( slash + 1 ) ); + return ( base == "ngps_acq" || base == "ngps_acquire" ); +} + +int cb_tcs_set_native_units( void *user, int dry_run, int verbose ) { + auto *ctx = static_cast( user ); + if ( !ctx || !ctx->iface ) return 1; + + if ( dry_run ) return 0; + + std::string retstring; + if ( !ctx->iface->tcsd.client.is_open() && ctx->iface->tcs_init( "", retstring ) != NO_ERROR ) { + if ( verbose ) logwrite( "Slicecam::autoacq", "WARNING: unable to initialize tcsd client for autoacq" ); + return 1; + } + + if ( ctx->iface->tcsd.client.command( TCSD_NATIVE + " dra arcsec", retstring ) != NO_ERROR ) return 1; + if ( ctx->iface->tcsd.client.command( TCSD_NATIVE + " ddec arcsec", retstring ) != NO_ERROR ) return 1; + return 0; +} + +int cb_tcs_move_arcsec( void *user, double dra_arcsec, double ddec_arcsec, int dry_run, int verbose ) { + auto *ctx = static_cast( user ); + if ( !ctx || !ctx->iface ) return 1; + + if ( dry_run ) return 0; + + std::string retstring; + if ( !ctx->iface->tcsd.client.is_open() && ctx->iface->tcs_init( "", retstring ) != NO_ERROR ) { + if ( verbose ) logwrite( "Slicecam::autoacq", "WARNING: unable to initialize tcsd client for autoacq move" ); + return 1; + } + + std::ostringstream cmd; + cmd << TCSD_NATIVE << " pt " + << std::fixed << std::setprecision(3) << dra_arcsec << " " + << std::fixed << std::setprecision(3) << ddec_arcsec; + return ( ctx->iface->tcsd.client.command( cmd.str(), retstring ) == NO_ERROR ) ? 0 : 1; +} + +int cb_scam_putonslit_deg( void *user, + double slit_ra_deg, double slit_dec_deg, + double cross_ra_deg, double cross_dec_deg, + int dry_run, int /*verbose*/ ) { + auto *ctx = static_cast( user ); + if ( !ctx || !ctx->iface ) return 1; + + if ( dry_run ) return 0; + + std::ostringstream args; + args << std::fixed << std::setprecision(10) + << slit_ra_deg << " " << slit_dec_deg << " " + << cross_ra_deg << " " << cross_dec_deg; + + std::string retstring; + return ( ctx->iface->put_on_slit( args.str(), retstring ) == NO_ERROR ) ? 0 : 1; +} + +int cb_acam_query_state( void *user, char *state, size_t state_sz, int /*verbose*/ ) { + auto *ctx = static_cast( user ); + if ( !ctx || !ctx->iface || !state || state_sz == 0 ) return 2; + + bool is_guiding = false; + if ( ctx->iface->get_acam_guide_state( is_guiding ) != NO_ERROR ) return 1; + + snprintf( state, state_sz, "%s", is_guiding ? "guiding" : "acquiring" ); + return 0; +} + +int cb_scam_framegrab_one( void *user, const char *outpath, int /*verbose*/ ) { + auto *ctx = static_cast( user ); + if ( !ctx || !ctx->iface || !outpath || !*outpath ) return 1; + + std::string retstring; + std::string args = std::string("one ") + outpath; + return ( ctx->iface->framegrab( args, retstring ) == NO_ERROR ) ? 0 : 1; +} + +int cb_scam_set_exptime( void *user, double exptime_sec, int dry_run, int /*verbose*/ ) { + auto *ctx = static_cast( user ); + if ( !ctx || !ctx->iface ) return 1; + if ( dry_run ) return 0; + + std::ostringstream args; + args << std::fixed << std::setprecision(3) << exptime_sec; + + std::string retstring; + return ( ctx->iface->exptime( args.str(), retstring ) == NO_ERROR ) ? 0 : 1; +} + +int cb_scam_set_avgframes( void *user, int avgframes, int dry_run, int /*verbose*/ ) { + auto *ctx = static_cast( user ); + if ( !ctx || !ctx->iface ) return 1; + if ( dry_run ) return 0; + + if ( avgframes < 1 ) avgframes = 1; + std::string retstring; + return ( ctx->iface->avg_frames( std::to_string(avgframes), retstring ) == NO_ERROR ) ? 0 : 1; +} + +int cb_is_stop_requested( void *user ) { + auto *ctx = static_cast( user ); + if ( !ctx || !ctx->iface ) return 1; + return ctx->iface->autoacq_stop_requested() ? 1 : 0; +} + +void cb_log_message( void *user, const char *line ) { + auto *ctx = static_cast( user ); + if ( !ctx || !line || !*line ) return; + + if ( !ctx->logfile.empty() ) { + std::lock_guard lock( ctx->log_mtx ); + std::ofstream logfile( ctx->logfile, std::ios::app ); + if ( logfile.is_open() ) logfile << line; + } +} + +} // namespace namespace Slicecam { @@ -1364,6 +1567,16 @@ namespace Slicecam { applied++; } + if ( config.param[entry] == "AUTOACQ_ARGS" || config.param[entry] == "AUTOACQ_CMD" ) { + this->autoacq_args = trim_copy( config.arg[entry] ); + message.str(""); message << "SLICECAMD:config:" << config.param[entry] << "=" << this->autoacq_args; + logwrite( function, message.str() ); + if ( config.param[entry] == "AUTOACQ_CMD" ) { + logwrite( function, "NOTICE AUTOACQ_CMD is deprecated; use AUTOACQ_ARGS" ); + } + applied++; + } + if ( config.param[entry] == "SKYSIM_IMAGE_SIZE" ) { try { this->camera.set_simsize( std::stoi( config.arg[entry] ) ); @@ -2265,6 +2478,330 @@ namespace Slicecam { /***** Slicecam::Interface::dothread_fpoffset *******************************/ + /***** Slicecam::Interface::publish_autoacq_state ***************************/ + /** + * @brief publish auto-acquire state changes for sequencer + * + */ + void Interface::publish_autoacq_state( const std::string &state, uint64_t run_id, + int exit_code, const std::string &message ) { + if ( !this->publisher ) return; + + nlohmann::json jmessage_out; + jmessage_out["source"] = Slicecam::DAEMON_NAME; + jmessage_out["state"] = state; + jmessage_out["run_id"] = run_id; + jmessage_out["active"] = this->autoacq_running.load(std::memory_order_acquire); + jmessage_out["exit_code"] = exit_code; + jmessage_out["message"] = message; + + try { + this->publisher->publish( jmessage_out, "slicecam_autoacq" ); + } + catch ( const std::exception &e ) { + logwrite( "Slicecam::Interface::publish_autoacq_state", + "ERROR publishing message: "+std::string(e.what()) ); + } + } + /***** Slicecam::Interface::publish_autoacq_state ***************************/ + + + /***** Slicecam::Interface::dothread_autoacq ********************************/ + /** + * @brief run embedded NGPS auto-acquire logic in a worker thread + * + */ + void Interface::dothread_autoacq( std::string args, std::string logfile, uint64_t run_id ) { + const std::string function = "Slicecam::Interface::dothread_autoacq"; + + std::vector tokenized_args; + std::string split_error; + if ( !split_shell_words( trim_copy(args), tokenized_args, split_error ) ) { + const std::string final_message = "invalid autoacq args: " + split_error; + { + std::lock_guard lock( this->autoacq_state_mtx ); + this->autoacq_state = "failed"; + this->autoacq_message = final_message; + this->autoacq_exit_code = 4; + } + this->autoacq_running.store( false, std::memory_order_release ); + this->autoacq_abort_requested.store( false, std::memory_order_release ); + logwrite( function, final_message ); + this->publish_autoacq_state( "failed", run_id, 4, final_message ); + return; + } + + while ( !tokenized_args.empty() && is_autoacq_program_token( tokenized_args.front() ) ) { + tokenized_args.erase( tokenized_args.begin() ); + } + + if ( tokenized_args.empty() ) { + std::vector default_tokens; + std::string default_error; + if ( !split_shell_words( trim_copy(this->autoacq_args), default_tokens, default_error ) ) { + const std::string final_message = "invalid configured AUTOACQ_ARGS: " + default_error; + { + std::lock_guard lock( this->autoacq_state_mtx ); + this->autoacq_state = "failed"; + this->autoacq_message = final_message; + this->autoacq_exit_code = 4; + } + this->autoacq_running.store( false, std::memory_order_release ); + this->autoacq_abort_requested.store( false, std::memory_order_release ); + logwrite( function, final_message ); + this->publish_autoacq_state( "failed", run_id, 4, final_message ); + return; + } + + while ( !default_tokens.empty() && is_autoacq_program_token( default_tokens.front() ) ) { + default_tokens.erase( default_tokens.begin() ); + } + tokenized_args = std::move( default_tokens ); + } + + if ( tokenized_args.empty() ) { + const std::string final_message = "AUTOACQ_ARGS is empty"; + { + std::lock_guard lock( this->autoacq_state_mtx ); + this->autoacq_state = "failed"; + this->autoacq_message = final_message; + this->autoacq_exit_code = 4; + } + this->autoacq_running.store( false, std::memory_order_release ); + this->autoacq_abort_requested.store( false, std::memory_order_release ); + logwrite( function, final_message ); + this->publish_autoacq_state( "failed", run_id, 4, final_message ); + return; + } + + std::vector argv_storage; + argv_storage.reserve( tokenized_args.size() + 1 ); + argv_storage.emplace_back( "ngps_acq" ); + for ( const auto &token : tokenized_args ) argv_storage.push_back( token ); + + std::vector argv; + argv.reserve( argv_storage.size() ); + for ( auto &token : argv_storage ) argv.push_back( const_cast( token.c_str() ) ); + + AutoacqRunContext ctx; + ctx.iface = this; + ctx.logfile = logfile; + + if ( !ctx.logfile.empty() ) { + try { + auto parent = std::filesystem::path( ctx.logfile ).parent_path(); + if ( !parent.empty() ) std::filesystem::create_directories( parent ); + } + catch ( const std::exception &e ) { + logwrite( function, "WARNING: unable to create autoacq log path: " + std::string(e.what()) ); + } + std::ofstream out( ctx.logfile, std::ios::app ); + if ( out.is_open() ) { + out << "=== SLICECAMD AUTOACQ run_id=" << run_id << " start ===" << std::endl; + } + } + + ngps_acq_hooks_t hooks {}; + hooks.user = &ctx; + hooks.tcs_set_native_units = cb_tcs_set_native_units; + hooks.tcs_move_arcsec = cb_tcs_move_arcsec; + hooks.scam_putonslit_deg = cb_scam_putonslit_deg; + hooks.acam_query_state = cb_acam_query_state; + hooks.scam_framegrab_one = cb_scam_framegrab_one; + hooks.scam_set_exptime = cb_scam_set_exptime; + hooks.scam_set_avgframes = cb_scam_set_avgframes; + hooks.is_stop_requested = cb_is_stop_requested; + hooks.log_message = cb_log_message; + + ngps_acq_set_hooks( &hooks ); + ngps_acq_request_stop( 0 ); + const int exit_code = ngps_acq_run_from_argv( static_cast(argv.size()), argv.data() ); + ngps_acq_request_stop( 0 ); + ngps_acq_clear_hooks(); + + std::string final_state; + std::string final_message; + if ( this->autoacq_abort_requested.load(std::memory_order_acquire) ) { + final_state = "aborted"; + final_message = "autoacq aborted"; + } + else if ( exit_code == 0 ) { + final_state = "success"; + final_message = "autoacq complete"; + } + else { + final_state = "failed"; + final_message = "autoacq failed with exit code " + std::to_string(exit_code); + } + + { + std::lock_guard lock( this->autoacq_state_mtx ); + this->autoacq_state = final_state; + this->autoacq_message = final_message; + this->autoacq_exit_code = exit_code; + } + + this->autoacq_running.store( false, std::memory_order_release ); + this->autoacq_abort_requested.store( false, std::memory_order_release ); + + if ( !ctx.logfile.empty() ) { + std::ofstream out( ctx.logfile, std::ios::app ); + if ( out.is_open() ) { + out << "=== SLICECAMD AUTOACQ run_id=" << run_id + << " state=" << final_state + << " exit_code=" << exit_code + << " ===" << std::endl; + } + } + + logwrite( function, final_message ); + this->publish_autoacq_state( final_state, run_id, exit_code, final_message ); + } + /***** Slicecam::Interface::dothread_autoacq ********************************/ + + + /***** Slicecam::Interface::autoacq *****************************************/ + /** + * @brief controls in-process auto-acquire logic + * + */ + long Interface::autoacq( std::string args, std::string &retstring ) { + const std::string function = "Slicecam::Interface::autoacq"; + std::stringstream message; + std::vector tokens; + Tokenize( args, tokens, " " ); + + std::string action = tokens.empty() ? "status" : tokens.at(0); + + if ( action == "?" || action == "help" ) { + retstring = SLICECAMD_AUTOACQ; + retstring.append( " [ start [--log-file ] [] | stop | status ]\n" ); + retstring.append( " Run or monitor in-process auto-acquire logic.\n" ); + retstring.append( " override AUTOACQ_ARGS for this run.\n" ); + retstring.append( " Status returns state, run id, active state, and last exit code.\n" ); + return HELP; + } + + if ( action == "status" ) { + std::string state; + std::string state_message; + int exit_code; + { + std::lock_guard lock( this->autoacq_state_mtx ); + state = this->autoacq_state; + state_message = this->autoacq_message; + exit_code = this->autoacq_exit_code; + } + retstring = "state="+state + + " run_id=" + std::to_string(this->autoacq_run_counter.load(std::memory_order_acquire)) + + " active=" + std::string(this->autoacq_running.load(std::memory_order_acquire) ? "true" : "false") + + " exit_code=" + std::to_string(exit_code); + if ( !state_message.empty() ) retstring += " message=\"" + state_message + "\""; + return NO_ERROR; + } + + if ( action == "stop" ) { + if ( !this->autoacq_running.load(std::memory_order_acquire) ) { + retstring = "not_running"; + return NO_ERROR; + } + + this->autoacq_abort_requested.store( true, std::memory_order_release ); + ngps_acq_request_stop( 1 ); + retstring = "stopping run_id=" + std::to_string(this->autoacq_run_counter.load(std::memory_order_acquire)); + return NO_ERROR; + } + + if ( action == "start" ) { + if ( this->autoacq_running.load(std::memory_order_acquire) ) { + retstring = "autoacq already running"; + return BUSY; + } + + std::string effective_args = trim_copy( this->autoacq_args ); + std::string logfile; + + auto pos = args.find_first_of( " \t" ); + if ( pos != std::string::npos ) { + std::string trailing = trim_copy( args.substr( pos + 1 ) ); + if ( !trailing.empty() ) { + std::vector trailing_tokens; + std::string split_error; + if ( !split_shell_words( trailing, trailing_tokens, split_error ) ) { + retstring = "invalid autoacq args: " + split_error; + return ERROR; + } + + std::vector filtered_tokens; + for ( size_t i = 0; i < trailing_tokens.size(); ) { + if ( trailing_tokens.at(i) == "--log-file" ) { + if ( (i+1) >= trailing_tokens.size() ) { + retstring = "missing value for --log-file"; + return ERROR; + } + logfile = trailing_tokens.at(i+1); + i += 2; + continue; + } + filtered_tokens.push_back( trailing_tokens.at(i) ); + i++; + } + + if ( !filtered_tokens.empty() ) { + std::ostringstream rebuilt; + for ( size_t i = 0; i < filtered_tokens.size(); i++ ) { + if ( i > 0 ) rebuilt << " "; + rebuilt << filtered_tokens.at(i); + } + effective_args = rebuilt.str(); + } + } + } + + std::vector check_tokens; + std::string split_error; + if ( !split_shell_words( effective_args, check_tokens, split_error ) ) { + retstring = "invalid AUTOACQ_ARGS: " + split_error; + return ERROR; + } + if ( check_tokens.size() == 1 && is_autoacq_program_token( check_tokens.front() ) ) { + effective_args = trim_copy( this->autoacq_args ); + } + + if ( trim_copy(effective_args).empty() ) { + logwrite( function, "ERROR AUTOACQ_ARGS is empty" ); + retstring = "AUTOACQ_ARGS is empty"; + return ERROR; + } + + uint64_t run_id = this->autoacq_run_counter.fetch_add( 1, std::memory_order_acq_rel ) + 1; + { + std::lock_guard lock( this->autoacq_state_mtx ); + this->autoacq_state = "running"; + this->autoacq_message = "autoacq started"; + this->autoacq_exit_code = 0; + } + this->autoacq_abort_requested.store( false, std::memory_order_release ); + this->autoacq_running.store( true, std::memory_order_release ); + ngps_acq_request_stop( 0 ); + + message.str(""); message << "NOTICE: starting autoacq run_id=" << run_id << " args=\"" << effective_args << "\""; + if ( !logfile.empty() ) message << " log=" << logfile; + logwrite( function, message.str() ); + + this->publish_autoacq_state( "running", run_id, 0, "autoacq started" ); + std::thread( &Slicecam::Interface::dothread_autoacq, this, effective_args, logfile, run_id ).detach(); + + retstring = "started run_id=" + std::to_string(run_id); + return NO_ERROR; + } + + retstring = "invalid_argument"; + return ERROR; + } + /***** Slicecam::Interface::autoacq *****************************************/ + + /***** Slicecam::Interface::get_acam_guide_state ****************************/ /** * @brief asks if ACAM is guiding @@ -2414,8 +2951,18 @@ namespace Slicecam { return ERROR; } } - else - if ( !is_guiding && this->tcs_online.load(std::memory_order_acquire) && this->tcsd.client.is_open() ) { + else if ( !is_guiding ) { + // Ensure tcsd/tcs connection is available before issuing PT. + // + if ( !this->tcs_online.load(std::memory_order_acquire) || !this->tcsd.client.is_open() ) { + std::string tcsret; + if ( this->tcs_init( "", tcsret ) != NO_ERROR || !this->tcsd.client.is_open() ) { + logwrite( function, "ERROR not connected to tcsd" ); + retstring="tcs_not_connected"; + return ERROR; + } + } + // offsets are in degrees, convert to arcsec (required for PT command) // ra_off *= 3600.; @@ -2429,11 +2976,6 @@ namespace Slicecam { 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() ); diff --git a/slicecamd/slicecam_interface.h b/slicecamd/slicecam_interface.h index 68fd95be..2c6ad29f 100644 --- a/slicecamd/slicecam_interface.h +++ b/slicecamd/slicecam_interface.h @@ -10,6 +10,8 @@ #include #include +#include +#include #include #include "network.h" #include "logentry.h" @@ -18,6 +20,7 @@ #include "atmcdLXd.h" #include #include +#include #include "slicecam_fits.h" #include "config.h" #include "tcsd_commands.h" @@ -239,6 +242,13 @@ namespace Slicecam { std::chrono::steady_clock::time_point framegrab_time; std::mutex framegrab_mtx; std::condition_variable cv; + std::atomic autoacq_running; + std::atomic autoacq_abort_requested; + std::atomic autoacq_run_counter; + std::mutex autoacq_state_mtx; + std::string autoacq_state; + std::string autoacq_message; + int autoacq_exit_code; public: std::unique_ptr publisher; ///< publisher object @@ -287,6 +297,12 @@ namespace Slicecam { : context(), tcs_online(false), err(false), + autoacq_running(false), + autoacq_abort_requested(false), + autoacq_run_counter(0), + autoacq_state("idle"), + autoacq_message(""), + autoacq_exit_code(0), subscriber(std::make_unique(context, Common::PubSub::Mode::SUB)), is_subscriber_thread_running(false), should_subscriber_thread_run(false), @@ -294,7 +310,8 @@ namespace Slicecam { is_framegrab_running(false), nsave_preserve_frames(0), nskip_preserve_frames(0), - snapshot_status { { "slitd", false }, {"tcsd", false} } + snapshot_status { { "slitd", false }, {"tcsd", false} }, + autoacq_args("--frame-mode stream --input /tmp/slicecam.fits --goal-x 150.0 --goal-y 115.5 --bg-x1 80 --bg-x2 165 --bg-y1 30 --bg-y2 210 --pixel-origin 1 --max-dist 40 --snr 3 --min-adj 4 --centroid-hw 4 --centroid-sigma 1.2 --loop 1 --cadence-sec 4 --prec-arcsec 0.4 --goal-arcsec 0.3 --gain 1.0 --dry-run 0 --tcs-set-units 0 --verbose 1 --debug 1 --use-putonslit 1 --adaptive 1 --adaptive-bright 40000 --adaptive-bright-goal 10000") { topic_handlers = { { "_snapshot", std::function( @@ -329,6 +346,7 @@ namespace Slicecam { Common::DaemonClient acamd { "acamd" }; /// for communicating with acamd SkyInfo::FPOffsets fpoffsets; /// for calling Python fpoffsets, defined in ~/Software/common/skyinfo.h + std::string autoacq_args; /// default arguments for in-process auto-acquire // publish/subscribe functions // @@ -359,6 +377,7 @@ namespace Slicecam { long framegrab( std::string args ); /// wrapper to control Andor frame grabbing long framegrab( std::string args, std::string &retstring ); /// wrapper to control Andor frame grabbing long framegrab_fix( std::string args, std::string &retstring ); /// wrapper to control Andor frame grabbing + long autoacq( std::string args, std::string &retstring ); /// run/monitor fine-acquire helper long image_quality( std::string args, std::string &retstring ); /// wrapper for Astrometry::image_quality long put_on_slit( std::string args, std::string &retstring ); /// put target on slit long solve( std::string args, std::string &retstring ); /// wrapper for Astrometry::solve @@ -371,6 +390,9 @@ namespace Slicecam { long gain( std::string args, std::string &retstring ); long get_acam_guide_state( bool &is_guiding ); + bool autoacq_stop_requested() const { + return this->autoacq_abort_requested.load(std::memory_order_acquire); + } long collect_header_info( std::unique_ptr &slicecam ); @@ -380,6 +402,9 @@ namespace Slicecam { static void dothread_fpoffset( Slicecam::Interface &iface ); void dothread_framegrab( const std::string whattodo, const std::string sourcefile ); + void dothread_autoacq( std::string args, std::string logfile, uint64_t run_id ); + void publish_autoacq_state( const std::string &state, uint64_t run_id, + int exit_code, const std::string &message ); void preserve_framegrab(); long collect_header_info_threaded(); }; diff --git a/slicecamd/slicecam_server.cpp b/slicecamd/slicecam_server.cpp index 1c8dd2e8..598394ba 100644 --- a/slicecamd/slicecam_server.cpp +++ b/slicecamd/slicecam_server.cpp @@ -585,6 +585,10 @@ namespace Slicecam { ret = this->interface.saveframes( args, retstring ); } else + if ( cmd == SLICECAMD_AUTOACQ ) { + ret = this->interface.autoacq( args, retstring ); + } + else if ( cmd == SLICECAMD_PUTONSLIT ) { ret = this->interface.put_on_slit( args, retstring ); } diff --git a/utils/CMakeLists.txt b/utils/CMakeLists.txt index baa453ba..f565177f 100644 --- a/utils/CMakeLists.txt +++ b/utils/CMakeLists.txt @@ -63,6 +63,31 @@ target_link_libraries( listener PRIVATE utilities ) # -- ZeroMQ -------------------------------------------------------------------- # -find_library( ZMQPP_LIB zmqpp NAMES libzmqpp PATHS /usr/local/lib ) -find_library( ZMQ_LIB zmq NAMES libzmq PATHS /usr/local/lib ) +find_library( ZMQPP_LIB NAMES zmqpp libzmqpp + PATHS /usr/local/lib /opt/homebrew/lib /usr/local/opt/zmqpp/lib /opt/homebrew/opt/zmqpp/lib ) +find_library( ZMQ_LIB NAMES zmq libzmq + PATHS /usr/local/lib /opt/homebrew/lib /usr/local/opt/zeromq/lib /opt/homebrew/opt/zeromq/lib ) + +# -- X11 GUI tools ------------------------------------------------------------ +# +find_package(X11) +if (X11_FOUND) + add_executable(seq_progress_gui + ${PROJECT_UTILS_DIR}/seq_progress_gui.cpp + ) + target_include_directories(seq_progress_gui PRIVATE + ${X11_INCLUDE_DIR} + ${PROJECT_BASE_DIR}/common + ${PROJECT_UTILS_DIR} + ) + target_link_libraries(seq_progress_gui PRIVATE + ${STDCXXFS_LIB} + logentry + network + utilities + ${ZMQPP_LIB} + ${ZMQ_LIB} + ${X11_LIBRARIES} + ) +endif() diff --git a/utils/seq_progress_gui.cpp b/utils/seq_progress_gui.cpp new file mode 100644 index 00000000..5172961b --- /dev/null +++ b/utils/seq_progress_gui.cpp @@ -0,0 +1,1247 @@ +// Small X11 sequencer progress popup with ontarget/usercontinue controls. +// Uses ZMQ telemetry and TCP polling fallback. + +#define Time X11Time +#include +#include +#include +#undef Time + +#include +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "common.h" +#include "config.h" +#include "logentry.h" +#include "network.h" + +namespace { + +constexpr int kPhaseCount = 5; +enum PhaseIndex { + PHASE_SLEW = 0, + PHASE_SOLVE = 1, + PHASE_FINE = 2, + PHASE_OFFSET = 3, + PHASE_EXPOSE = 4 +}; + +struct Rect { + int x = 0; + int y = 0; + int w = 0; + int h = 0; + bool contains(int px, int py) const { + return px >= x && px <= (x + w) && py >= y && py <= (y + h); + } +}; + +struct Options { + std::string config_path; + std::string host = "127.0.0.1"; + int nbport = 0; + std::string acam_config_path; + std::string acam_host; + int acam_nbport = 0; + std::string pub_endpoint; + std::string sub_endpoint; + bool pub_endpoint_set = false; + bool sub_endpoint_set = false; + int poll_ms = 10000; +}; + +struct SequenceState { + bool phase_complete[kPhaseCount] = {false, false, false, false, false}; + bool phase_active[kPhaseCount] = {false, false, false, false, false}; + bool offset_applicable = false; + bool waiting_for_user = false; + bool waiting_for_tcsop = false; + bool ontarget = false; + bool guiding_on = false; + double exposure_progress = 0.0; + double exposure_elapsed = 0.0; + double exposure_total = 0.0; + int current_phase = -1; + bool prev_wait_tcsop = false; + bool prev_wait_guide = false; + std::string seqstate; + std::string waitstate; + std::chrono::steady_clock::time_point last_ontarget; + int current_obsid = -1; + std::string current_target_state; + double offset_ra = 0.0; + double offset_dec = 0.0; + int nexp = 1; + int acqmode = 1; + int current_frame = 0; + int max_frame_seen = 0; + + void reset() { + for (int i = 0; i < kPhaseCount; ++i) { + phase_complete[i] = false; + phase_active[i] = false; + } + offset_applicable = false; + waiting_for_user = false; + waiting_for_tcsop = false; + ontarget = false; + guiding_on = false; + exposure_progress = 0.0; + exposure_elapsed = 0.0; + exposure_total = 0.0; + current_phase = -1; + prev_wait_tcsop = false; + prev_wait_guide = false; + seqstate.clear(); + waitstate.clear(); + current_obsid = -1; + current_target_state.clear(); + nexp = 1; + current_frame = 0; + max_frame_seen = 0; + } + + void reset_progress_only() { + for (int i = 0; i < kPhaseCount; ++i) { + phase_complete[i] = false; + phase_active[i] = false; + } + offset_applicable = false; + waiting_for_user = false; + waiting_for_tcsop = false; + ontarget = false; + guiding_on = false; + exposure_progress = 0.0; + exposure_elapsed = 0.0; + exposure_total = 0.0; + current_phase = -1; + prev_wait_tcsop = false; + prev_wait_guide = false; + } +}; + +static bool starts_with_local(const std::string &s, const std::string &prefix) { + return s.rfind(prefix, 0) == 0; +} + +static std::string trim_copy(std::string s) { + s.erase(s.begin(), std::find_if(s.begin(), s.end(), [](unsigned char ch) { return !std::isspace(ch); })); + s.erase(std::find_if(s.rbegin(), s.rend(), [](unsigned char ch) { return !std::isspace(ch); }).base(), s.end()); + return s; +} + +static std::vector split_ws(const std::string &s) { + std::istringstream iss(s); + std::vector out; + std::string tok; + while (iss >> tok) out.push_back(tok); + return out; +} + +static bool has_token(const std::vector &tokens, const std::string &needle) { + return std::find(tokens.begin(), tokens.end(), needle) != tokens.end(); +} + +static std::string to_upper_copy(std::string s); + +static std::string strip_token_edges(std::string s) { + while (!s.empty() && !std::isalnum(static_cast(s.front()))) s.erase(s.begin()); + while (!s.empty() && !std::isalnum(static_cast(s.back()))) s.pop_back(); + return s; +} + +static std::vector split_state_tokens(const std::string &s) { + std::vector out; + auto tokens = split_ws(to_upper_copy(s)); + out.reserve(tokens.size()); + for (auto &tok : tokens) { + std::string cleaned = strip_token_edges(tok); + if (!cleaned.empty()) out.push_back(cleaned); + } + return out; +} + +static Options parse_args(int argc, char **argv) { + Options opt; + if (const char *env_host = std::getenv("NGPS_HOST"); env_host && *env_host) { + opt.host = env_host; + } + for (int i = 1; i < argc; ++i) { + std::string arg = argv[i]; + if (arg == "--config" && i + 1 < argc) { + opt.config_path = argv[++i]; + } else if (arg == "--host" && i + 1 < argc) { + opt.host = argv[++i]; + } else if (arg == "--nbport" && i + 1 < argc) { + opt.nbport = std::stoi(argv[++i]); + } else if (arg == "--acam-config" && i + 1 < argc) { + opt.acam_config_path = argv[++i]; + } else if (arg == "--acam-host" && i + 1 < argc) { + opt.acam_host = argv[++i]; + } else if (arg == "--acam-nbport" && i + 1 < argc) { + opt.acam_nbport = std::stoi(argv[++i]); + } else if (arg == "--pub-endpoint" && i + 1 < argc) { + opt.pub_endpoint = argv[++i]; + opt.pub_endpoint_set = true; + } else if (arg == "--sub-endpoint" && i + 1 < argc) { + opt.sub_endpoint = argv[++i]; + opt.sub_endpoint_set = true; + } else if (arg == "--poll-ms" && i + 1 < argc) { + opt.poll_ms = std::stoi(argv[++i]); + } else if (arg == "--help" || arg == "-h") { + std::cout << "Usage: seq_progress_gui [--config ] [--host ] [--nbport ]\n" + " [--acam-config ] [--acam-host ] [--acam-nbport ]\n" + " [--pub-endpoint ] [--sub-endpoint ] [--poll-ms ]\n"; + std::exit(0); + } + } + return opt; +} + +static void load_config(const std::string &path, Options &opt) { + if (path.empty()) return; + Config cfg(path); + if (cfg.read_config(cfg) != 0) return; + for (int i = 0; i < cfg.n_entries; ++i) { + if (cfg.param[i] == "NBPORT" && opt.nbport <= 0) { + opt.nbport = std::stoi(cfg.arg[i]); + } else if (cfg.param[i] == "PUB_ENDPOINT" && !opt.pub_endpoint_set) { + opt.pub_endpoint = cfg.arg[i]; + } else if (cfg.param[i] == "SUB_ENDPOINT" && !opt.sub_endpoint_set) { + opt.sub_endpoint = cfg.arg[i]; + } + } +} + +static std::string default_config_path() { + const char *root = std::getenv("NGPS_ROOT"); + if (root && *root) { + return std::string(root) + "/Config/sequencerd.cfg"; + } + return "Config/sequencerd.cfg"; +} + +static std::string default_acam_config_path() { + const char *root = std::getenv("NGPS_ROOT"); + if (root && *root) { + return std::string(root) + "/Config/acamd.cfg"; + } + return "Config/acamd.cfg"; +} + +static void load_acam_config(const std::string &path, Options &opt) { + if (path.empty()) return; + Config cfg(path); + if (cfg.read_config(cfg) != 0) return; + for (int i = 0; i < cfg.n_entries; ++i) { + if (cfg.param[i] == "NBPORT" && opt.acam_nbport <= 0) { + opt.acam_nbport = std::stoi(cfg.arg[i]); + } else if (cfg.param[i] == "HOST" && opt.acam_host.empty()) { + opt.acam_host = cfg.arg[i]; + } + } +} + +static std::string to_lower_copy(std::string s) { + std::transform(s.begin(), s.end(), s.begin(), [](unsigned char c) { return std::tolower(c); }); + return s; +} + +static std::string to_upper_copy(std::string s) { + std::transform(s.begin(), s.end(), s.begin(), [](unsigned char c) { return std::toupper(c); }); + return s; +} + +} // namespace + +class SeqProgressGui { + public: + SeqProgressGui(const Options &opt) + : options_(opt), + cmd_iface_("sequencer", options_.host, static_cast(options_.nbport)), + acam_iface_("acam", options_.acam_host, static_cast(options_.acam_nbport)) {} + + bool init() { + display_ = XOpenDisplay(nullptr); + if (!display_) { + std::cerr << "ERROR opening X display\n"; + return false; + } + + int screen = DefaultScreen(display_); + unsigned long black = BlackPixel(display_, screen); + unsigned long white = WhitePixel(display_, screen); + + window_ = XCreateSimpleWindow(display_, DefaultRootWindow(display_), 100, 100, kWinW, kWinH, 1, black, white); + XStoreName(display_, window_, "NGPS Observation Sequence"); + XSelectInput(display_, window_, ExposureMask | ButtonPressMask | KeyPressMask | StructureNotifyMask); + + wm_delete_window_ = XInternAtom(display_, "WM_DELETE_WINDOW", False); + XSetWMProtocols(display_, window_, &wm_delete_window_, 1); + + XMapWindow(display_, window_); + gc_ = XCreateGC(display_, window_, 0, nullptr); + + load_colors(); + load_font(); + compute_layout(); + + init_zmq(); + + if (options_.nbport > 0) { + if (cmd_iface_.open() == 0) { + cmd_iface_.send_command("state"); + cmd_iface_.send_command("wstate"); + } + } + if (options_.acam_nbport > 0) { + acam_iface_.open(); + } + + return true; + } + + void run() { + const int xfd = ConnectionNumber(display_); + bool running = true; + bool need_redraw = true; + auto last_blink = std::chrono::steady_clock::now(); + bool blink_on = false; + + while (running) { + fd_set fds; + FD_ZERO(&fds); + FD_SET(xfd, &fds); + int maxfd = xfd; + + struct timeval tv; + tv.tv_sec = 0; + tv.tv_usec = 200000; // 200ms + + int ret = select(maxfd + 1, &fds, nullptr, nullptr, &tv); + if (ret > 0) { + if (FD_ISSET(xfd, &fds)) { + while (XPending(display_)) { + XEvent ev; + XNextEvent(display_, &ev); + if (ev.type == Expose) { + need_redraw = true; + } else if (ev.type == ButtonPress) { + handle_click(ev.xbutton.x, ev.xbutton.y); + need_redraw = true; + } else if (ev.type == KeyPress) { + KeySym ks = XLookupKeysym(&ev.xkey, 0); + if (ks == XK_Escape) { + running = false; + } + } else if (ev.type == ClientMessage) { + if (static_cast(ev.xclient.data.l[0]) == wm_delete_window_) { + running = false; + } + } + } + } + } + + auto now = std::chrono::steady_clock::now(); + if (process_zmq()) { + need_redraw = true; + } + if (maybe_poll(now)) { + need_redraw = true; + } + if (std::chrono::duration_cast(now - last_blink).count() > 600) { + blink_on = !blink_on; + last_blink = now; + need_redraw = true; + } + + blink_on_ = blink_on; + if (need_redraw) { + draw(); + need_redraw = false; + } + } + } + + private: + const int kWinW = 900; + const int kWinH = 220; + + Options options_; + Network::Interface cmd_iface_; + Network::Interface acam_iface_; + zmqpp::context zmq_context_; + std::unique_ptr zmq_sub_; + std::unique_ptr zmq_pub_; + SequenceState state_; + std::chrono::steady_clock::time_point last_zmq_seqstate_; + std::chrono::steady_clock::time_point last_zmq_waitstate_; + std::chrono::steady_clock::time_point last_zmq_any_; + std::chrono::steady_clock::time_point last_snapshot_request_; + std::chrono::steady_clock::time_point last_acam_poll_; + std::chrono::steady_clock::time_point last_seq_poll_; + std::chrono::steady_clock::time_point last_seqstate_update_; + std::chrono::steady_clock::time_point last_waitstate_update_; + + Display *display_ = nullptr; + Window window_ = 0; + GC gc_ = 0; + Atom wm_delete_window_{}; + bool blink_on_ = false; + + Rect bar_; + Rect segments_[kPhaseCount]; + Rect ontarget_btn_; + Rect continue_btn_; + Rect guiding_box_; + Rect seqstatus_box_; + + unsigned long color_bg_ = 0; + unsigned long color_text_ = 0; + unsigned long color_complete_ = 0; + unsigned long color_active_ = 0; + unsigned long color_pending_ = 0; + unsigned long color_disabled_ = 0; + unsigned long color_button_ = 0; + unsigned long color_button_disabled_ = 0; + unsigned long color_button_text_ = 0; + unsigned long color_wait_ = 0; + + XFontStruct *font_ = nullptr; + + void init_zmq() { + if (!options_.sub_endpoint.empty()) { + try { + zmq_sub_ = std::make_unique(zmq_context_, Common::PubSub::Mode::SUB); + zmq_sub_->connect(options_.sub_endpoint); + zmq_sub_->subscribe("seq_seqstate"); + zmq_sub_->subscribe("seq_waitstate"); + zmq_sub_->subscribe("seq_threadstate"); + zmq_sub_->subscribe("seq_daemonstate"); + zmq_sub_->subscribe("seq_progress"); + zmq_sub_->subscribe("acamd"); + } catch (const std::exception &e) { + std::cerr << "ERROR initializing ZMQ subscriber: " << e.what() << "\n"; + } + } + + if (!options_.pub_endpoint.empty()) { + try { + zmq_pub_ = std::make_unique(zmq_context_, Common::PubSub::Mode::PUB); + zmq_pub_->connect_to_broker(options_.pub_endpoint, "seq_progress_gui"); + } catch (const std::exception &e) { + std::cerr << "ERROR initializing ZMQ publisher: " << e.what() << "\n"; + } + } + + request_snapshot(); + } + + void load_colors() { + int screen = DefaultScreen(display_); + Colormap cmap = DefaultColormap(display_, screen); + color_bg_ = alloc_color(cmap, "#e6e6e6"); + color_text_ = alloc_color(cmap, "#111111"); + color_complete_ = alloc_color(cmap, "#2e7d32"); + color_active_ = alloc_color(cmap, "#f9a825"); + color_pending_ = alloc_color(cmap, "#b0b0b0"); + color_disabled_ = alloc_color(cmap, "#808080"); + color_button_ = alloc_color(cmap, "#1565c0"); + color_button_disabled_ = alloc_color(cmap, "#9e9e9e"); + color_button_text_ = alloc_color(cmap, "#ffffff"); + color_wait_ = alloc_color(cmap, "#c62828"); + } + + unsigned long alloc_color(Colormap cmap, const char *hex) { + XColor color; + XParseColor(display_, cmap, hex, &color); + XAllocColor(display_, cmap, &color); + return color.pixel; + } + + void load_font() { + font_ = XLoadQueryFont(display_, "9x15bold"); + if (!font_) { + font_ = XLoadQueryFont(display_, "fixed"); + } + if (font_) { + XSetFont(display_, gc_, font_->fid); + } + } + + void compute_layout() { + const int margin = 16; + bar_.x = margin; + bar_.y = 48; + bar_.w = kWinW - 2 * margin; + bar_.h = 26; + + int seg_w = bar_.w / kPhaseCount; + for (int i = 0; i < kPhaseCount; ++i) { + segments_[i] = {bar_.x + i * seg_w, bar_.y, seg_w, bar_.h}; + } + segments_[kPhaseCount - 1].w = bar_.w - (seg_w * (kPhaseCount - 1)); + + ontarget_btn_ = {margin, 150, 160, 34}; + continue_btn_ = {margin + 180, 150, 160, 34}; + guiding_box_ = {margin + 520, 150, 160, 34}; + seqstatus_box_ = {guiding_box_.x + guiding_box_.w + 12, 150, 180, 34}; + } + + void draw() { + XSetForeground(display_, gc_, color_bg_); + XFillRectangle(display_, window_, gc_, 0, 0, kWinW, kWinH); + + draw_title(); + draw_acqmode_indicator(); + draw_bar(); + draw_labels(); + draw_user_instruction(); + draw_buttons(); + draw_ontarget_indicator(); + draw_offset_values(); + draw_guiding_indicator(); + draw_seqstatus_indicator(); + + XFlush(display_); + } + + void draw_title() { + XSetForeground(display_, gc_, color_text_); + const char *title = "Observation Sequence Progress"; + XDrawString(display_, window_, gc_, 16, 24, title, std::strlen(title)); + } + + void draw_acqmode_indicator() { + const char *label; + switch (state_.acqmode) { + case 2: label = "ACQMODE: 2 SEMI-AUTO"; break; + case 3: label = "ACQMODE: 3 AUTO"; break; + default: label = "ACQMODE: 1 MANUAL"; break; + } + int len = std::strlen(label); + int text_width = XTextWidth(XQueryFont(display_, XGContextFromGC(gc_)), label, len); + XSetForeground(display_, gc_, color_text_); + XDrawString(display_, window_, gc_, kWinW - text_width - 16, 24, label, len); + } + + void draw_bar() { + for (int i = 0; i < kPhaseCount; ++i) { + unsigned long fill = color_pending_; + if (i == PHASE_OFFSET && !state_.offset_applicable) { + fill = color_disabled_; + } else if (state_.phase_complete[i]) { + fill = color_complete_; + } else if (state_.phase_active[i]) { + fill = color_active_; + } + + XSetForeground(display_, gc_, fill); + XFillRectangle(display_, window_, gc_, segments_[i].x, segments_[i].y, segments_[i].w, segments_[i].h); + + if (i == PHASE_EXPOSE && state_.phase_active[i] && state_.exposure_progress > 0.0) { + int prog_w = static_cast(segments_[i].w * std::min(1.0, state_.exposure_progress)); + XSetForeground(display_, gc_, color_complete_); + XFillRectangle(display_, window_, gc_, segments_[i].x, segments_[i].y, prog_w, segments_[i].h); + } + + XSetForeground(display_, gc_, color_text_); + XDrawRectangle(display_, window_, gc_, segments_[i].x, segments_[i].y, segments_[i].w, segments_[i].h); + } + } + + void draw_labels() { + XSetForeground(display_, gc_, color_text_); + const char *labels[kPhaseCount] = {"SLEW", "ASTROM SOLVE", "FINE TUNE", "OFFSET", "EXPOSURE"}; + int label_y = bar_.y + bar_.h + 16; + for (int i = 0; i < kPhaseCount; ++i) { + int tx = segments_[i].x + 6; + // Add info to EXPOSURE label when active + if (i == PHASE_EXPOSE && state_.phase_active[PHASE_EXPOSE]) { + char exp_label[64]; + int percent = static_cast(state_.exposure_progress * 100.0); + if (state_.nexp > 1) { + snprintf(exp_label, sizeof(exp_label), "EXPOSURE %d/%d %d%%", + state_.current_frame, state_.nexp, percent); + } else { + snprintf(exp_label, sizeof(exp_label), "EXPOSURE %d%%", percent); + } + static int debug_counter = 0; + if (++debug_counter % 10 == 0) { // Print every 10th frame to avoid spam + std::cerr << "DEBUG drawing label: " << exp_label + << " (exposure_progress=" << state_.exposure_progress << ")\n"; + } + XDrawString(display_, window_, gc_, tx, label_y, exp_label, std::strlen(exp_label)); + } else { + XDrawString(display_, window_, gc_, tx, label_y, labels[i], std::strlen(labels[i])); + } + } + } + + void draw_status() { + std::string status = "IDLE"; + if (state_.waiting_for_tcsop) { + status = "WAITING FOR TCS OPERATOR (ONTARGET)"; + } else if (state_.waiting_for_user) { + status = "WAITING FOR USER ACTION"; + } else if (state_.phase_active[PHASE_SLEW]) { + status = "SLEWING"; + } else if (state_.phase_active[PHASE_SOLVE]) { + status = "ASTROM SOLVE"; + } else if (state_.phase_active[PHASE_FINE]) { + status = "FINE TUNE"; + } else if (state_.phase_active[PHASE_OFFSET]) { + status = "APPLYING OFFSET"; + } else if (state_.phase_active[PHASE_EXPOSE]) { + std::ostringstream oss; + // Show frame count if NEXP > 1 + if (state_.nexp > 1) { + oss << "EXPOSURE " << state_.current_frame << " / " << state_.nexp; + if (state_.exposure_total > 0.0) { + oss.setf(std::ios::fixed); + oss.precision(1); + oss << " (" << state_.exposure_elapsed << " / " << state_.exposure_total << " s)"; + } + } else { + oss << "EXPOSURE"; + if (state_.exposure_total > 0.0) { + oss.setf(std::ios::fixed); + oss.precision(1); + oss << " " << state_.exposure_elapsed << " / " << state_.exposure_total << " s"; + } + if (state_.exposure_progress > 0.0) { + oss << " " << static_cast(state_.exposure_progress * 100.0) << "%"; + } + } + status = oss.str(); + } + + unsigned long color = (state_.waiting_for_tcsop || state_.waiting_for_user) && blink_on_ ? color_wait_ : color_text_; + XSetForeground(display_, gc_, color); + XDrawString(display_, window_, gc_, 16, 116, status.c_str(), static_cast(status.size())); + } + + void draw_button(const Rect &r, const char *label, bool enabled) { + unsigned long fill = enabled ? color_button_ : color_button_disabled_; + XSetForeground(display_, gc_, fill); + XFillRectangle(display_, window_, gc_, r.x, r.y, r.w, r.h); + XSetForeground(display_, gc_, color_text_); + XDrawRectangle(display_, window_, gc_, r.x, r.y, r.w, r.h); + XSetForeground(display_, gc_, color_button_text_); + int tx = r.x + 10; + int ty = r.y + 22; + XDrawString(display_, window_, gc_, tx, ty, label, std::strlen(label)); + } + + enum class UserGateIntent { + CONTINUE, + ACQUIRE, + EXPOSE, + OFFSET_EXPOSE + }; + + bool has_pending_target_offset() const { + return state_.offset_applicable || + std::fabs(state_.offset_ra) > 1.0e-6 || + std::fabs(state_.offset_dec) > 1.0e-6; + } + + bool is_pre_acquire_gate() const { + if (!state_.waiting_for_user) return false; + if (state_.acqmode != 2) return false; + return !state_.phase_active[PHASE_SOLVE] && + !state_.phase_complete[PHASE_SOLVE] && + !state_.phase_active[PHASE_FINE] && + !state_.phase_complete[PHASE_FINE] && + !state_.phase_active[PHASE_OFFSET] && + !state_.phase_complete[PHASE_OFFSET] && + !state_.phase_active[PHASE_EXPOSE] && + state_.current_frame == 0 && + !state_.guiding_on; + } + + UserGateIntent infer_user_gate_intent() const { + if (!state_.waiting_for_user) return UserGateIntent::CONTINUE; + if (is_pre_acquire_gate()) return UserGateIntent::ACQUIRE; + if (has_pending_target_offset()) return UserGateIntent::OFFSET_EXPOSE; + if (state_.acqmode == 2 || state_.acqmode == 3) return UserGateIntent::EXPOSE; + return UserGateIntent::CONTINUE; + } + + const char *continue_label() const { + switch (infer_user_gate_intent()) { + case UserGateIntent::ACQUIRE: return "ACQUIRE"; + case UserGateIntent::EXPOSE: return "EXPOSE"; + case UserGateIntent::OFFSET_EXPOSE:return "OFFSET & EXPOSE"; + default: return "CONTINUE"; + } + } + + void draw_user_instruction() { + if (!state_.waiting_for_user) return; + if (!blink_on_) return; // blink the instruction for visibility + + char instruction[256]; + switch (infer_user_gate_intent()) { + case UserGateIntent::ACQUIRE: + snprintf(instruction, sizeof(instruction), + ">>> Click ACQUIRE to start acquisition <<<"); + break; + case UserGateIntent::OFFSET_EXPOSE: + snprintf(instruction, sizeof(instruction), + ">>> Click OFFSET & EXPOSE to apply offset (RA=%.2f\" DEC=%.2f\") then expose <<<", + state_.offset_ra, state_.offset_dec); + break; + case UserGateIntent::EXPOSE: + snprintf(instruction, sizeof(instruction), + ">>> Click EXPOSE to begin exposure <<<"); + break; + default: + snprintf(instruction, sizeof(instruction), + ">>> Click CONTINUE <<<"); + break; + } + + XSetForeground(display_, gc_, color_wait_); // red bold + XDrawString(display_, window_, gc_, 16, 118, instruction, std::strlen(instruction)); + } + + void draw_buttons() { + draw_button(ontarget_btn_, "ONTARGET", state_.waiting_for_tcsop); + draw_button(continue_btn_, continue_label(), state_.waiting_for_user); + } + + void draw_ontarget_indicator() { + const char *label = state_.ontarget ? "ONTARGET: YES" : "ONTARGET: NO"; + unsigned long color = state_.ontarget ? color_complete_ : color_pending_; + XSetForeground(display_, gc_, color); + XFillArc(display_, window_, gc_, 370, 150, 18, 18, 0, 360 * 64); + XSetForeground(display_, gc_, color_text_); + XDrawString(display_, window_, gc_, 394, 164, label, std::strlen(label)); + } + + void draw_offset_values() { + // Display offset values above the guiding indicator box + char label[64]; + snprintf(label, sizeof(label), "Offset: RA=%.2f\" DEC=%.2f\"", + state_.offset_ra, state_.offset_dec); + XSetForeground(display_, gc_, color_text_); + XDrawString(display_, window_, gc_, guiding_box_.x, guiding_box_.y - 8, label, std::strlen(label)); + } + + void draw_guiding_indicator() { + const char *label = state_.guiding_on ? "GUIDING ON" : "GUIDING OFF"; + unsigned long fill = state_.guiding_on ? color_complete_ : color_wait_; + XSetForeground(display_, gc_, fill); + XFillRectangle(display_, window_, gc_, guiding_box_.x, guiding_box_.y, guiding_box_.w, guiding_box_.h); + XSetForeground(display_, gc_, color_text_); + XDrawRectangle(display_, window_, gc_, guiding_box_.x, guiding_box_.y, guiding_box_.w, guiding_box_.h); + XSetForeground(display_, gc_, color_button_text_); + int tx = guiding_box_.x + 10; + int ty = guiding_box_.y + 22; + XDrawString(display_, window_, gc_, tx, ty, label, std::strlen(label)); + } + + void draw_seqstatus_indicator() { + std::string label; + unsigned long fill = color_pending_; + auto seq_tokens = split_state_tokens(state_.seqstate); + const bool has_ready = has_token(seq_tokens, "READY"); + const bool has_notready = has_token(seq_tokens, "NOTREADY") || (has_token(seq_tokens, "NOT") && has_ready); + const bool has_running = has_token(seq_tokens, "RUNNING"); + const bool has_paused = has_token(seq_tokens, "PAUSED"); + const bool has_starting = has_token(seq_tokens, "STARTING"); + const bool has_stopping = has_token(seq_tokens, "STOPPING"); + const bool have_zmq = (zmq_sub_ != nullptr); + const int stale_ms = have_zmq ? 5000 : (options_.poll_ms > 0 ? options_.poll_ms * 2 : 5000); + const bool seq_stale = is_stale(last_seqstate_update_, stale_ms); + const bool wait_stale = is_stale(last_waitstate_update_, stale_ms); + + if (seq_stale && wait_stale) { + label = "STALE"; + fill = color_disabled_; + } else if (state_.waiting_for_tcsop) { + label = "WAITING: TCSOP"; + fill = color_wait_; + } else if (state_.waiting_for_user) { + label = "WAITING: USER"; + fill = color_wait_; + } else if (!state_.waitstate.empty()) { + label = "WAITING"; + fill = color_wait_; + } else if (has_stopping) { + label = "STOPPING"; + fill = color_active_; + } else if (has_starting) { + label = "STARTING"; + fill = color_active_; + } else if (has_paused) { + label = "PAUSED"; + fill = color_wait_; + } else if (has_running) { + label = "RUNNING"; + fill = color_active_; + } else if (has_notready) { + label = "NOTREADY"; + fill = color_disabled_; + } else if (has_ready) { + label = "READY"; + fill = color_complete_; + } else { + label = "UNKNOWN"; + fill = color_pending_; + } + + std::string text = "SEQ " + label; + XSetForeground(display_, gc_, fill); + XFillRectangle(display_, window_, gc_, seqstatus_box_.x, seqstatus_box_.y, seqstatus_box_.w, seqstatus_box_.h); + XSetForeground(display_, gc_, color_text_); + XDrawRectangle(display_, window_, gc_, seqstatus_box_.x, seqstatus_box_.y, seqstatus_box_.w, seqstatus_box_.h); + XSetForeground(display_, gc_, color_button_text_); + int tx = seqstatus_box_.x + 10; + int ty = seqstatus_box_.y + 22; + XDrawString(display_, window_, gc_, tx, ty, text.c_str(), static_cast(text.size())); + } + + void handle_click(int x, int y) { + if (ontarget_btn_.contains(x, y) && state_.waiting_for_tcsop) { + send_command("ontarget"); + } + if (continue_btn_.contains(x, y) && state_.waiting_for_user) { + send_command("usercontinue"); + } + } + + void send_command(const std::string &cmd) { + if (!cmd_iface_.isopen()) { + cmd_iface_.open(); + } + if (cmd_iface_.send_command(cmd) != 0) { + cmd_iface_.reconnect(); + cmd_iface_.send_command(cmd); + } + } + + bool process_zmq() { + if (!zmq_sub_) return false; + if (!zmq_sub_->has_message()) return false; + auto [topic, payload] = zmq_sub_->receive(); + handle_zmq_message(topic, payload); + return true; + } + + void handle_zmq_message(const std::string &topic, const std::string &payload) { + try { + nlohmann::json jmessage = nlohmann::json::parse(payload); + auto now = std::chrono::steady_clock::now(); + last_zmq_any_ = now; + if (topic == "seq_seqstate" && jmessage.contains("seqstate")) { + handle_message("SEQSTATE: " + jmessage["seqstate"].get()); + last_zmq_seqstate_ = now; + } else if (topic == "seq_waitstate") { + static const char *kWaitTokens[] = {"ACAM", "CALIB", "CAMERA", "FLEXURE", "FOCUS", + "POWER", "SLICECAM", "SLIT", "TCS", "ACQUIRE", + "GUIDE", "EXPOSE", "READOUT", "TCSOP", "USER"}; + std::string waitstate; + for (const char *token : kWaitTokens) { + if (jmessage.contains(token) && jmessage[token].is_boolean() && jmessage[token].get()) { + if (!waitstate.empty()) waitstate += " "; + waitstate += token; + } + } + handle_waitstate(waitstate); + last_zmq_waitstate_ = now; + } else if (topic == "seq_progress") { + if (jmessage.contains("obsid") && jmessage["obsid"].is_number_integer()) { + int obsid = jmessage["obsid"].get(); + if (obsid > 0 && state_.current_obsid != obsid) { + state_.reset_progress_only(); + } + state_.current_obsid = obsid; + } + if (jmessage.contains("target_state") && jmessage["target_state"].is_string()) { + std::string tstate = to_upper_copy(jmessage["target_state"].get()); + if (!tstate.empty() && tstate != state_.current_target_state) { + if (tstate == "ACTIVE" || tstate == "COMPLETE" || tstate == "PENDING") { + state_.reset_progress_only(); + } + } + state_.current_target_state = tstate; + } + if (jmessage.contains("event") && jmessage["event"].is_string()) { + std::string event = to_lower_copy(jmessage["event"].get()); + if (event == "target_start" || event == "target_complete" || event == "wait_tcsop") { + state_.reset_progress_only(); + } + } + if (jmessage.contains("ontarget") && jmessage["ontarget"].is_boolean()) { + state_.ontarget = jmessage["ontarget"].get(); + } + if (jmessage.contains("fine_tune_active") && jmessage["fine_tune_active"].is_boolean()) { + bool active = jmessage["fine_tune_active"].get(); + if (active) { + set_phase(PHASE_FINE); + } else if (state_.phase_active[PHASE_FINE]) { + state_.phase_complete[PHASE_FINE] = true; + clear_phase_active(PHASE_FINE); + } + } + bool offset_active = false; + if (jmessage.contains("offset_active") && jmessage["offset_active"].is_boolean()) { + offset_active = jmessage["offset_active"].get(); + } + if (jmessage.contains("offset_settle") && jmessage["offset_settle"].is_boolean()) { + offset_active = offset_active || jmessage["offset_settle"].get(); + } + if (offset_active) { + state_.offset_applicable = true; + set_phase(PHASE_OFFSET); + } else if (state_.phase_active[PHASE_OFFSET]) { + state_.phase_complete[PHASE_OFFSET] = true; + clear_phase_active(PHASE_OFFSET); + } + if (jmessage.contains("offset_ra") && jmessage["offset_ra"].is_number()) { + state_.offset_ra = jmessage["offset_ra"].get(); + } + if (jmessage.contains("offset_dec") && jmessage["offset_dec"].is_number()) { + state_.offset_dec = jmessage["offset_dec"].get(); + } + if (jmessage.contains("acqmode") && jmessage["acqmode"].is_number()) { + state_.acqmode = jmessage["acqmode"].get(); + } + if (jmessage.contains("nexp") && jmessage["nexp"].is_number()) { + int new_nexp = jmessage["nexp"].get(); + if (new_nexp != state_.nexp) { + state_.nexp = new_nexp; + // Reset frame tracking when nexp changes + state_.current_frame = 0; + state_.max_frame_seen = 0; + } + } + if (jmessage.contains("current_frame") && jmessage["current_frame"].is_number()) { + int frame = jmessage["current_frame"].get(); + if (frame > state_.max_frame_seen) { + state_.max_frame_seen = frame; + state_.current_frame = frame; + // Keep EXPOSE phase active if more frames expected + if (state_.current_frame < state_.nexp) { + set_phase(PHASE_EXPOSE); + } else if (state_.current_frame >= state_.nexp) { + // All frames complete + state_.phase_complete[PHASE_EXPOSE] = true; + clear_phase_active(PHASE_EXPOSE); + } + } + } + if (jmessage.contains("exptime_percent") && jmessage["exptime_percent"].is_number()) { + int percent = jmessage["exptime_percent"].get(); + percent = std::max(0, std::min(100, percent)); + state_.exposure_progress = percent / 100.0; + set_phase(PHASE_EXPOSE); + } + } else if (topic == "acamd") { + if (jmessage.contains("ACAM_GUIDING") && jmessage["ACAM_GUIDING"].is_boolean()) { + state_.guiding_on = jmessage["ACAM_GUIDING"].get(); + } else if (jmessage.contains("ACAM_ACQUIRE_MODE") && jmessage["ACAM_ACQUIRE_MODE"].is_string()) { + std::string mode = to_lower_copy(jmessage["ACAM_ACQUIRE_MODE"].get()); + state_.guiding_on = (mode.find("guiding") != std::string::npos); + } + } + } catch (const std::exception &) { + // ignore malformed telemetry + } + } + + void request_snapshot() { + if (!zmq_pub_) return; + nlohmann::json jmessage; + jmessage["sequencerd"] = true; + jmessage["acamd"] = true; + zmq_pub_->publish(jmessage, "_snapshot"); + last_snapshot_request_ = std::chrono::steady_clock::now(); + } + + bool is_stale(const std::chrono::steady_clock::time_point &tp, int stale_ms) const { + if (tp.time_since_epoch().count() == 0) return true; + auto now = std::chrono::steady_clock::now(); + return std::chrono::duration_cast(now - tp).count() > stale_ms; + } + + bool should_poll_acam() const { + if (state_.seqstate.find("RUNNING") != std::string::npos) return true; + if (state_.waitstate.find("ACQUIRE") != std::string::npos) return true; + if (state_.waitstate.find("GUIDE") != std::string::npos) return true; + if (state_.waitstate.find("EXPOSE") != std::string::npos) return true; + if (state_.waitstate.find("READOUT") != std::string::npos) return true; + return false; + } + + bool maybe_poll(const std::chrono::steady_clock::time_point &now) { + bool updated = false; + const int stale_ms = 5000; + const bool have_zmq = (zmq_sub_ != nullptr); + const bool stale_seq = have_zmq && + (is_stale(last_zmq_seqstate_, stale_ms) || + is_stale(last_zmq_waitstate_, stale_ms)); + const bool zmq_quiet = have_zmq && is_stale(last_zmq_any_, options_.poll_ms > 0 ? options_.poll_ms * 3 : stale_ms * 3); + const bool allow_tcp_poll = !have_zmq || (zmq_quiet && stale_seq); + + if (options_.poll_ms > 0) { + if (have_zmq && zmq_pub_ && state_.waiting_for_user && + std::chrono::duration_cast(now - last_snapshot_request_).count() >= 1000) { + request_snapshot(); + updated = true; + } + if (have_zmq && zmq_pub_) { + if (stale_seq && + std::chrono::duration_cast(now - last_snapshot_request_).count() >= stale_ms) { + request_snapshot(); + updated = true; + } + } + if (allow_tcp_poll) { + if (std::chrono::duration_cast(now - last_seq_poll_).count() >= options_.poll_ms) { + poll_sequencer(); + last_seq_poll_ = now; + updated = true; + } + if (std::chrono::duration_cast(now - last_acam_poll_).count() >= options_.poll_ms) { + if (should_poll_acam()) { + poll_acam(); + updated = true; + } + last_acam_poll_ = now; + } + } + } else if (have_zmq && zmq_pub_) { + if (std::chrono::duration_cast(now - last_snapshot_request_).count() >= stale_ms) { + request_snapshot(); + updated = true; + } + } + + return updated; + } + + void poll_status() { + poll_sequencer(); + poll_acam(); + } + + void poll_sequencer() { + if (options_.nbport <= 0) return; + if (!cmd_iface_.isopen()) { + if (cmd_iface_.open() != 0) return; + } + + std::string reply; + if (cmd_iface_.send_command("state", reply, 200) == 0 && !reply.empty()) { + handle_message("SEQSTATE: " + reply); + } else if (!cmd_iface_.isopen()) { + cmd_iface_.reconnect(); + return; + } + + reply.clear(); + if (cmd_iface_.send_command("wstate", reply, 200) == 0 && !reply.empty()) { + handle_waitstate(reply); + } else if (!cmd_iface_.isopen()) { + cmd_iface_.reconnect(); + return; + } + + // Poll acqmode — the no-arg response includes "current mode = N" + reply.clear(); + if (cmd_iface_.send_command("acqmode", reply, 200) == 0 && !reply.empty()) { + auto pos = reply.find("current mode = "); + if (pos != std::string::npos) { + try { state_.acqmode = std::stoi(reply.substr(pos + 15)); } catch (...) {} + } + } + } + + void poll_acam() { + if (options_.acam_nbport <= 0) return; + if (!acam_iface_.isopen()) { + if (acam_iface_.open() != 0) return; + } + + std::string reply; + if (acam_iface_.send_command("acquire", reply, 200) == 0 && !reply.empty()) { + std::string lower = to_lower_copy(reply); + if (lower.find("guiding") != std::string::npos) { + state_.guiding_on = true; + } else if (lower.find("stopped") != std::string::npos || lower.find("acquir") != std::string::npos) { + state_.guiding_on = false; + } + } else if (!acam_iface_.isopen()) { + acam_iface_.reconnect(); + return; + } + } + + void set_phase(int idx) { + if (idx < 0 || idx >= kPhaseCount) return; + if (state_.current_phase != idx) { + if (state_.current_phase >= 0 && state_.current_phase < idx) { + state_.phase_complete[state_.current_phase] = true; + } + for (int i = 0; i < kPhaseCount; ++i) state_.phase_active[i] = false; + state_.current_phase = idx; + } + state_.phase_active[idx] = true; + } + + void clear_phase_active(int idx) { + if (idx < 0 || idx >= kPhaseCount) return; + state_.phase_active[idx] = false; + if (state_.current_phase == idx) { + state_.current_phase = -1; + } + } + + void handle_waitstate(const std::string &waitstate) { + state_.waitstate = waitstate; + last_waitstate_update_ = std::chrono::steady_clock::now(); + auto tokens = split_ws(waitstate); + bool has_tcsop = has_token(tokens, "TCSOP"); + bool has_user = has_token(tokens, "USER"); + bool has_acquire = has_token(tokens, "ACQUIRE"); + bool has_guide = has_token(tokens, "GUIDE"); + bool has_expose = has_token(tokens, "EXPOSE"); + bool has_readout = has_token(tokens, "READOUT"); + + state_.waiting_for_tcsop = has_tcsop; + state_.waiting_for_user = has_user; + if (has_user) { + request_snapshot(); + } + + if (!has_tcsop && (has_acquire || has_guide || has_expose || has_readout || has_user)) { + state_.ontarget = true; + } + + if (has_tcsop) { + set_phase(PHASE_SLEW); + state_.ontarget = false; + } else if (state_.prev_wait_tcsop && !has_tcsop) { + state_.phase_complete[PHASE_SLEW] = true; + clear_phase_active(PHASE_SLEW); + } + + if (has_acquire || has_guide) { + set_phase(PHASE_SOLVE); + } + if (has_expose) { + set_phase(PHASE_EXPOSE); + } + if (has_readout) { + state_.phase_complete[PHASE_EXPOSE] = true; + clear_phase_active(PHASE_EXPOSE); + } + + state_.prev_wait_tcsop = has_tcsop; + state_.prev_wait_guide = has_guide; + } + + void handle_message(const std::string &raw) { + std::string msg = trim_copy(raw); + if (starts_with_local(msg, "SEQSTATE:")) { + std::string prev_state = state_.seqstate; + state_.seqstate = trim_copy(msg.substr(9)); + last_seqstate_update_ = std::chrono::steady_clock::now(); + auto is_ready_only = [](const std::string &s) { + auto tokens = split_state_tokens(s); + const bool has_ready = has_token(tokens, "READY"); + const bool has_notready = has_token(tokens, "NOTREADY") || (has_token(tokens, "NOT") && has_ready); + if (!has_ready || has_notready) return false; + if (has_token(tokens, "RUNNING")) return false; + if (has_token(tokens, "STARTING")) return false; + if (has_token(tokens, "STOPPING")) return false; + if (has_token(tokens, "PAUSED")) return false; + return true; + }; + if (is_ready_only(state_.seqstate) && !is_ready_only(prev_state)) { + std::string keep_state = state_.seqstate; + state_.reset(); + state_.seqstate = keep_state; + } + } else if (starts_with_local(msg, "WAITSTATE:")) { + handle_waitstate(trim_copy(msg.substr(10))); + } else if (starts_with_local(msg, "TARGETSTATE:")) { + std::string upper = to_upper_copy(msg); + int obsid = -1; + auto pos = upper.find("OBSID:"); + if (pos != std::string::npos) { + try { + obsid = std::stoi(upper.substr(pos + 6)); + } catch (...) { + } + } + if (obsid > 0 && state_.current_obsid != obsid) { + state_.reset_progress_only(); + state_.current_obsid = obsid; + } + auto state_pos = upper.find("TARGETSTATE:"); + if (state_pos != std::string::npos) { + std::string rest = upper.substr(state_pos + 12); + rest = trim_copy(rest); + auto space = rest.find(' '); + std::string tstate = (space == std::string::npos) ? rest : rest.substr(0, space); + if (!tstate.empty() && tstate != state_.current_target_state) { + if (tstate == "ACTIVE" || tstate == "COMPLETE" || tstate == "PENDING") { + state_.reset_progress_only(); + } + state_.current_target_state = tstate; + } + } + } + } +}; + +int main(int argc, char **argv) { + std::signal(SIGPIPE, SIG_IGN); + Options opt = parse_args(argc, argv); + if (opt.config_path.empty()) { + opt.config_path = default_config_path(); + } + if (opt.acam_config_path.empty()) { + opt.acam_config_path = default_acam_config_path(); + } + if (opt.acam_host.empty()) { + opt.acam_host = opt.host; + } + + load_config(opt.config_path, opt); + load_acam_config(opt.acam_config_path, opt); + + if (opt.nbport <= 0) { + std::cerr << "ERROR: NBPORT not set (check sequencerd.cfg)\n"; + return 1; + } + if (opt.sub_endpoint.empty()) { + std::cerr << "WARNING: SUB_ENDPOINT not set; seq-progress will run in polling-only mode\n"; + } + + SeqProgressGui gui(opt); + if (!gui.init()) { + return 1; + } + gui.run(); + return 0; +} diff --git a/utils/utilities.h b/utils/utilities.h index 5308568d..7203c578 100644 --- a/utils/utilities.h +++ b/utils/utilities.h @@ -371,7 +371,7 @@ class BoolState { */ class PreciseTimer { private: - static const long max_short_sleep = 3000000; // units are microseconds + static inline constexpr long max_short_sleep = 3000000; // units are microseconds std::atomic should_hold; std::atomic on_hold;