Getting started#
Installation#
basecam
can be installed by doing
pip install --upgrade sdss-basecam
To install from source, develop, or report an issue, visit basecam
’s GitHub repository. basecam
uses poetry for development.
Basic concepts#
basecam
is built around some basic concept that are meant to be general enough that can be applied to any astronomical camera API:
The camera system is in charge of reporting what cameras are connected to the system and provide access to each one of them. It can provide its own system for automatic discovery of new cameras but this is not required. In
basecam
, the camera system is abstracted by theCameraSystem
class.A camera represents a physical CCD or group of them, along with its cooling mechanisms, shutter, etc. A camera is defined by its name and a unique identifier, usually the serial number but in general any value that uniquely identifies the camera. Cameras are represented by subclasses of
BaseCamera
.BaseCamera
provides an abstract implementation for connecting and disconnecting a camera, retrieving its status, and exposing it. This is expected to be the minimum that all cameras must provide. Additional features (shutter and temperature control, binning) are implemented as mixins.
A minimal example#
Let’s assume we have a camera that provides a functional programmatic API. This API can be written in C/C++ or we can use an already existing Python wrapping. We don’t care how that API has been implemented or whether is part of the library we are trying to write or external. For now, we’ll just assume that we can access the functions of that library through the module lib
.
To wrap the camera API with basecam
we need to subclass CameraSystem
and BaseCamera
and override the internal abstract methods to connect it to the camera low-level implementation.
# file: camera.py
import lib
from basecam import CameraSystem, BaseCamera, CameraEvent
class MyCameraSystem(CameraSystem):
__version__ = '0.0.1'
def list_available_cameras(self):
return lib.cameras
class MyCamera(BaseCamera):
async def _connect_internal(self, uid):
self.device = lib.open(uid)
async def _expose_internal(self, exposure):
exptime = exposure.exptime
self.notify(CameraEvent.EXPOSURE_INTEGRATING)
await self.loop.run_in_executor(None, self.device.expose, exptime)
self.notify(CameraEvent.EXPOSURE_READING)
array = await self.loop.run_in_executor(None, self.device.read_frame)
exposure.data = array
return
That’s it! Of course, a real camera implementation can be a bit more complicated, but this is all we minimally need to do. Now we can control the cameras using basecam
’s API
>>> camera_system = MyCameraSystem(MyCamera)
>>> camera = await camera_system.add_camera(name='my_camera', uid='S12345', autoconnect=True)
>>> camera.connected
True
>>> camera.name, camera.uid
('my_camera', 'S12345')
>>> exposure = await camera.expose(1)
>>> exposure.write()
>>> exposure.filename
'S12345-0001.fits'
Note that when we instantiate MyCameraSystem
we pass it the class we want to use for it to connect new cameras, in this case MyCamera
. The rest is pretty straightforward.
Normally we instantiate the camera system with a configuration dictionary or file that includes information about the available cameras and how to connect them. For example, imagine that to connect the camera we need to know the device port in addition to the unique identifier
def _connect_internal(self, uid, port):
self.device = lib.open(uid, port)
We can instantiate MyCameraSystem
as follows
>>> config = {
'cameras': {
'my_camera': {
'uid': 'S12345'
'connection_params': {
'uid': 'S12345'
'port': '/dev/cam1'
}
}
}
}
>>> camera_system = MyCameraSystem(MyCamera, camera_config=config)
Now we can use the camera poller to automatically detect when cameras connect or disconnect
>>> await camera_system.start_camera_poller()
start_camera_poller
periodically checks the list of available cameras; when a new camera is connected, it calls add_camera
. The configuration for the camera is accessible via BaseCamera.camera_config
and the connection_params
section is passed to _connect_internal
.
Note that when interacting with the camera system or the camera we do not use the internal methods we have overridden. To expose the camera, we call expose
which provides a common interface regardless of the specific camera. expose
returns an Exposure
object which contains the image and a FITS model
. More details are provided in the Exposures section.
A more complete example#
For a more complete example of a full implementation of a camera API with basecam
we refer the reader to flicamera. flicamera
provides a full wrapping of Finger Lakes Instrumentation cameras as part of the SDSS-V project. The structure of the project is quite simple and can be summarised as follows
flicamera
|
-- actor.py
|
-- camera.py
|
-- lib.py
In lib.py we wrap the vendor C library using ctypes
. This is a typical approach but we could have also used Cython or pybind11, or an already existing Python implementation such as python-FLI. This exposes the low-level functions we need to wrap using basecam
.
camera.py includes the subclasses of CameraSystem
and BaseCamera
that implement basecam
’s API for the FLI cameras. Note that, although more complicated than the example above, the whole file has fewer than 200 lines.
Finally actor.py provides the implementation of the camera actor.
General recommendations#
basecam
is an asynchronous library so you’ll need a basic understanding of how asyncio
works. That said, most of the wrapping code can be written synchronously. An exception, as seen in the example above, is calling long-running blocking routines from the camera library. A typical example is the function that reads the camera buffer, which in some case may take up to several seconds. In that case you want to run that code in an executor
await self.loop.run_in_executor(None, lib.grab_frame)
Note that you can access the event loop from CameraSystem.loop
or BaseCamera.loop
.
As a general rule, when wrapping the camera library, you want to minimally provide access to the features in the camera API but avoiding any additional implementation: leave that to basecam
. The implementation of the abstract methods and mixins must also be as minimal as possible, with each method doing only what is required.