From bac525761ee612cecfa1a5f9655f8a56d3598abf Mon Sep 17 00:00:00 2001 From: Mike Langmayr <1809691+mikelangmayr@users.noreply.github.com> Date: Wed, 19 Aug 2026 18:53:57 -0700 Subject: [PATCH 1/8] Add FITS header dictionary and wire it into the async writer Instrument-agnostic property->keyword/comment/default/enum table in utils/fits_header_dictionary.*; FrameMetadata gains per-frame scalars and a per-exposure header_set snapshot; FitsWriter emits both. --- utils/CMakeLists.txt | 6 ++ utils/fits_header_dictionary.cpp | 95 ++++++++++++++++++++++++++++++++ utils/fits_header_dictionary.h | 34 ++++++++++++ utils/fits_writer.cpp | 40 ++++++++++++++ utils/frame_output.h | 15 ++++- 5 files changed, 189 insertions(+), 1 deletion(-) create mode 100644 utils/fits_header_dictionary.cpp create mode 100644 utils/fits_header_dictionary.h diff --git a/utils/CMakeLists.txt b/utils/CMakeLists.txt index 7212759..39a1aff 100644 --- a/utils/CMakeLists.txt +++ b/utils/CMakeLists.txt @@ -29,6 +29,11 @@ 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_header_dictionary STATIC + ${PROJECT_UTILS_DIR}/fits_header_dictionary.cpp +) +target_include_directories(fits_header_dictionary PRIVATE ${PROJECT_BASE_DIR}/common ${PROJECT_BASE_DIR}/utils) + add_library(fits_writer STATIC ${PROJECT_UTILS_DIR}/fits_writer.cpp ) @@ -37,6 +42,7 @@ find_library(FITSWRITER_CCFITS_LIB CCfits NAMES libCCfits PATHS /usr/local/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 + fits_header_dictionary ${FITSWRITER_CCFITS_LIB} ${FITSWRITER_CFITS_LIB} ) diff --git a/utils/fits_header_dictionary.cpp b/utils/fits_header_dictionary.cpp new file mode 100644 index 0000000..1ede3e4 --- /dev/null +++ b/utils/fits_header_dictionary.cpp @@ -0,0 +1,95 @@ +/** + * @file fits_header_dictionary.cpp + * @brief FITS keyword dictionary — property -> keyword/comment/default/enum + */ + +#include "fits_header_dictionary.h" + +namespace Camera { + + namespace { + using Type = HeaderValueType; + } + + const std::vector& fits_header_dictionary() { + // bitpix->DETBITS: BITPIX is a reserved CFITSIO keyword. + // pixel_time->PIXTIME: PIXELTIME is 9 chars, over the FITS keyword limit. + static const std::vector dictionary = { + { "mjd_start", "EXPMJDST", + "Exposure start Modified Julian Date of first read", + Type::Number, "", "", {} }, + { "read_mode", "READMODE", "Readout mode", + Type::String, "", "", {} }, + { "subframe_mode", "SUBFRAME", + "Subframe mode, e.g. guiding, ROI, or fullframe", + Type::String, "", "", {} }, + { "operational_mode", "OPSMODE", "Operational mode, e.g. freerun or autofetch", + Type::String, "", "", {} }, + { "exposure_time", "EXPTIME", "Effective exposure time", + Type::Number, "", "0", {} }, + { "bitpix", "DETBITS", "Detector bit depth, needed for offset correction", + Type::Integer, "16", "16", {} }, + { "clock_rate", "CLOCKRAT", "Clock rate of the detector readout", + Type::Number, "", "", {} }, + { "pixel_time", "PIXTIME", "Pixel time spent reading out each pixel", + Type::Number, "5.92", "", {} }, + { "frame_time", "FRAMETME", "Time to read out each amplifier region", + Type::Number, "", "", {} }, + { "n_channels", "NCHANLS", "Number of detector channels", + Type::Integer, "4", "64", {} }, + { "refpix_channel", "REFPXAMP", "The channel corresponding to the reference pixel", + Type::Integer, "5", "", {} }, + { "ref_channel_position", "REFCHPOS", + "Position of the reference channel in the data array", + Type::String, "right", "RIGHT", {"TOP", "BOTTOM", "LEFT", "RIGHT", "NONE"} }, + { "channels_are_vertical", "CHANVERT", + "Are the spectral channels aligned vertically in the data array?", + Type::Boolean, "N/A", "TRUE", {} }, + { "filename", "FILENAME", "Name of the file", + Type::String, "", "", {} }, + { "file_type", "FILETYPE", "Type of file", + Type::String, "", "FITS", {"FITS"} }, + { "LVLC_V1", "LVLC_V1", "Gate voltage", + Type::Number, "from ACF", "", {} }, + { "LVLC_V2", "LVLC_V2", "Bias power", + Type::Number, "from ACF", "", {} }, + { "LVLC_V3", "LVLC_V3", "Diode substrate voltage", + Type::Number, "from ACF", "", {} }, + { "CAMD_VER", "CAMD_VER", "camerad build date", + Type::String, "", "", {} }, + { "FIRMWARE", "FIRMWARE", "Controller ACF file used", + Type::String, "", "", {} }, + { "nskip_rows", "SKIPROWS", "Number of rows skipped", + Type::Integer, "", "", {} }, + { "nskip_lines", "SKIPLNES", "Number of lines skipped", + Type::Integer, "", "", {} }, + { "n_reads", "NREADS", "Number of reads in this exposure (freerun: running frame counter)", + Type::Integer, "", "", {} }, + { "acq_time", "ACQTIME", "UTC time of acquisition", + Type::String, "", "", {} }, + { "FREERUN", "FREERUN", "Exposure was acquired in freerun mode", + Type::Boolean, "FALSE", "FALSE", {} }, + }; + return dictionary; + } + + const HeaderDictEntry* find_header_entry(const std::string& property) { + for (const auto& entry : fits_header_dictionary()) { + if (entry.property == property) return &entry; + } + return nullptr; + } + + const std::string& header_default(const HeaderDictEntry& entry, FitsCamera camera) { + return (camera == FitsCamera::ATC) ? entry.default_atc : entry.default_spec; + } + + bool is_valid_header_enum(const HeaderDictEntry& entry, const std::string& value) { + if (entry.enum_values.empty()) return true; + for (const auto& allowed : entry.enum_values) { + if (allowed == value) return true; + } + return false; + } + +} diff --git a/utils/fits_header_dictionary.h b/utils/fits_header_dictionary.h new file mode 100644 index 0000000..9f9ee07 --- /dev/null +++ b/utils/fits_header_dictionary.h @@ -0,0 +1,34 @@ +/** + * @file fits_header_dictionary.h + * @brief FITS keyword dictionary — property -> keyword/comment/default/enum + */ +#pragma once + +#include +#include + +namespace Camera { + + enum class HeaderValueType { Number, Integer, String, Boolean }; + + enum class FitsCamera { ATC, SPEC }; + + struct HeaderDictEntry { + std::string property; + std::string keyword; + std::string comment; + HeaderValueType type; + std::string default_atc; // empty = no default + std::string default_spec; // empty = no default + std::vector enum_values; // empty = unconstrained + }; + + const std::vector& fits_header_dictionary(); + + const HeaderDictEntry* find_header_entry(const std::string& property); + + const std::string& header_default(const HeaderDictEntry& entry, FitsCamera camera); + + bool is_valid_header_enum(const HeaderDictEntry& entry, const std::string& value); + +} diff --git a/utils/fits_writer.cpp b/utils/fits_writer.cpp index e2d3cbc..fcd86dd 100644 --- a/utils/fits_writer.cpp +++ b/utils/fits_writer.cpp @@ -8,6 +8,7 @@ #include "fits_writer.h" #include "common.h" +#include "fits_header_dictionary.h" #include "utilities.h" #include @@ -19,6 +20,26 @@ namespace Camera { + namespace { + void add_key_from_fits_keys(CCfits::PHDU &phdu, const Common::FitsKeys::user_key_t &entry) { + const std::string function("Camera::FitsWriter::add_key_from_fits_keys"); + try { + if (entry.keytype == "DOUBLE" || entry.keytype == "FLOAT") { + phdu.addKey(entry.keyword, std::stod(entry.keyvalue), entry.keycomment); + } else if (entry.keytype == "INT" || entry.keytype == "LONG") { + phdu.addKey(entry.keyword, std::stol(entry.keyvalue), entry.keycomment); + } else if (entry.keytype == "BOOL") { + phdu.addKey(entry.keyword, entry.keyvalue == "T", entry.keycomment); + } else { + phdu.addKey(entry.keyword, entry.keyvalue, entry.keycomment); + } + } + catch (const std::exception &e) { + logwrite(function, "ERROR formatting key " + entry.keyword + ": " + e.what()); + } + } + } + FitsWriter::FitsWriter(FitsWriterConfig cfg) : cfg_(std::move(cfg)) { } @@ -179,6 +200,25 @@ namespace Camera { "Archon timestamp (0.01 us units)"); phdu.addKey("DATE", get_timestamp(), "FITS file write time"); + if (const auto *entry = find_header_entry("mjd_start")) { + phdu.addKey(entry->keyword, meta.mjd_start, entry->comment); + } + if (const auto *entry = find_header_entry("acq_time")) { + if (!meta.acq_time.empty()) phdu.addKey(entry->keyword, meta.acq_time, entry->comment); + } + if (const auto *entry = find_header_entry("exposure_time")) { + phdu.addKey(entry->keyword, meta.exposure_time_sec, entry->comment); + } + if (const auto *entry = find_header_entry("n_reads")) { + phdu.addKey(entry->keyword, static_cast(meta.n_reads), entry->comment); + } + + if (meta.header_set) { + for (const auto &key_entry : meta.header_set->keydb) { + add_key_from_fits_keys(phdu, key_entry.second); + } + } + const long first_pixel = 1; if (meta.bytes_per_pixel == 2) { const auto *src = reinterpret_cast(frame.data.data()); diff --git a/utils/frame_output.h b/utils/frame_output.h index bd3a65e..fb4fb0a 100644 --- a/utils/frame_output.h +++ b/utils/frame_output.h @@ -7,6 +7,10 @@ #include #include +#include +#include + +namespace Common { class FitsKeys; } namespace Camera { @@ -16,7 +20,16 @@ namespace Camera { uint32_t width{0}; uint32_t height{0}; uint32_t bytes_per_pixel{0}; - uint64_t sequence_number{0}; + uint64_t sequence_number{0}; // monotonic per-stream counter + + // Values that vary per frame, not per exposure + double mjd_start{0.0}; // EXPMJDST + std::string acq_time; // ACQTIME, UTC + double exposure_time_sec{0.0}; // EXPTIME + uint64_t n_reads{0}; // NREADS + + // Resolved static keys, built once per exposure and shared across its frames + std::shared_ptr header_set; }; class FrameOutput { From 97f237c8c29c9628691e419d304a8cb44529a4d5 Mon Sep 17 00:00:00 2001 From: Mike Langmayr <1809691+mikelangmayr@users.noreply.github.com> Date: Thu, 20 Aug 2026 11:10:24 -0700 Subject: [PATCH 2/8] Make FitsWriter/FrameMetadata generic; move HISPEC dictionary to instrument submodule --- utils/CMakeLists.txt | 6 -- utils/fits_header_dictionary.cpp | 95 -------------------------------- utils/fits_header_dictionary.h | 34 ------------ utils/fits_writer.cpp | 26 +++------ utils/frame_output.h | 10 +--- 5 files changed, 10 insertions(+), 161 deletions(-) delete mode 100644 utils/fits_header_dictionary.cpp delete mode 100644 utils/fits_header_dictionary.h diff --git a/utils/CMakeLists.txt b/utils/CMakeLists.txt index 39a1aff..7212759 100644 --- a/utils/CMakeLists.txt +++ b/utils/CMakeLists.txt @@ -29,11 +29,6 @@ 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_header_dictionary STATIC - ${PROJECT_UTILS_DIR}/fits_header_dictionary.cpp -) -target_include_directories(fits_header_dictionary PRIVATE ${PROJECT_BASE_DIR}/common ${PROJECT_BASE_DIR}/utils) - add_library(fits_writer STATIC ${PROJECT_UTILS_DIR}/fits_writer.cpp ) @@ -42,7 +37,6 @@ find_library(FITSWRITER_CCFITS_LIB CCfits NAMES libCCfits PATHS /usr/local/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 - fits_header_dictionary ${FITSWRITER_CCFITS_LIB} ${FITSWRITER_CFITS_LIB} ) diff --git a/utils/fits_header_dictionary.cpp b/utils/fits_header_dictionary.cpp deleted file mode 100644 index 1ede3e4..0000000 --- a/utils/fits_header_dictionary.cpp +++ /dev/null @@ -1,95 +0,0 @@ -/** - * @file fits_header_dictionary.cpp - * @brief FITS keyword dictionary — property -> keyword/comment/default/enum - */ - -#include "fits_header_dictionary.h" - -namespace Camera { - - namespace { - using Type = HeaderValueType; - } - - const std::vector& fits_header_dictionary() { - // bitpix->DETBITS: BITPIX is a reserved CFITSIO keyword. - // pixel_time->PIXTIME: PIXELTIME is 9 chars, over the FITS keyword limit. - static const std::vector dictionary = { - { "mjd_start", "EXPMJDST", - "Exposure start Modified Julian Date of first read", - Type::Number, "", "", {} }, - { "read_mode", "READMODE", "Readout mode", - Type::String, "", "", {} }, - { "subframe_mode", "SUBFRAME", - "Subframe mode, e.g. guiding, ROI, or fullframe", - Type::String, "", "", {} }, - { "operational_mode", "OPSMODE", "Operational mode, e.g. freerun or autofetch", - Type::String, "", "", {} }, - { "exposure_time", "EXPTIME", "Effective exposure time", - Type::Number, "", "0", {} }, - { "bitpix", "DETBITS", "Detector bit depth, needed for offset correction", - Type::Integer, "16", "16", {} }, - { "clock_rate", "CLOCKRAT", "Clock rate of the detector readout", - Type::Number, "", "", {} }, - { "pixel_time", "PIXTIME", "Pixel time spent reading out each pixel", - Type::Number, "5.92", "", {} }, - { "frame_time", "FRAMETME", "Time to read out each amplifier region", - Type::Number, "", "", {} }, - { "n_channels", "NCHANLS", "Number of detector channels", - Type::Integer, "4", "64", {} }, - { "refpix_channel", "REFPXAMP", "The channel corresponding to the reference pixel", - Type::Integer, "5", "", {} }, - { "ref_channel_position", "REFCHPOS", - "Position of the reference channel in the data array", - Type::String, "right", "RIGHT", {"TOP", "BOTTOM", "LEFT", "RIGHT", "NONE"} }, - { "channels_are_vertical", "CHANVERT", - "Are the spectral channels aligned vertically in the data array?", - Type::Boolean, "N/A", "TRUE", {} }, - { "filename", "FILENAME", "Name of the file", - Type::String, "", "", {} }, - { "file_type", "FILETYPE", "Type of file", - Type::String, "", "FITS", {"FITS"} }, - { "LVLC_V1", "LVLC_V1", "Gate voltage", - Type::Number, "from ACF", "", {} }, - { "LVLC_V2", "LVLC_V2", "Bias power", - Type::Number, "from ACF", "", {} }, - { "LVLC_V3", "LVLC_V3", "Diode substrate voltage", - Type::Number, "from ACF", "", {} }, - { "CAMD_VER", "CAMD_VER", "camerad build date", - Type::String, "", "", {} }, - { "FIRMWARE", "FIRMWARE", "Controller ACF file used", - Type::String, "", "", {} }, - { "nskip_rows", "SKIPROWS", "Number of rows skipped", - Type::Integer, "", "", {} }, - { "nskip_lines", "SKIPLNES", "Number of lines skipped", - Type::Integer, "", "", {} }, - { "n_reads", "NREADS", "Number of reads in this exposure (freerun: running frame counter)", - Type::Integer, "", "", {} }, - { "acq_time", "ACQTIME", "UTC time of acquisition", - Type::String, "", "", {} }, - { "FREERUN", "FREERUN", "Exposure was acquired in freerun mode", - Type::Boolean, "FALSE", "FALSE", {} }, - }; - return dictionary; - } - - const HeaderDictEntry* find_header_entry(const std::string& property) { - for (const auto& entry : fits_header_dictionary()) { - if (entry.property == property) return &entry; - } - return nullptr; - } - - const std::string& header_default(const HeaderDictEntry& entry, FitsCamera camera) { - return (camera == FitsCamera::ATC) ? entry.default_atc : entry.default_spec; - } - - bool is_valid_header_enum(const HeaderDictEntry& entry, const std::string& value) { - if (entry.enum_values.empty()) return true; - for (const auto& allowed : entry.enum_values) { - if (allowed == value) return true; - } - return false; - } - -} diff --git a/utils/fits_header_dictionary.h b/utils/fits_header_dictionary.h deleted file mode 100644 index 9f9ee07..0000000 --- a/utils/fits_header_dictionary.h +++ /dev/null @@ -1,34 +0,0 @@ -/** - * @file fits_header_dictionary.h - * @brief FITS keyword dictionary — property -> keyword/comment/default/enum - */ -#pragma once - -#include -#include - -namespace Camera { - - enum class HeaderValueType { Number, Integer, String, Boolean }; - - enum class FitsCamera { ATC, SPEC }; - - struct HeaderDictEntry { - std::string property; - std::string keyword; - std::string comment; - HeaderValueType type; - std::string default_atc; // empty = no default - std::string default_spec; // empty = no default - std::vector enum_values; // empty = unconstrained - }; - - const std::vector& fits_header_dictionary(); - - const HeaderDictEntry* find_header_entry(const std::string& property); - - const std::string& header_default(const HeaderDictEntry& entry, FitsCamera camera); - - bool is_valid_header_enum(const HeaderDictEntry& entry, const std::string& value); - -} diff --git a/utils/fits_writer.cpp b/utils/fits_writer.cpp index fcd86dd..edb9bae 100644 --- a/utils/fits_writer.cpp +++ b/utils/fits_writer.cpp @@ -8,7 +8,6 @@ #include "fits_writer.h" #include "common.h" -#include "fits_header_dictionary.h" #include "utilities.h" #include @@ -38,6 +37,11 @@ namespace Camera { logwrite(function, "ERROR formatting key " + entry.keyword + ": " + e.what()); } } + + void add_keys_from(CCfits::PHDU &phdu, const Common::FitsKeys *keys) { + if (!keys) return; + for (const auto &entry : keys->keydb) add_key_from_fits_keys(phdu, entry.second); + } } FitsWriter::FitsWriter(FitsWriterConfig cfg) @@ -200,24 +204,8 @@ namespace Camera { "Archon timestamp (0.01 us units)"); phdu.addKey("DATE", get_timestamp(), "FITS file write time"); - if (const auto *entry = find_header_entry("mjd_start")) { - phdu.addKey(entry->keyword, meta.mjd_start, entry->comment); - } - if (const auto *entry = find_header_entry("acq_time")) { - if (!meta.acq_time.empty()) phdu.addKey(entry->keyword, meta.acq_time, entry->comment); - } - if (const auto *entry = find_header_entry("exposure_time")) { - phdu.addKey(entry->keyword, meta.exposure_time_sec, entry->comment); - } - if (const auto *entry = find_header_entry("n_reads")) { - phdu.addKey(entry->keyword, static_cast(meta.n_reads), entry->comment); - } - - if (meta.header_set) { - for (const auto &key_entry : meta.header_set->keydb) { - add_key_from_fits_keys(phdu, key_entry.second); - } - } + add_keys_from(phdu, meta.header_set.get()); + add_keys_from(phdu, meta.frame_keys.get()); const long first_pixel = 1; if (meta.bytes_per_pixel == 2) { diff --git a/utils/frame_output.h b/utils/frame_output.h index fb4fb0a..f067ab9 100644 --- a/utils/frame_output.h +++ b/utils/frame_output.h @@ -8,7 +8,6 @@ #include #include #include -#include namespace Common { class FitsKeys; } @@ -22,13 +21,10 @@ namespace Camera { uint32_t bytes_per_pixel{0}; uint64_t sequence_number{0}; // monotonic per-stream counter - // Values that vary per frame, not per exposure - double mjd_start{0.0}; // EXPMJDST - std::string acq_time; // ACQTIME, UTC - double exposure_time_sec{0.0}; // EXPTIME - uint64_t n_reads{0}; // NREADS + // FITS keys that vary per frame (e.g. per read within one exposure); rebuilt each frame + std::shared_ptr frame_keys; - // Resolved static keys, built once per exposure and shared across its frames + // FITS keys resolved once per exposure and shared across its frames std::shared_ptr header_set; }; From 7daea63674f57d695f1b54d3a41a9e9588eb7b9a Mon Sep 17 00:00:00 2001 From: Mike Langmayr <1809691+mikelangmayr@users.noreply.github.com> Date: Thu, 20 Aug 2026 11:43:38 -0700 Subject: [PATCH 3/8] Bump hispec_tracking_camera submodule to merged header-dictionary PR (#10) --- 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 3f5ef1d..3f5901d 160000 --- a/camerad/Instruments/hispec_tracking_camera +++ b/camerad/Instruments/hispec_tracking_camera @@ -1 +1 @@ -Subproject commit 3f5ef1dfaec051f9c1725c83e0dc4631db26eb22 +Subproject commit 3f5901d55bfa5dd2b844d70c5d2411ad8d5e69bc From c4eebd71a2caabe5ed6f8aad149a7637184491bc Mon Sep 17 00:00:00 2001 From: Mike Langmayr <1809691+mikelangmayr@users.noreply.github.com> Date: Thu, 20 Aug 2026 16:03:50 -0700 Subject: [PATCH 4/8] lift and shift custom header command from v-1 --- camerad/camera_interface.cpp | 35 +++++++++++++++++++++++++++++++++++ camerad/camera_interface.h | 2 +- camerad/camera_server.cpp | 4 ++++ 3 files changed, 40 insertions(+), 1 deletion(-) diff --git a/camerad/camera_interface.cpp b/camerad/camera_interface.cpp index ddc60ac..41e5bbd 100644 --- a/camerad/camera_interface.cpp +++ b/camerad/camera_interface.cpp @@ -25,4 +25,39 @@ namespace Camera { this->disconnect_controller("", retstring); } /***** Camera::Interface::disconnect_controller *****************************/ + + + /***** Camera::Interface::key *************************************************/ + /** + * @brief add/list a custom FITS keyword written into every exposure + * @param[in] args "list", or "KEYWORD=VALUE//COMMENT" (comment optional) + * @param[out] retstring usage string on help/error + * @return NO_ERROR | ERROR | HELP + * + * KEYWORD=. deletes that keyword (see Common::FitsKeys::addkey). + * + */ + long Interface::key(std::string args, std::string &retstring) { + const std::string function("Camera::Interface::key"); + + if (args.empty() || args == "?" || args == "help") { + retstring = "key KEYWORD=VALUE//COMMENT | key list"; + return HELP; + } + + if (args == "list") { + logwrite(function, "systemkeys:"); + this->camera_info.systemkeys.listkeys(); + logwrite(function, "userkeys:"); + this->camera_info.userkeys.listkeys(); + return NO_ERROR; + } + + long error = this->camera_info.userkeys.addkey(args); + if (error != NO_ERROR) { + logwrite(function, "ERROR bad syntax: expected KEYWORD=VALUE//COMMENT"); + } + return error; + } + /***** Camera::Interface::key *************************************************/ } diff --git a/camerad/camera_interface.h b/camerad/camera_interface.h index 629d1aa..e019fd7 100644 --- a/camerad/camera_interface.h +++ b/camerad/camera_interface.h @@ -68,7 +68,6 @@ namespace Camera { output->write(data, size, meta); } } -// Common::FitsKeys systemkeys; move to Camera::Information? // These functions are shared by all interfaces with common implementations, // and are implemented in camera_interface.cpp @@ -76,6 +75,7 @@ namespace Camera { void set_server(Camera::Server* s); void func_shared(); void disconnect_controller(); + long key(std::string args, std::string &retstring); bool is_exposuremode_set() { return ( this->exposuremode && !this->exposuremode->get_type().empty() ); } void set_abortstate() { this->abortstate.store(true, std::memory_order_seq_cst); } diff --git a/camerad/camera_server.cpp b/camerad/camera_server.cpp index 054229b..e5575f4 100644 --- a/camerad/camera_server.cpp +++ b/camerad/camera_server.cpp @@ -292,6 +292,10 @@ namespace Camera { if ( cmd == CAMERAD_TEST ) { ret = interface->test(args, retstring); } + else + if ( cmd == CAMERAD_KEY ) { + ret = interface->key(args, retstring); + } /** * controller-specific commands From f096f2a3797c3bc7fda499b6abe9b693f4a38f7b Mon Sep 17 00:00:00 2001 From: Mike Langmayr <1809691+mikelangmayr@users.noreply.github.com> Date: Fri, 21 Aug 2026 16:03:22 -0700 Subject: [PATCH 5/8] Add datacube command (M4), lifted from v1, to toggle FitsWriter's cube mode --- camerad/camera_information.h | 2 ++ camerad/camera_interface.cpp | 39 ++++++++++++++++++++++++++++++++++++ camerad/camera_interface.h | 1 + camerad/camera_server.cpp | 4 ++++ common/camerad_commands.h | 2 ++ utils/cadence_gate.cpp | 4 ++++ utils/cadence_gate.h | 2 ++ utils/fits_writer.cpp | 16 +++++++++++++++ utils/fits_writer.h | 3 +++ utils/frame_output.h | 4 ++++ 10 files changed, 77 insertions(+) diff --git a/camerad/camera_information.h b/camerad/camera_information.h index 064ed2c..3f8e7f5 100644 --- a/camerad/camera_information.h +++ b/camerad/camera_information.h @@ -53,6 +53,8 @@ namespace Camera { Common::FitsKeys userkeys; //!< FITS keys for command line use Common::FitsKeys systemkeys; //!< FITS keys for system use + bool is_datacube{false}; //!< true: reads written as extensions of one multi-extension FITS file + std::vector naxes; //!< axis lengths element 0=cols, 1=rows, 2=cubedepth std::array binning; std::array region_of_interest; diff --git a/camerad/camera_interface.cpp b/camerad/camera_interface.cpp index 41e5bbd..f8dfdb3 100644 --- a/camerad/camera_interface.cpp +++ b/camerad/camera_interface.cpp @@ -1,5 +1,8 @@ #include "camera_interface.h" +#include +#include + namespace Camera { // this is the code for shared functions common to all implementations @@ -60,4 +63,40 @@ namespace Camera { return error; } /***** Camera::Interface::key *************************************************/ + + + /***** Camera::Interface::datacube *********************************************/ + /** + * @brief set/report whether reads are written as one multi-extension FITS file + * @param[in] args "true", "false" (case-insensitive), or empty to report state + * @param[out] retstring current state ("true"/"false"), or usage on error + * @return NO_ERROR | ERROR | HELP + * + */ + long Interface::datacube(std::string args, std::string &retstring) { + const std::string function("Camera::Interface::datacube"); + + if (args == "?" || args == "help") { + retstring = "datacube [ true | false ]"; + return HELP; + } + + if (!args.empty()) { + std::transform(args.begin(), args.end(), args.begin(), + [](unsigned char c) { return std::tolower(c); }); + if (args != "true" && args != "false") { + retstring = "datacube [ true | false ]"; + logwrite(function, "ERROR expected true or false, got " + args); + return ERROR; + } + this->camera_info.is_datacube = (args == "true"); + for (auto &output : this->frame_outputs) { + output->set_option("datacube", args); + } + } + + retstring = (this->camera_info.is_datacube ? "true" : "false"); + return NO_ERROR; + } + /***** Camera::Interface::datacube *********************************************/ } diff --git a/camerad/camera_interface.h b/camerad/camera_interface.h index e019fd7..4f129d1 100644 --- a/camerad/camera_interface.h +++ b/camerad/camera_interface.h @@ -76,6 +76,7 @@ namespace Camera { void func_shared(); void disconnect_controller(); long key(std::string args, std::string &retstring); + long datacube(std::string args, std::string &retstring); bool is_exposuremode_set() { return ( this->exposuremode && !this->exposuremode->get_type().empty() ); } void set_abortstate() { this->abortstate.store(true, std::memory_order_seq_cst); } diff --git a/camerad/camera_server.cpp b/camerad/camera_server.cpp index e5575f4..be6aca9 100644 --- a/camerad/camera_server.cpp +++ b/camerad/camera_server.cpp @@ -296,6 +296,10 @@ namespace Camera { if ( cmd == CAMERAD_KEY ) { ret = interface->key(args, retstring); } + else + if ( cmd == CAMERAD_DATACUBE ) { + ret = interface->datacube(args, retstring); + } /** * controller-specific commands diff --git a/common/camerad_commands.h b/common/camerad_commands.h index 8dacf64..fdec96e 100644 --- a/common/camerad_commands.h +++ b/common/camerad_commands.h @@ -17,6 +17,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_DATACUBE("datacube"); const std::string CAMERAD_ECHO("echo"); const std::string CAMERAD_EXIT("exit"); const std::string CAMERAD_EXPOSE("expose"); @@ -64,6 +65,7 @@ const std::vector CAMERAD_SYNTAX = { CAMERAD_BUFFER+" ? | | [ | ]", CAMERAD_CLOSE, CAMERAD_CONFIG, + CAMERAD_DATACUBE+" [ true | false ]", CAMERAD_ECHO, CAMERAD_EXPOSE, CAMERAD_EXPOSUREMODE, diff --git a/utils/cadence_gate.cpp b/utils/cadence_gate.cpp index b8083e3..db12a1d 100644 --- a/utils/cadence_gate.cpp +++ b/utils/cadence_gate.cpp @@ -33,4 +33,8 @@ namespace Camera { inner_->close(); } + bool CadenceGate::set_option(const std::string &key, const std::string &value) { + return inner_->set_option(key, value); + } + } diff --git a/utils/cadence_gate.h b/utils/cadence_gate.h index 5ce457a..c260f12 100644 --- a/utils/cadence_gate.h +++ b/utils/cadence_gate.h @@ -14,6 +14,7 @@ #include #include #include +#include namespace Camera { @@ -29,6 +30,7 @@ namespace Camera { long open() override; long write(const char* data, size_t size, const FrameMetadata& meta) override; void close() override; + bool set_option(const std::string &key, const std::string &value) override; uint64_t frames_skipped() const { return n_skipped_.load(); } diff --git a/utils/fits_writer.cpp b/utils/fits_writer.cpp index edb9bae..ca8bb0e 100644 --- a/utils/fits_writer.cpp +++ b/utils/fits_writer.cpp @@ -11,6 +11,8 @@ #include "utilities.h" #include +#include +#include #include #include #include @@ -140,6 +142,20 @@ namespace Camera { " dropped_shutdown=" + std::to_string(n_dropped_shutdown_.load())); } + bool FitsWriter::set_option(const std::string &key, const std::string &value) { + if (key != "datacube") return false; + + std::string lower = value; + std::transform(lower.begin(), lower.end(), lower.begin(), + [](unsigned char c) { return std::tolower(c); }); + if (lower == "true") cube_enabled_.store(true); + else if (lower == "false") cube_enabled_.store(false); + else return false; + + logwrite("Camera::FitsWriter::set_option", "datacube=" + lower); + return true; + } + FitsWriter::Stats FitsWriter::stats() const { Stats s; s.frames_received = n_received_.load(); diff --git a/utils/fits_writer.h b/utils/fits_writer.h index 74d5cc3..65252ec 100644 --- a/utils/fits_writer.h +++ b/utils/fits_writer.h @@ -42,6 +42,7 @@ namespace Camera { long open() override; long write(const char* data, size_t size, const FrameMetadata& meta) override; void close() override; + bool set_option(const std::string &key, const std::string &value) override; struct Stats { uint64_t frames_received{0}; @@ -76,6 +77,8 @@ namespace Camera { std::atomic stop_{false}; std::atomic started_{false}; + // Set via set_option("datacube", "true"/"false"); read by write_fits_file() + std::atomic cube_enabled_{false}; std::thread worker_; // Set in close() before stop_, so worker can read race-free std::chrono::steady_clock::time_point stop_time_; diff --git a/utils/frame_output.h b/utils/frame_output.h index f067ab9..289646a 100644 --- a/utils/frame_output.h +++ b/utils/frame_output.h @@ -8,6 +8,7 @@ #include #include #include +#include namespace Common { class FitsKeys; } @@ -34,6 +35,9 @@ namespace Camera { virtual long open() = 0; virtual long write(const char* data, size_t size, const FrameMetadata& meta) = 0; virtual void close() = 0; + + // Runtime option toggle (e.g. "datacube"/"true"); false if key is unrecognized + virtual bool set_option(const std::string &key, const std::string &value) { return false; } }; } From 8034b046d4a409210b52f506caba17479c37f5bf Mon Sep 17 00:00:00 2001 From: Mike Langmayr <1809691+mikelangmayr@users.noreply.github.com> Date: Mon, 24 Aug 2026 14:40:43 -0700 Subject: [PATCH 6/8] Add multi-extension datacube writing: one image extension per read --- utils/fits_writer.cpp | 84 +++++++++++++++++++++++++++++++++++++++---- utils/fits_writer.h | 10 ++++++ 2 files changed, 87 insertions(+), 7 deletions(-) diff --git a/utils/fits_writer.cpp b/utils/fits_writer.cpp index ca8bb0e..c88717a 100644 --- a/utils/fits_writer.cpp +++ b/utils/fits_writer.cpp @@ -22,17 +22,18 @@ namespace Camera { namespace { - void add_key_from_fits_keys(CCfits::PHDU &phdu, const Common::FitsKeys::user_key_t &entry) { + // HDU: common base of PHDU and ExtHDU, so this works for both + void add_key_from_fits_keys(CCfits::HDU &hdu, const Common::FitsKeys::user_key_t &entry) { const std::string function("Camera::FitsWriter::add_key_from_fits_keys"); try { if (entry.keytype == "DOUBLE" || entry.keytype == "FLOAT") { - phdu.addKey(entry.keyword, std::stod(entry.keyvalue), entry.keycomment); + hdu.addKey(entry.keyword, std::stod(entry.keyvalue), entry.keycomment); } else if (entry.keytype == "INT" || entry.keytype == "LONG") { - phdu.addKey(entry.keyword, std::stol(entry.keyvalue), entry.keycomment); + hdu.addKey(entry.keyword, std::stol(entry.keyvalue), entry.keycomment); } else if (entry.keytype == "BOOL") { - phdu.addKey(entry.keyword, entry.keyvalue == "T", entry.keycomment); + hdu.addKey(entry.keyword, entry.keyvalue == "T", entry.keycomment); } else { - phdu.addKey(entry.keyword, entry.keyvalue, entry.keycomment); + hdu.addKey(entry.keyword, entry.keyvalue, entry.keycomment); } } catch (const std::exception &e) { @@ -40,9 +41,9 @@ namespace Camera { } } - void add_keys_from(CCfits::PHDU &phdu, const Common::FitsKeys *keys) { + void add_keys_from(CCfits::HDU &hdu, const Common::FitsKeys *keys) { if (!keys) return; - for (const auto &entry : keys->keydb) add_key_from_fits_keys(phdu, entry.second); + for (const auto &entry : keys->keydb) add_key_from_fits_keys(hdu, entry.second); } } @@ -132,6 +133,7 @@ namespace Camera { cv_.notify_all(); if (worker_.joinable()) worker_.join(); + close_cube(); // worker has exited, safe to touch its state here started_.store(false); const std::string function("Camera::FitsWriter::close"); @@ -199,6 +201,9 @@ namespace Camera { } long FitsWriter::write_fits_file(const QueuedFrame &frame) { + if (cube_enabled_.load()) return write_cube_frame(frame); + if (cube_fits_) close_cube(); // datacube was just turned off mid-cube; finalize it + const std::string function("Camera::FitsWriter::write_fits_file"); const auto &meta = frame.meta; const size_t npixels = static_cast(meta.width) * meta.height; @@ -246,6 +251,71 @@ namespace Camera { return NO_ERROR; } + long FitsWriter::write_cube_frame(const QueuedFrame &frame) { + const std::string function("Camera::FitsWriter::write_cube_frame"); + const auto &meta = frame.meta; + const size_t npixels = static_cast(meta.width) * meta.height; + const int bitpix = (meta.bytes_per_pixel == 2) ? USHORT_IMG : ULONG_IMG; + + try { + if (!cube_fits_) { + const std::string filename = make_filename(meta.frame_number); + long axes[2] = {0, 0}; // NAXIS=0: header-only primary, matches v1's cube primary + cube_fits_ = std::make_unique(filename, bitpix, 0, axes); + add_keys_from(cube_fits_->pHDU(), meta.header_set.get()); + cube_extension_count_ = 0; + logwrite(function, "opened cube " + filename); + } + + std::vector ext_axes = { static_cast(meta.width), + static_cast(meta.height) }; + const std::string extname = std::to_string(cube_extension_count_ + 1); + auto *ext = cube_fits_->addImage(extname, bitpix, ext_axes); + + if (bitpix == USHORT_IMG) { + ext->addKey("BZERO", 32768, "offset for signed short int"); + ext->addKey("BSCALE", 1, "scaling factor"); + } + ext->addKey("FRAMENO", static_cast(meta.frame_number), "Frame number"); + ext->addKey("TIMESTMP", static_cast(meta.timestamp), + "Archon timestamp (0.01 us units)"); + add_keys_from(*ext, meta.frame_keys.get()); + ext->addKey("WRTTIME", get_timestamp(), "FITS extension 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); + ext->write(first_pixel, npixels, data); + } else { + const auto *src = reinterpret_cast(frame.data.data()); + std::valarray data(src, npixels); + ext->write(first_pixel, npixels, data); + } + cube_fits_->flush(); + + if (++cube_extension_count_ >= cfg_.max_extensions) close_cube(); + } + catch (const CCfits::FitsException &e) { + logwrite(function, "ERROR FITS exception writing cube extension: " + e.message()); + return ERROR; + } + catch (const std::exception &e) { + logwrite(function, "ERROR exception writing cube extension: " + std::string(e.what())); + return ERROR; + } + + return NO_ERROR; + } + + void FitsWriter::close_cube() { + if (!cube_fits_) return; + logwrite("Camera::FitsWriter::close_cube", + "closed cube with " + std::to_string(cube_extension_count_) + " extensions"); + cube_fits_.reset(); + cube_extension_count_ = 0; + } + std::string FitsWriter::resolve_output_dir() { if (!cfg_.autodir) return cfg_.output_dir; diff --git a/utils/fits_writer.h b/utils/fits_writer.h index 65252ec..0b1277e 100644 --- a/utils/fits_writer.h +++ b/utils/fits_writer.h @@ -16,11 +16,14 @@ #include #include #include +#include #include #include #include #include +namespace CCfits { class FITS; } + namespace Camera { struct FitsWriterConfig { @@ -29,6 +32,7 @@ namespace Camera { size_t queue_size{32}; uint32_t drain_timeout_ms{5000}; bool autodir{false}; // write into a YYYYMMDD subdir of output_dir + uint32_t max_extensions{100}; // datacube mode: reads per file before rollover }; class FitsWriter : public FrameOutput { @@ -61,6 +65,8 @@ namespace Camera { void worker_loop(); long write_fits_file(const QueuedFrame &frame); + long write_cube_frame(const QueuedFrame &frame); + void close_cube(); 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). @@ -71,6 +77,10 @@ namespace Camera { // Cache of the last autodir path created; touched only on the worker thread std::string cur_date_dir_; + // Datacube state — worker-thread only, no locking needed + std::unique_ptr cube_fits_; + uint32_t cube_extension_count_{0}; + std::deque queue_; mutable std::mutex mtx_; std::condition_variable cv_; From 31a63fdafe93bb5a9c8e5a7617cb2d42601dd3ab Mon Sep 17 00:00:00 2001 From: Mike Langmayr <1809691+mikelangmayr@users.noreply.github.com> Date: Mon, 24 Aug 2026 15:36:24 -0700 Subject: [PATCH 7/8] Finalize datacube files when the exposure command ends, not after N extensions; drop unused CadenceGate frame throttling --- camerad/archon_interface.cpp | 2 ++ camerad/camera_interface.h | 7 +++++ utils/CMakeLists.txt | 7 ----- utils/cadence_gate.cpp | 40 ---------------------------- utils/cadence_gate.h | 48 ---------------------------------- utils/fits_writer.cpp | 20 ++++++++++++-- utils/fits_writer.h | 3 ++- utils/frame_output.h | 3 +++ utils/frame_output_factory.cpp | 9 +------ utils/frame_output_factory.h | 1 - 10 files changed, 33 insertions(+), 107 deletions(-) delete mode 100644 utils/cadence_gate.cpp delete mode 100644 utils/cadence_gate.h diff --git a/camerad/archon_interface.cpp b/camerad/archon_interface.cpp index c48f83f..5d39957 100644 --- a/camerad/archon_interface.cpp +++ b/camerad/archon_interface.cpp @@ -446,6 +446,8 @@ namespace Camera { do_expose(); } + this->end_exposure(); + return NO_ERROR; } /***** Camera::ArchonInterface::expose **************************************/ diff --git a/camerad/camera_interface.h b/camerad/camera_interface.h index 4f129d1..d08074d 100644 --- a/camerad/camera_interface.h +++ b/camerad/camera_interface.h @@ -69,6 +69,13 @@ namespace Camera { } } + // Tell every FrameOutput the current exposure command has finished + void end_exposure() { + for (auto &output : this->frame_outputs) { + output->end_exposure(); + } + } + // These functions are shared by all interfaces with common implementations, // and are implemented in camera_interface.cpp // diff --git a/utils/CMakeLists.txt b/utils/CMakeLists.txt index 7212759..61ff1e0 100644 --- a/utils/CMakeLists.txt +++ b/utils/CMakeLists.txt @@ -41,12 +41,6 @@ target_link_libraries(fits_writer ${FITSWRITER_CFITS_LIB} ) -add_library(cadence_gate STATIC - ${PROJECT_UTILS_DIR}/cadence_gate.cpp -) -target_include_directories(cadence_gate PRIVATE ${PROJECT_BASE_DIR}/common ${PROJECT_BASE_DIR}/utils) -target_link_libraries(cadence_gate nlohmann_json::nlohmann_json) - add_library(frame_output_factory STATIC ${PROJECT_UTILS_DIR}/frame_output_factory.cpp ) @@ -55,7 +49,6 @@ target_link_libraries(frame_output_factory nlohmann_json::nlohmann_json shared_memory_writer fits_writer - cadence_gate ) add_library(md5 STATIC diff --git a/utils/cadence_gate.cpp b/utils/cadence_gate.cpp deleted file mode 100644 index db12a1d..0000000 --- a/utils/cadence_gate.cpp +++ /dev/null @@ -1,40 +0,0 @@ -/** - * @file cadence_gate.cpp - * @brief FrameOutput decorator that rate-limits frames to a wrapped output - */ - -#include "cadence_gate.h" -#include "common.h" - -#include - -namespace Camera { - - CadenceGate::CadenceGate(std::unique_ptr inner, uint32_t write_interval_ms) - : inner_(std::move(inner)), - interval_(std::chrono::milliseconds(write_interval_ms)) { - } - - long CadenceGate::open() { - return inner_->open(); - } - - long CadenceGate::write(const char* data, size_t size, const FrameMetadata& meta) { - const auto now = std::chrono::steady_clock::now(); - if (now - last_forwarded_ < interval_) { - n_skipped_.fetch_add(1, std::memory_order_relaxed); - return NO_ERROR; - } - last_forwarded_ = now; - return inner_->write(data, size, meta); - } - - void CadenceGate::close() { - inner_->close(); - } - - bool CadenceGate::set_option(const std::string &key, const std::string &value) { - return inner_->set_option(key, value); - } - -} diff --git a/utils/cadence_gate.h b/utils/cadence_gate.h deleted file mode 100644 index c260f12..0000000 --- a/utils/cadence_gate.h +++ /dev/null @@ -1,48 +0,0 @@ -/** - * @file cadence_gate.h - * @brief FrameOutput decorator that rate-limits frames to a wrapped output - * - * Forwards a frame only when at least write_interval_ms has elapsed since the - * last forwarded frame; otherwise the frame is dropped. Keeps the cadence - * policy out of the concrete writers, which then just do their I/O. - */ -#pragma once - -#include "frame_output.h" - -#include -#include -#include -#include -#include - -namespace Camera { - - /// FrameOutput that thins frames by minimum interval before delegating - class CadenceGate : public FrameOutput { - public: - CadenceGate(std::unique_ptr inner, uint32_t write_interval_ms); - ~CadenceGate() override = default; - - CadenceGate(const CadenceGate&) = delete; - CadenceGate& operator=(const CadenceGate&) = delete; - - long open() override; - long write(const char* data, size_t size, const FrameMetadata& meta) override; - void close() override; - bool set_option(const std::string &key, const std::string &value) override; - - uint64_t frames_skipped() const { return n_skipped_.load(); } - - private: - std::unique_ptr inner_; - std::chrono::milliseconds interval_; - - // Only touched on the producer thread - std::chrono::steady_clock::time_point last_forwarded_{ - std::chrono::steady_clock::time_point::min()}; - - std::atomic n_skipped_{0}; - }; - -} diff --git a/utils/fits_writer.cpp b/utils/fits_writer.cpp index c88717a..1963542 100644 --- a/utils/fits_writer.cpp +++ b/utils/fits_writer.cpp @@ -144,6 +144,18 @@ namespace Camera { " dropped_shutdown=" + std::to_string(n_dropped_shutdown_.load())); } + void FitsWriter::end_exposure() { + if (!started_.load()) return; + // Same queue as real frames, so it's processed after this exposure's frames are written + QueuedFrame end_marker; + end_marker.end_of_exposure = true; + { + std::lock_guard lock(mtx_); + queue_.push_back(std::move(end_marker)); + } + cv_.notify_one(); + } + bool FitsWriter::set_option(const std::string &key, const std::string &value) { if (key != "datacube") return false; @@ -192,6 +204,11 @@ namespace Camera { queue_.pop_front(); } + if (frame.end_of_exposure) { + close_cube(); + continue; + } + if (write_fits_file(frame) == NO_ERROR) { n_written_.fetch_add(1, std::memory_order_relaxed); } else { @@ -293,8 +310,7 @@ namespace Camera { ext->write(first_pixel, npixels, data); } cube_fits_->flush(); - - if (++cube_extension_count_ >= cfg_.max_extensions) close_cube(); + ++cube_extension_count_; } catch (const CCfits::FitsException &e) { logwrite(function, "ERROR FITS exception writing cube extension: " + e.message()); diff --git a/utils/fits_writer.h b/utils/fits_writer.h index 0b1277e..6fa2eda 100644 --- a/utils/fits_writer.h +++ b/utils/fits_writer.h @@ -32,7 +32,6 @@ namespace Camera { size_t queue_size{32}; uint32_t drain_timeout_ms{5000}; bool autodir{false}; // write into a YYYYMMDD subdir of output_dir - uint32_t max_extensions{100}; // datacube mode: reads per file before rollover }; class FitsWriter : public FrameOutput { @@ -47,6 +46,7 @@ namespace Camera { long write(const char* data, size_t size, const FrameMetadata& meta) override; void close() override; bool set_option(const std::string &key, const std::string &value) override; + void end_exposure() override; struct Stats { uint64_t frames_received{0}; @@ -61,6 +61,7 @@ namespace Camera { struct QueuedFrame { FrameMetadata meta; std::vector data; + bool end_of_exposure{false}; // sentinel: finalize any open cube, no frame data }; void worker_loop(); diff --git a/utils/frame_output.h b/utils/frame_output.h index 289646a..84627d0 100644 --- a/utils/frame_output.h +++ b/utils/frame_output.h @@ -38,6 +38,9 @@ namespace Camera { // Runtime option toggle (e.g. "datacube"/"true"); false if key is unrecognized virtual bool set_option(const std::string &key, const std::string &value) { return false; } + + // Called when a whole exposure command finishes, so a multi-frame output (e.g. a datacube) can finalize its file + virtual void end_exposure() {} }; } diff --git a/utils/frame_output_factory.cpp b/utils/frame_output_factory.cpp index d684da7..35f9270 100644 --- a/utils/frame_output_factory.cpp +++ b/utils/frame_output_factory.cpp @@ -5,7 +5,6 @@ #include "frame_output_factory.h" #include "fits_writer.h" -#include "cadence_gate.h" #include "shared_memory_writer.h" #include "common.h" @@ -33,7 +32,6 @@ namespace Camera { 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)); else if (key == "FITS_DRAIN_TIMEOUT_MS") out.fits.drain_timeout_ms = static_cast(std::stoul(val)); } @@ -72,13 +70,8 @@ namespace Camera { 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)); - std::unique_ptr output = std::move(fits); - if (cfg.fits_write_interval_ms > 0) { - output = std::make_unique(std::move(output), cfg.fits_write_interval_ms); - } - outputs.push_back(std::move(output)); + outputs.push_back(std::move(fits)); } else { logwrite(function, "WARNING FITS output failed to open; skipped"); diff --git a/utils/frame_output_factory.h b/utils/frame_output_factory.h index 1d713c7..b1443e4 100644 --- a/utils/frame_output_factory.h +++ b/utils/frame_output_factory.h @@ -27,7 +27,6 @@ namespace Camera { uint32_t shm_num_frames{4}; bool fits_enabled{false}; - uint32_t fits_write_interval_ms{0}; FitsWriterConfig fits; }; From 9fb2fa8214344c4d7c8d6b270159d3249aff59e6 Mon Sep 17 00:00:00 2001 From: Mike Langmayr <1809691+mikelangmayr@users.noreply.github.com> Date: Mon, 24 Aug 2026 18:21:28 -0700 Subject: [PATCH 8/8] chore: update hispec_tracking_camera submodule to latest main --- 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 3f5901d..320528d 160000 --- a/camerad/Instruments/hispec_tracking_camera +++ b/camerad/Instruments/hispec_tracking_camera @@ -1 +1 @@ -Subproject commit 3f5901d55bfa5dd2b844d70c5d2411ad8d5e69bc +Subproject commit 320528d7aac4befa6263363e5e172684c75421e6