"""FLI camera driver implementation for POCS.
Provides a Camera class backed by the libfli SDK via FLIDriver, implementing the
AbstractSDKCamera interface for cooled FLI CCD/CMOS cameras.
"""
from contextlib import suppress
import numpy as np
from astropy import units as u
from astropy.io import fits
from panoptes.utils import error
from panoptes.pocs.camera import libfliconstants as c
from panoptes.pocs.camera.libfli import FLIDriver
from panoptes.pocs.camera.sdk import AbstractSDKCamera
[docs]
class Camera(AbstractSDKCamera):
"""FLI camera implementation backed by the libfli SDK.
This class wraps FLIDriver calls to provide cooling, exposure, and readout
control consistent with the AbstractSDKCamera interface.
"""
_driver = None
_cameras = {}
_assigned_cameras = set()
def __init__(self, name="FLI Camera", target_temperature=25 * u.Celsius, *args, **kwargs):
kwargs["target_temperature"] = target_temperature
super().__init__(name, FLIDriver, *args, **kwargs)
self.logger.info(f"{self} initialised")
def __del__(self):
with suppress(AttributeError):
handle = self._handle
self._driver.FLIClose(handle)
self.logger.debug(f"Closed FLI camera handle {handle.value}")
super().__del__()
# Properties
@property
def temperature(self):
"""
Current temperature of the camera's image sensor.
"""
return self._driver.FLIGetTemperature(self._handle)
@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.
"""
return self._target_temperature
@property
def cooling_enabled(self):
"""Whether the camera's cooling is enabled.
Returns:
bool: Always True for FLI cameras; cooling cannot be disabled.
"""
return True
@cooling_enabled.setter
def cooling_enabled(self, enable):
"""Attempt to enable/disable cooling (not supported).
Args:
enable (bool): Desired cooling state. Only True is supported for FLI.
Raises:
panoptes.utils.error.NotSupported: If attempting to disable cooling.
"""
# Cooling is always enabled on FLI cameras
if not enable:
raise error.NotSupported(f"Cannot disable cooling on {self.name}")
@property
def cooling_power(self):
"""
Current power level of the camera's image sensor cooling system (as
a percentage of the maximum).
"""
return self._driver.FLIGetCoolerPower(self._handle)
@property
def is_exposing(self):
"""True if an exposure is currently under way, otherwise False"""
return bool(self._driver.FLIGetExposureStatus(self._handle).value)
# Methods
[docs]
def connect(self):
"""
Connect to FLI camera.
Gets a 'handle', serial number and specs/capabilities from the driver
"""
self.logger.debug(f"Connecting to {self}")
self._handle = self._driver.FLIOpen(port=self._address)
if self._handle == c.FLI_INVALID_DEVICE:
message = f"Could not connect to {self.name} on {self._camera_address}!"
raise error.PanError(message)
self._get_camera_info()
self.model = self.properties["camera model"]
# All FLI camera models are cooled
self._is_cooled_camera = True
self._connected = True
# Private Methods
def _set_target_temperature(self, target):
self._driver.FLISetTemperature(self._handle, target)
# Check for success?
self._target_temperature = target
def _set_cooling_enabled(self, enable):
"""Enable or disable cooling (not supported).
Args:
enable (bool): Desired state; FLI cameras always have cooling enabled.
Raises:
NotImplementedError: Cooling state cannot be changed for FLI cameras.
"""
raise NotImplementedError
def _start_exposure(self, seconds, filename, dark, header, *args, **kwargs):
self._driver.FLISetExposureTime(self._handle, exposure_time=seconds)
if dark:
frame_type = c.FLI_FRAME_TYPE_DARK
else:
frame_type = c.FLI_FRAME_TYPE_NORMAL
self._driver.FLISetFrameType(self._handle, frame_type)
# For now set to 'visible' (i.e. light sensitive) area of image sensor.
# Can later use this for windowed exposures.
self._driver.FLISetImageArea(
self._handle,
self.properties["visible corners"][0],
self.properties["visible corners"][1],
)
# No on chip binning for now.
self._driver.FLISetHBin(self._handle, bin_factor=1)
self._driver.FLISetVBin(self._handle, bin_factor=1)
# No pre-exposure image sensor flushing, either.
self._driver.FLISetNFlushes(self._handle, n_flushes=0)
# In principle can set bit depth here (16 or 8 bit) but most FLI cameras don't support it.
# Start exposure
self._driver.FLIExposeFrame(self._handle)
readout_args = (
filename,
self.properties["visible width"],
self.properties["visible height"],
header,
)
return readout_args
def _readout(self, filename, width, height, header):
# Use FLIGrabRow for now at least because I can't get FLIGrabFrame to work.
# image_data = self._FLIDriver.FLIGrabFrame(self._handle, width, height)
image_data = np.zeros((height, width), dtype=np.uint16)
rows_got = 0
try:
for i in range(image_data.shape[0]):
image_data[i] = self._driver.FLIGrabRow(self._handle, image_data.shape[1])
rows_got += 1
except RuntimeError as err:
message = f"Readout error on {self}, expected {image_data.shape[0]} rows, got {rows_got}: {err}"
raise error.PanError(message)
else:
self.write_fits(data=image_data, header=header, filename=filename)
def _create_fits_header(self, seconds, dark=None, metadata=None) -> fits.Header:
header = super()._create_fits_header(seconds, dark)
header.set("CAM-HW", self.properties["hardware version"], "Camera hardware version")
header.set("CAM-FW", self.properties["firmware version"], "Camera firmware version")
header.set("XPIXSZ", self.properties["pixel width"].value, "Microns")
header.set("YPIXSZ", self.properties["pixel height"].value, "Microns")
return header
def _get_camera_info(self):
serial_number = self._driver.FLIGetSerialString(self._handle)
camera_model = self._driver.FLIGetModel(self._handle)
hardware_version = self._driver.FLIGetHWRevision(self._handle)
firmware_version = self._driver.FLIGetFWRevision(self._handle)
pixel_width, pixel_height = self._driver.FLIGetPixelSize(self._handle)
ccd_corners = self._driver.FLIGetArrayArea(self._handle)
visible_corners = self._driver.FLIGetVisibleArea(self._handle)
self._info = {
"serial number": serial_number,
"camera model": camera_model,
"hardware version": hardware_version,
"firmware version": firmware_version,
"pixel width": pixel_width,
"pixel height": pixel_height,
"array corners": ccd_corners,
"array height": ccd_corners[1][1] - ccd_corners[0][1],
"array width": ccd_corners[1][0] - ccd_corners[0][0],
"visible corners": visible_corners,
"visible height": visible_corners[1][1] - visible_corners[0][1],
"visible width": visible_corners[1][0] - visible_corners[0][0],
}