From 4afb7edf30300e1783d7bc6a7a8154705fac6112 Mon Sep 17 00:00:00 2001 From: Mike Langmayr Date: Tue, 20 Oct 2026 16:54:58 -0700 Subject: [PATCH 1/2] Remove in-tree instrument submodules and the INSTRUMENT build path --- .github/workflows/cmake-workflow.yml | 45 +----- .gitmodules | 12 -- CMakeLists.txt | 12 -- README.md | 36 +---- camerad/CMakeLists.txt | 55 +------ camerad/Instruments/cryoscope | 1 - camerad/Instruments/deimos | 1 - camerad/Instruments/hispec | 1 - camerad/Instruments/hispec_tracking_camera | 1 - docs/_ext/camerad_tables.py | 178 +-------------------- docs/architecture/index.md | 10 +- docs/configuration/index.md | 1 - docs/development/index.md | 13 +- docs/getting-started/index.md | 15 +- docs/index.md | 6 +- docs/instruments/cryoscope.md | 23 +-- docs/instruments/hispec-tracking-camera.md | 28 ++-- docs/instruments/index.md | 24 +-- docs/python/index.md | 9 +- emulator/CMakeLists.txt | 6 +- packaging/tracking/pyproject.toml | 18 --- python/CMakeLists.txt | 37 +---- utils/CMakeLists.txt | 3 +- 23 files changed, 73 insertions(+), 462 deletions(-) delete mode 100644 .gitmodules delete mode 160000 camerad/Instruments/cryoscope delete mode 160000 camerad/Instruments/deimos delete mode 160000 camerad/Instruments/hispec delete mode 160000 camerad/Instruments/hispec_tracking_camera delete mode 100644 packaging/tracking/pyproject.toml diff --git a/.github/workflows/cmake-workflow.yml b/.github/workflows/cmake-workflow.yml index 0f0459b..62f5404 100644 --- a/.github/workflows/cmake-workflow.yml +++ b/.github/workflows/cmake-workflow.yml @@ -8,7 +8,7 @@ on: workflow_dispatch: inputs: target: - description: "Optional build target or instrument name (leave blank for default build)" + description: "Optional build target, such as emulator (leave blank for default build)" required: false default: "" type: string @@ -52,35 +52,6 @@ jobs: sudo cmake --install /tmp/zmqpp/build sudo ldconfig - - name: Rewrite GitHub SSH URLs for submodules - shell: bash - run: | - git config --global url."https://github.com/".insteadOf "git@github.com:" - git submodule sync --recursive - - - name: Update matching submodule - if: env.TARGET_NAME != '' && env.TARGET_NAME != 'emulator' - shell: bash - run: | - if [[ ! -f .gitmodules ]]; then - echo "No .gitmodules file found; skipping submodule update." - exit 0 - fi - - MATCH_PATH=$(git config -f .gitmodules --get-regexp '^submodule\..*\.path$' | awk -v target="$TARGET_NAME" ' - $2 == target { print $2; exit } - $2 ~ "/" target "$" { print $2; exit } - ') - - if [[ -z "$MATCH_PATH" ]]; then - echo "No submodule path matched '$TARGET_NAME'; continuing without submodule update." - exit 0 - fi - - echo "Matched submodule path: $MATCH_PATH" - git submodule sync -- "$MATCH_PATH" - git submodule update --init --recursive --remote -- "$MATCH_PATH" - - name: Configure CMake shell: bash run: | @@ -93,10 +64,6 @@ jobs: -DCONTROLLER=archon ) - if [[ -n "$TARGET_NAME" && "$TARGET_NAME" != "emulator" ]]; then - CMAKE_ARGS+=("-DINSTRUMENT=${TARGET_NAME}") - fi - cmake "${CMAKE_ARGS[@]}" .. - name: Build default @@ -110,17 +77,11 @@ jobs: echo "run_unit_tests target not available; skipping unit tests." fi - - name: Build emulator target - if: env.TARGET_NAME == 'emulator' - shell: bash - run: | - cmake --build build --target emulator -j"$(nproc)" - - name: Build named target - if: env.TARGET_NAME != '' && env.TARGET_NAME != 'emulator' + if: env.TARGET_NAME != '' shell: bash run: | - cmake --build build -j"$(nproc)" + cmake --build build --target "$TARGET_NAME" -j"$(nproc)" cmake-3-24-subproject: runs-on: ubuntu-latest diff --git a/.gitmodules b/.gitmodules deleted file mode 100644 index f095d71..0000000 --- a/.gitmodules +++ /dev/null @@ -1,12 +0,0 @@ -[submodule "camerad/Instruments/hispec_tracking_camera"] - path = camerad/Instruments/hispec_tracking_camera - url = https://github.com/CaltechOpticalObservatories/hispec-tracking-camera-instrument.git -[submodule "camerad/Instruments/deimos"] - path = camerad/Instruments/deimos - url = https://github.com/CaltechOpticalObservatories/deimos-instrument.git -[submodule "camerad/Instruments/cryoscope"] - path = camerad/Instruments/cryoscope - url = https://github.com/CaltechOpticalObservatories/cryoscope-instrument.git -[submodule "camerad/Instruments/hispec"] - path = camerad/Instruments/hispec - url = https://github.com/CaltechOpticalObservatories/hispec-instrument.git diff --git a/CMakeLists.txt b/CMakeLists.txt index e6ad364..6001265 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -99,18 +99,6 @@ endif() option(CAMERAD_BUILD_TOOLS "Build the emulator, listener, socksend and shm_reader" ${PROJECT_IS_TOP_LEVEL}) option(CAMERAD_INSTALL "Generate install rules" ${PROJECT_IS_TOP_LEVEL}) -# This lets an instrument package share an environment with the generic package -# and other instruments. The module installs into a Python package of this name, -# and camerad and the emulator are named for it. The name is supplied by -# whoever builds the instrument package. -# -# This is temporary and I'll remove it once each instrument's -# repository declares its own module and writes its own install rules -set(CAMERAD_PYTHON_PACKAGE "" CACHE STRING "Python package an instrument package installs its module into") -if (CAMERAD_PYTHON_PACKAGE) - string(REPLACE "_" "-" CAMERAD_PACKAGE_SUFFIX ${CAMERAD_PYTHON_PACKAGE}) -endif() - add_subdirectory(${PROJECT_BASE_DIR}/utils) add_subdirectory(${PROJECT_BASE_DIR}/common) add_subdirectory(${PROJECT_BASE_DIR}/camerad) diff --git a/README.md b/README.md index 78122c9..141f7b1 100644 --- a/README.md +++ b/README.md @@ -46,11 +46,7 @@ If you encounter any problems or have questions about this project, please open |-----------------------------------|--------------------------------------| | `$ cmake -DCONTROLLER=archon ..` | `$ cmake -DCONTROLLER=astrocam ..` | - Add `-DINSTRUMENT=` to build an instrument module, whose sources come from `camerad/Instruments/`: - - ```bash - $ cmake -DCONTROLLER=archon -DINSTRUMENT=hispec_tracking_camera .. - ``` + This builds the core with no instrument. An instrument builds from its own repository, which pulls in this one; see [Composing the core](#composing-the-core). `-DINTERFACE_TYPE=` is separate from `-DCONTROLLER=` and selects only which emulator is built. It defaults to `Archon`; `-DINTERFACE_TYPE=AstroCam` skips the emulator, since none is implemented for ARC. @@ -205,43 +201,21 @@ An instrument repository that installs its module puts it inside a Python packag `pip install` builds the same artifacts and places them in the target environment, so `import camera_interface` needs no `PYTHONPATH` and `camerad` is on `PATH` whenever the environment is active: ```bash -$ pip install ./camera-interface \ - --config-settings=cmake.define.INSTRUMENT=hispec_tracking_camera +$ pip install ./camera-interface ``` Any CMake option can be passed the same way, so `--config-settings=cmake.define.ENABLE_SHM_OUTPUT=ON` works as well. `CONTROLLER` defaults to `archon` and `BUILD_PYTHON_MODULE` is forced on. pybind11 comes from `[build-system] requires`, so pip fetches it into an isolated build environment. It is never installed into the environment being built for. -The controller and instrument are fixed when the wheel is built, so a caller should assert which build it loaded: +The controller is fixed when the wheel is built, so a caller should assert which build it loaded: ```python -assert camera_interface.instrument_name() == "hispec_tracking_camera" +assert camera_interface.controller_name() == "archon" ``` A compiler and the full dependency set have to be present wherever `pip install` runs, since it compiles camerad and the module from source. -### Instrument packages - -An instrument package can share an environment with the generic package and with other instruments. Its build sets `CAMERAD_PYTHON_PACKAGE` to the package's name, e.g. in a `pyproject.toml` in the instrument's own repository that builds the core from its checkout: - -```toml -[tool.scikit-build] -cmake.source-dir = "external/camera-interface" -wheel.packages = [] -cmake.define.BUILD_PYTHON_MODULE = "ON" -cmake.define.CONTROLLER = "archon" -cmake.define.INSTRUMENT = "myinst" -cmake.define.CAMERAD_PYTHON_PACKAGE = "myinst_camera" -``` - -That package installs: - -- `myinst_camera/camera_interface…so` and `myinst_camera/__init__.py`, imported as `import myinst_camera` or `import myinst_camera.camera_interface`; -- `camerad-myinst-camera` and `camerad-emulator-myinst-camera`, named for the package with `_` replaced by `-`. - -It does not install `camerad-socksend` or `camerad-shm-reader`, which come with the generic package. The generic package is unchanged: `import camera_interface`, plus `camerad`, `camerad-emulator` and `camerad-socksend`. Uninstalling one package leaves the others' files in place. - ## Python Module Built with `-DBUILD_PYTHON_MODULE=ON`, `camera_interface` lets a Python process own a camera directly, with no `camerad` process and no text protocol in between. It performs the same startup `camerad` does, then exposes the interface as methods: @@ -265,7 +239,7 @@ Every command `camerad` accepts is reachable: the base commands are bound as met The controller and instrument are fixed at CMake configure time, so `instrument_name()` and `controller_name()` report which build was loaded. A failed command raises `RuntimeError`. -Every build's module is named `camera_interface`. The generic build installs it at the top level, and an instrument package installs it inside a package of its own ([Instrument packages](#instrument-packages)), so several instruments can share one environment. Importing several instruments into one process is intended to be supported but is not yet tested; for now, use one instrument per process. +Every build's module is named `camera_interface`. The generic build installs it at the top level, and an instrument repository installs it inside a package of its own ([Composing the core](#composing-the-core)), so several instruments can share one environment. Importing several instruments into one process is intended to be supported but is not yet tested; for now, use one instrument per process. `output_status()` is a snapshot, never a barrier: the FITS writer queues and drops frames by design because disk is slower than acquisition can be, so nothing here lets a caller stall acquisition by waiting on an output. Anything needing to be woken per frame should attach to the shared-memory segment, which posts semaphores. diff --git a/camerad/CMakeLists.txt b/camerad/CMakeLists.txt index 6bac0fc..be54e1e 100644 --- a/camerad/CMakeLists.txt +++ b/camerad/CMakeLists.txt @@ -11,7 +11,6 @@ set( CAMERAD_DIR ${PROJECT_BASE_DIR}/camerad ) set( CMAKE_CXX_STANDARD 17 ) include_directories(${CAMERAD_DIR} - ${CAMERAD_DIR}/Instruments ${PROJECT_BASE_DIR}/utils ${PROJECT_BASE_DIR}/common ) @@ -103,34 +102,10 @@ else() endif() # ---------------------------------------------------------------------------- -# default instrument is none +# the controller's own interface factory, compiled into this project's final +# targets. An instrument builds from its own repository and supplies its own # ---------------------------------------------------------------------------- -option(INSTRUMENT "Choose instrument { generic ... }") -if (INSTRUMENT) - message(STATUS "instrument: ${INSTRUMENT}") - include(Instruments/${INSTRUMENT}/${INSTRUMENT}.cmake) -else() - message(STATUS "instrument: none") - set (INSTRUMENT_SOURCES "") - # If no instrument is defined then include the factory for the specified controller - if (CONTROLLER STREQUAL "archon") - set (INSTRUMENT_SOURCES ${CAMERAD_DIR}/archon_interface_factory.cpp) - elseif (CONTROLLER STREQUAL "astrocam") - set (INSTRUMENT_SOURCES ${CAMERAD_DIR}/astrocam_interface_factory.cpp) - elseif (CONTROLLER STREQUAL "bob") - set (INSTRUMENT_SOURCES ${CAMERAD_DIR}/bob_interface_factory.cpp) - elseif (CONTROLLER STREQUAL "joe") - set (INSTRUMENT_SOURCES ${CAMERAD_DIR}/joe_interface_factory.cpp) - endif() -endif() - -# ---------------------------------------------------------------------------- -# Separate the registration TU from the instrument implementation so that -# the registration is compiled into the final target. -# ---------------------------------------------------------------------------- -set (INSTRUMENT_REGISTRATION ${INSTRUMENT_SOURCES}) -list (FILTER INSTRUMENT_REGISTRATION INCLUDE REGEX "_interface_factory\\.cpp$") -list (FILTER INSTRUMENT_SOURCES EXCLUDE REGEX "_interface_factory\\.cpp$") +set (INTERFACE_FACTORY ${CAMERAD_DIR}/${CONTROLLER}_interface_factory.cpp) # ---------------------------------------------------------------------------- # Camera Interface Base @@ -170,18 +145,6 @@ target_include_directories(${INTERFACE_TARGET} PUBLIC ${INTERFACE_INCLUDES}) target_compile_definitions(${INTERFACE_TARGET} INTERFACE ${INTERFACE_DEFINITIONS}) add_library(camerad::${INTERFACE_TARGET} ALIAS ${INTERFACE_TARGET}) -# ---------------------------------------------------------------------------- -# instrument implementation, if any. -# objects shared by the final targets, not depended on as a library -# ---------------------------------------------------------------------------- -if (INSTRUMENT_SOURCES) - add_library(instrument_obj OBJECT ${INSTRUMENT_SOURCES}) - target_link_libraries(instrument_obj ${INTERFACE_TARGET}) - set (INSTRUMENT_OBJECTS $) -else() - set (INSTRUMENT_OBJECTS "") -endif() - # ---------------------------------------------------------------------------- # External libraries, such as FITS, etc. # ---------------------------------------------------------------------------- @@ -237,24 +200,18 @@ endif() # composes this one declares its own, linking the libraries above # ---------------------------------------------------------------------------- if (PROJECT_IS_TOP_LEVEL) - add_executable(camerad - ${INSTRUMENT_REGISTRATION} - ${INSTRUMENT_OBJECTS} - ) + add_executable(camerad ${INTERFACE_FACTORY}) target_link_libraries(camerad camerad_daemon ${INTERFACE_TARGET} ) - if (CAMERAD_PYTHON_PACKAGE) - set_target_properties(camerad PROPERTIES OUTPUT_NAME camerad-${CAMERAD_PACKAGE_SUFFIX}) - endif() if (CAMERAD_INSTALL) install(TARGETS camerad RUNTIME DESTINATION ${CAMERAD_INSTALL_BINDIR}) endif() - # Added here, not at the top level, to inherit the controller and instrument - # selection above. The option itself is declared at the top level + # Added here, not at the top level, to inherit the controller selection + # above. The option itself is declared at the top level if (BUILD_PYTHON_MODULE) add_subdirectory(${PROJECT_BASE_DIR}/python ${CMAKE_BINARY_DIR}/python) endif() diff --git a/camerad/Instruments/cryoscope b/camerad/Instruments/cryoscope deleted file mode 160000 index 35dd0ec..0000000 --- a/camerad/Instruments/cryoscope +++ /dev/null @@ -1 +0,0 @@ -Subproject commit 35dd0eccc1f0a1ac7e870972dfc515380645adae diff --git a/camerad/Instruments/deimos b/camerad/Instruments/deimos deleted file mode 160000 index d39ee92..0000000 --- a/camerad/Instruments/deimos +++ /dev/null @@ -1 +0,0 @@ -Subproject commit d39ee9272876741ca7dc22e8a293f6f87530ff27 diff --git a/camerad/Instruments/hispec b/camerad/Instruments/hispec deleted file mode 160000 index 1be486b..0000000 --- a/camerad/Instruments/hispec +++ /dev/null @@ -1 +0,0 @@ -Subproject commit 1be486bf35181a56ef34b7dd11b703c69a1283d1 diff --git a/camerad/Instruments/hispec_tracking_camera b/camerad/Instruments/hispec_tracking_camera deleted file mode 160000 index 60382fd..0000000 --- a/camerad/Instruments/hispec_tracking_camera +++ /dev/null @@ -1 +0,0 @@ -Subproject commit 60382fdfceadf4ca6055033b79bdcce70492c52d diff --git a/docs/_ext/camerad_tables.py b/docs/_ext/camerad_tables.py index 50db31a..8730d39 100644 --- a/docs/_ext/camerad_tables.py +++ b/docs/_ext/camerad_tables.py @@ -1,8 +1,8 @@ """Sphinx directives that build reference tables from the camerad sources. Each table is generated from the code that defines the thing being documented, and cross-checked -against hand-written descriptions in ``docs/data``. A command, configuration key or FITS keyword -that gains or loses a definition without a matching description fails the build, so the reference +against hand-written descriptions in ``docs/data``. A command or configuration key that +gains or loses a definition without a matching description fails the build, so the reference cannot silently drift from the source. """ @@ -23,15 +23,9 @@ REPO_ROOT = DOCS_DIR.parent DATA_DIR = DOCS_DIR / "data" -# Where a configuration key may be recognized. Instrument submodules are excluded so that -# instrument-specific keys do not leak into the core table. +# Where a configuration key may be recognized CONFIG_SEARCH_DIRS = ("camerad", "common", "utils", "emulator") -ATC_DICTIONARY = ( - REPO_ROOT - / "camerad/Instruments/hispec_tracking_camera/fits_header_dictionary.cpp" -) - # Reading the server config always goes through a config object's `param` array, which is what # separates a real key from the Archon protocol tokens matched the same way elsewhere _PARAM = ( @@ -72,26 +66,10 @@ class ConfigKey: group: str -@dataclass(frozen=True) -class FitsKeyword: - """One entry of an instrument's FITS header dictionary.""" - - keyword: str - property: str - comment: str - type: str - default_atc: str - default_spec: str - enum_values: tuple[str, ...] = () - - def _read(relative_path: str) -> str: path = REPO_ROOT / relative_path if not path.is_file(): - raise ExtensionError( - f"{relative_path} is missing. Instrument modules are submodules; run " - "`git submodule update --init --recursive` before building the documentation." - ) + raise ExtensionError(f"{relative_path} is missing") return path.read_text(encoding="utf-8") @@ -172,7 +150,7 @@ def load_config_keys() -> list[ConfigKey]: found: set[str] = set() for directory in CONFIG_SEARCH_DIRS: for source in sorted((REPO_ROOT / directory).rglob("*")): - if source.suffix not in (".cpp", ".h") or "Instruments" in source.parts: + if source.suffix not in (".cpp", ".h"): continue text = source.read_text(encoding="utf-8", errors="replace") for pattern in CONFIG_KEY_PATTERNS: @@ -188,120 +166,6 @@ def load_config_keys() -> list[ConfigKey]: ] -def _split_top_level(text: str) -> list[str]: - """Split on commas that are not nested inside braces or a string literal.""" - parts, depth, in_string, escaped, current = [], 0, False, False, [] - for char in text: - if in_string: - current.append(char) - if escaped: - escaped = False - elif char == "\\": - escaped = True - elif char == '"': - in_string = False - continue - if char == '"': - in_string = True - current.append(char) - elif char == "{": - depth += 1 - current.append(char) - elif char == "}": - depth -= 1 - current.append(char) - elif char == "," and depth == 0: - parts.append("".join(current)) - current = [] - else: - current.append(char) - parts.append("".join(current)) - return [part.strip() for part in parts] - - -def _entry_bodies(block: str) -> list[str]: - """Yield the text inside each top-level ``{ ... }`` of an initializer list.""" - bodies, depth, in_string, escaped, start = [], 0, False, False, 0 - for index, char in enumerate(block): - if in_string: - if escaped: - escaped = False - elif char == "\\": - escaped = True - elif char == '"': - in_string = False - continue - if char == '"': - in_string = True - elif char == "{": - if depth == 0: - start = index + 1 - depth += 1 - elif char == "}": - depth -= 1 - if depth == 0: - bodies.append(block[start:index]) - return bodies - - -def _unquote(field_text: str) -> str: - """Join adjacent C++ string literals into their value.""" - return "".join(re.findall(r'"((?:[^"\\]|\\.)*)"', field_text)).replace('\\"', '"') - - -def _balanced_body(text: str, open_index: int) -> str: - """Return what is between the brace at ``open_index`` and its match.""" - depth, in_string, escaped = 0, False, False - for index in range(open_index, len(text)): - char = text[index] - if in_string: - if escaped: - escaped = False - elif char == "\\": - escaped = True - elif char == '"': - in_string = False - continue - if char == '"': - in_string = True - elif char == "{": - depth += 1 - elif char == "}": - depth -= 1 - if depth == 0: - return text[open_index + 1:index] - raise ExtensionError("unbalanced braces in the FITS header dictionary") - - -@cache -def load_fits_keywords() -> list[FitsKeyword]: - # Instruments live in their own repositories, so this source is often absent - if not ATC_DICTIONARY.is_file(): - return [] - text = ATC_DICTIONARY.read_text(encoding="utf-8") - block = _balanced_body(text, text.index("dictionary = {") + len("dictionary = ")) - - keywords = [] - for body in _entry_bodies(block): - fields = _split_top_level(body) - if len(fields) != 7: - continue - keywords.append( - FitsKeyword( - property=_unquote(fields[0]), - keyword=_unquote(fields[1]), - comment=_unquote(fields[2]), - type=fields[3].replace("Type::", ""), - default_atc=_unquote(fields[4]), - default_spec=_unquote(fields[5]), - enum_values=tuple(re.findall(r'"([^"]*)"', fields[6])), - ) - ) - if not keywords: - raise ExtensionError(f"parsed no entries from {ATC_DICTIONARY}") - return keywords - - def _literal(text: str) -> str: return f"``{text}``" if text else "" @@ -366,46 +230,14 @@ def run(self) -> list[nodes.Node]: return self._render(["Key", "Meaning"], rows) -class CameradFitsKeywords(_TableDirective): - """Render an instrument's FITS header dictionary.""" - - option_spec = {"widths": str} - - def run(self) -> list[nodes.Node]: - keywords = load_fits_keywords() - if not keywords: - return [nodes.note("", nodes.paragraph( - text="This instrument's sources are not in this checkout, so its FITS " - "header dictionary cannot be listed here."))] - - rows = [] - for entry in keywords: - default = entry.default_atc or entry.default_spec - comment = entry.comment - if entry.enum_values: - comment += " (" + ", ".join(f"``{v}``" for v in entry.enum_values) + ")" - rows.append( - [ - _literal(entry.keyword), - _literal(entry.property), - entry.type, - _literal(default), - comment, - ] - ) - return self._render(["Keyword", "Property", "Type", "Default", "Comment"], rows) - - def _validate(app) -> None: """Fail the build early when the source and the descriptions disagree.""" load_commands() load_config_keys() - load_fits_keywords() def setup(app): app.add_directive("camerad-commands", CameradCommands) app.add_directive("camerad-config-keys", CameradConfigKeys) - app.add_directive("camerad-fits-keywords", CameradFitsKeywords) app.connect("builder-inited", _validate) return {"parallel_read_safe": True, "parallel_write_safe": True} diff --git a/docs/architecture/index.md b/docs/architecture/index.md index 91c0918..b8f3a9f 100644 --- a/docs/architecture/index.md +++ b/docs/architecture/index.md @@ -102,10 +102,11 @@ See [frame output keys](../configuration/frame-outputs.md) for configuring them. ## Instrument modules -An instrument is a separate repository, checked out as a submodule under -`camerad/Instruments/` and selected with `-DINSTRUMENT=`. It contributes: +An instrument is a separate repository that builds on its own, pulling in the core with +`FetchContent`. It contributes: -1. A `.cmake` fragment setting `INSTRUMENT_SOURCES`. That is the whole build integration. +1. A `CMakeLists.txt` declaring its own `camerad` and Python module, linking the core's `camerad::` + libraries. {source}`reference_instrument` is the template. 2. An `Interface` subclass deriving from `ArchonInterface` or `AstroCamInterface`, overriding what the detector needs and adding commands through `instrument_cmd()` and `is_instrument_command()`. 3. Its own `ExposureMode` implementations, where the stock ones do not fit. @@ -118,4 +119,5 @@ Instrument commands are checked *before* the base command chain, so an instrumen and also override a base one of the same name. See [instruments](../instruments/index.md) for the modules that exist, and -{source}`camerad/Instruments/hispec_tracking_camera` for the most complete example. +[hispec-tracking-camera-instrument](https://github.com/CaltechOpticalObservatories/hispec-tracking-camera-instrument) +for the most complete example. diff --git a/docs/configuration/index.md b/docs/configuration/index.md index a99afc8..d0e2e5b 100644 --- a/docs/configuration/index.md +++ b/docs/configuration/index.md @@ -47,7 +47,6 @@ These are CMake options, fixed when the software is compiled, not `.cfg` keys. | Option | Default | Meaning | |---|---|---| | `CONTROLLER` | none, required | `archon` or `astrocam`. CMake errors out if unset. | -| `INSTRUMENT` | none | Instrument module to build, from `camerad/Instruments/` | | `INTERFACE_TYPE` | `Archon` | Selects which emulator is built, independently of `CONTROLLER`. `AstroCam` builds none, since none exists for ARC. | | `ENABLE_SHM_OUTPUT` | `OFF` | Build the ImageStreamIO shared-memory output. Also needs `-DImageStreamIO_DIR=/lib/cmake`. | | `BUILD_PYTHON_MODULE` | `OFF` | Build the `camera_interface` Python module. Needs pybind11 at compile time only. | diff --git a/docs/development/index.md b/docs/development/index.md index f777ac8..12c75e3 100644 --- a/docs/development/index.md +++ b/docs/development/index.md @@ -24,7 +24,6 @@ cross-checked against the source, so the build fails when they diverge: |---|---| | Base commands | The dispatch chain in {source}`camerad/camera_server.cpp`, with syntax from `CAMERAD_SYNTAX` | | Configuration keys | Reads through a config object's `param` array, plus the frame output parser | -| ATC FITS keywords | The `HeaderDictEntry` table in {source}`camerad/Instruments/hispec_tracking_camera/fits_header_dictionary.cpp` | Descriptions are written by hand in `docs/data/`, keyed by command or key name. Adding a command or a config key to the source without a description there fails the docs build; so does describing one @@ -87,13 +86,11 @@ The end-to-end tests run against the [emulator](../emulator/index.md) in CI, in `.github/workflows/emulator-integration.yml`. :::{warning} -The build workflow compiles the default target, and an instrument only when one is named explicitly. -`hispec_tracking_camera` is the only instrument any workflow builds, so the other modules can fall -behind changes to the core without CI noticing. +CI builds {source}`reference_instrument` but no real instrument, so an instrument can fall behind +changes to the core without CI here noticing. ::: -## Instrument submodules +## Instruments -Instrument modules are pinned submodules. Updating one is a commit to `camera-interface` that moves -the pin, which `.github/workflows/update-submodules.yml` automates. Documentation for an instrument -lives here, in this repository, while each instrument repository keeps its own README. +Each instrument builds from its own repository and pins the core there. Documentation for an +instrument lives here, in this repository, while each instrument repository keeps its own README. diff --git a/docs/getting-started/index.md b/docs/getting-started/index.md index 6fde315..49dca03 100644 --- a/docs/getting-started/index.md +++ b/docs/getting-started/index.md @@ -24,14 +24,13 @@ sudo apt-get install -y build-essential cmake ninja-build \ ## Build -`-DCONTROLLER=` is required; CMake stops with an error without it. `-DINSTRUMENT=` selects an -instrument module from `camerad/Instruments/`. +`-DCONTROLLER=` is required; CMake stops with an error without it. This builds the core with no +instrument; an instrument builds from its own repository, see [instruments](../instruments/index.md). ```bash -git clone --recurse-submodules \ - https://github.com/CaltechOpticalObservatories/camera-interface.git +git clone https://github.com/CaltechOpticalObservatories/camera-interface.git cd camera-interface/build -cmake -DCONTROLLER=archon -DINSTRUMENT=hispec_tracking_camera .. +cmake -DCONTROLLER=archon .. make ``` @@ -39,12 +38,6 @@ Binaries land in `bin/` in the source tree. `make install` copies them under `CMAKE_INSTALL_PREFIX` instead, which is the better choice for anything deployed. The full set of build options is in the [configuration reference](../configuration/index.md). -:::{note} -Instrument modules are git submodules. A clone without `--recurse-submodules` leaves -`camerad/Instruments/` empty and `-DINSTRUMENT=` will fail. Fix it with -`git submodule update --init --recursive`. -::: - ## Run it This uses `config/frame_outputs_test/frame_outputs_test.cfg`, which is the config the CI integration diff --git a/docs/index.md b/docs/index.md index 7f47814..5a0cc9c 100644 --- a/docs/index.md +++ b/docs/index.md @@ -14,9 +14,9 @@ It supports two controller families, selected at build time with `-DCONTROLLER=` : Astronomical Research Cameras ("Leach") controllers, reached over a PCIe driver. Requires ARC API 3.6 and the Arc66PCIe driver. -A detector-specific **instrument** module is layered on top of the controller, also selected at build -time, with `-DINSTRUMENT=`. The instrument supplies the exposure modes, extra commands and FITS -keywords that a particular camera needs. +A detector-specific **instrument** module is layered on top of the controller. It lives in its own +repository and builds there, pulling in this one. The instrument supplies the exposure modes, extra +commands and FITS keywords that a particular camera needs. Clients can drive the server three ways: the text protocol over a socket, the `camerad-socksend` command line tool, or the `camera_interface` Python module, which skips the server process entirely diff --git a/docs/instruments/cryoscope.md b/docs/instruments/cryoscope.md index 559d777..83208a1 100644 --- a/docs/instruments/cryoscope.md +++ b/docs/instruments/cryoscope.md @@ -2,18 +2,13 @@ An H2RG on an Archon controller, reading in RXR mode. -Repository: [cryoscope-instrument](https://github.com/CaltechOpticalObservatories/cryoscope-instrument), -checked out at `camerad/Instruments/cryoscope`. +Repository: [cryoscope-instrument](https://github.com/CaltechOpticalObservatories/cryoscope-instrument). :::{warning} -This module does not currently build against the core. Its exposure mode instantiates -`ExposureModeTemplate` with two template parameters where the base declares one, and assigns to -`modetype` and `modeargs`, which are named `type` and `args` in -{source}`camerad/exposure_modes.h`. It was left behind by a refactor of the exposure mode base -class. - -CI does not catch this, because the build workflow compiles the default target and only builds an -instrument when one is named explicitly. +This module does not currently build. It has no `CMakeLists.txt` of its own, only a `cryoscope.cmake` +for the retired in-core build. Its exposure mode also instantiates `ExposureModeTemplate` with two +template parameters where the base declares one, and assigns to `modetype` and `modeargs`, which +are named `type` and `args` in {source}`camerad/exposure_modes.h`. ::: ## What it defines @@ -33,11 +28,3 @@ placeholder named `XXX`. `START_PARAM` is read only by this module, which is why it does not appear in the [core key tables](../configuration/core.md). - -## Build - -```bash -cd build -cmake -DCONTROLLER=archon -DINSTRUMENT=cryoscope .. -make -``` diff --git a/docs/instruments/hispec-tracking-camera.md b/docs/instruments/hispec-tracking-camera.md index e057ce7..90d5e9b 100644 --- a/docs/instruments/hispec-tracking-camera.md +++ b/docs/instruments/hispec-tracking-camera.md @@ -1,20 +1,20 @@ # HISPEC tracking camera The HISPEC acquisition and tracking camera (ATC): a 2048x2048 H2RG on an Archon controller. This is -the most complete instrument module and the one the emulator integration tests exercise. +the most complete instrument module. -Repository: [hispec-tracking-camera-instrument](https://github.com/CaltechOpticalObservatories/hispec-tracking-camera-instrument), -checked out at `camerad/Instruments/hispec_tracking_camera`. +Repository: [hispec-tracking-camera-instrument](https://github.com/CaltechOpticalObservatories/hispec-tracking-camera-instrument). ## Build +From that repository's checkout: + ```bash -cd build -cmake -DCONTROLLER=archon -DINSTRUMENT=hispec_tracking_camera .. -make +cmake -S . -B build +cmake --build build ``` -Shipped configuration is in the submodule's `config/`: `hispecatc.cfg` and `hispecatc.acf`. +Shipped configuration is in the repository's `config/`: `hispecatc.cfg` and `hispecatc.acf`. ## Three things called "mode" @@ -118,21 +118,11 @@ has no effect. ## FITS keywords The module carries its own keyword dictionary mapping each internal property to a keyword, comment, -type and default. It covers two cameras, ATC and SPEC, with separate defaults; the table shows the -ATC default, falling back to the SPEC one where ATC has none. +type and default. It covers two cameras, ATC and SPEC, with separate defaults. It is +`fits_header_dictionary.cpp` in the instrument repository. `python/tests/fits_header_check.py` validates a written file against this dictionary. -```{eval-rst} -.. camerad-fits-keywords:: - :widths: 12 20 10 10 48 -``` - -:::{note} -Generated from {source}`camerad/Instruments/hispec_tracking_camera/fits_header_dictionary.cpp`, so -it cannot drift from the dictionary the instrument actually writes. -::: - ## Error reporting Every error this module logs carries a one-line state summary, so a failure records the camera state diff --git a/docs/instruments/index.md b/docs/instruments/index.md index 9e05b92..276b115 100644 --- a/docs/instruments/index.md +++ b/docs/instruments/index.md @@ -1,11 +1,11 @@ # Instruments An instrument module adapts the core to one detector: its exposure modes, its extra commands, its -FITS keywords. One is chosen at build time with `-DINSTRUMENT=`, and a given binary serves exactly -one instrument. +FITS keywords. A given binary serves exactly one instrument. -Each lives in its own repository, pulled in as a submodule under `camerad/Instruments/`. The -submodule is pinned, so a given `camera-interface` commit builds one specific instrument revision. +Each lives in its own repository and builds there, pulling in `camera-interface` with +`FetchContent`. The instrument pins the core, so a given instrument commit builds against one +specific core revision. ## Status @@ -17,11 +17,11 @@ submodule is pinned, so a given `camera-interface` commit builds one specific in | `deimos` | | | Repository exists, no sources yet | :::{note} -`hispec` and `deimos` are README-only submodules today. They are listed so the set is not misleading +`hispec` and `deimos` are README-only repositories today. They are listed so the set is not misleading about what exists; there is nothing to document until they carry sources. -Only `hispec_tracking_camera` is built in CI, so the others can fall behind changes to the core -without anything noticing. +The core's CI builds only {source}`reference_instrument`, so an instrument can fall behind changes +to the core without anything here noticing. ::: ```{toctree} @@ -33,13 +33,15 @@ cryoscope ## Building one +From the instrument's own checkout: + ```bash -git submodule update --init camerad/Instruments/ -cd build -cmake -DCONTROLLER=archon -DINSTRUMENT= .. -make +cmake -S . -B build +cmake --build build ``` +{source}`reference_instrument` is the template for a new one, including its `CMakeLists.txt`. + The instrument name is also checked against the `INSTRUMENT` key in the `.cfg`, so a config cannot be pointed at a binary built for a different camera. diff --git a/docs/python/index.md b/docs/python/index.md index da024c8..bb22e31 100644 --- a/docs/python/index.md +++ b/docs/python/index.md @@ -24,8 +24,7 @@ Use it when the caller *is* the control system, so the text protocol would only `import camera_interface` needs no `PYTHONPATH` and `camerad` is on `PATH`: ```bash -pip install ./camera-interface \ - --config-settings=cmake.define.INSTRUMENT=hispec_tracking_camera +pip install ./camera-interface ``` Any CMake option can be passed the same way, so @@ -37,9 +36,9 @@ A compiler and the full C++ dependency set must be present wherever `pip install builds from source. :::{important} -The controller and instrument are fixed when the wheel is built, and the module is always named -`camera_interface`, so one environment holds one instrument. Install into a separate environment per -instrument, and have the caller assert which one it got: +This builds the core with no instrument. An instrument's module comes from that instrument's own +repository. The controller and instrument are fixed when a wheel is built, so have the caller +assert which build it got: ```python assert camera_interface.instrument_name() == "hispec_tracking_camera" diff --git a/emulator/CMakeLists.txt b/emulator/CMakeLists.txt index e796725..18235f6 100644 --- a/emulator/CMakeLists.txt +++ b/emulator/CMakeLists.txt @@ -50,11 +50,7 @@ target_include_directories(emulator PRIVATE ${CFITSIO_INCLUDE_DIRS}) target_link_directories(emulator PRIVATE ${CFITSIO_LIBRARY_DIRS}) # Prefixed because "emulator" is too generic for a shared bin directory -if( CAMERAD_PYTHON_PACKAGE ) - set_target_properties(emulator PROPERTIES OUTPUT_NAME camerad-emulator-${CAMERAD_PACKAGE_SUFFIX}) -else() - set_target_properties(emulator PROPERTIES OUTPUT_NAME camerad-emulator) -endif() +set_target_properties(emulator PROPERTIES OUTPUT_NAME camerad-emulator) if( CAMERAD_INSTALL ) install(TARGETS emulator RUNTIME DESTINATION ${CAMERAD_INSTALL_BINDIR}) diff --git a/packaging/tracking/pyproject.toml b/packaging/tracking/pyproject.toml deleted file mode 100644 index c770646..0000000 --- a/packaging/tracking/pyproject.toml +++ /dev/null @@ -1,18 +0,0 @@ -[build-system] -requires = ["scikit-build-core>=0.10", "pybind11>=2.12"] -build-backend = "scikit_build_core.build" - -[project] -name = "camera-interface-tracking" -version = "2.0.0" -description = "camera_interface built for the HISPEC tracking camera" -requires-python = ">=3.10" - -[tool.scikit-build] -cmake.version = ">=3.24" -cmake.source-dir = "../.." -wheel.packages = [] - -cmake.define.BUILD_PYTHON_MODULE = "ON" -cmake.define.CONTROLLER = "archon" -cmake.define.INSTRUMENT = "hispec_tracking_camera" diff --git a/python/CMakeLists.txt b/python/CMakeLists.txt index 968b9dd..a6f2259 100644 --- a/python/CMakeLists.txt +++ b/python/CMakeLists.txt @@ -27,26 +27,15 @@ endif() find_package(pybind11 CONFIG REQUIRED) -pybind11_add_module(camera_interface - ${INSTRUMENT_REGISTRATION} - ${INSTRUMENT_OBJECTS} -) +pybind11_add_module(camera_interface ${INTERFACE_FACTORY}) target_include_directories(camera_interface PRIVATE ${PROJECT_BASE_DIR}/camerad - ${PROJECT_BASE_DIR}/camerad/Instruments ${PROJECT_BASE_DIR}/utils ${PROJECT_BASE_DIR}/common ${INTERFACE_INCLUDES} ) -# Without an instrument the module source reports "none" -if (INSTRUMENT) - target_compile_definitions(camera_interface PRIVATE - CAMERAD_INSTRUMENT_NAME="${INSTRUMENT}" - ) -endif() - # camerad_python carries the module source, so it compiles here, with this # target's instrument name target_link_libraries(camera_interface PRIVATE @@ -55,33 +44,13 @@ target_link_libraries(camera_interface PRIVATE ) if (CAMERAD_INSTALL) - if (CAMERAD_PYTHON_PACKAGE) - # The module keeps its name inside the package; the __init__.py lets - # "import " behave as the module too - file(WRITE ${CMAKE_CURRENT_BINARY_DIR}/package/__init__.py [[ -from . import camera_interface as _module -from .camera_interface import * # noqa: F401,F403 - -__all__ = [name for name in dir(_module) if not name.startswith("_")] -]]) - install(TARGETS camera_interface - LIBRARY DESTINATION ${CAMERAD_INSTALL_LIBDIR}/${CAMERAD_PYTHON_PACKAGE}) - install(FILES ${CMAKE_CURRENT_BINARY_DIR}/package/__init__.py - DESTINATION ${CAMERAD_INSTALL_LIBDIR}/${CAMERAD_PYTHON_PACKAGE}) - else() - install(TARGETS camera_interface LIBRARY DESTINATION ${CAMERAD_INSTALL_LIBDIR}) - endif() + install(TARGETS camera_interface LIBRARY DESTINATION ${CAMERAD_INSTALL_LIBDIR}) endif() # A shared library may leave symbols undefined, so a module missing its # interface factory definition links and fails only when imported -if (INSTRUMENT) - set(CAMERAD_EXPECTED_INSTRUMENT ${INSTRUMENT}) -else() - set(CAMERAD_EXPECTED_INSTRUMENT none) -endif() add_test(NAME python_import - COMMAND ${Python3_EXECUTABLE} -c "import camera_interface\nassert camera_interface.instrument_name() == '${CAMERAD_EXPECTED_INSTRUMENT}', camera_interface.instrument_name()\nassert camera_interface.controller_name() == '${CONTROLLER}', camera_interface.controller_name()" + COMMAND ${Python3_EXECUTABLE} -c "import camera_interface\nassert camera_interface.instrument_name() == 'none', camera_interface.instrument_name()\nassert camera_interface.controller_name() == '${CONTROLLER}', camera_interface.controller_name()" ) set_tests_properties(python_import PROPERTIES ENVIRONMENT PYTHONPATH=$ diff --git a/utils/CMakeLists.txt b/utils/CMakeLists.txt index d5dd7bc..fe1ba83 100644 --- a/utils/CMakeLists.txt +++ b/utils/CMakeLists.txt @@ -80,8 +80,7 @@ if(CAMERAD_BUILD_TOOLS) set_target_properties(socksend PROPERTIES OUTPUT_NAME camerad-socksend) endif() -# An instrument package leaves the generic tools to the generic package -if(CAMERAD_BUILD_TOOLS AND CAMERAD_INSTALL AND NOT CAMERAD_PYTHON_PACKAGE) +if(CAMERAD_BUILD_TOOLS AND CAMERAD_INSTALL) install(TARGETS socksend RUNTIME DESTINATION ${CAMERAD_INSTALL_BINDIR}) if (ENABLE_SHM_OUTPUT) install(TARGETS shm_reader RUNTIME DESTINATION ${CAMERAD_INSTALL_BINDIR}) From 34d2703b93764db391e52b8011177e28ebbaa2f3 Mon Sep 17 00:00:00 2001 From: Mike Langmayr Date: Thu, 22 Oct 2026 15:22:15 -0700 Subject: [PATCH 2/2] Remove instrument pages from the core docs --- README.md | 6 +- docs/architecture/index.md | 9 +- docs/configuration/frame-outputs.md | 2 +- docs/fits/index.md | 4 +- docs/instruments/cryoscope.md | 30 ----- docs/instruments/hispec-tracking-camera.md | 131 --------------------- docs/instruments/index.md | 21 +--- docs/python/index.md | 6 +- 8 files changed, 14 insertions(+), 195 deletions(-) delete mode 100644 docs/instruments/cryoscope.md delete mode 100644 docs/instruments/hispec-tracking-camera.md diff --git a/README.md b/README.md index 141f7b1..5634f90 100644 --- a/README.md +++ b/README.md @@ -223,7 +223,7 @@ Built with `-DBUILD_PYTHON_MODULE=ON`, `camera_interface` lets a Python process ```python import camera_interface -camera = camera_interface.Camera("hispecatc.cfg") +camera = camera_interface.Camera("reference.cfg") camera.open() camera.load() camera.power("on") @@ -255,7 +255,7 @@ $ python python/examples/raw_channel_sweep.py --config lris2.cfg --fits-dir /tmp ## Frame Outputs -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. +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). Both outputs are independent; either, both, or neither can be enabled per instrument. ### Streams @@ -290,7 +290,7 @@ Frame geometry (width/height/pixel depth) isn't a config key: it's fixed for an Two readers ship with the repo. `camerad-shm-reader` prints geometry, keywords and pixel statistics once, for diagnostics. `python/examples/shm_read_frames.py` is a sample streaming consumer: it blocks on the stream's semaphore and reports every frame as it arrives, flagging any it missed. ```bash -$ python python/examples/shm_read_frames.py --segment hispec_tracking_camera --count 10 +$ python python/examples/shm_read_frames.py --segment camera --count 10 ``` It needs numpy and `ImageStreamIOWrap`, the Python wrapper from the ImageStreamIO source tree, built with `-DPYTHON_WRAPPER=ON`; the wrapper is not on PyPI. diff --git a/docs/architecture/index.md b/docs/architecture/index.md index b8f3a9f..0ab5994 100644 --- a/docs/architecture/index.md +++ b/docs/architecture/index.md @@ -59,8 +59,8 @@ An exposure mode is a producer/consumer pair over a bounded queue: The base class carries the synchronization (`queue_mutex`, `queue_cv`, and the `is_producer_finished` / `is_producer_error` / `is_consumer_error` flags), so a concrete mode -implements only the parts that differ. In the tracking camera module, for instance, a shared base -owns the queue and the consumer and each subclass implements only the producer. +implements only the parts that differ, for instance a shared base that owns the queue and the +consumer with each subclass implementing only the producer. Two lifetimes exist. For a counted exposure the consumer is one-shot: `do_expose()` spawns it, and it terminates once the producer is finished and the queue is drained. In freerun, one producer @@ -118,6 +118,5 @@ concrete instrument. Instrument commands are checked *before* the base command chain, so an instrument can add commands and also override a base one of the same name. -See [instruments](../instruments/index.md) for the modules that exist, and -[hispec-tracking-camera-instrument](https://github.com/CaltechOpticalObservatories/hispec-tracking-camera-instrument) -for the most complete example. +See [instruments](../instruments/index.md) for building one, and {source}`reference_instrument` +for a minimal example. diff --git a/docs/configuration/frame-outputs.md b/docs/configuration/frame-outputs.md index bfcaeca..784eb39 100644 --- a/docs/configuration/frame-outputs.md +++ b/docs/configuration/frame-outputs.md @@ -54,7 +54,7 @@ camerad-shm-reader [shm-dir] frames it missed: ```bash -python python/examples/shm_read_frames.py --segment hispec_tracking_camera --count 10 +python python/examples/shm_read_frames.py --segment camera --count 10 ``` It needs numpy and `ImageStreamIOWrap`, the Python wrapper from the ImageStreamIO source tree built diff --git a/docs/fits/index.md b/docs/fits/index.md index 4702b71..5f166bf 100644 --- a/docs/fits/index.md +++ b/docs/fits/index.md @@ -55,8 +55,8 @@ User keywords : Added at runtime with `key KEYWORD=VALUE//COMMENT`. `key list` shows both the system and user sets, and `key KEYWORD=.` deletes one. -Instrument modules supply their own dictionary on top of this; see the -[tracking camera keyword table](../instruments/hispec-tracking-camera.md) for the worked example. +Instrument modules supply their own dictionary on top of this, documented in each instrument's +repository. ## Checking a header diff --git a/docs/instruments/cryoscope.md b/docs/instruments/cryoscope.md deleted file mode 100644 index 83208a1..0000000 --- a/docs/instruments/cryoscope.md +++ /dev/null @@ -1,30 +0,0 @@ -# CryoScope - -An H2RG on an Archon controller, reading in RXR mode. - -Repository: [cryoscope-instrument](https://github.com/CaltechOpticalObservatories/cryoscope-instrument). - -:::{warning} -This module does not currently build. It has no `CMakeLists.txt` of its own, only a `cryoscope.cmake` -for the retired in-core build. Its exposure mode also instantiates `ExposureModeTemplate` with two -template parameters where the base declares one, and assigns to `modetype` and `modeargs`, which -are named `type` and `args` in {source}`camerad/exposure_modes.h`. -::: - -## What it defines - -`CryoScope` derives from `ArchonInterface` and overrides `instrument_cmd`, -`configure_instrument`, `power`, `get_exposure_modes` and `set_exposure_mode`, plus a private -`setup_detector()`. - -It registers no instrument-specific commands beyond the base set, and its exposure mode is still a -placeholder named `XXX`. - -## Configuration - -| Key | Meaning | -|---|---| -| `START_PARAM` | Archon parameter used to start the timing script | - -`START_PARAM` is read only by this module, which is why it does not appear in the -[core key tables](../configuration/core.md). diff --git a/docs/instruments/hispec-tracking-camera.md b/docs/instruments/hispec-tracking-camera.md deleted file mode 100644 index 90d5e9b..0000000 --- a/docs/instruments/hispec-tracking-camera.md +++ /dev/null @@ -1,131 +0,0 @@ -# HISPEC tracking camera - -The HISPEC acquisition and tracking camera (ATC): a 2048x2048 H2RG on an Archon controller. This is -the most complete instrument module. - -Repository: [hispec-tracking-camera-instrument](https://github.com/CaltechOpticalObservatories/hispec-tracking-camera-instrument). - -## Build - -From that repository's checkout: - -```bash -cmake -S . -B build -cmake --build build -``` - -Shipped configuration is in the repository's `config/`: `hispecatc.cfg` and `hispecatc.acf`. - -## Three things called "mode" - -The single most confusing thing about this module is that three unrelated settings are all called a -mode. They are selected by different commands and do different things. - -Camera mode (`mode`) -: Names a `[MODE_*]` section of the loaded ACF. Selecting one loads that section's geometry - (`PIXELCOUNT`, `LINECOUNT`), its parameters, and its tapline layout (`TAPLINES`, `TAPLINE0..N`) - into the controller. This is the heavyweight one: it changes the shape of the data coming back. - Requires firmware to already be loaded. - -Exposure mode (`exposure`) -: Selects the H2RG readout scheme. Implemented by setting the matching `mode_*` ACF parameter to 1 - and every other one to 0, so the ACF must define all four. - - | Argument | ACF parameter | Scheme | - |---|---|---| - | `utr_rr` | `mode_UTR_RR` | Up the ramp, reset-read | - | `utr_gr` | `mode_UTR_GR` | Up the ramp, guided read | - | `rx` | `mode_RX` | Reset-execute | - | `rxr` | `mode_RXR` | Reset-execute-read | - -Acquisition mode (`exposuremode`) -: The base command, selecting which `Camera::ExposureMode` implementation drives acquisition. This - module provides `DEFAULT` and `AUTOFETCH`. See [architecture](../architecture/index.md) for what - an exposure mode is. - -:::{tip} -`exposure` with no argument reports the current readout scheme. `mode` with no argument reports the -current camera mode. Neither changes anything when queried. -::: - -## Instrument commands - -Reached through the normal command interface, and from Python via `instrument_cmd()`. - -| Command | Purpose | -|---|---| -| `h2rg_init` | Re-trigger the H2RG main reset and enable Pad B output with HIGHOHM | -| `mode` | Select or report the camera mode from the ACF | -| `exposure` | Select or report the H2RG readout scheme | -| `exposuremode` | Select the acquisition mode (base command) | -| `autofetch_mode` | Control autofetch, where the controller streams frames continuously | -| `freerun` | Arm (`1`) or disarm (`0`) continuous exposure | -| `window_mode` | Enter or leave windowed readout | -| `roi` | Set or report the region of interest | -| `take_stats` | Report pixel statistics | -| `debug` | Development diagnostics | - -`instrument_commands()` enumerates them at runtime, which is authoritative for a given build. - -:::{note} -`h2rg_init` exists because the ACF defaults `Start` to 1, so it is already true at load time and -never sees the 0 to 1 edge the H2RG main reset needs. Running it after power-up re-triggers that -edge. It is not optional on a cold start. -::: - -Setting `autofetch_mode` also resets the readout scheme to the default, so select `exposure` after -`autofetch_mode`, not before. - -## Region of interest - -`roi` takes four argument shapes: - -| Form | Meaning | -|---|---| -| `roi` | Report the current window as `vstart vstop hstart hstop` | -| `roi ` | A centred region of that size | -| `roi ` | An explicit region | -| `roi fullframe` | Return to the full 2048x2048 frame | - -`window_mode` is the underlying toggle. Leaving window mode restores the saved `TAPLINES` and -`TAPLINE0` values, switches the camera back to the `DEFAULT` ACF mode to reset the internal buffer -geometry, and reapplies that mode's `PIXELCOUNT`. That teardown is why leaving window mode is not -simply the inverse of entering it, and why it needs the ACF to have a `DEFAULT` mode. - -## Configuration - -Alongside the [core keys](../configuration/index.md), this module reads its own: - -| Key | Default | Meaning | -|---|---|---| -| `REFPIX_AMP` | `AM52` | Amplifier reading the reference channel, recorded as `REFPXAMP`. The reference channel's tapline moves with the camera mode; its amplifier does not. | -| `PIXEL_TIME_USEC` | built-in | Pixel time used to model the readout deadline, when the ACF does not supply one | -| `READOUT_MARGIN_MSEC` | built-in | Slack added to the computed per-frame readout deadline | - -`PIXEL_TIME_USEC` and `READOUT_MARGIN_MSEC` set how long the acquisition thread waits for a frame -before calling it lost. Both must be positive, and a non-numeric value makes startup fail rather -than silently falling back. - -Fixed in `configure_instrument()` rather than configured: the LVDS module is 10 and the detector's -maximum pixel index is 2047. The Archon socket is also tuned there for streaming, with `TCP_NODELAY` -and 1 MB buffers. - -:::{warning} -`WRITE_TAPINFO_TO_FITS` appears in the shipped `hispecatc.cfg` but is read by nothing. Setting it -has no effect. -::: - -## FITS keywords - -The module carries its own keyword dictionary mapping each internal property to a keyword, comment, -type and default. It covers two cameras, ATC and SPEC, with separate defaults. It is -`fits_header_dictionary.cpp` in the instrument repository. - -`python/tests/fits_header_check.py` validates a written file against this dictionary. - -## Error reporting - -Every error this module logs carries a one-line state summary, so a failure records the camera state -that produced it rather than leaving it to be reconstructed from surrounding log lines that a -concurrent command may have interleaved. Callers get a short reason; the log gets the root cause and -the state. diff --git a/docs/instruments/index.md b/docs/instruments/index.md index 276b115..6aed8c5 100644 --- a/docs/instruments/index.md +++ b/docs/instruments/index.md @@ -5,32 +5,13 @@ FITS keywords. A given binary serves exactly one instrument. Each lives in its own repository and builds there, pulling in `camera-interface` with `FetchContent`. The instrument pins the core, so a given instrument commit builds against one -specific core revision. - -## Status - -| Instrument | Controller | Detector | State | -|---|---|---|---| -| [hispec_tracking_camera](hispec-tracking-camera.md) | Archon | H2RG | In active use. The reference implementation. | -| [cryoscope](cryoscope.md) | Archon | H2RG | Scaffolding, and does not currently build | -| `hispec` | Archon | | Repository exists, no sources yet | -| `deimos` | | | Repository exists, no sources yet | +specific core revision. Each instrument documents itself in its own repository. :::{note} -`hispec` and `deimos` are README-only repositories today. They are listed so the set is not misleading -about what exists; there is nothing to document until they carry sources. - The core's CI builds only {source}`reference_instrument`, so an instrument can fall behind changes to the core without anything here noticing. ::: -```{toctree} -:hidden: - -hispec-tracking-camera -cryoscope -``` - ## Building one From the instrument's own checkout: diff --git a/docs/python/index.md b/docs/python/index.md index bb22e31..7e0940c 100644 --- a/docs/python/index.md +++ b/docs/python/index.md @@ -7,7 +7,7 @@ then the command set is available as methods. ```python import camera_interface -camera = camera_interface.Camera("hispecatc.cfg") +camera = camera_interface.Camera("reference.cfg") camera.open() camera.load() camera.power("on") @@ -41,7 +41,7 @@ repository. The controller and instrument are fixed when a wheel is built, so ha assert which build it got: ```python -assert camera_interface.instrument_name() == "hispec_tracking_camera" +assert camera_interface.instrument_name() == "reference" ``` ::: @@ -113,7 +113,7 @@ serialize acquisition behind disk I/O. Anything that must be woken per frame sho Logging follows `LOG_STDERR` from the `.cfg`, overridable per session with `log_to_stderr=`: ```python -camera = camera_interface.Camera("hispecatc.cfg", log_to_stderr=True) +camera = camera_interface.Camera("reference.cfg", log_to_stderr=True) ``` The C++ log always goes to its daily file under `LOGPATH` regardless.