from contextlib import suppress
from astropy import units as u
from astropy.io import fits
from panoptes.pocs.camera.sdk import AbstractSDKCamera
from panoptes.pocs.camera.sbigudrv import INVALID_HANDLE_VALUE
from panoptes.pocs.camera.sbigudrv import SBIGDriver
from panoptes.utils.images import fits as fits_utils
from panoptes.utils import error
[docs]
class Camera(AbstractSDKCamera):
_driver = None
_cameras = {}
_assigned_cameras = set()
def __init__(self,
name='SBIG Camera',
*args, **kwargs):
super().__init__(name, SBIGDriver, *args, **kwargs)
self.logger.info('{} initialised'.format(self))
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('Connecting to {}'.format(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("Could not establish link to {}.".format(self))
self._handle = self._driver.get_driver_handle()
if self._handle == INVALID_HANDLE_VALUE:
raise error.PanError("Could not connect to {}.".format(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