From 2186fb68ece9e46ced9a3152dcbb68187e3af16b Mon Sep 17 00:00:00 2001 From: Mike Langmayr <1809691+mikelangmayr@users.noreply.github.com> Date: Tue, 22 Sep 2026 15:25:24 -0700 Subject: [PATCH 1/3] Add Sphinx documentation with pull request preview deployments --- .github/workflows/docs.yml | 100 +++++++++++++++++++++ .gitignore | 1 + README.md | 7 ++ _config.yml | 2 - docs/architecture/index.md | 69 ++++++++++++++ docs/commands/async-messages.md | 24 +++++ docs/commands/base.md | 40 +++++++++ docs/commands/controller.md | 38 ++++++++ docs/commands/index.md | 74 +++++++++++++++ docs/conf.py | 56 ++++++++++++ docs/configuration/core.md | 80 +++++++++++++++++ docs/configuration/exposure-time.md | 26 ++++++ docs/configuration/frame-outputs.md | 61 +++++++++++++ docs/configuration/index.md | 58 ++++++++++++ docs/development/index.md | 72 +++++++++++++++ docs/emulator/index.md | 44 +++++++++ docs/fits/index.md | 49 ++++++++++ docs/getting-started/index.md | 96 ++++++++++++++++++++ docs/index.md | 85 ++++++++++++++++++ docs/instruments/cryoscope.md | 19 ++++ docs/instruments/hispec-tracking-camera.md | 62 +++++++++++++ docs/instruments/index.md | 43 +++++++++ docs/python/index.md | 92 +++++++++++++++++++ docs/requirements.txt | 5 ++ 24 files changed, 1201 insertions(+), 2 deletions(-) create mode 100644 .github/workflows/docs.yml delete mode 100644 _config.yml create mode 100644 docs/architecture/index.md create mode 100644 docs/commands/async-messages.md create mode 100644 docs/commands/base.md create mode 100644 docs/commands/controller.md create mode 100644 docs/commands/index.md create mode 100644 docs/conf.py create mode 100644 docs/configuration/core.md create mode 100644 docs/configuration/exposure-time.md create mode 100644 docs/configuration/frame-outputs.md create mode 100644 docs/configuration/index.md create mode 100644 docs/development/index.md create mode 100644 docs/emulator/index.md create mode 100644 docs/fits/index.md create mode 100644 docs/getting-started/index.md create mode 100644 docs/index.md create mode 100644 docs/instruments/cryoscope.md create mode 100644 docs/instruments/hispec-tracking-camera.md create mode 100644 docs/instruments/index.md create mode 100644 docs/python/index.md create mode 100644 docs/requirements.txt diff --git a/.github/workflows/docs.yml b/.github/workflows/docs.yml new file mode 100644 index 0000000..f989f51 --- /dev/null +++ b/.github/workflows/docs.yml @@ -0,0 +1,100 @@ +name: Documentation + +on: + push: + branches: ["main"] + pull_request: + types: [opened, synchronize, reopened, closed] + workflow_dispatch: + +# Serialize per ref so a rapid second push cannot race the first one's gh-pages commit +concurrency: + group: docs-${{ github.ref }} + cancel-in-progress: true + +permissions: + contents: write + pull-requests: write + +jobs: + build: + if: github.event.action != 'closed' + runs-on: ubuntu-latest + timeout-minutes: 30 + + steps: + - name: Checkout repository + uses: actions/checkout@v4 + with: + submodules: recursive + + - name: Set up Python + uses: actions/setup-python@v5 + with: + python-version: "3.12" + + - name: Install dependencies + run: | + bash .github/workflows/install-deps.sh + sudo apt-get install -y python3-dev + + - name: Build and install zmqpp from source + run: | + git clone --depth 1 https://github.com/zeromq/zmqpp.git /tmp/zmqpp + cmake -S /tmp/zmqpp -B /tmp/zmqpp/build + cmake --build /tmp/zmqpp/build -j"$(nproc)" + sudo cmake --install /tmp/zmqpp/build + sudo ldconfig + + # autodoc imports the compiled module, so the Python API page cannot drift from the bindings + - name: Install camera_interface + run: | + pip install . \ + --config-settings=cmake.define.INSTRUMENT=hispec_tracking_camera + + - name: Install documentation toolchain + run: pip install -r docs/requirements.txt + + - name: Build documentation + run: | + python -c "import camera_interface; print(camera_interface.instrument_name())" + sphinx-build -W -b html docs docs/_build/html + touch docs/_build/html/.nojekyll + + - name: Upload rendered site + uses: actions/upload-artifact@v4 + with: + name: documentation + path: docs/_build/html + retention-days: 14 + + - name: Publish to the site root + if: github.event_name == 'push' && github.ref == 'refs/heads/main' + uses: peaceiris/actions-gh-pages@v4 + with: + github_token: ${{ secrets.GITHUB_TOKEN }} + publish_dir: docs/_build/html + # Preserve previews/, which lives on the same branch + keep_files: true + + # Fork pull requests get a read-only token and cannot deploy; they use the artifact instead + - name: Publish pull request preview + if: > + github.event_name == 'pull_request' && + github.event.pull_request.head.repo.full_name == github.repository + uses: rossjrw/pr-preview-action@v1 + with: + source-dir: docs/_build/html + umbrella-dir: previews + action: deploy + + remove-preview: + if: github.event.action == 'closed' + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + + - uses: rossjrw/pr-preview-action@v1 + with: + umbrella-dir: previews + action: remove diff --git a/.gitignore b/.gitignore index 246dfab..af29076 100644 --- a/.gitignore +++ b/.gitignore @@ -5,3 +5,4 @@ cmake-build* /lib/ /bin/ __pycache__/ +/docs/_build/ diff --git a/README.md b/README.md index 8c6d9d7..2501c5c 100644 --- a/README.md +++ b/README.md @@ -2,6 +2,13 @@ Camera Detector Controller Interface Software +## Documentation + +Full documentation is at +, covering the architecture, the +command and configuration references, the instruments, the emulator and the Python bindings. This +README stays focused on building and running; the site is the reference. + ## Reporting Issues If you encounter any problems or have questions about this project, please open an issue on the [GitHub Issues page](https://github.com/CaltechOpticalObservatories/camera-interface/issues). Your feedback helps us improve the project! diff --git a/_config.yml b/_config.yml deleted file mode 100644 index 4f9c932..0000000 --- a/_config.yml +++ /dev/null @@ -1,2 +0,0 @@ -theme: jekyll-theme-minimal -show_downloads: true diff --git a/docs/architecture/index.md b/docs/architecture/index.md new file mode 100644 index 0000000..885fc4e --- /dev/null +++ b/docs/architecture/index.md @@ -0,0 +1,69 @@ +# Architecture + +:::{note} +Expands in M3 with the thread model, the frame path and the instrument plugin contract in full. The +layering below is accurate now. +::: + +The C++ reference here is narrative and links to source rather than reproducing signatures. + +## Layers + +``` + client (socket, socksend, or Python module) + | + Camera::Server ........|...... command dispatch, ports, logging + | + Camera::Interface .....|...... the camera, as commands + | | + | +------ Camera::ExposureMode ..... how one exposure runs + | + Camera::Controller ....|...... the wire to the hardware + | + Archon (TCP) or ARC (PCIe) +``` + +`Camera::Server` ({source}`camerad/camera_server.h`) +: Owns the ports and the command dispatch loop. Parses a command line, calls the matching + `Interface` method, and formats the reply. Knows nothing about detectors. + +`Camera::Interface` ({source}`camerad/camera_interface.h`) +: The abstract camera. Declares one pure virtual per command: `expose`, `exptime`, `bias`, `bin`, + `load_firmware`, `power` and the rest. `ArchonInterface` and `AstroCamInterface` implement it for + the two controller families, and an instrument module subclasses one of those. + +`Camera::Controller` +: The transport to the hardware, separate from `Interface` so that the command semantics and the + wire protocol can vary independently. + +`Camera::ExposureMode` ({source}`camerad/exposure_modes.h`) +: One way of running an exposure, as a producer/consumer pair: an acquisition thread pulls frames + from the controller, a processing thread turns them into images. A single `expose` command means + different things for a slow CCD readout and a continuously reading H2RG, and this is where that + difference lives. `Interface::select_expose_mode()` picks one. + +## Instrument modules + +An instrument is a separate git repository, checked out as a submodule under +`camerad/Instruments/` and selected with `-DINSTRUMENT=`. It contributes four things: + +1. A `.cmake` fragment setting `INSTRUMENT_SOURCES`, which is the whole of the build + integration. +2. An `Interface` subclass, deriving from `ArchonInterface` or `AstroCamInterface`, overriding what + the detector needs and adding instrument commands via `instrument_cmd()` and + `is_instrument_command()`. +3. Its own `ExposureMode` implementations, if the stock ones do not fit. +4. Optionally a FITS header dictionary and shipped `.cfg` and `.acf` files. + +An interface factory function ties it together, so the core builds against the base class and never +names a concrete instrument. + +See [instruments](../instruments/index.md) for the four that exist, and +{source}`camerad/Instruments/hispec_tracking_camera` for the most complete example. + +## Frame path + +Acquisition is deliberately decoupled from output. The exposure mode's processing thread publishes a +completed frame to every configured [frame output](../configuration/frame-outputs.md); each output +owns its own queue and thread. Nothing an output does can block acquisition, which is why the FITS +writer drops frames instead of applying backpressure. diff --git a/docs/commands/async-messages.md b/docs/commands/async-messages.md new file mode 100644 index 0000000..ec0da06 --- /dev/null +++ b/docs/commands/async-messages.md @@ -0,0 +1,24 @@ +# Asynchronous messages + +Messages the server multicasts to `ASYNCGROUP` on `ASYNCPORT`, unprompted. Each is prefixed with a +tag naming its type, so a listener can filter without parsing the payload. + +:::{note} +The tag list below comes from the superseded 2022 ICD and is being re-verified against the source in +M2. Treat it as indicative until then. +::: + +| Tag | Meaning | +|---|---| +| `ERROR:message` | An error occurred | +| `NOTICE:message` | Informational notice | +| `EXPOSURE:n` | Exposure progress, Archon only | +| `EXPOSURE_d:n` | Exposure progress for device `d`, ARC only | +| `LINECOUNT:n` | Lines read out so far, Archon only | +| `PIXELCOUNT:n` | Pixels read out so far, ARC only | +| `FILE: COMPLETE` | A FITS file finished writing | +| `DATACUBE:n COMPLETE` or `ERROR` | A data cube finished | + +A listener wanting to wake on every frame should attach to the +[shared-memory output](../configuration/frame-outputs.md) instead, which posts a semaphore per +frame. The asynchronous port is for status, and is not a delivery guarantee. diff --git a/docs/commands/base.md b/docs/commands/base.md new file mode 100644 index 0000000..b33ae12 --- /dev/null +++ b/docs/commands/base.md @@ -0,0 +1,40 @@ +# Base commands + +Commands the server accepts regardless of which instrument it was built for. Availability still +depends on the controller: some are Archon-only, some ARC-only. + +:::{note} +This page currently lists the command set grouped by purpose. M2 of the documentation work replaces +it with a table generated from `CAMERAD_SYNTAX` in {source}`common/camerad_commands.h`, carrying the +full argument syntax and a description per command. +::: + +Any command accepts `?` as its argument to return its own syntax. + +## Connection and firmware + +`open`, `close`, `isopen`, `load`, `loadtiming`, `readacf`, `power`, `interface`, `config` + +## Exposure + +`expose`, `abort`, `stop`, `pause`, `resume`, `exptime`, `modexptime`, `exposuremode`, +`preexposures`, `shutter`, `readout`, `useframes` + +## Geometry and readout + +`geometry`, `imsize`, `buffer`, `bin`, `boi`, `bias`, `frametransfer`, `mode` + +## Output and FITS + +`imdir`, `autodir`, `basename`, `imnum`, `fitsname`, `fitsnaming`, `datacube`, `mex`, `mexamps`, +`key`, `writekeys` + +## Controller access + +`native`, `raw`, `heater`, `sensor` + +## Diagnostics + +`echo`, `test`, `longerror`, `exit` + +Instrument modules add their own commands on top of these; see [instruments](../instruments/index.md). diff --git a/docs/commands/controller.md b/docs/commands/controller.md new file mode 100644 index 0000000..7a51c96 --- /dev/null +++ b/docs/commands/controller.md @@ -0,0 +1,38 @@ +# Controller commands + +Commands passed through to the detector controller largely untouched. The server does not interpret +them, so this is the escape hatch for anything the higher-level commands do not cover. + +:::{note} +Fills out in M3 alongside the architecture chapter. +::: + +## Archon + +`native` sends an Archon command directly and returns its reply. The commands most worth knowing: + +`FRAME` +: Frame buffer status: which buffer is complete, frame numbers, timestamps, sizes. + +`STATUS` +: Backplane status, including module temperatures, voltages and currents. + +`SYSTEM` +: Module complement: what is in each slot, with type, revision and version. + +`raw` reaches the Archon configuration memory directly, to read the loaded configuration or set keys +in it. + +## ARC (AstroCam) + +ARC controllers take three-letter DSP commands. The ones the server exposes: + +| Command | Meaning | +|---|---| +| `PON` | Power on | +| `POF` | Power off | +| `RDM` | Read memory | +| `WRM` | Write memory | +| `SBN` | Set bias number | +| `SMX` | Set multiplexer | +| `TDL` | Test data link | diff --git a/docs/commands/index.md b/docs/commands/index.md new file mode 100644 index 0000000..0a2bd3f --- /dev/null +++ b/docs/commands/index.md @@ -0,0 +1,74 @@ +# Command reference + +The server speaks a line-oriented ASCII protocol over TCP. Commands are short mnemonics with +space-separated arguments; replies are plain text. + +## Ports + +Three ports are configured in the `.cfg` file, and they behave differently. + +Blocking port (`BLKPORT`) +: The connection stays open for as long as the client holds it, so it works directly with `telnet` + as an ad hoc command line. One command at a time: a command sent before the previous one has + replied is ignored. The reply on the same connection is what signals completion. Use this when the + order of execution matters. + +Non-blocking port (`NBPORT`) +: Accepts one command, then closes the connection. Each connection is handled on its own thread, so + commands can run concurrently. Their relative order is not guaranteed, which is the trade for the + concurrency. + +Asynchronous message port (`ASYNCPORT`) +: Connectionless UDP, multicast to `ASYNCGROUP`. Listen-only. Carries status the server emits on its + own schedule, such as exposure progress, along with replies to non-blocking commands and messages + too long for a command reply. Each message is prefixed with a tag naming its type. + +Connections to the non-blocking port that sit idle are closed after 3 seconds +({source}`utils/network.h`), so a client that opens a connection and never sends anything cannot +accumulate threads. + +The server serializes access to hardware that cannot tolerate concurrent use, whichever port the +commands arrive on. + +## Replies + +A reply is the command's return value, if any, followed by `DONE` or `ERROR`: + +``` +exptime 1.5 +1.500 DONE + +expose +DONE + +bogus +ERROR +``` + +`ERROR` means the server rejected or failed the command. With `LONGERROR=true` the reply carries a +human-readable reason as well. + +Two commands break the pattern: a command invoked with `?` returns its syntax with no `DONE` +suffix, and commands that reply with JSON return the JSON document alone. + +:::{warning} +`DONE` reports on the command, not on the frame outputs. `expose` returns `DONE` once the exposure +and readout complete, whether or not the FITS writer kept up. See +[frame outputs](../configuration/frame-outputs.md). +::: + +## Command tables + +```{toctree} +:maxdepth: 1 + +base +controller +async-messages +``` + +:::{note} +The base command table is generated from `CAMERAD_SYNTAX` in +{source}`common/camerad_commands.h` and cross-checked against the descriptions kept alongside the +docs, so a command added to the server without a description here fails the documentation build. +::: diff --git a/docs/conf.py b/docs/conf.py new file mode 100644 index 0000000..cfecdfd --- /dev/null +++ b/docs/conf.py @@ -0,0 +1,56 @@ +"""Sphinx configuration for the camera-interface documentation.""" + +import importlib.util +import tomllib +from pathlib import Path + +REPO_ROOT = Path(__file__).resolve().parent.parent + +project = "camera-interface" +author = "Caltech Optical Observatories" +copyright = "Caltech Optical Observatories" + +with (REPO_ROOT / "pyproject.toml").open("rb") as pyproject: + release = tomllib.load(pyproject)["project"]["version"] +version = release + +extensions = [ + "myst_parser", + "sphinx.ext.autodoc", + "sphinx.ext.extlinks", + "sphinx_copybutton", + "sphinx_design", +] + +exclude_patterns = ["_build"] + +# Link to a source file with {source}`camerad/camera_interface.h` instead of pasting signatures, +# since the C++ reference is narrative rather than generated +extlinks = { + "source": ( + "https://github.com/CaltechOpticalObservatories/camera-interface/blob/main/%s", + "%s", + ), +} + +myst_enable_extensions = ["colon_fence", "deflist", "substitution"] +myst_heading_anchors = 3 + +# The Python module is a compiled extension, so autodoc needs a real build to import. CI always has +# one, and a genuine import failure there must break the build; a local docs build without one still +# succeeds and shows a note in place of the API reference. +if importlib.util.find_spec("camera_interface"): + tags.add("has_python_module") # noqa: F821 (Sphinx injects `tags` into this namespace) +else: + suppress_warnings = ["autodoc"] + +autodoc_member_order = "bysource" +autodoc_default_options = {"members": True} + +html_theme = "furo" +html_title = f"camera-interface {release}" +html_theme_options = { + "source_repository": "https://github.com/CaltechOpticalObservatories/camera-interface/", + "source_branch": "main", + "source_directory": "docs/", +} diff --git a/docs/configuration/core.md b/docs/configuration/core.md new file mode 100644 index 0000000..0a229a4 --- /dev/null +++ b/docs/configuration/core.md @@ -0,0 +1,80 @@ +# Core keys + +Keys read by the server itself, independent of instrument or frame output. + +:::{note} +This table is written by hand today. M2 of the documentation work replaces it with a generated table +cross-checked against the keys the source actually honours. +::: + +## Controller connection + +| Key | Meaning | +|---|---| +| `ARCHON_IP` | IP address of the Archon controller. Point at the emulator to run without hardware. | +| `ARCHON_PORT` | Port of the Archon controller | +| `DEFAULT_FIRMWARE` | Firmware loaded by `load` with no argument. Indexed array for multi-controller ARC systems. | +| `INSTRUMENT` | Instrument name, checked against the instrument the binary was built for | + +## Archon parameters + +The server drives an Archon by writing named parameters defined in the ACF. These keys tell it which +names to use, so the same binary works with differently authored timing scripts. + +| Key | Meaning | +|---|---| +| `EXPOSE_PARAM` | Parameter written to trigger an exposure | +| `START_PARAM` | Parameter written to start the timing script | +| `ABORT_PARAM` | Parameter written to abort an exposure | +| `EXPTIME_MSEC_PARAM` | Parameter holding the milliseconds part of the exposure time. Required. | +| `EXPTIME_SEC_PARAM` | Parameter holding the whole-seconds part. Optional, but see [exposure time](exposure-time.md) for the limit without it. | + +## Ports + +| Key | Meaning | +|---|---| +| `BLKPORT` | Blocking command port. One command at a time, connection stays open. | +| `NBPORT` | Non-blocking command port. One command per connection, then closed. | +| `ASYNCPORT` | UDP port for asynchronous status messages | +| `ASYNCGROUP` | Multicast group the asynchronous messages are sent to | + +See [the command protocol](../commands/index.md) for how the ports differ in behaviour. + +## Files and logging + +| Key | Meaning | +|---|---| +| `IMDIR` | Base directory for image files | +| `AUTODIR` | `yes` to write into a `YYYYMMDD` subdirectory of `IMDIR` | +| `BASENAME` | Base filename for images | +| `DIRMODE` | Permissions for directories the server creates | +| `LOGPATH` | Directory for the daily log file | +| `LOG_STDERR` | Also write the log to stderr, overriding what `--foreground` implies | +| `TM_ZONE_LOG` | `UTC` or `local`, for log entry timestamps only | +| `TM_ZONE` | `UTC` or `local`, for everything else including FITS times and `AUTODIR` | +| `TZ_ENV` | POSIX `TZ` string used when a zone is set to `local` | + +:::{tip} +`TM_ZONE=local` is useful in the lab, where a UTC date rollover in the middle of a working day splits +one session across two `AUTODIR` directories. The time zone is recorded in the FITS header either way. +::: + +## Behaviour + +| Key | Meaning | +|---|---| +| `DAEMON` | `yes` or `no`. The `--foreground` command line option overrides it. | +| `LONGERROR` | `true` to return long error messages on the command port | +| `LONGEXPOSURE` | Unit for bare `exptime` arguments. See [exposure time](exposure-time.md). | +| `READOUT_TIME` | Expected readout time in msec, used to time out a readout that never completes | +| `HEATER_TARGET_MIN` | Lower bound for heater targets, overriding the backplane default | +| `HEATER_TARGET_MAX` | Upper bound for heater targets, overriding the backplane default | + +## Emulator + +Read by `camerad-emulator`, not by the server, but conventionally kept in the same file. + +| Key | Meaning | +|---|---| +| `EMULATOR_PORT` | Port the emulator listens on. `ARCHON_PORT` points here to run without hardware. | +| `EMULATOR_SYSTEM` | Path to the `.system` file describing the emulated module complement | diff --git a/docs/configuration/exposure-time.md b/docs/configuration/exposure-time.md new file mode 100644 index 0000000..a3036fc --- /dev/null +++ b/docs/configuration/exposure-time.md @@ -0,0 +1,26 @@ +# Exposure time + +``` +exptime [