"""SBIG camera driver implementation backed by the SBIG Universal Driver.
Provides a Camera class that uses the ctypes-based SBIGDriver to control cooled
SBIG CCD/CMOS cameras and integrate with the AbstractSDKCamera interface.
"""
from contextlib import suppress
from astropy.io import fits
from panoptes.utils import error
from panoptes.pocs.camera.sbigudrv import INVALID_HANDLE_VALUE, SBIGDriver
from panoptes.pocs.camera.sdk import AbstractSDKCamera
[docs]
class Camera(AbstractSDKCamera):
"""SBIG camera implementation using the SBIG Universal Driver (sbigudrv).
Wraps SBIGDriver calls to provide cooling, exposure, and readout control
consistent with AbstractSDKCamera.
"""
_driver = None
_cameras = {}
_assigned_cameras = set()
def __init__(self, name="SBIG Camera", *args, **kwargs):
super().__init__(name, SBIGDriver, *args, **kwargs)
self.logger.info(f"{self} initialised")
def __del__(self):
with suppress(AttributeError):
self._driver.set_handle(INVALID_HANDLE_VALUE) # Shortcut to closing device & driver
self.logger.debug("Closed SBIG camera device & driver")
super().__del__()
# Properties
@property
def egain(self):
"""Image sensor gain in e-/ADU as reported by the camera."""
return self.properties["readout modes"]["RM_1X1"]["gain"]
@property
def temperature(self):
"""
Current temperature of the camera's image sensor.
"""
temp_status = self._driver.query_temp_status(self._handle)
return temp_status["imaging_ccd_temperature"]
@AbstractSDKCamera.target_temperature.getter
def target_temperature(self):
"""
Current value of the target temperature for the camera's image sensor cooling control.
Can be set by assigning an astropy.units.Quantity.
"""
temp_status = self._driver.query_temp_status(self._handle)
return temp_status["ccd_set_point"]
@AbstractSDKCamera.cooling_enabled.getter
def cooling_enabled(self):
"""
Current status of the camera's image sensor cooling system (enabled/disabled).
Can be set by assigning a bool.
"""
temp_status = self._driver.query_temp_status(self._handle)
return temp_status["cooling_enabled"]
@property
def cooling_power(self):
"""
Current power level of the camera's image sensor cooling system (as
a percentage of the maximum).
"""
temp_status = self._driver.query_temp_status(self._handle)
return temp_status["imaging_ccd_power"]
@property
def is_exposing(self):
"""True if an exposure is currently under way, otherwise False"""
return self._driver.get_exposure_status(self._handle) == "CS_INTEGRATING"
# Methods
[docs]
def connect(self):
"""
Connect to SBIG camera.
Gets a 'handle', serial number and specs/capabilities from the driver
"""
self.logger.debug(f"Connecting to {self}")
# This will close device and driver, ensuring it is ready to access a new camera
self._driver.set_handle(handle=INVALID_HANDLE_VALUE)
self._driver.open_driver()
self._driver.open_device(self._address)
self._driver.establish_link()
link_status = self._driver.get_link_status()
if not link_status["established"]:
raise error.PanError(f"Could not establish link to {self}.")
self._handle = self._driver.get_driver_handle()
if self._handle == INVALID_HANDLE_VALUE:
raise error.PanError(f"Could not connect to {self}.")
self._info = self._driver.get_ccd_info(self._handle)
self.model = self.properties["camera name"]
# No way to directly ask the camera whether it has image sensor cooling or not. Need to
# check camera type and infer from that. As far as I can tell all models apart from the
# ST-i range and the SG-4 (which isn't included in the SDK yet) have cooling.
if self.properties["camera type"] != "STI_CAMERA":
self._is_cooled_camera = True
if self.properties["colour"]:
if self.properties["Truesense"]:
self._filter_type = "CRGB"
else:
self._filter_type = "RGGB"
else:
self._filter_type = "M"
# Stop camera from skipping lowering of Vdd for exposures of 3 seconds of less
self._driver.disable_vdd_optimized(self._handle)
self._connected = True
if self.filterwheel and not self.filterwheel.is_connected:
# Need to defer connection of SBIG filter wheels until after camera is connected
# so do it here.
self.filterwheel.connect()
# Private methods
def _set_target_temperature(self, target):
self._driver.set_temp_regulation(self._handle, target, self.cooling_enabled)
self._target_temperature = target
def _set_cooling_enabled(self, enable):
target = self.target_temperature
self._driver.set_temp_regulation(self._handle, target, enable)
def _start_exposure(self, seconds, filename, dark, header, *args, **kwargs):
readout_mode = "RM_1X1" # Unbinned mode
top = 0 # Unwindowed too
left = 0
height = self.properties["readout modes"][readout_mode]["height"]
width = self.properties["readout modes"][readout_mode]["width"]
self._driver.start_exposure(
handle=self._handle,
seconds=seconds,
dark=dark,
antiblooming=self.properties["imaging ABG"],
readout_mode=readout_mode,
top=top,
left=left,
height=height,
width=width,
)
readout_args = (filename, readout_mode, top, left, height, width, header)
return readout_args
def _readout(self, filename, readout_mode, top, left, height, width, header):
exposure_status = self._driver.get_exposure_status(self._handle)
if exposure_status == "CS_INTEGRATION_COMPLETE":
try:
image_data = self._driver.readout(self._handle, readout_mode, top, left, height, width)
except RuntimeError as err:
raise error.PanError(f"Readout error on {self}, {err}")
else:
self.write_fits(data=image_data, header=header, filename=filename)
elif exposure_status == "CS_IDLE":
raise error.PanError(f"Exposure missing on {self}")
else:
raise error.PanError(f"Unexpected exposure status on {self}: '{exposure_status}'")
def _create_fits_header(self, seconds, dark=None, metadata=None) -> fits.Header:
header = super()._create_fits_header(seconds, dark)
# Unbinned. Need to chance if binning gets implemented.
readout_mode = "RM_1X1"
header.set("CAM-FW", self.properties["firmware version"], "Camera firmware version")
header.set("XPIXSZ", self.properties["readout modes"][readout_mode]["pixel width"].value, "Microns")
header.set(
"YPIXSZ",
self.properties["readout modes"][readout_mode]["pixel height"].value,
"Microns",
)
return header