Source code for panoptes.pocs.camera.gphoto.canon

"""Canon DSLR camera driver using gphoto2.

Provides a concrete Camera subclass for Canon EOS models controlled via gphoto2.
Implements exposure sequencing, property setup, and a shutterspeed index helper
compatible with the CLI-driven gphoto2 interface.
"""

from functools import lru_cache

from astropy import units as u

from panoptes.utils import error
from panoptes.utils.error import PanError
from panoptes.utils.time import current_time
from panoptes.utils.utils import get_quantity_value

from panoptes.pocs.camera.gphoto.base import AbstractGPhotoCamera


[docs] class Camera(AbstractGPhotoCamera): """Canon EOS DSLR implementation using gphoto2. Provides Canon-specific property defaults and exposure sequencing on top of AbstractGPhotoCamera. """ def __init__( self, readout_time: float = 1.0, file_extension: str = "cr2", setup_properties: bool = False, *args, **kwargs, ): """Create a camera object for a Canon EOS DSLR. Args: readout (float): The time it takes to read out the file from the camera, default 1.0 second. file_extension (str): The file extension to use, default `cr2`. setup_properties (bool): If True, will call `setup_camera()` to set properties on the camera. """ kwargs["readout_time"] = readout_time kwargs["file_extension"] = file_extension super().__init__(*args, **kwargs) self.logger.debug("Creating Canon DSLR GPhoto2 camera") self.connect() if setup_properties: self.setup_camera() @property def bit_depth(self): """ADC bit depth reported by the camera. Returns: astropy.units.Quantity: The analog-to-digital converter resolution in bits. """ return 12 * u.bit @property def egain(self): """Estimated sensor gain (e-/ADU). Returns: astropy.units.Quantity: Electrons per ADU, used for photometric calibration. """ return 1.5 * (u.electron / u.adu)
[docs] def connect(self): """Connect to the camera. This will attempt to connect to the camera using gphoto2. """ # Get serial number _serial_number = self.get_property("serialnumber") if not _serial_number: raise error.CameraNotFound(f"Camera not responding: {self}") self._serial_number = _serial_number self._connected = True
[docs] def setup_camera(self): """Set up the camera. Usually called as part of an initial setup, this will set properties on the canon cameras that should persist across power cycles. """ # Properties to be set upon init. owner_name = "PANOPTES" artist_name = self.get_config("pan_id", default=owner_name) copy_right = f"{owner_name}_{current_time().datetime:%Y}" prop2value = { "drivemode": "Single", "focusmode": "Manual", "imageformat": "RAW", # 'autoexposuremode': 'Manual', # Need physical toggle. # 'imageformatsd': 'RAW', # We shouldn't need to set this. "capturetarget": "Memory Card", "reviewtime": "None", "iso": 100, "shutterspeed": "bulb", "artist": artist_name, "copyright": copy_right, "ownername": owner_name, "datetime": "now", "datetimeutc": "now", } self.set_properties(prop2value=prop2value) self.model = self.get_property("model")
def _start_exposure(self, seconds=None, filename=None, dark=False, header=None, iso=100, *args, **kwargs): """Start the exposure. Tested With: * Canon EOS 100D Args: seconds (u.second, optional): Length of exposure. filename (str, optional): Image is saved to this filename. header (dict or Header, optional): The metadata to be added as FITS headers. iso (int, optional): The ISO setting to use for the exposure, default 100. """ # Make sure we have just the value, no units seconds = get_quantity_value(seconds) shutterspeed_idx = self.get_shutterspeed_index(seconds=seconds, return_minimum=True) cmd_args = [ "--set-config", f"iso={iso}", "--filename", f"{filename}", "--set-config-index", f"shutterspeed={shutterspeed_idx}", "--wait-event=1s", ] if shutterspeed_idx == 0: # Bulb setting. cmd_args.extend( [ "--set-config-index", "eosremoterelease=2", f"--wait-event={int(seconds):d}s", "--set-config-index", "eosremoterelease=4", "--wait-event-and-download=CAPTURECOMPLETE", ] ) else: # Known shutterspeed value. cmd_args.extend( [ "--capture-image-and-download", ] ) try: self.command(cmd_args, check_exposing=False) except error.InvalidCommand as e: self.logger.warning(e) raise PanError(f"Problem taking picture with {self.name}: {e}") else: readout_args = (filename, header) return readout_args
[docs] @classmethod @lru_cache(maxsize=52) def get_shutterspeed_index(cls, seconds: float, return_minimum: bool = False): """Look up the gphoto2 shutterspeed index for a given exposure time. Args: seconds (float): Desired exposure length in seconds. return_minimum (bool): If True and the requested time is shorter than the minimum supported shutterspeed, return the index of the shortest available speed instead of 0 (bulb). Defaults to False. Returns: int: The index expected by gphoto2 for the nearest matching shutter speed. Returns 0 to indicate 'bulb' when no direct match is found, unless return_minimum is True and the time is below the minimum discrete setting. """ seconds = get_quantity_value(seconds, unit="second") # TODO derive these from `load_properties`. # The index corresponds to what gphoto2 expects. shutter_speeds = { "bulb": "bulb", "30": 30, "25": 25, "20": 20, "15": 15, "13": 13, "10.3": 10.3, "8": 8, "6.3": 6.3, "5": 5, "4": 4, "3.2": 3.2, "2.5": 2.5, "2": 2, "1.6": 1.6, "1.3": 1.3, "1": 1, "0.8": 0.8, "0.6": 0.6, "0.5": 0.5, "0.4": 0.4, "0.3": 0.3, "1/4": 1 / 4, "1/5": 1 / 5, "1/6": 1 / 6, "1/8": 1 / 8, "1/10": 1 / 10, "1/13": 1 / 13, "1/15": 1 / 15, "1/20": 1 / 20, "1/25": 1 / 25, "1/30": 1 / 30, "1/40": 1 / 40, "1/50": 1 / 50, "1/60": 1 / 60, "1/80": 1 / 80, "1/100": 1 / 100, "1/125": 1 / 125, "1/160": 1 / 160, "1/200": 1 / 200, "1/250": 1 / 250, "1/320": 1 / 320, "1/400": 1 / 400, "1/500": 1 / 500, "1/640": 1 / 640, "1/800": 1 / 800, "1/1000": 1 / 1000, "1/1250": 1 / 1250, "1/1600": 1 / 1600, "1/2000": 1 / 2000, "1/2500": 1 / 2500, "1/3200": 1 / 3200, "1/4000": 1 / 4000, } try: # First check by key. return list(shutter_speeds.keys()).index(seconds) except ValueError: # Then check by value. try: # Check minimum of everything after 'bulb'. if return_minimum and seconds < min(list(shutter_speeds.values())[1:]): return len(shutter_speeds) - 1 else: return list(shutter_speeds.values()).index(seconds) except ValueError: return 0