Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
81 changes: 81 additions & 0 deletions .github/workflows/emulator-integration.yml
Original file line number Diff line number Diff line change
Expand Up @@ -87,3 +87,84 @@ jobs:
assert ci.instrument_name() == "none", ci.instrument_name()
print("imported", ci.__file__)
PY

reference-instrument-test:
runs-on: ubuntu-latest
timeout-minutes: 15

steps:
- uses: actions/checkout@v4

- name: Install dependencies
run: |
sudo apt-get update
sudo apt-get install -y \
libccfits-dev libcfitsio-dev libcurl4-openssl-dev \
nlohmann-json3-dev libzmq3-dev \
libboost-thread-dev libboost-chrono-dev \
python3-dev python3-pip
python3 -m pip install --upgrade pybind11

- name: Build and install zmqpp from source
run: |
git clone --depth 1 https://github.com/zeromq/zmqpp.git /tmp/zmqpp
cd /tmp/zmqpp
mkdir build && cd build
cmake ..
make -j$(nproc)
sudo make install
sudo ldconfig

- name: Build the emulator and socksend
run: |
mkdir -p ${{github.workspace}}/build
cd ${{github.workspace}}/build
cmake -DCONTROLLER=archon ..
make emulator socksend -j$(nproc)

- name: Build the reference instrument
run: |
cmake -S reference_instrument -B build-reference
cmake --build build-reference -j$(nproc)

- name: Reference instrument against the emulator
run: |
cfg=reference_instrument/reference.cfg

bin/camerad-emulator $cfg -i generic &
emulator=$!
sleep 2
build-reference/camerad --foreground --config $cfg &
camerad=$!
trap 'kill $camerad $emulator 2>/dev/null || true' EXIT
sleep 3

send() { bin/camerad-socksend -p 3231 -t 60 "$1"; }

# fail unless the reply to $1 matches the pattern $2
expect() {
resp=$(send "$1")
echo "$1 -> $resp"
echo "$resp" | grep -q "$2" || { echo "expected \"$2\""; exit 1; }
}

expect "open" "DONE"
expect "load" "DONE"
expect "power on" "DONE"
expect "exptime 0.1" "DONE"
expect "expose 1" "DONE"

# instrument command, replying with the value configure_instrument() read
expect "refvalue" "^reference-cfg-value DONE"

# mode selection: the instrument's mode is listed and runs, and the
# parent's modes are still constructed by the parent
expect "exposuremode ?" "REFERENCE"
expect "exposuremode REFERENCE" "^REFERENCE DONE"
expect "expose 1" "DONE"
expect "exposuremode" "^REFERENCE DONE"
expect "exposuremode SINGLE" "^SINGLE DONE"
expect "expose 1" "DONE"

- name: Reference module import test
run: ctest --test-dir build-reference --output-on-failure
73 changes: 73 additions & 0 deletions reference_instrument/CMakeLists.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
# ----------------------------------------------------------------------------
# @file reference_instrument/CMakeLists.txt
# @brief builds the reference instrument's camerad and Python module
# @details Built the way an instrument repository builds: the core is a
# FetchContent dependency and this project declares the final targets
# ----------------------------------------------------------------------------

cmake_minimum_required(VERSION 3.24)

project(reference_instrument LANGUAGES CXX)

include(FetchContent)

# A real instrument pins a released version of the core. This copy lives inside
# the core and builds the surrounding tree instead, so the source directory
# below overrides the fetch and the git URL is never contacted. A copy made for
# a new instrument removes this default
if (NOT DEFINED FETCHCONTENT_SOURCE_DIR_CAMERAD)
cmake_path(GET CMAKE_CURRENT_SOURCE_DIR PARENT_PATH FETCHCONTENT_SOURCE_DIR_CAMERAD)
endif()

FetchContent_Declare(camerad
GIT_REPOSITORY https://github.com/CaltechOpticalObservatories/camera-interface
GIT_TAG d4815d100ad88a1c0773b5dc1dee086479b8d24d
)

set(CONTROLLER archon CACHE STRING "")
set(BUILD_PYTHON_MODULE ON CACHE BOOL "")
FetchContent_MakeAvailable(camerad)

# each final target compiles exactly one interface factory definition
set(REFERENCE_SOURCES
reference_interface_factory.cpp
reference_instrument.cpp
)

add_executable(reference_camerad ${REFERENCE_SOURCES})
target_link_libraries(reference_camerad camerad::daemon camerad::archon)
set_target_properties(reference_camerad PROPERTIES OUTPUT_NAME camerad)

# pybind11 comes from the Python environment the module is built against, so
# the module and the interpreter that loads it always agree on the ABI
find_package(Python3 COMPONENTS Interpreter Development REQUIRED)
if (NOT pybind11_DIR)
execute_process(
COMMAND ${Python3_EXECUTABLE} -c "import pybind11; print(pybind11.get_cmake_dir())"
OUTPUT_VARIABLE PYBIND11_CMAKE_DIR
OUTPUT_STRIP_TRAILING_WHITESPACE
RESULT_VARIABLE PYBIND11_PROBE_RESULT
ERROR_QUIET
)
if (NOT PYBIND11_PROBE_RESULT EQUAL 0)
message(FATAL_ERROR
"the module needs pybind11 in ${Python3_EXECUTABLE}. "
"Install it with: ${Python3_EXECUTABLE} -m pip install pybind11")
endif()
list(APPEND CMAKE_PREFIX_PATH ${PYBIND11_CMAKE_DIR})
endif()
find_package(pybind11 CONFIG REQUIRED)

