Python bindings

camera_interface lets a Python process own a camera directly: no camerad process, no socket, no text protocol. Constructing a Camera performs the same one-time setup camerad does at startup, then the command set is available as methods.

import camera_interface

camera = camera_interface.Camera("hispecatc.cfg")
camera.open()
camera.load()
camera.power("on")
camera.exptime("1.5")
camera.expose("1")

Use it when the caller is the control system, so the text protocol would only be overhead. Keep camerad when several clients share one camera, or when the camera must outlive the client.

Installing

pip install compiles camerad and the module together and puts both in the environment, so import camera_interface needs no PYTHONPATH and camerad is on PATH:

pip install ./camera-interface \
  --config-settings=cmake.define.INSTRUMENT=hispec_tracking_camera

Any CMake option can be passed the same way, so --config-settings=cmake.define.ENABLE_SHM_OUTPUT=ON works too. CONTROLLER defaults to archon and BUILD_PYTHON_MODULE is forced on. pybind11 comes from the build requirements into an isolated build environment and is never installed into the target environment.

A compiler and the full C++ dependency set must be present wherever pip install runs, since it 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:

assert camera_interface.instrument_name() == "hispec_tracking_camera"

Alternatively cmake -DBUILD_PYTHON_MODULE=ON builds it in the source tree, where importing means putting the build’s lib directory on PYTHONPATH.

Command coverage

Every command camerad accepts is reachable, through three routes:

Base commands

Bound as methods: open, close, load, power, exptime, expose, abort, key, datacube and the rest.

Instrument commands

instrument_cmd(command, args), a deliberate passthrough rather than one binding each, so a build whose instrument gains a command exposes it with no change to the module. Enumerate with instrument_commands(), or test one with is_instrument_command().

Controller commands

controller_cmd(command, args) for mode, raw, readacf, loadtiming, heater, sensor and the other Archon-only commands.

Only exit is missing, since the process belongs to the caller.

Errors

A command that fails raises RuntimeError, carrying the server’s own error detail where there is one:

try:
    camera.expose("1")
except RuntimeError as error:
    log.error("exposure failed: %s", error)

Commands that succeed return the command’s return string, which is often empty.

Concurrency

Blocking commands release the GIL while they run, so a long expose() leaves the rest of the process responsive. For a daemon that is the difference between one exposure stalling and its whole RPC loop stalling.

This does not make the object thread-safe. The underlying interface serializes hardware access, but issuing conflicting commands from several threads is still a logic error.

Frame outputs

The frame outputs are configured from the .cfg exactly as they are for camerad, and output_status() reports on them. It returns a list of dicts, one per configured output:

for output in camera.output_status():
    print(output["name"], output["frames_written"],
          output["frames_dropped"], output["last_written"])

Warning

output_status() is a snapshot, never a barrier. The FITS writer queues and drops frames by design, and nothing in this API lets a caller stall acquisition by waiting on an output, because that would serialize acquisition behind disk I/O. Anything that must be woken per frame should attach to the shared-memory segment, which posts a semaphore per frame.

Logging

Logging follows LOG_STDERR from the .cfg, overridable per session with log_to_stderr=:

camera = camera_interface.Camera("hispecatc.cfg", log_to_stderr=True)

The C++ log always goes to its daily file under LOGPATH regardless.

API

Direct control of a camera-interface camera, without camerad

class camera_interface.Camera

One camera, configured from a camerad .cfg file.

Construction performs the same setup camerad does at startup: read the config, initialize logging, then configure the controller, interface, instrument and frame outputs. It does not connect to the controller; call open() for that.

abort(self: camera_interface.Camera, args: str = '') → str

Abort the exposure in progress

autodir(self: camera_interface.Camera, args: str = '') → str

Query whether images go in a dated subdirectory, or set it

basename(self: camera_interface.Camera, args: str = '') → str

Query the image base filename, or set it

bias(self: camera_interface.Camera, args: str = '') → str

Query a bias voltage, or set it

bin(self: camera_interface.Camera, args: str = '') → str

Query the binning factor for an axis, or set it

close(self: camera_interface.Camera, args: str = '') → str

Disconnect from the controller

controller_cmd(self: camera_interface.Camera, command: str, args: str = '') → str

Run a controller-specific command

datacube(self: camera_interface.Camera, args: str = '') → str

Query whether frames are written as a datacube, or set it

expose(self: camera_interface.Camera, args: str = '') → str

Take an exposure, or a counted series of them

exposure_mode(self: camera_interface.Camera, args: str = '') → str

Query the exposure mode pipeline, or set it

exposure_modes(self: camera_interface.Camera) → list[str]

Return the exposure mode names this build supports

exptime(self: camera_interface.Camera, args: str = '') → str

Query the exposure time in seconds, or set it

instrument_cmd(self: camera_interface.Camera, command: str, args: str = '') → str

Run an instrument-specific command

instrument_commands(self: camera_interface.Camera) → list[str]

Return the instrument-specific command names this build supports

is_instrument_command(self: camera_interface.Camera, command: str) → bool

Return True if this build’s instrument handles the named command

key(self: camera_interface.Camera, args: str = '') → str

Add, list or remove a user FITS header key

load(self: camera_interface.Camera, args: str = '') → str

Load firmware, defaulting to DEFAULT_FIRMWARE from the config

native(self: camera_interface.Camera, args: str) → str

Send a raw command straight to the controller

open(self: camera_interface.Camera, args: str = '') → str

Connect to the controller

output_status(self: camera_interface.Camera) → list

Return a per-output snapshot of frames written, dropped, and last file

power(self: camera_interface.Camera, args: str = '') → str

Query power state, or set it with “on” or “off”

test(self: camera_interface.Camera, args: str = '') → str

Run a named interface test

camera_interface.controller_name() → str

Return the controller this module was built for

camera_interface.instrument_name() → str

Return the instrument this module was built for

Examples

python/examples/shm_read_frames.py is a streaming shared-memory consumer that blocks on the stream’s semaphore and flags frames it missed. See frame outputs for what it needs.