diff --git a/Config/hispecatc.cfg b/Config/hispecatc.cfg new file mode 100644 index 0000000..7d42914 --- /dev/null +++ b/Config/hispecatc.cfg @@ -0,0 +1,34 @@ +# configuration file for camera-server +# +ARCHON_IP=10.0.0.2 +ARCHON_PORT=4242 +DEFAULT_FIRMWARE=/home/hsdev/camera-dev/elijahab/wdlfiles/src/hispecatc/hispecatc.acf +EXPOSE_PARAM=Expose # Archon parameter which triggers exposure +ABORT_PARAM=abort # Archon parameter to trigger an abort +READOUT_TIME=50000 # Timeout waiting for new frame (ms) +WRITE_TAPINFO_TO_FITS=yes # Tapinfo (gain, offset) be written to FITS headers {yes|no} + +IMDIR=/home/hsdev/camera-dev/freedom/images # base directory to save images +BASENAME=image # base image filename + +# --- Asynchronous FITS writer (frame_output_factory) --- +FITS_ENABLED=yes # enable FITS writer output {yes|no} +FITS_OUTPUT_DIR=/home/hsdev/camera-dev/freedom/images # base directory for FITS files +FITS_AUTODIR=yes # write into a YYYYMMDD subdir of FITS_OUTPUT_DIR {yes|no} +FITS_BASENAME=image # base filename for FITS files +FITS_WRITE_INTERVAL_MS=0 # 0 = write a FITS file for every exposure (no cadence gating) + +DIRMODE=0077 +TM_ZONE_LOG=local # time zone for log entries only, can be "UTC" or "local" +TM_ZONE=UTC # time zone for everything else, can be "UTC" or "local" +TZ_ENV=PST8PDT,M3.2.0/2,M11.1.0/2 # TZ ENV variable to use when TM_ZONE_* is "local" (follows POSIX standard) +AUTODIR=no +LONGERROR=true + +NBPORT=3030 # server non-blocking port +BLKPORT=3031 # server blocking port +ASYNCGROUP=239.1.1.234 # asynchronous broadcast group (or "none") +ASYNCPORT=1234 # asynchronous message port (if ASYNCGROUP defined) +#ASYNCGROUP=none +LOGPATH=/home/hsdev/camera-dev/freedom/logs # fully qualified path to save log files +#LONGEXPOSURE=True # exptimes in seconds (not ms) diff --git a/camerad/Instruments/hispec_tracking_camera b/camerad/Instruments/hispec_tracking_camera index 39f900e..36deef2 160000 --- a/camerad/Instruments/hispec_tracking_camera +++ b/camerad/Instruments/hispec_tracking_camera @@ -1 +1 @@ -Subproject commit 39f900e06a937e714b6afab64b8cfd33f8b37b66 +Subproject commit 36deef25c0415e62f52ca90e58c56c149b11c740 diff --git a/camerad/archon_controller.cpp b/camerad/archon_controller.cpp index bbf808d..547f700 100644 --- a/camerad/archon_controller.cpp +++ b/camerad/archon_controller.cpp @@ -9,6 +9,7 @@ #include "archon_controller.h" #include "archon_interface.h" +#include namespace Camera { @@ -1035,7 +1036,6 @@ namespace Camera { error = ERROR; logwrite(function, "ERROR from Archon processing \""+cmd+"\""); } - else // First 3 bytes of reply must equal checksum else reply doesn't belong to command if (reply.size()<3 || std::memcmp(reply.data(), check, 3) != 0) { error = ERROR; @@ -1570,6 +1570,12 @@ namespace Camera { logwrite(function, "loaded Archon Config File OK"); this->is_firmwareloaded = true; + // now that every [MODE_*] configmap is fully populated, parse each mode's + // tapline layout into its tapinfo_t (amp name, direction, gain, offset) + for (auto &[modename, modeinfo] : this->modemap) { + this->parse_tapinfo(modeinfo); + } + // add to systemkeys keyword database // std::stringstream keystr; @@ -1635,6 +1641,13 @@ namespace Camera { write_config_key("PIXELCOUNT", std::to_string(mode->geometry.pixelcount).c_str(), changed); if (changed) { + // WCONFIG only stages LINECOUNT/PIXELCOUNT into config memory; APPLYCDS + // activates the new readout geometry in the timing/CDS core without + // power-cycling the detector (unlike APPLYALL). + if (this->send_cmd(APPLYCDS) != NO_ERROR) { + logwrite(function, "ERROR applying mode geometry (APPLYCDS) to controller"); + return ERROR; + } logwrite(function, "applied mode geometry to controller"); } @@ -1652,6 +1665,75 @@ namespace Camera { /***** Camera::ArchonController::load_mode_settings *************************/ + /***** Camera::ArchonController::parse_tapinfo *****************************/ + /** + * @brief populate a mode's tapinfo_t from its TAPLINEn configmap entries + * @details Called once per mode after the ACF is parsed, so that each + * modemap entry carries its tapline layout alongside the other + * per-mode parameters (geometry, params, etc.). Each TAPLINEn is a + * string ",,", e.g. "AM54L,1,0", where the + * amp name is "AM54" and the trailing L/R is the readout direction. + * An empty value ("") means that slot is not read out: it is + * skipped and does not count toward num_taps. + * @param[in,out] mode reference to the modeinfo_t to populate + * + */ + void ArchonController::parse_tapinfo(modeinfo_t &mode) { + const std::string function("Camera::ArchonController::parse_tapinfo"); + + mode.tapinfo.num_taps = 0; + + for (int i = 0; i < 16; ++i) { + auto it = mode.configmap.find("TAPLINE" + std::to_string(i)); + if (it == mode.configmap.end()) continue; + + std::string val = it->second.value; + val.erase(std::remove(val.begin(), val.end(), '"'), val.end()); // drop quotes + if (val.empty()) continue; // unused tap slot + + std::stringstream ss(val); + std::string name, gain, offset; + std::getline(ss, name, ','); + std::getline(ss, gain, ','); + std::getline(ss, offset, ','); + + const int t = mode.tapinfo.num_taps; + + // split "AM54L" into amp name "AM54" and readout direction "L"/"R" + if (!name.empty() && (name.back()=='L' || name.back()=='R')) { + mode.tapinfo.readoutdir[t] = std::string(1, name.back()); + mode.tapinfo.ampname[t] = name.substr(0, name.size()-1); + } else { + mode.tapinfo.readoutdir[t].clear(); + mode.tapinfo.ampname[t] = name; + } + + try { mode.tapinfo.gain[t] = gain.empty() ? 1.0f : std::stof(gain); } + catch (const std::exception &) { mode.tapinfo.gain[t] = 1.0f; } + try { mode.tapinfo.offset[t] = offset.empty() ? 0.0f : std::stof(offset); } + catch (const std::exception &) { mode.tapinfo.offset[t] = 0.0f; } + + ++mode.tapinfo.num_taps; + } + + // sanity-check against the ACF TAPLINES key, which drives controller readout + auto it = mode.configmap.find("TAPLINES"); + if (it != mode.configmap.end()) { + try { + int declared = std::stoi(it->second.value); + if (declared != mode.tapinfo.num_taps) { + logwrite(function, "WARNING TAPLINES="+std::to_string(declared)+" but " + +std::to_string(mode.tapinfo.num_taps)+" non-empty taplines parsed"); + } + } + catch (const std::exception &e) { + logwrite(function, "ERROR parsing TAPLINES: "+std::string(e.what())); + } + } + } + /***** Camera::ArchonController::parse_tapinfo *****************************/ + + /***** Camera::ArchonController::lock_buffer ********************************/ /** * @brief lock Archon frame buffer @@ -2062,10 +2144,20 @@ namespace Camera { // double waittime_ms = this->readout_time_msec * 1.1; // this is in msec - // if readout_time_msec was not defined or defined=0 - // then do not use a timeout timer + // readout_time_msec defaults to 0 and is currently never populated, which + // would leave the loop below with no timeout and able to hang forever if a + // frame never arrives. Fall back to a timeout derived from the exposure + // time plus a generous fixed margin so acquisition can never freeze + // silently; a late/missing frame becomes a logged timeout error instead. // - bool timeout_timer_enabled = (waittime_ms <= 0) ? false : true; + if ( waittime_ms <= 0 ) { + const double fallback_margin_ms = 10000.0; // 10 s over exposure time + waittime_ms = this->get_exptime()*1000.0 + fallback_margin_ms; + } + + // the timeout timer is always enabled now that waittime_ms is guaranteed > 0 + // + bool timeout_timer_enabled = true; uint64_t start_ns = get_clock_time_nsec(); // returns nanoseconds uint64_t timeout_ns = (uint64_t)(waittime_ms * 1e6); // convert waittime msec to nsec @@ -2210,10 +2302,10 @@ namespace Camera { << key << "=" << newvalue; - + error = this->send_cmd((char*)sscmd.str().c_str()); // send the WCONFIG command here - sscmd.str(""); sscmd << key << "=" << newvalue << (error==ERROR ? "":"not") << " written"; + sscmd.str(""); sscmd << key << "=" << newvalue << (error==ERROR ? " not written":" written"); if (error==NO_ERROR) { this->configmap[key].value = newvalue; // save newvalue in the STL map diff --git a/camerad/archon_controller.h b/camerad/archon_controller.h index a4d0a04..e04c3f6 100644 --- a/camerad/archon_controller.h +++ b/camerad/archon_controller.h @@ -190,9 +190,10 @@ namespace Camera { struct tapinfo_t { int num_taps; int tap[16]; + std::string ampname[64]; //!< amplifier name for each tap, e.g. "AM54" (from TAPLINEn in the ACF) float gain[16]; float offset[16]; - std::string readoutdir[16]; + std::string readoutdir[64]; }; /** @@ -294,7 +295,7 @@ namespace Camera { std::atomic_flag archon_busy = ATOMIC_FLAG_INIT; //!< indicates a thread is accessing Archon bool is_firmwareloaded; std::string firmware; - int msgref; + int msgref{0}; //!< message reference id, incremented per command; MUST start initialized std::string backplaneversion; std::vector modtype; //!< type of each module from SYSTEM command std::vector modversion; //!< version of each module from SYSTEM command @@ -333,6 +334,7 @@ namespace Camera { long fetchlog(); long load_acf(const std::string &filename, bool write_to_archon=true); long load_mode_settings(modeinfo_t* mode); + void parse_tapinfo(modeinfo_t &mode); //!< populate mode.tapinfo from its TAPLINEn configmap entries long lock_buffer(int buffernumber); long unlock_buffer(); template void get_configmap_value(const std::string &key_in, T &value_out); diff --git a/utils/fits_writer.cpp b/utils/fits_writer.cpp index 0e367ea..e2d3cbc 100644 --- a/utils/fits_writer.cpp +++ b/utils/fits_writer.cpp @@ -202,12 +202,31 @@ namespace Camera { return NO_ERROR; } - std::string FitsWriter::make_filename(uint64_t frame_number) const { + std::string FitsWriter::resolve_output_dir() { + if (!cfg_.autodir) return cfg_.output_dir; + + // Date subdir (YYYYMMDD, configured time zone), matching the imdir convention + const std::string dir = cfg_.output_dir + "/" + get_system_date(); + if (dir != cur_date_dir_) { + std::error_code ec; + std::filesystem::create_directories(dir, ec); + if (ec) { + logwrite("Camera::FitsWriter::resolve_output_dir", + "ERROR creating " + dir + ": " + ec.message() + + " -- falling back to " + cfg_.output_dir); + return cfg_.output_dir; + } + cur_date_dir_ = dir; // cache so we only create/log on date rollover + } + return dir; + } + + std::string FitsWriter::make_filename(uint64_t frame_number) { char num[32]; std::snprintf(num, sizeof(num), "%08llu", static_cast(frame_number)); - const std::string prefix = cfg_.output_dir + "/" + cfg_.basename + "_" + num; + const std::string prefix = resolve_output_dir() + "/" + cfg_.basename + "_" + num; std::string filename = prefix + ".fits"; int suffix = 1; diff --git a/utils/fits_writer.h b/utils/fits_writer.h index 43bcaee..74d5cc3 100644 --- a/utils/fits_writer.h +++ b/utils/fits_writer.h @@ -28,6 +28,7 @@ namespace Camera { std::string basename{"tracking"}; size_t queue_size{32}; uint32_t drain_timeout_ms{5000}; + bool autodir{false}; // write into a YYYYMMDD subdir of output_dir }; class FitsWriter : public FrameOutput { @@ -59,10 +60,16 @@ namespace Camera { void worker_loop(); long write_fits_file(const QueuedFrame &frame); - std::string make_filename(uint64_t frame_number) const; + std::string make_filename(uint64_t frame_number); + // Resolve the target directory, creating today's autodir subdir on demand. + // Worker-thread only (uses cur_date_dir_ without locking). + std::string resolve_output_dir(); FitsWriterConfig cfg_; + // Cache of the last autodir path created; touched only on the worker thread + std::string cur_date_dir_; + std::deque queue_; mutable std::mutex mtx_; std::condition_variable cv_; diff --git a/utils/frame_output_factory.cpp b/utils/frame_output_factory.cpp index 4350df3..d684da7 100644 --- a/utils/frame_output_factory.cpp +++ b/utils/frame_output_factory.cpp @@ -31,6 +31,7 @@ namespace Camera { else if (key == "SHM_NUM_FRAMES") out.shm_num_frames = static_cast(std::stoul(val)); else if (key == "FITS_ENABLED") out.fits_enabled = parse_bool(val); else if (key == "FITS_OUTPUT_DIR") out.fits.output_dir = val; + else if (key == "FITS_AUTODIR") out.fits.autodir = parse_bool(val); else if (key == "FITS_BASENAME") out.fits.basename = val; else if (key == "FITS_WRITE_INTERVAL_MS") out.fits_write_interval_ms = static_cast(std::stoul(val)); else if (key == "FITS_QUEUE_SIZE") out.fits.queue_size = static_cast(std::stoul(val)); diff --git a/utils/shared_memory_writer.h b/utils/shared_memory_writer.h index 00eb1de..08b0b82 100644 --- a/utils/shared_memory_writer.h +++ b/utils/shared_memory_writer.h @@ -45,7 +45,7 @@ namespace Camera { SharedMemoryWriter(const std::string &segment_name, size_t max_frame_bytes, uint32_t num_frames = 4); - ~SharedMemoryWriter() override; + ~SharedMemoryWriter(); long open() override; long write(const char* data, size_t size, const FrameMetadata& meta) override;