pybind11_add_module(camera_interface ${REFERENCE_SOURCES})
target_link_libraries(camera_interface PRIVATE camerad::python camerad::archon)
target_compile_definitions(camera_interface PRIVATE CAMERAD_INSTRUMENT_NAME="reference")

# A shared library may leave symbols undefined, so a module missing its
# interface factory definition links and fails only when imported
enable_testing()
add_test(NAME python_import
COMMAND ${Python3_EXECUTABLE} -c "import camera_interface\nassert camera_interface.instrument_name() == 'reference', camera_interface.instrument_name()\nassert camera_interface.controller_name() == 'archon', camera_interface.controller_name()"
)
set_tests_properties(python_import PROPERTIES
ENVIRONMENT PYTHONPATH=$<TARGET_FILE_DIR:camera_interface>
)
55 changes: 55 additions & 0 deletions reference_instrument/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
# Reference instrument

This is an instrument that belongs to no project. The core's CI builds it the way an instrument repository builds,
and runs it against the emulator, so that each point where an instrument specializes the core is exercised on every change.

It derives from `ArchonInterface` and overrides the three specialization points, each observable over the socket:

| Point | Override | Observed by |
|---|---|---|
| Instrument command | `instrument_commands`, `instrument_cmd` | `refvalue` replies with the configured value |
| Instrument configuration | `configure_instrument` | reads `REFERENCE_VALUE` from the cfg, which `refvalue` reports |
| Mode selection | `get_exposure_modes`, `set_exposure_mode` | `exposuremode ?` lists `REFERENCE`; `exposuremode REFERENCE` selects it |

`is_instrument_command` is also overridden, only because the current server consults it before every command. That
is not the design, and the dispatch fix removes it.

`REFERENCE` reads out as `SINGLE` and differs only in the type it reports. Any other mode is constructed by
`ArchonInterface`.

The CI job checks command dispatch, not frame readout: `expose` replies DONE whatever `do_expose()` returns, so a
passing job does not show that a frame was read.

## Building

```bash
cmake -S reference_instrument -B build-reference
cmake --build build-reference
ctest --test-dir build-reference
```

This produces `camerad` and the `camera_interface` module, each compiled with `reference_interface_factory.cpp`.
`ctest` imports the module and checks `instrument_name()` and `controller_name()`.

`CMakeLists.txt` declares the core with `FetchContent` at a pinned commit, but inside the core it defaults
`FETCHCONTENT_SOURCE_DIR_CAMERAD` to the enclosing checkout, so it always builds the surrounding tree and never
fetches. Passing `-DFETCHCONTENT_SOURCE_DIR_CAMERAD=<path>` points it at another checkout.

## Running

Paths in `reference.cfg` are relative to the working directory, so run from the core's root. The emulator and
`camerad-socksend` come from a top-level build of the core:

```bash
bin/camerad-emulator reference_instrument/reference.cfg -i generic &
build-reference/camerad --foreground --config reference_instrument/reference.cfg &
bin/camerad-socksend -p 3231 "refvalue"
```

A DONE reply to `expose` means the command was accepted, not that a frame was read; check the log for that.

## Starting a new instrument from a copy

- Remove the `FETCHCONTENT_SOURCE_DIR_CAMERAD` default, and set `GIT_TAG` to a released version of the core.
- Rename `reference`, `ReferenceInstrument` and `REFERENCE` throughout, including `CAMERAD_INSTRUMENT_NAME`.
- Bring your own ACF and system file; `reference.cfg` uses the core's `config/demo/` ones.
41 changes: 41 additions & 0 deletions reference_instrument/reference.cfg
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
# configuration for the reference instrument against the emulator, used by
# the core's CI. Paths are relative to the working directory, which must be
# the core's root
#

DAEMON=no
IMDIR=/tmp
LOGPATH=/tmp
BASENAME=reference
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=3230 # server non-blocking port
BLKPORT=3231 # blocking port
EMULATOR_PORT=3232
EMULATOR_SYSTEM=config/demo/demo.system
#
# If you have a firewall, you must allow
# multicast address 239.192.13.165, ports 1304-1305.
#
ASYNCGROUP=239.1.1.234 # asynchronous broadcast group
ASYNCPORT=1234 # asynchronous message port

# the following should not be changed
ARCHON_IP=localhost
ARCHON_PORT=3232
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
READOUT_TIME=5000 # Timeout waiting for new frame (ms)
WRITE_TAPINFO_TO_FITS=no # Tapinfo (gain, offset) be written to FITS headers {yes|no}

# read by the reference instrument's configure_instrument()
REFERENCE_VALUE=reference-cfg-value
28 changes: 28 additions & 0 deletions reference_instrument/reference_exposure_modes.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
/**
* @file reference_exposure_modes.h
* @brief declares the exposure mode the reference instrument adds
*
*/

#pragma once

#include "archon_exposure_modes.h"

namespace Camera {

/**
* @namespace exposure modes added to those of ArchonExposureMode
*/
namespace ReferenceExposureMode {
constexpr const char* REFERENCE = "REFERENCE";
};

// Reads out as SINGLE and differs only in the type it reports
class ExposureModeReference : public ExposureModeSingle {
public:
ExposureModeReference(Camera::ArchonInterface* iface)
: ExposureModeSingle(iface) {
type=ReferenceExposureMode::REFERENCE;
}
};
}
Loading
Loading