diff --git a/.github/workflows/emulator-integration.yml b/.github/workflows/emulator-integration.yml index e52fe11..0d9394e 100644 --- a/.github/workflows/emulator-integration.yml +++ b/.github/workflows/emulator-integration.yml @@ -7,7 +7,7 @@ on: branches: [ "main" ] jobs: - hispec-shm-emulator-test: + frame-outputs-emulator-test: runs-on: ubuntu-latest timeout-minutes: 10 @@ -52,13 +52,13 @@ jobs: -DENABLE_SHM_OUTPUT=ON -DImageStreamIO_DIR=/usr/local/lib/cmake .. make camerad emulator socksend shm_reader -j$(nproc) - - name: HISPEC tracking camera SHM test + - name: SHM + FITS frame outputs test run: | - mkdir -p /tmp/ci_milk_shm + mkdir -p /tmp/ci_milk_shm /tmp/ci_fits_test - bin/emulator Config/hispec_shm_test/hispec_shm_test.cfg -i generic & + bin/emulator config/frame_outputs_test/frame_outputs_test.cfg -i generic & sleep 2 - bin/camerad --foreground --config Config/hispec_shm_test/hispec_shm_test.cfg & + bin/camerad --foreground --config config/frame_outputs_test/frame_outputs_test.cfg & sleep 3 send() { bin/socksend -p 3131 -t 60 "$1"; } @@ -70,10 +70,12 @@ jobs: echo "expose -> $resp" echo "$resp" | grep -q "DONE" || exit 1 - # DONE only means camerad accepted the command, not that SharedMemoryWriter - # succeeded -- shm_reader's exit code is the real pass/fail signal + # DONE only means camerad accepted the command, not that the writers + # succeeded -- shm_reader's exit code and the FITS file's existence + # are the real pass/fail signal sleep 1 - bin/shm_reader ci_hispec_shm /tmp/ci_milk_shm + bin/shm_reader ci_frame_outputs_shm /tmp/ci_milk_shm + ls /tmp/ci_fits_test/ci_frame_outputs_*.fits pkill -f 'bin/camerad' || true pkill -f 'bin/emulator' || true @@ -114,9 +116,9 @@ jobs: - name: CryoScope synthetic test run: | - bin/emulator Config/cryoscope/cryoscope.cfg -i generic & + bin/emulator config/cryoscope/cryoscope.cfg -i generic & sleep 2 - bin/camerad --foreground --config Config/cryoscope/cryoscope.cfg & + bin/camerad --foreground --config config/cryoscope/cryoscope.cfg & sleep 3 send() { bin/socksend -p 3031 -t 60 "$1"; } @@ -135,8 +137,8 @@ jobs: - name: CryoScope FITS playback test run: | - cp Config/cryoscope/cryoscope.cfg /tmp/cryoscope_fits_test.cfg - echo "EMULATOR_DATADIR=Config/cryoscope" >> /tmp/cryoscope_fits_test.cfg + cp config/cryoscope/cryoscope.cfg /tmp/cryoscope_fits_test.cfg + echo "EMULATOR_DATADIR=config/cryoscope" >> /tmp/cryoscope_fits_test.cfg bin/emulator /tmp/cryoscope_fits_test.cfg -i generic & sleep 2 diff --git a/Config/cameraserver.cfg b/Config/cameraserver.cfg deleted file mode 100644 index a2839ef..0000000 --- a/Config/cameraserver.cfg +++ /dev/null @@ -1,16 +0,0 @@ -# configuration file for camera-server -# -ARCHON_IP=192.168.1.2 -ARCHON_PORT=4242 -DEFAULT_FIRMWARE=/home/user/Software/acf/kpf-getimage-slot5.acf -EXPOSE_PARAM=Expose # Archon parameter which triggers exposure -#HEATER_TARGET_MIN=-150 # heater target lower limit (C) -#HEATER_TARGET_MAX=50 # heater target upper limit (C) -IMDIR=/tmp # base directory to save images -BASENAME=image # base image filename -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/user/Logs/ # fully qualified path to save log files diff --git a/Config/hispec_shm_test/hispec_shm_test.cfg b/Config/hispec_shm_test/hispec_shm_test.cfg deleted file mode 100644 index 889cb24..0000000 --- a/Config/hispec_shm_test/hispec_shm_test.cfg +++ /dev/null @@ -1,30 +0,0 @@ -# hispec_tracking_camera configuration for camerad + emulator SHM testing (CI) - -DAEMON=no -IMDIR=/tmp -LOGPATH=/tmp -BASENAME=hispecshmtest -DIRMODE=0077 -TM_ZONE_LOG=local -TM_ZONE=UTC -TZ_ENV=PST8PDT,M3.2.0/2,M11.1.0/2 - -NBPORT=3130 -BLKPORT=3131 -EMULATOR_PORT=3132 -EMULATOR_SYSTEM=Config/hispec_shm_test/hispec_shm_test.system - -ASYNCGROUP=none - -ARCHON_IP=localhost -ARCHON_PORT=3132 -DEFAULT_FIRMWARE=camerad/Instruments/hispec_tracking_camera/config/hispecatc.acf -EXPOSE_PARAM=Expose -ABORT_PARAM=abort -READOUT_TIME=5000 -WRITE_TAPINFO_TO_FITS=no - -SHM_ENABLED=yes -SHM_SEGMENT_NAME=ci_hispec_shm -SHM_RING_BUFFER_SIZE=2 -SHM_DIR=/tmp/ci_milk_shm diff --git a/Config/hispecatc.cfg b/Config/hispecatc.cfg deleted file mode 100644 index 7d42914..0000000 --- a/Config/hispecatc.cfg +++ /dev/null @@ -1,34 +0,0 @@ -# configuration file for camera-server -# -ARCHON_IP=10.0.0.2 -ARCHON_PORT=4242 -DEFAULT_FIRMWARE=/home/hsdev/camera-dev/elijahab/wdlfiles/src/hispecatc/hispecatc.acf -EXPOSE_PARAM=Expose # Archon parameter which triggers exposure -ABORT_PARAM=abort # Archon parameter to trigger an abort -READOUT_TIME=50000 # Timeout waiting for new frame (ms) -WRITE_TAPINFO_TO_FITS=yes # Tapinfo (gain, offset) be written to FITS headers {yes|no} - -IMDIR=/home/hsdev/camera-dev/freedom/images # base directory to save images -BASENAME=image # base image filename - -# --- Asynchronous FITS writer (frame_output_factory) --- -FITS_ENABLED=yes # enable FITS writer output {yes|no} -FITS_OUTPUT_DIR=/home/hsdev/camera-dev/freedom/images # base directory for FITS files -FITS_AUTODIR=yes # write into a YYYYMMDD subdir of FITS_OUTPUT_DIR {yes|no} -FITS_BASENAME=image # base filename for FITS files -FITS_WRITE_INTERVAL_MS=0 # 0 = write a FITS file for every exposure (no cadence gating) - -DIRMODE=0077 -TM_ZONE_LOG=local # time zone for log entries only, can be "UTC" or "local" -TM_ZONE=UTC # time zone for everything else, can be "UTC" or "local" -TZ_ENV=PST8PDT,M3.2.0/2,M11.1.0/2 # TZ ENV variable to use when TM_ZONE_* is "local" (follows POSIX standard) -AUTODIR=no -LONGERROR=true - -NBPORT=3030 # server non-blocking port -BLKPORT=3031 # server blocking port -ASYNCGROUP=239.1.1.234 # asynchronous broadcast group (or "none") -ASYNCPORT=1234 # asynchronous message port (if ASYNCGROUP defined) -#ASYNCGROUP=none -LOGPATH=/home/hsdev/camera-dev/freedom/logs # fully qualified path to save log files -#LONGEXPOSURE=True # exptimes in seconds (not ms) diff --git a/README.md b/README.md index 24b383f..4f9a387 100644 --- a/README.md +++ b/README.md @@ -77,7 +77,7 @@ If you encounter any problems or have questions about this project, please open $ ../bin/camerad -d ``` - *Replace `` with an appropriate configuration file. See the example `.cfg` files in the `Config` and `Config/demo` directories.* + *Replace `` with an appropriate configuration file. See the example `.cfg` files in the `config` directory (per-instrument deployment configs live in each instrument's own repo under its `config/` directory; `config/demo` here is a generic example).* 6. **(Optional) Run the Archon Emulator:** @@ -95,7 +95,7 @@ If you encounter any problems or have questions about this project, please open ## Frame Outputs -Instruments that opt in publish each acquired frame to one or more outputs, configured via `.cfg` file keys read by `Camera::apply_config_overrides()`. Both outputs are independent; either or both can be enabled. +Every instrument publishes each acquired frame to one or more outputs, configured entirely via `.cfg` file keys (`Camera::Interface::configure_frame_outputs()` builds them from `Camera::apply_config_overrides()`, called once at startup for every instrument, not just HISPEC). Both outputs are independent; either, both, or neither can be enabled per instrument. ### FITS diff --git a/camerad/Instruments/hispec_tracking_camera b/camerad/Instruments/hispec_tracking_camera index 320528d..08502aa 160000 --- a/camerad/Instruments/hispec_tracking_camera +++ b/camerad/Instruments/hispec_tracking_camera @@ -1 +1 @@ -Subproject commit 320528d7aac4befa6263363e5e172684c75421e6 +Subproject commit 08502aadd33cb63cf0fdaad3e934701611462e4c diff --git a/camerad/camera_interface.cpp b/camerad/camera_interface.cpp index f8dfdb3..9e72d4f 100644 --- a/camerad/camera_interface.cpp +++ b/camerad/camera_interface.cpp @@ -99,4 +99,19 @@ namespace Camera { return NO_ERROR; } /***** Camera::Interface::datacube *********************************************/ + + + /***** Camera::Interface::configure_frame_outputs *******************************/ + /** + * @brief build frame_outputs (SHM/FITS) from the config file + * @details Called unconditionally from camerad.cpp after configure_instrument(), + * so no derived override can silently skip wiring frame_outputs. + * + */ + void Interface::configure_frame_outputs() { + Camera::FrameOutputsConfig fo_cfg; + Camera::apply_config_overrides(fo_cfg, this->configfile); + this->frame_outputs = Camera::make_frame_outputs(fo_cfg); + } + /***** Camera::Interface::configure_frame_outputs *******************************/ } diff --git a/camerad/camera_interface.h b/camerad/camera_interface.h index d08074d..a9c52cc 100644 --- a/camerad/camera_interface.h +++ b/camerad/camera_interface.h @@ -13,6 +13,7 @@ #include "camerad_commands.h" #include "exposure_modes.h" #include "frame_output.h" +#include "frame_output_factory.h" #include #include @@ -84,6 +85,7 @@ namespace Camera { void disconnect_controller(); long key(std::string args, std::string &retstring); long datacube(std::string args, std::string &retstring); + void configure_frame_outputs(); 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/camerad.cpp b/camerad/camerad.cpp index 02a173c..49d9746 100644 --- a/camerad/camerad.cpp +++ b/camerad/camerad.cpp @@ -48,6 +48,7 @@ int main( int argc, char** argv ) { camerad.interface->configure_controller(); camerad.interface->configure_interface(); camerad.interface->configure_instrument(); + camerad.interface->configure_frame_outputs(); } catch (const std::exception &e) { logwrite(function, "ERROR configuring system: "+std::string(e.what())); diff --git a/Config/astrocam.cfg b/config/astrocam/astrocam.cfg similarity index 100% rename from Config/astrocam.cfg rename to config/astrocam/astrocam.cfg diff --git a/Config/cryoscope/cryoscope-test_4224x2048_19.fits.gz b/config/cryoscope/cryoscope-test_4224x2048_19.fits.gz similarity index 100% rename from Config/cryoscope/cryoscope-test_4224x2048_19.fits.gz rename to config/cryoscope/cryoscope-test_4224x2048_19.fits.gz diff --git a/Config/cryoscope/cryoscope.acf b/config/cryoscope/cryoscope.acf similarity index 100% rename from Config/cryoscope/cryoscope.acf rename to config/cryoscope/cryoscope.acf diff --git a/Config/cryoscope/cryoscope.cfg b/config/cryoscope/cryoscope.cfg similarity index 82% rename from Config/cryoscope/cryoscope.cfg rename to config/cryoscope/cryoscope.cfg index b6d152b..bcb3399 100644 --- a/Config/cryoscope/cryoscope.cfg +++ b/config/cryoscope/cryoscope.cfg @@ -12,14 +12,14 @@ TZ_ENV=PST8PDT,M3.2.0/2,M11.1.0/2 NBPORT=3030 BLKPORT=3031 EMULATOR_PORT=3032 -EMULATOR_SYSTEM=Config/demo/demo.system +EMULATOR_SYSTEM=config/demo/demo.system ASYNCGROUP=239.1.1.234 ASYNCPORT=1234 ARCHON_IP=localhost ARCHON_PORT=3032 -DEFAULT_FIRMWARE=Config/cryoscope/cryoscope.acf +DEFAULT_FIRMWARE=config/cryoscope/cryoscope.acf ABORT_PARAM=Abort EXPOSE_PARAM=Expose EXPTIME_MSEC_PARAM=exptime diff --git a/Config/demo/demo.acf b/config/demo/demo.acf similarity index 100% rename from Config/demo/demo.acf rename to config/demo/demo.acf diff --git a/Config/demo/demo.cfg b/config/demo/demo.cfg similarity index 94% rename from Config/demo/demo.cfg rename to config/demo/demo.cfg index c28c841..c1a84e8 100644 --- a/Config/demo/demo.cfg +++ b/config/demo/demo.cfg @@ -17,7 +17,7 @@ LONGERROR=true NBPORT=3030 # server non-blocking port BLKPORT=3031 # 3031 or 3041 blocking port EMULATOR_PORT=3032 -EMULATOR_SYSTEM=Config/demo/demo.system +EMULATOR_SYSTEM=config/demo/demo.system # # If you have a firewall, you must allow # multicast address 239.192.13.165, ports 1304-1305. @@ -28,7 +28,7 @@ ASYNCPORT=1234 # asynchronous message port # the following should not be changed ARCHON_IP=localhost ARCHON_PORT=3032 -DEFAULT_FIRMWARE=Config/demo/demo.acf +DEFAULT_FIRMWARE=config/demo/demo.acf ABORT_PARAM=abort # Archon parameter to trigger an abort EXPOSE_PARAM=Expose # Archon parameter to trigger exposure EXPTIME_MSEC_PARAM=exptime # Archon parameter for exposure time in msec diff --git a/Config/demo/demo.system b/config/demo/demo.system similarity index 100% rename from Config/demo/demo.system rename to config/demo/demo.system diff --git a/config/frame_outputs_test/frame_outputs_test.cfg b/config/frame_outputs_test/frame_outputs_test.cfg new file mode 100644 index 0000000..2af3391 --- /dev/null +++ b/config/frame_outputs_test/frame_outputs_test.cfg @@ -0,0 +1,38 @@ +# camerad + emulator SHM/FITS frame_outputs testing (CI). Exercises the +# generic frame_outputs mechanism (Camera::Interface::configure_frame_outputs), +# not anything hispec_tracking_camera-specific; hispec_tracking_camera is +# just the build's INSTRUMENT since it's the only fully-working one today. + +DAEMON=no +IMDIR=/tmp +LOGPATH=/tmp +BASENAME=frameoutputstest +DIRMODE=0077 +TM_ZONE_LOG=local +TM_ZONE=UTC +TZ_ENV=PST8PDT,M3.2.0/2,M11.1.0/2 + +NBPORT=3130 +BLKPORT=3131 +EMULATOR_PORT=3132 +EMULATOR_SYSTEM=config/frame_outputs_test/frame_outputs_test.system + +ASYNCGROUP=none + +ARCHON_IP=localhost +ARCHON_PORT=3132 +DEFAULT_FIRMWARE=camerad/Instruments/hispec_tracking_camera/config/hispecatc.acf +EXPOSE_PARAM=Expose +ABORT_PARAM=abort +READOUT_TIME=5000 +WRITE_TAPINFO_TO_FITS=no + +SHM_ENABLED=yes +SHM_SEGMENT_NAME=ci_frame_outputs_shm +SHM_RING_BUFFER_SIZE=2 +SHM_DIR=/tmp/ci_milk_shm + +FITS_ENABLED=yes +FITS_OUTPUT_DIR=/tmp/ci_fits_test +FITS_AUTODIR=no +FITS_BASENAME=ci_frame_outputs diff --git a/Config/hispec_shm_test/hispec_shm_test.system b/config/frame_outputs_test/frame_outputs_test.system similarity index 100% rename from Config/hispec_shm_test/hispec_shm_test.system rename to config/frame_outputs_test/frame_outputs_test.system diff --git a/utils/frame_output_factory.cpp b/utils/frame_output_factory.cpp index a943f1a..badc2ef 100644 --- a/utils/frame_output_factory.cpp +++ b/utils/frame_output_factory.cpp @@ -52,22 +52,16 @@ namespace Camera { if (cfg.shm_enabled) { #ifdef CAMERAD_HAVE_SHM - if (cfg.shm_max_frame_bytes == 0) { - logwrite(function, "WARNING shm_enabled but shm_max_frame_bytes==0; SHM skipped"); + auto shm = std::make_unique( + cfg.shm_segment_name, cfg.shm_ring_buffer_size, cfg.shm_dir); + if (shm->open() == NO_ERROR) { + logwrite(function, "SHM output enabled: segment=" + cfg.shm_segment_name + + " ring_buffer_size=" + std::to_string(cfg.shm_ring_buffer_size) + + " dir=" + (cfg.shm_dir.empty() ? "(default)" : cfg.shm_dir)); + outputs.push_back(std::move(shm)); } else { - auto shm = std::make_unique( - cfg.shm_segment_name, cfg.shm_max_frame_bytes, cfg.shm_ring_buffer_size, cfg.shm_dir); - 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) + - " ring_buffer_size=" + std::to_string(cfg.shm_ring_buffer_size) + - " dir=" + (cfg.shm_dir.empty() ? "(default)" : cfg.shm_dir)); - outputs.push_back(std::move(shm)); - } - else { - logwrite(function, "WARNING SHM output failed to open; skipped"); - } + logwrite(function, "WARNING SHM output failed to open; skipped"); } #else logwrite(function, "WARNING shm_enabled but this build was compiled without SHM support (ENABLE_SHM_OUTPUT=OFF)"); diff --git a/utils/frame_output_factory.h b/utils/frame_output_factory.h index 99b770f..f66fbed 100644 --- a/utils/frame_output_factory.h +++ b/utils/frame_output_factory.h @@ -23,7 +23,6 @@ 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_ring_buffer_size{4}; // depth of ImageStreamIO's internal history ring buffer std::string shm_dir{}; // ImageStreamIO base directory; empty uses its own default resolution diff --git a/utils/shared_memory_writer.cpp b/utils/shared_memory_writer.cpp index 0d7da3d..74d8b78 100644 --- a/utils/shared_memory_writer.cpp +++ b/utils/shared_memory_writer.cpp @@ -41,11 +41,9 @@ namespace { namespace Camera { SharedMemoryWriter::SharedMemoryWriter(const std::string &segment_name, - size_t max_frame_bytes, uint32_t ring_buffer_size, const std::string &shm_dir) : segment_name_(segment_name), - max_frame_bytes_(max_frame_bytes), ring_buffer_size_(ring_buffer_size), shm_dir_(shm_dir) { } @@ -61,10 +59,6 @@ namespace Camera { logwrite(function, "ERROR segment name is empty"); return ERROR; } - if (max_frame_bytes_ == 0) { - logwrite(function, "ERROR max_frame_bytes must be > 0"); - return ERROR; - } if (ring_buffer_size_ == 0) { logwrite(function, "ERROR ring_buffer_size must be > 0"); return ERROR; @@ -83,8 +77,7 @@ namespace Camera { opened_ = true; // Geometry is fixed for a stream's whole life, so create happens in write(), not here - logwrite(function, "ready to publish \"" + segment_name_ + "\" (max " + - std::to_string(max_frame_bytes_) + " bytes/frame, " + + logwrite(function, "ready to publish \"" + segment_name_ + "\" (" + std::to_string(ring_buffer_size_) + " frames, dir=" + (shm_dir_.empty() ? "(default)" : shm_dir_) + ")"); return NO_ERROR; @@ -109,11 +102,6 @@ namespace Camera { const size_t frame_bytes = static_cast(meta.width) * meta.height * meta.bytes_per_pixel; - if (frame_bytes > max_frame_bytes_) { - logwrite(function, "ERROR frame size " + std::to_string(frame_bytes) + - " exceeds max " + std::to_string(max_frame_bytes_)); - return ERROR; - } if (size < frame_bytes) { logwrite(function, "ERROR frame data " + std::to_string(size) + " < expected " + std::to_string(frame_bytes)); diff --git a/utils/shared_memory_writer.h b/utils/shared_memory_writer.h index b73a1c1..5b2ab88 100644 --- a/utils/shared_memory_writer.h +++ b/utils/shared_memory_writer.h @@ -19,7 +19,6 @@ namespace Camera { class SharedMemoryWriter : public FrameOutput { public: SharedMemoryWriter(const std::string &segment_name, - size_t max_frame_bytes, uint32_t ring_buffer_size = 4, const std::string &shm_dir = ""); ~SharedMemoryWriter(); @@ -30,7 +29,6 @@ namespace Camera { private: std::string segment_name_; - size_t max_frame_bytes_; uint32_t ring_buffer_size_; std::string shm_dir_; bool opened_{false};