From f0bc395ca71e457f6fefb7511d1aadef072ec1f3 Mon Sep 17 00:00:00 2001 From: Mike Langmayr <1809691+mikelangmayr@users.noreply.github.com> Date: Fri, 15 May 2026 13:29:24 -0700 Subject: [PATCH 01/15] add asyc FITS writer --- utils/CMakeLists.txt | 12 +++ utils/fits_writer.cpp | 226 ++++++++++++++++++++++++++++++++++++++++++ utils/fits_writer.h | 90 +++++++++++++++++ 3 files changed, 328 insertions(+) create mode 100644 utils/fits_writer.cpp create mode 100644 utils/fits_writer.h diff --git a/utils/CMakeLists.txt b/utils/CMakeLists.txt index 47a17c6..b5d2440 100644 --- a/utils/CMakeLists.txt +++ b/utils/CMakeLists.txt @@ -29,6 +29,18 @@ add_library(shared_memory_writer STATIC target_include_directories(shared_memory_writer PRIVATE ${PROJECT_BASE_DIR}/common ${PROJECT_BASE_DIR}/utils) target_link_libraries(shared_memory_writer nlohmann_json::nlohmann_json $,rt,>) +add_library(fits_writer STATIC + ${PROJECT_UTILS_DIR}/fits_writer.cpp +) +target_include_directories(fits_writer PRIVATE ${PROJECT_BASE_DIR}/common ${PROJECT_BASE_DIR}/utils) +find_library(FITSWRITER_CCFITS_LIB CCfits NAMES libCCfits PATHS /usr/local/lib /opt/homebrew/lib) +find_library(FITSWRITER_CFITS_LIB cfitsio NAMES libcfitsio PATHS /usr/local/lib /opt/homebrew/lib) +target_link_libraries(fits_writer + nlohmann_json::nlohmann_json + ${FITSWRITER_CCFITS_LIB} + ${FITSWRITER_CFITS_LIB} +) + add_library(md5 STATIC ${PROJECT_UTILS_DIR}/md5.cpp ) diff --git a/utils/fits_writer.cpp b/utils/fits_writer.cpp new file mode 100644 index 0000000..413de9a --- /dev/null +++ b/utils/fits_writer.cpp @@ -0,0 +1,226 @@ +/** + * @file fits_writer.cpp + * @brief FrameOutput implementation that writes FITS files asynchronously + * + * Producer enqueues from the readout thread; a dedicated worker thread + * drains the queue and writes one FITS file per frame via CCfits. + */ + +#include "fits_writer.h" +#include "common.h" +#include "utilities.h" + +#include +#include +#include +#include +#include +#include + +namespace Camera { + + FitsWriter::FitsWriter(FitsWriterConfig cfg) + : cfg_(std::move(cfg)) { + } + + FitsWriter::~FitsWriter() { + this->close(); + } + + long FitsWriter::open() { + const std::string function("Camera::FitsWriter::open"); + + bool expected = false; + if (!started_.compare_exchange_strong(expected, true)) { + logwrite(function, "ERROR already opened"); + return ERROR; + } + + std::error_code ec; + if (!std::filesystem::is_directory(cfg_.output_dir, ec)) { + logwrite(function, "ERROR output_dir does not exist: " + cfg_.output_dir); + started_.store(false); + return ERROR; + } + + worker_ = std::thread(&FitsWriter::worker_loop, this); + + logwrite(function, "started: dir=" + cfg_.output_dir + + " basename=" + cfg_.basename + + " queue_size=" + std::to_string(cfg_.queue_size) + + " write_interval_ms=" + std::to_string(cfg_.write_interval_ms)); + return NO_ERROR; + } + + long FitsWriter::write(const char* data, size_t size, const FrameMetadata& meta) { + if (!started_.load()) return ERROR; + + // Cadence gate — skipped frames never enter the queue + if (cfg_.write_interval_ms > 0) { + const auto now = std::chrono::steady_clock::now(); + const auto interval = std::chrono::milliseconds(cfg_.write_interval_ms); + if (now - last_accepted_ < interval) { + n_skipped_cadence_.fetch_add(1, std::memory_order_relaxed); + return NO_ERROR; + } + last_accepted_ = now; + } + + QueuedFrame frame; + frame.meta = meta; + frame.data.assign(data, data + size); // the one memcpy — outside the lock + + { + std::lock_guard lock(mtx_); + if (queue_.size() >= cfg_.queue_size) { + queue_.pop_front(); + n_dropped_queue_.fetch_add(1, std::memory_order_relaxed); + } + queue_.push_back(std::move(frame)); + } + n_received_.fetch_add(1, std::memory_order_relaxed); + cv_.notify_one(); + return NO_ERROR; + } + + void FitsWriter::close() { + if (!started_.load()) return; + + stop_time_ = std::chrono::steady_clock::now(); + stop_.store(true); + cv_.notify_all(); + + if (worker_.joinable()) worker_.join(); + started_.store(false); + + const std::string function("Camera::FitsWriter::close"); + logwrite(function, "stopped: received=" + std::to_string(n_received_.load()) + + " written=" + std::to_string(n_written_.load()) + + " dropped_queue=" + std::to_string(n_dropped_queue_.load()) + + " skipped_cadence=" + std::to_string(n_skipped_cadence_.load()) + + " failed=" + std::to_string(n_failed_.load()) + + " dropped_shutdown=" + std::to_string(n_dropped_shutdown_.load())); + } + + FitsWriter::Stats FitsWriter::stats() const { + Stats s; + s.frames_received = n_received_.load(); + s.frames_written = n_written_.load(); + s.frames_dropped_queue = n_dropped_queue_.load(); + s.frames_skipped_cadence = n_skipped_cadence_.load(); + s.frames_failed = n_failed_.load(); + s.frames_dropped_shutdown= n_dropped_shutdown_.load(); + return s; + } + + void FitsWriter::worker_loop() { + const auto drain_timeout = std::chrono::milliseconds(cfg_.drain_timeout_ms); + + while (true) { + QueuedFrame frame; + { + std::unique_lock lock(mtx_); + cv_.wait(lock, [this]{ return !queue_.empty() || stop_.load(); }); + + if (queue_.empty()) return; // stop_ is set and we're drained + + if (stop_.load()) { + const auto elapsed = std::chrono::steady_clock::now() - stop_time_; + if (elapsed > drain_timeout) { + n_dropped_shutdown_.fetch_add(queue_.size(), std::memory_order_relaxed); + queue_.clear(); + return; + } + } + + frame = std::move(queue_.front()); + queue_.pop_front(); + } + + if (write_fits_file(frame) == NO_ERROR) { + n_written_.fetch_add(1, std::memory_order_relaxed); + } else { + n_failed_.fetch_add(1, std::memory_order_relaxed); + } + } + } + + long FitsWriter::write_fits_file(const QueuedFrame &frame) { + const std::string function("Camera::FitsWriter::write_fits_file"); + const auto &meta = frame.meta; + + if (meta.width == 0 || meta.height == 0 || meta.bytes_per_pixel == 0) { + logwrite(function, "ERROR invalid frame metadata"); + return ERROR; + } + if (meta.bytes_per_pixel != 2 && meta.bytes_per_pixel != 4) { + logwrite(function, "ERROR unsupported bytes_per_pixel=" + + std::to_string(meta.bytes_per_pixel)); + return ERROR; + } + + const size_t npixels = static_cast(meta.width) * meta.height; + const size_t expected_bytes = npixels * meta.bytes_per_pixel; + if (frame.data.size() < expected_bytes) { + logwrite(function, "ERROR frame data " + std::to_string(frame.data.size()) + + " < expected " + std::to_string(expected_bytes)); + return ERROR; + } + + const std::string filename = make_filename(meta.frame_number); + // CCfits requires a non-existing path; "!" prefix would overwrite, + // but make_filename() already resolved any conflict + const int bitpix = (meta.bytes_per_pixel == 2) ? USHORT_IMG : ULONG_IMG; + long axes[2] = { static_cast(meta.width), + static_cast(meta.height) }; + + try { + auto pFits = std::make_unique(filename, bitpix, 2, axes); + auto &phdu = pFits->pHDU(); + + phdu.addKey("FRAMENO", static_cast(meta.frame_number), + "Frame number"); + phdu.addKey("TIMESTMP", static_cast(meta.timestamp), + "Archon timestamp (0.01 us units)"); + phdu.addKey("DATE", get_timestamp(), "FITS file write time"); + + const long first_pixel = 1; + if (meta.bytes_per_pixel == 2) { + const auto *src = reinterpret_cast(frame.data.data()); + std::valarray data(src, npixels); + phdu.write(first_pixel, npixels, data); + } else { + const auto *src = reinterpret_cast(frame.data.data()); + std::valarray data(src, npixels); + phdu.write(first_pixel, npixels, data); + } + } + catch (const CCfits::FitsException &e) { + logwrite(function, "ERROR FITS exception writing " + filename + ": " + e.message()); + return ERROR; + } + catch (const std::exception &e) { + logwrite(function, "ERROR exception writing " + filename + ": " + e.what()); + return ERROR; + } + + return NO_ERROR; + } + + std::string FitsWriter::make_filename(uint64_t frame_number) const { + char num[32]; + std::snprintf(num, sizeof(num), "%08llu", + static_cast(frame_number)); + + const std::string prefix = cfg_.output_dir + "/" + cfg_.basename + "_" + num; + std::string filename = prefix + ".fits"; + + int suffix = 1; + while (std::filesystem::exists(filename)) { + filename = prefix + "_" + std::to_string(suffix) + ".fits"; + suffix++; + } + return filename; + } + +} diff --git a/utils/fits_writer.h b/utils/fits_writer.h new file mode 100644 index 0000000..1ef8fb8 --- /dev/null +++ b/utils/fits_writer.h @@ -0,0 +1,90 @@ +/** + * @file fits_writer.h + * @brief FrameOutput implementation that writes FITS files asynchronously + * + * Producer calls write() from the readout thread; data is memcpy'd into + * a bounded queue (drop-oldest on overflow) and a dedicated worker thread + * drains the queue to disk. write() never touches CCfits or blocks on + * disk I/O. + */ +#pragma once + +#include "frame_output.h" + +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace Camera { + + struct FitsWriterConfig { + std::string output_dir{"/tmp"}; + std::string basename{"tracking"}; + uint32_t write_interval_ms{0}; // 0 = write every accepted frame + size_t queue_size{32}; + uint32_t drain_timeout_ms{5000}; + }; + + class FitsWriter : public FrameOutput { + public: + explicit FitsWriter(FitsWriterConfig cfg); + ~FitsWriter() override; + + FitsWriter(const FitsWriter&) = delete; + FitsWriter& operator=(const FitsWriter&) = delete; + + long open() override; + long write(const char* data, size_t size, const FrameMetadata& meta) override; + void close() override; + + struct Stats { + uint64_t frames_received{0}; + uint64_t frames_written{0}; + uint64_t frames_dropped_queue{0}; + uint64_t frames_skipped_cadence{0}; + uint64_t frames_failed{0}; + uint64_t frames_dropped_shutdown{0}; + }; + Stats stats() const; + + private: + struct QueuedFrame { + FrameMetadata meta; + std::vector data; + }; + + void worker_loop(); + long write_fits_file(const QueuedFrame &frame); + std::string make_filename(uint64_t frame_number) const; + + FitsWriterConfig cfg_; + + std::deque queue_; + mutable std::mutex mtx_; + std::condition_variable cv_; + + std::atomic stop_{false}; + std::atomic started_{false}; + std::thread worker_; + // Set in close() before stop_, so worker can read race-free + std::chrono::steady_clock::time_point stop_time_; + + // Cadence-gate state — only touched on producer thread + std::chrono::steady_clock::time_point last_accepted_{ + std::chrono::steady_clock::time_point::min()}; + + std::atomic n_received_{0}; + std::atomic n_written_{0}; + std::atomic n_dropped_queue_{0}; + std::atomic n_skipped_cadence_{0}; + std::atomic n_failed_{0}; + std::atomic n_dropped_shutdown_{0}; + }; + +} From 75e7d375729b1d068a9f43e6c72b17d657137193 Mon Sep 17 00:00:00 2001 From: Mike Langmayr <1809691+mikelangmayr@users.noreply.github.com> Date: Fri, 15 May 2026 13:36:14 -0700 Subject: [PATCH 02/15] add frame output factory, make frame output configurable, hook up frame dispatch with base interface --- camerad/CMakeLists.txt | 2 +- camerad/archon_exposure_modes.cpp | 56 +++++++++++--------- camerad/camera_interface.h | 14 +++++ utils/CMakeLists.txt | 10 ++++ utils/frame_output_factory.cpp | 88 +++++++++++++++++++++++++++++++ utils/frame_output_factory.h | 40 ++++++++++++++ 6 files changed, 185 insertions(+), 25 deletions(-) create mode 100644 utils/frame_output_factory.cpp create mode 100644 utils/frame_output_factory.h diff --git a/camerad/CMakeLists.txt b/camerad/CMakeLists.txt index 7fbecc9..cbb8bdd 100644 --- a/camerad/CMakeLists.txt +++ b/camerad/CMakeLists.txt @@ -128,6 +128,7 @@ list (APPEND INTERFACE_SOURCES add_library(${INTERFACE_TARGET} ${INTERFACE_SOURCES}) target_link_libraries(${INTERFACE_TARGET} common + frame_output_factory ) target_include_directories(${INTERFACE_TARGET} PUBLIC ${INTERFACE_INCLUDES}) @@ -172,7 +173,6 @@ target_link_libraries(camerad network utilities logentry - shared_memory_writer ${INTERFACE_TARGET} ${INTERFACE_LIBS} ${CMAKE_THREAD_LIBS_INIT} diff --git a/camerad/archon_exposure_modes.cpp b/camerad/archon_exposure_modes.cpp index 892dc82..a23a3a8 100644 --- a/camerad/archon_exposure_modes.cpp +++ b/camerad/archon_exposure_modes.cpp @@ -7,6 +7,8 @@ #include "archon_exposure_modes.h" #include "archon_interface.h" +#include + namespace Camera { /***** Camera::ExposureModeSingle *******************************************/ @@ -111,43 +113,49 @@ namespace Camera { /***** Camera::ExposureModeSingle::image_processing_thread ******************/ /** - * @brief implementation of Archon-specific expose for Single - * + * @brief Consumer thread: pop each frame off the queue and fan out to + * every configured FrameOutput on the interface */ void ExposureModeSingle::image_processing_thread() { const std::string function("Camera::ExposureModeSingle::image_processing_thread"); logwrite(function, "enter"); -// open FITS file ? + auto* camera_info = &this->interface->camera_info; + auto* controller = this->interface->controller; + auto* mode = &controller->modemap[controller->selectedmode]; + const size_t bufferbytes = static_cast(camera_info->image_data_bytes) * camera_info->cubedepth; + const uint32_t bpp = (mode->samplemode == 1) ? 4 : 2; + const uint32_t width = static_cast(mode->geometry.pixelcount); + const uint32_t height = static_cast(mode->geometry.linecount); - // pop an image out of the queue, - // wait until producer stops producing data, or aborted - // while (!this->interface->is_aborted()) { + std::shared_ptr buf; { - std::unique_lock lock(this->queue_mutex); - // keep trying to get the queue lock until success or aborted - this->queue_cv.wait(lock, [this] { - return !this->imagebuf_queue.empty() || this->is_producer_finished || this->interface->is_aborted(); - }); - if (this->interface->is_aborted()) break; - if (this->imagebuf_queue.empty()) { - if (this->is_producer_finished) { - logwrite(function, "queue empty and producer finished"); - break; - } - else { - logwrite(function, "queue empty, producer not finished"); + std::unique_lock lock(this->queue_mutex); + this->queue_cv.wait(lock, [this] { + return !this->imagebuf_queue.empty() || this->is_producer_finished || this->interface->is_aborted(); + }); + if (this->interface->is_aborted()) break; + if (this->imagebuf_queue.empty()) { + if (this->is_producer_finished) { + logwrite(function, "queue empty and producer finished"); + break; + } continue; } + buf = this->imagebuf_queue.front(); + this->imagebuf_queue.pop(); } - this->imagebuf_queue.pop(); - } -// process_image - std::this_thread::sleep_for(std::chrono::milliseconds(500)); + + Camera::FrameMetadata meta; + meta.frame_number = buf->bufframen_slice.empty() ? 0 : static_cast(buf->bufframen_slice[0]); + meta.timestamp = buf->buftimestamp_slice.empty() ? 0 : buf->buftimestamp_slice[0]; + meta.width = width; + meta.height = height; + meta.bytes_per_pixel = bpp; + this->interface->dispatch_frame(buf->rawpixels.get(), bufferbytes, meta); } -// close FITS file ? logwrite(function, "exit"); } /***** Camera::ExposureModeSingle::image_processing_thread ******************/ diff --git a/camerad/camera_interface.h b/camerad/camera_interface.h index 3f96bf5..df79802 100644 --- a/camerad/camera_interface.h +++ b/camerad/camera_interface.h @@ -12,6 +12,10 @@ #include "camera_information.h" #include "camerad_commands.h" #include "exposure_modes.h" +#include "frame_output.h" + +#include +#include namespace Camera { @@ -54,6 +58,16 @@ namespace Camera { Config configfile; Camera::Information camera_info; + + // Frame output destinations populated by Camera::make_frame_outputs() + std::vector> frame_outputs; + + // Fan a frame out to every configured FrameOutput + void dispatch_frame(const char* data, size_t size, const FrameMetadata &meta) { + for (auto &output : this->frame_outputs) { + output->write(data, size, meta); + } + } // Common::FitsKeys systemkeys; move to Camera::Information? // These functions are shared by all interfaces with common implementations, diff --git a/utils/CMakeLists.txt b/utils/CMakeLists.txt index b5d2440..61ff1e0 100644 --- a/utils/CMakeLists.txt +++ b/utils/CMakeLists.txt @@ -41,6 +41,16 @@ target_link_libraries(fits_writer ${FITSWRITER_CFITS_LIB} ) +add_library(frame_output_factory STATIC + ${PROJECT_UTILS_DIR}/frame_output_factory.cpp +) +target_include_directories(frame_output_factory PRIVATE ${PROJECT_BASE_DIR}/common ${PROJECT_BASE_DIR}/utils) +target_link_libraries(frame_output_factory + nlohmann_json::nlohmann_json + shared_memory_writer + fits_writer +) + add_library(md5 STATIC ${PROJECT_UTILS_DIR}/md5.cpp ) diff --git a/utils/frame_output_factory.cpp b/utils/frame_output_factory.cpp new file mode 100644 index 0000000..784967a --- /dev/null +++ b/utils/frame_output_factory.cpp @@ -0,0 +1,88 @@ +/** + * @file frame_output_factory.cpp + * @brief factory that constructs configured FrameOutput implementations + */ + +#include "frame_output_factory.h" +#include "fits_writer.h" +#include "shared_memory_writer.h" +#include "common.h" + +#include +#include + +namespace { + bool parse_bool(const std::string &v) { + return v == "yes" || v == "YES" || v == "true" || v == "TRUE" || v == "1"; + } +} + +namespace Camera { + + void apply_config_overrides(FrameOutputsConfig &out, const Config &cfg) { + const std::string function("Camera::apply_config_overrides"); + for (int row = 0; row < cfg.n_rows; ++row) { + const auto &key = cfg.param[row]; + const auto &val = cfg.arg[row]; + try { + if (key == "SHM_ENABLED") out.shm_enabled = parse_bool(val); + else if (key == "SHM_SEGMENT_NAME") out.shm_segment_name = val; + 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_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)); + else if (key == "FITS_DRAIN_TIMEOUT_MS") out.fits.drain_timeout_ms = static_cast(std::stoul(val)); + } + catch (const std::exception &e) { + logwrite(function, "WARNING bad value for " + key + "=" + val + ": " + e.what()); + } + } + } + + std::vector> + make_frame_outputs(const FrameOutputsConfig &cfg) { + const std::string function("Camera::make_frame_outputs"); + std::vector> outputs; + + if (cfg.shm_enabled) { + if (cfg.shm_max_frame_bytes == 0) { + logwrite(function, "WARNING shm_enabled but shm_max_frame_bytes==0; SHM skipped"); + } + else { + auto shm = std::make_unique( + cfg.shm_segment_name, cfg.shm_max_frame_bytes, cfg.shm_num_frames); + if (shm->open() == NO_ERROR) { + logwrite(function, "SHM output enabled: segment=" + cfg.shm_segment_name + + " max_bytes=" + std::to_string(cfg.shm_max_frame_bytes) + + " frames=" + std::to_string(cfg.shm_num_frames)); + outputs.push_back(std::move(shm)); + } + else { + logwrite(function, "WARNING SHM output failed to open; skipped"); + } + } + } + + if (cfg.fits_enabled) { + auto fits = std::make_unique(cfg.fits); + if (fits->open() == NO_ERROR) { + logwrite(function, "FITS output enabled: dir=" + cfg.fits.output_dir + + " basename=" + cfg.fits.basename + + " interval_ms=" + std::to_string(cfg.fits.write_interval_ms) + + " queue=" + std::to_string(cfg.fits.queue_size)); + outputs.push_back(std::move(fits)); + } + else { + logwrite(function, "WARNING FITS output failed to open; skipped"); + } + } + + if (outputs.empty()) { + logwrite(function, "no frame outputs configured"); + } + return outputs; + } + +} diff --git a/utils/frame_output_factory.h b/utils/frame_output_factory.h new file mode 100644 index 0000000..b1443e4 --- /dev/null +++ b/utils/frame_output_factory.h @@ -0,0 +1,40 @@ +/** + * @file frame_output_factory.h + * @brief factory that constructs configured FrameOutput implementations + * + * Lets instruments obtain their frame_outputs without depending on + * concrete writer subclasses. Adding a new writer touches only this + * factory and its .cpp. + */ +#pragma once + +#include "config.h" +#include "frame_output.h" +#include "fits_writer.h" + +#include +#include +#include +#include +#include + +namespace Camera { + + struct FrameOutputsConfig { + bool shm_enabled{false}; + std::string shm_segment_name{"camera"}; + size_t shm_max_frame_bytes{0}; // required > 0 when shm_enabled + uint32_t shm_num_frames{4}; + + bool fits_enabled{false}; + FitsWriterConfig fits; + }; + + // Defaults set on `out` by the caller survive for keys not present in cfg + void apply_config_overrides(FrameOutputsConfig &out, const Config &cfg); + + // open() is called on each writer; failures are logged and the writer is skipped + std::vector> + make_frame_outputs(const FrameOutputsConfig &cfg); + +} From ff314e056c1051272469f13cf7c32e27bf95b230 Mon Sep 17 00:00:00 2001 From: Mike Langmayr <1809691+mikelangmayr@users.noreply.github.com> Date: Fri, 15 May 2026 13:37:37 -0700 Subject: [PATCH 03/15] remove unused is_camera_mode flag --- camerad/archon_controller.cpp | 2 -- camerad/archon_controller.h | 1 - 2 files changed, 3 deletions(-) diff --git a/camerad/archon_controller.cpp b/camerad/archon_controller.cpp index 26b7109..1e9ce74 100644 --- a/camerad/archon_controller.cpp +++ b/camerad/archon_controller.cpp @@ -1584,8 +1584,6 @@ namespace Camera { } else error=this->fetchlog(); - this->is_camera_mode = false; // require that a mode be selected after loading new firmware - return error; } /***** Camera::ArchonController::load_acf ***********************************/ diff --git a/camerad/archon_controller.h b/camerad/archon_controller.h index 5ce1d01..a4d0a04 100644 --- a/camerad/archon_controller.h +++ b/camerad/archon_controller.h @@ -294,7 +294,6 @@ namespace Camera { std::atomic_flag archon_busy = ATOMIC_FLAG_INIT; //!< indicates a thread is accessing Archon bool is_firmwareloaded; std::string firmware; - bool is_camera_mode; //!< has a camera mode been selected int msgref; std::string backplaneversion; std::vector modtype; //!< type of each module from SYSTEM command From 59977707defe2e3ffc158ed1da56d6cd0f7d0a3d Mon Sep 17 00:00:00 2001 From: Mike Langmayr <1809691+mikelangmayr@users.noreply.github.com> Date: Fri, 15 May 2026 13:39:44 -0700 Subject: [PATCH 04/15] make default SINGLE exposure mode override-able --- camerad/archon_interface.cpp | 3 +-- camerad/archon_interface.h | 3 +++ 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/camerad/archon_interface.cpp b/camerad/archon_interface.cpp index 9e078ad..34b42ed 100644 --- a/camerad/archon_interface.cpp +++ b/camerad/archon_interface.cpp @@ -810,8 +810,7 @@ namespace Camera { // Set the exposure mode to match the camera mode name if recognized if (this->set_exposure_mode(modeselect, {}) != NO_ERROR) { - // Fall back to SINGLE if the camera mode name doesn't match an exposure mode - this->set_exposure_mode(std::string(ArchonExposureMode::SINGLE), {}); + this->set_exposure_mode(this->default_exposure_mode_name(), {}); } return NO_ERROR; diff --git a/camerad/archon_interface.h b/camerad/archon_interface.h index be985f5..8db6bcd 100644 --- a/camerad/archon_interface.h +++ b/camerad/archon_interface.h @@ -68,6 +68,9 @@ namespace Camera { long set_vcpu_inreg(const std::string &args, std::string &retstring); long autofetch_mode(const std::string &args, std::string &retstring); + // Fallback for set_camera_mode when the camera-mode name is unknown + virtual std::string default_exposure_mode_name() const { return "SINGLE"; } + char* get_framebuf() { return controller->framebuf; } bool is_autofetch_mode{false}; From 91f6da92693cf3f2efb0e92059e18dfe8bab5bce Mon Sep 17 00:00:00 2001 From: Mike Langmayr <1809691+mikelangmayr@users.noreply.github.com> Date: Fri, 15 May 2026 13:43:00 -0700 Subject: [PATCH 05/15] make ExposureMode::expose optional --- camerad/exposure_modes.h | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/camerad/exposure_modes.h b/camerad/exposure_modes.h index 26ba477..ecf7735 100644 --- a/camerad/exposure_modes.h +++ b/camerad/exposure_modes.h @@ -48,7 +48,7 @@ namespace Camera { } virtual ~ExposureMode() = default; - virtual long expose() = 0; + virtual long expose() { return NO_ERROR; } virtual void image_acquisition_thread() { }; virtual void image_processing_thread() { }; From 881004e6057c73951ea1bbd52f5b0f887956c85d Mon Sep 17 00:00:00 2001 From: Mike Langmayr <1809691+mikelangmayr@users.noreply.github.com> Date: Mon, 18 May 2026 14:25:46 -0700 Subject: [PATCH 06/15] Reset stop_ in open() and reject queue_size==0 in FitsWriter --- utils/fits_writer.cpp | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/utils/fits_writer.cpp b/utils/fits_writer.cpp index 413de9a..6880cc4 100644 --- a/utils/fits_writer.cpp +++ b/utils/fits_writer.cpp @@ -36,6 +36,12 @@ namespace Camera { return ERROR; } + if (cfg_.queue_size == 0) { + logwrite(function, "ERROR queue_size must be > 0"); + started_.store(false); + return ERROR; + } + std::error_code ec; if (!std::filesystem::is_directory(cfg_.output_dir, ec)) { logwrite(function, "ERROR output_dir does not exist: " + cfg_.output_dir); @@ -43,6 +49,7 @@ namespace Camera { return ERROR; } + stop_.store(false); worker_ = std::thread(&FitsWriter::worker_loop, this); logwrite(function, "started: dir=" + cfg_.output_dir + From 973a2a6d3f726c2b127363ed0f218ff3ee1a0da5 Mon Sep 17 00:00:00 2001 From: Elijah AB Date: Wed, 10 Jun 2026 16:49:35 -0700 Subject: [PATCH 07/15] temporary fits writer --- camerad/Instruments/hispec_tracking_camera | 2 +- utils/fits_writer.h | 2 +- utils/frame_output_factory.cpp | 2 ++ 3 files changed, 4 insertions(+), 2 deletions(-) diff --git a/camerad/Instruments/hispec_tracking_camera b/camerad/Instruments/hispec_tracking_camera index c90a42b..39f900e 160000 --- a/camerad/Instruments/hispec_tracking_camera +++ b/camerad/Instruments/hispec_tracking_camera @@ -1 +1 @@ -Subproject commit c90a42ba8456f63cd29ffcffbf7ec114d7aa3ed4 +Subproject commit 39f900e06a937e714b6afab64b8cfd33f8b37b66 diff --git a/utils/fits_writer.h b/utils/fits_writer.h index 1ef8fb8..cf8db21 100644 --- a/utils/fits_writer.h +++ b/utils/fits_writer.h @@ -24,7 +24,7 @@ namespace Camera { struct FitsWriterConfig { - std::string output_dir{"/tmp"}; + std::string output_dir{"/tmp/images"}; std::string basename{"tracking"}; uint32_t write_interval_ms{0}; // 0 = write every accepted frame size_t queue_size{32}; diff --git a/utils/frame_output_factory.cpp b/utils/frame_output_factory.cpp index 784967a..3a8ce26 100644 --- a/utils/frame_output_factory.cpp +++ b/utils/frame_output_factory.cpp @@ -46,6 +46,8 @@ namespace Camera { const std::string function("Camera::make_frame_outputs"); std::vector> outputs; + logwrite(function, "Creating frame outputs!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!"); + if (cfg.shm_enabled) { if (cfg.shm_max_frame_bytes == 0) { logwrite(function, "WARNING shm_enabled but shm_max_frame_bytes==0; SHM skipped"); From e37d93918cf041ec72d8ca4bc7d373512b68b789 Mon Sep 17 00:00:00 2001 From: Elijah AB Date: Fri, 12 Jun 2026 15:06:40 -0700 Subject: [PATCH 08/15] slight changes --- camerad/Instruments/hispec_tracking_camera | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/camerad/Instruments/hispec_tracking_camera b/camerad/Instruments/hispec_tracking_camera index 39f900e..3a11c90 160000 --- a/camerad/Instruments/hispec_tracking_camera +++ b/camerad/Instruments/hispec_tracking_camera @@ -1 +1 @@ -Subproject commit 39f900e06a937e714b6afab64b8cfd33f8b37b66 +Subproject commit 3a11c901aeb25c948c65c569cbd71e1a20ecc8ee From 1067041709d75f14856ae5e4b7bd2b17bb3d6702 Mon Sep 17 00:00:00 2001 From: Elijah AB Date: Tue, 21 Jul 2026 13:10:36 -0700 Subject: [PATCH 09/15] Pushing to give to mike for Shared memory fix --- camerad/Instruments/hispec_tracking_camera | 2 +- utils/frame_output_factory.cpp | 2 -- utils/shared_memory_writer.h | 4 ++-- 3 files changed, 3 insertions(+), 5 deletions(-) diff --git a/camerad/Instruments/hispec_tracking_camera b/camerad/Instruments/hispec_tracking_camera index 3a11c90..8dda392 160000 --- a/camerad/Instruments/hispec_tracking_camera +++ b/camerad/Instruments/hispec_tracking_camera @@ -1 +1 @@ -Subproject commit 3a11c901aeb25c948c65c569cbd71e1a20ecc8ee +Subproject commit 8dda392a1e9eae4647c8b03a0cb718fdcd0e0a50 diff --git a/utils/frame_output_factory.cpp b/utils/frame_output_factory.cpp index 3a8ce26..784967a 100644 --- a/utils/frame_output_factory.cpp +++ b/utils/frame_output_factory.cpp @@ -46,8 +46,6 @@ namespace Camera { const std::string function("Camera::make_frame_outputs"); std::vector> outputs; - logwrite(function, "Creating frame outputs!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!"); - if (cfg.shm_enabled) { if (cfg.shm_max_frame_bytes == 0) { logwrite(function, "WARNING shm_enabled but shm_max_frame_bytes==0; SHM skipped"); diff --git a/utils/shared_memory_writer.h b/utils/shared_memory_writer.h index 00eb1de..edcd160 100644 --- a/utils/shared_memory_writer.h +++ b/utils/shared_memory_writer.h @@ -45,9 +45,9 @@ 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 open(); long write(const char* data, size_t size, const FrameMetadata& meta) override; void close() override; From 902b8d564180206dfc06329339ab147e03e32834 Mon Sep 17 00:00:00 2001 From: Elijah AB Date: Tue, 21 Jul 2026 16:11:15 -0700 Subject: [PATCH 10/15] fix msgref init issue. Mike L --- camerad/archon_controller.h | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/camerad/archon_controller.h b/camerad/archon_controller.h index a4d0a04..229ac34 100644 --- a/camerad/archon_controller.h +++ b/camerad/archon_controller.h @@ -294,7 +294,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 From 29c74a7e7af34db78a95ff34bc301bd6bf4b766b Mon Sep 17 00:00:00 2001 From: Elijah AB Date: Wed, 22 Jul 2026 10:13:43 -0700 Subject: [PATCH 11/15] connection with archon fix and config write fix --- Config/hispecatc.cfg | 27 +++++++++++++++++++++ camerad/Instruments/hispec_tracking_camera | 2 +- camerad/archon_controller.cpp | 28 +++++++++++++++++----- utils/shared_memory_writer.h | 2 +- 4 files changed, 51 insertions(+), 8 deletions(-) create mode 100644 Config/hispecatc.cfg diff --git a/Config/hispecatc.cfg b/Config/hispecatc.cfg new file mode 100644 index 0000000..82014c3 --- /dev/null +++ b/Config/hispecatc.cfg @@ -0,0 +1,27 @@ +# 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 + +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 8dda392..c722025 160000 --- a/camerad/Instruments/hispec_tracking_camera +++ b/camerad/Instruments/hispec_tracking_camera @@ -1 +1 @@ -Subproject commit 8dda392a1e9eae4647c8b03a0cb718fdcd0e0a50 +Subproject commit c7220250705f2c651d01096d01118bd9c1dde48b diff --git a/camerad/archon_controller.cpp b/camerad/archon_controller.cpp index bbf808d..4720e9c 100644 --- a/camerad/archon_controller.cpp +++ b/camerad/archon_controller.cpp @@ -1035,7 +1035,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; @@ -1635,6 +1634,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"); } @@ -2062,10 +2068,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 +2226,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/utils/shared_memory_writer.h b/utils/shared_memory_writer.h index edcd160..08b0b82 100644 --- a/utils/shared_memory_writer.h +++ b/utils/shared_memory_writer.h @@ -47,7 +47,7 @@ namespace Camera { uint32_t num_frames = 4); ~SharedMemoryWriter(); - long open(); + long open() override; long write(const char* data, size_t size, const FrameMetadata& meta) override; void close() override; From afdd5e8a07e3f1384f04ee11f01b8cf570f4f95f Mon Sep 17 00:00:00 2001 From: Elijah AB Date: Wed, 22 Jul 2026 15:32:04 -0700 Subject: [PATCH 12/15] some fixes with connection to archon --- camerad/Instruments/hispec_tracking_camera | 2 +- camerad/archon_controller.cpp | 76 ++++++++++++++++++++++ camerad/archon_controller.h | 2 + 3 files changed, 79 insertions(+), 1 deletion(-) diff --git a/camerad/Instruments/hispec_tracking_camera b/camerad/Instruments/hispec_tracking_camera index c722025..ddfa87a 160000 --- a/camerad/Instruments/hispec_tracking_camera +++ b/camerad/Instruments/hispec_tracking_camera @@ -1 +1 @@ -Subproject commit c7220250705f2c651d01096d01118bd9c1dde48b +Subproject commit ddfa87a8ac5a8445abb7c31773aa132f1e514d11 diff --git a/camerad/archon_controller.cpp b/camerad/archon_controller.cpp index 4720e9c..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 { @@ -1569,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; @@ -1658,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 diff --git a/camerad/archon_controller.h b/camerad/archon_controller.h index 229ac34..b51af17 100644 --- a/camerad/archon_controller.h +++ b/camerad/archon_controller.h @@ -190,6 +190,7 @@ namespace Camera { struct tapinfo_t { int num_taps; int tap[16]; + std::string ampname[16]; //!< amplifier name for each tap, e.g. "AM54" (from TAPLINEn in the ACF) float gain[16]; float offset[16]; std::string readoutdir[16]; @@ -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); From 1d7312d6e872b954ebd5f816abf661f9e68814f5 Mon Sep 17 00:00:00 2001 From: Elijah AB Date: Wed, 22 Jul 2026 16:09:35 -0700 Subject: [PATCH 13/15] increased tapline buffer to 64 --- camerad/archon_controller.h | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/camerad/archon_controller.h b/camerad/archon_controller.h index b51af17..e04c3f6 100644 --- a/camerad/archon_controller.h +++ b/camerad/archon_controller.h @@ -190,10 +190,10 @@ namespace Camera { struct tapinfo_t { int num_taps; int tap[16]; - std::string ampname[16]; //!< amplifier name for each tap, e.g. "AM54" (from TAPLINEn in the ACF) + 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]; }; /** From 6322f61043ecd285bb90f9c0c4313473e040b5d6 Mon Sep 17 00:00:00 2001 From: Elijah AB Date: Wed, 22 Jul 2026 16:16:41 -0700 Subject: [PATCH 14/15] update hispec tracking camera instrument --- camerad/Instruments/hispec_tracking_camera | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/camerad/Instruments/hispec_tracking_camera b/camerad/Instruments/hispec_tracking_camera index ddfa87a..36deef2 160000 --- a/camerad/Instruments/hispec_tracking_camera +++ b/camerad/Instruments/hispec_tracking_camera @@ -1 +1 @@ -Subproject commit ddfa87a8ac5a8445abb7c31773aa132f1e514d11 +Subproject commit 36deef25c0415e62f52ca90e58c56c149b11c740 From e5f24eebe94fdbd67fa5a6fd42d01c553f22b7f9 Mon Sep 17 00:00:00 2001 From: Elijah AB Date: Wed, 22 Jul 2026 17:08:21 -0700 Subject: [PATCH 15/15] fix fits writer --- Config/hispecatc.cfg | 7 +++++++ utils/fits_writer.cpp | 23 +++++++++++++++++++++-- utils/fits_writer.h | 9 ++++++++- utils/frame_output_factory.cpp | 1 + 4 files changed, 37 insertions(+), 3 deletions(-) diff --git a/Config/hispecatc.cfg b/Config/hispecatc.cfg index 82014c3..7d42914 100644 --- a/Config/hispecatc.cfg +++ b/Config/hispecatc.cfg @@ -11,6 +11,13 @@ WRITE_TAPINFO_TO_FITS=yes # Tapinfo (gain, offset) be written to FITS h 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" 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));