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 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_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 ddc60ac..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 @@ -25,4 +28,75 @@ 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 *************************************************/ + + + /***** 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 629d1aa..d08074d 100644 --- a/camerad/camera_interface.h +++ b/camerad/camera_interface.h @@ -68,7 +68,13 @@ namespace Camera { output->write(data, size, meta); } } -// Common::FitsKeys systemkeys; move to Camera::Information? + + // 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 @@ -76,6 +82,8 @@ namespace Camera { void set_server(Camera::Server* s); 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 054229b..be6aca9 100644 --- a/camerad/camera_server.cpp +++ b/camerad/camera_server.cpp @@ -292,6 +292,14 @@ namespace Camera { if ( cmd == CAMERAD_TEST ) { ret = interface->test(args, retstring); } + else + 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/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 b8083e3..0000000 --- a/utils/cadence_gate.cpp +++ /dev/null @@ -1,36 +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(); - } - -} diff --git a/utils/cadence_gate.h b/utils/cadence_gate.h deleted file mode 100644 index 5ce457a..0000000 --- a/utils/cadence_gate.h +++ /dev/null @@ -1,46 +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 - -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; - - 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 e2d3cbc..1963542 100644 --- a/utils/fits_writer.cpp +++ b/utils/fits_writer.cpp @@ -11,6 +11,8 @@ #include "utilities.h" #include +#include +#include #include #include #include @@ -19,6 +21,32 @@ namespace Camera { + namespace { + // 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") { + hdu.addKey(entry.keyword, std::stod(entry.keyvalue), entry.keycomment); + } else if (entry.keytype == "INT" || entry.keytype == "LONG") { + hdu.addKey(entry.keyword, std::stol(entry.keyvalue), entry.keycomment); + } else if (entry.keytype == "BOOL") { + hdu.addKey(entry.keyword, entry.keyvalue == "T", entry.keycomment); + } else { + hdu.addKey(entry.keyword, entry.keyvalue, entry.keycomment); + } + } + catch (const std::exception &e) { + logwrite(function, "ERROR formatting key " + entry.keyword + ": " + e.what()); + } + } + + 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(hdu, entry.second); + } + } + FitsWriter::FitsWriter(FitsWriterConfig cfg) : cfg_(std::move(cfg)) { } @@ -105,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"); @@ -115,6 +144,32 @@ 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; + + 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(); @@ -149,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 { @@ -158,6 +218,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; @@ -179,6 +242,9 @@ namespace Camera { "Archon timestamp (0.01 us units)"); phdu.addKey("DATE", get_timestamp(), "FITS file write time"); + 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) { const auto *src = reinterpret_cast(frame.data.data()); @@ -202,6 +268,70 @@ 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(); + ++cube_extension_count_; + } + 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 74d5cc3..6fa2eda 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 { @@ -42,6 +45,8 @@ 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; + void end_exposure() override; struct Stats { uint64_t frames_received{0}; @@ -56,10 +61,13 @@ 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(); 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). @@ -70,12 +78,18 @@ 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_; 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 bd3a65e..84627d0 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,13 @@ 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 + + // FITS keys that vary per frame (e.g. per read within one exposure); rebuilt each frame + std::shared_ptr frame_keys; + + // FITS keys resolved once per exposure and shared across its frames + std::shared_ptr header_set; }; class FrameOutput { @@ -25,6 +35,12 @@ 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; } + + // 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; };