Architecture

How the pieces fit together, and why acquisition is kept separate from output. The C++ reference here is narrative and links to source rather than reproducing signatures.

Layers

        client (socket, camerad-socksend, or the Python module)
                          |
   Camera::Server .......................  one TCP port, command dispatch, logging
                          |
   Camera::Interface ....................  the camera, expressed as commands
        |            |              |
        |            |              +-----  FrameOutput ....  FITS file, shared memory
        |            |
        |            +--------------------  Camera::ExposureMode ....  how one exposure runs
        |
   Camera::Controller ...................  the wire to the hardware
                          |
              Archon (TCP) or ARC (PCIe)
Camera::Server (camerad/camera_server.h)

Owns the listening socket and the command dispatch chain. Parses a command line, calls the matching Interface method, appends DONE or ERROR, writes the reply back. Knows nothing about detectors. Each client connection is served on its own thread.

Camera::Interface (camerad/camera_interface.h)

The abstract camera. Declares one pure virtual per command, and owns the config, the Camera::Information for the current exposure, and the list of frame outputs. ArchonInterface and AstroCamInterface implement it for the two controller families; an instrument module subclasses one of those.

Camera::Controller

The transport to the hardware, kept separate from Interface so command semantics and wire protocol vary independently.

Camera::ExposureMode (camerad/exposure_modes.h)

One way of running an exposure. ExposureModeTemplate<InterfaceType> is the base each concrete mode derives from; the template parameter gives a mode typed access to its interface. Each mode owns its own Camera::Information for processed and unprocessed images, and an ImageProcessor for deinterlacing.

A single expose means different things for a slow CCD readout and a continuously reading H2RG, which is exactly what the exposure mode abstracts.

Acquisition: producer and consumer

An exposure mode is a producer/consumer pair over a bounded queue:

image_acquisition_thread()

The producer. Pulls frames off the controller as fast as the hardware delivers them and enqueues them.

image_processing_thread()

The consumer. Dequeues a frame, runs it through the deinterlacer, and hands the result to the frame outputs.

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, 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 and one consumer run for the whole session and expose returns immediately, leaving them going until an abort or a producer error.

The frame path

A processed frame is fanned out synchronously to every configured output by Interface::dispatch_frame(), which simply calls write() on each in turn. When the whole exposure command finishes, end_exposure() tells each output, so a multi-frame output such as a data cube can finalize its file.

Outputs are built once at startup by Interface::configure_frame_outputs(), called from camerad/camerad.cpp for every instrument. It is deliberately not virtual, so no derived class can silently skip wiring its outputs.

Why outputs never block

dispatch_frame() runs on the consumer thread, so anything slow inside an output would stall acquisition. Each output is therefore required to return promptly, and absorbs the mismatch itself.

The FITS writer is the clearest case (utils/fits_writer.h): write() copies the pixels into a bounded queue and returns, never touching CCfits. A dedicated worker thread drains that queue to disk. When the queue is full the oldest frame is dropped. Disk is slower than acquisition can be, and the design chooses to lose a frame rather than apply backpressure to the detector.

This is also why FrameOutput::status() is documented as a snapshot and never a barrier: letting a caller wait on an output would serialize acquisition behind disk I/O, reintroducing the coupling the queue exists to remove.

The shared-memory output makes the opposite trade (utils/shared_memory_writer.cpp): it copies the frame into the stream buffer and calls ImageStreamIO_UpdateIm, which posts the semaphores. Nothing it does depends on a reader, so a reader that cannot keep up misses frames but never delays the writer. Anything that must be woken per frame should attach there rather than watch for files.

See frame output keys for configuring them.

Instrument modules

An instrument is a separate repository that builds on its own, pulling in the core with FetchContent. It contributes:

  1. A CMakeLists.txt declaring its own camerad and Python module, linking the core’s camerad:: libraries. 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 instrument_commands().

  3. Its own ExposureMode implementations, where the stock ones do not fit.

  4. Optionally a FITS header dictionary and shipped .cfg and .acf files.

An interface factory ties it together, so the core builds against the base class and never names a concrete instrument.

The server recognizes the base commands first, and an instrument changes one by overriding its method. Any other name goes to controller_cmd(), which handles the controller’s own commands and forwards the rest to instrument_cmd(). A controller command takes precedence over an instrument command of the same name.

See instruments for building one, and reference_instrument for a minimal example.