diff --git a/pyscope/observatory/ascom_camera.py b/pyscope/observatory/ascom_camera.py index 07989e01..97cc61dc 100644 --- a/pyscope/observatory/ascom_camera.py +++ b/pyscope/observatory/ascom_camera.py @@ -22,7 +22,7 @@ def __init__(self, identifier, alpaca=False, device_number=0, protocol="http"): Whether to use the Alpaca protocol for Alpaca-compatible devices. device_number : `int`, default : 0, optional The device number. This is only used if the identifier is a ProgID. - protocol : `str`, default : `http`, optional + protocol : `str`, default : "http", optional The protocol to use for Alpaca-compatible devices. """ super().__init__( @@ -39,18 +39,6 @@ def __init__(self, identifier, alpaca=False, device_number=0, protocol="http"): self._camera_time = True def AbortExposure(self): - """ - Abort the current exposure immediately and return camera to idle. - See `CanAbortExposure` for support and possible reasons to abort. - - Parameters - ---------- - None - - Returns - ------- - None - """ logger.debug(f"ASCOMCamera.AbortExposure() called") self._device.AbortExposure() @@ -87,7 +75,7 @@ def SetImageDataType(self): def PulseGuide(self, Direction, Duration): """ Moves scope in the given direction for the given interval or time at the rate - given by the :py:meth:`ASCOMTelescope.GuideRateRightAscension` and :py:meth:`ASCOMTelescope.GuideRateDeclination` properties. + given by the :py:attr:`ASCOMTelescope.GuideRateRightAscension` and :py:attr:`ASCOMTelescope.GuideRateDeclination` properties. Parameters ---------- @@ -116,48 +104,12 @@ def PulseGuide(self, Direction, Duration): self._device.PulseGuide(Direction, Duration) def StartExposure(self, Duration, Light): - """ - Starts an exposure with a given duration and light status. Check `ImageReady` for operation completion. - - Parameters - ---------- - Duration : `float` - The exposure duration in seconds. Can be zero if `Light` is `False`. - - Light : `bool` - Whether the exposure is a light frame (`True`) or a dark frame (`False`). - - Returns - ------- - None - - Notes - ----- - `Duration` can be shorter than `ExposureMin` if used for dark frame or bias exposure. - Bias frame also allows a `Duration` of zero. - """ logger.debug(f"ASCOMCamera.StartExposure({Duration}, {Light}) called") self._last_exposure_duration = Duration self._last_exposure_start_time = str(Time.now()) self._device.StartExposure(Duration, Light) def StopExposure(self): - """ - Stops the current exposure gracefully. - - Parameters - ---------- - None - - Returns - ------- - None - - Notes - ----- - Readout process will initiate if stop is called during an exposure. - Ignored if readout is already in process. - """ logger.debug(f"ASCOMCamera.StopExposure() called") self._device.StopExposure() @@ -185,11 +137,6 @@ def BayerOffsetY(self): # pragma: no cover @property def BinX(self): - """ - The binning factor in the X/column direction. (`int`) - - Default is 1 after camera connection is established. - """ logger.debug(f"ASCOMCamera.BinX property called") return self._device.BinX @@ -200,11 +147,6 @@ def BinX(self, value): @property def BinY(self): - """ - The binning factor in the Y/row direction. (`int`) - - Default is 1 after camera connection is established. - """ logger.debug(f"ASCOMCamera.BinY property called") return self._device.BinY @@ -232,13 +174,11 @@ def CameraState(self): @property def CameraXSize(self): - """The width of the CCD chip in unbinned pixels. (`int`)""" logger.debug(f"ASCOMCamera.CameraXSize property called") return self._device.CameraXSize @property def CameraYSize(self): - """The height of the CCD chip in unbinned pixels. (`int`)""" logger.debug(f"ASCOMCamera.CameraYSize property called") return self._device.CameraYSize @@ -250,35 +190,21 @@ def CameraTime(self): @property def CanAbortExposure(self): - """ - Whether the camera can abort exposures imminently. (`bool`) - - Aborting is not synonymous with stopping an exposure. - Aborting immediately stops the exposure and discards the data. - Used for urgent situations such as errors or temperature concerns. - See `CanStopExposure` for gracious cancellation of an exposure. - """ logger.debug(f"ASCOMCamera.CanAbortExposure property called") return self._device.CanAbortExposure @property def CanAsymmetricBin(self): - """ - Whether the camera supports asymmetric binning such that - `BinX` != `BinY`. (`bool`) - """ logger.debug(f"ASCOMCamera.CanAsymmetricBin property called") return self._device.CanAsymmetricBin @property def CanFastReadout(self): - """Whether the camera supports fast readout mode. (`bool`)""" logger.debug(f"ASCOMCamera.CanFastReadout property called") return self._device.CanFastReadout @property def CanGetCoolerPower(self): - """Whether the camera's cooler power setting can be read. (`bool`)""" logger.debug(f"ASCOMCamera.CanGetCoolerPower property called") return self._device.CanGetCoolerPower @@ -293,37 +219,21 @@ def CanPulseGuide(self): @property def CanSetCCDTemperature(self): - """ - Whether the camera's CCD temperature can be set. (`bool`) - - A false means either the camera uses an open-loop cooling system or - does not support adjusting the CCD temperature from software. - """ logger.debug(f"ASCOMCamera.CanSetCCDTemperature property called") return self._device.CanSetCCDTemperature @property def CanStopExposure(self): - """ - Whether the camera can stop exposures graciously. (`bool`) - - Stopping is not synonymous with aborting an exposure. - Stopping allows the camera to complete the current exposure cycle, then stop. - Image data up to the point of stopping is typically still available. - See `CanAbortExposure` for instant cancellation of an exposure. - """ logger.debug(f"ASCOMCamera.CanStopExposure property called") return self._device.CanStopExposure @property def CCDTemperature(self): - """The current CCD temperature in degrees Celsius. (`float`)""" logger.debug(f"ASCOMCamera.CCDTemperature property called") return self._device.CCDTemperature @property def CoolerOn(self): - """Whether the camera's cooler is on. (`bool`)""" logger.debug(f"ASCOMCamera.CoolerOn property called") return self._device.CoolerOn @@ -334,53 +244,31 @@ def CoolerOn(self, value): @property def CoolerPower(self): - """The current cooler power level as a percentage. (`float`)""" logger.debug(f"ASCOMCamera.CoolerPower property called") return self._device.CoolerPower @property def ElectronsPerADU(self): - """Gain of the camera in photoelectrons per analog-to-digital-unit. (`float`)""" logger.debug(f"ASCOMCamera.ElectronsPerADU() property called") return self._device.ElectronsPerADU @property def ExposureMax(self): - """The maximum exposure duration supported by `StartExposure` in seconds. (`float`)""" logger.debug(f"ASCOMCamera.ExposureMax property called") return self._device.ExposureMax @property def ExposureMin(self): - """ - The minimum exposure duration supported by `StartExposure` in seconds. (`float`) - - Non-zero number, except for bias frame acquisition, where an exposure < ExposureMin - may be possible. - """ logger.debug(f"ASCOMCamera.ExposureMin property called") return self._device.ExposureMin @property def ExposureResolution(self): - """ - The smallest increment in exposure duration supported by `StartExposure`. (`float`) - - This property could be useful if one wants to implement a 'spin control' interface - for fine-tuning exposure durations. - - Providing a `Duration` to `StartExposure` that is not a multiple of `ExposureResolution` - will choose the closest available value. - - A value of 0.0 indicates no minimum resolution increment, except that imposed by the - floating-point precision of `float` itself. - """ logger.debug(f"ASCOMCamera.ExposureResolution property called") return self._device.ExposureResolution @property def FastReadout(self): - """Whether the camera is in fast readout mode. (`bool`)""" logger.debug(f"ASCOMCamera.FastReadout property called") return self._device.FastReadout @@ -391,30 +279,11 @@ def FastReadout(self, value): @property def FullWellCapacity(self): - """ - The full well capacity of the camera in electrons with the - current camera settings. (`float`) - """ logger.debug(f"ASCOMCamera.FullWellCapacity property called") return self._device.FullWellCapacity @property def Gain(self): - """ - The camera's gain OR index of the selected camera gain description. - See below for more information. (`int`) - - Represents either the camera's gain in photoelectrons per analog-to-digital-unit, - or the 0-index of the selected camera gain description in the `Gains` array. - - Depending on a camera's capabilities, the driver can support none, one, or both - representation modes, but only one mode will be active at a time. - - To determine operational mode, read the `GainMin`, `GainMax`, and `Gains` properties. - - `ReadoutMode` may affect the gain of the camera, so it is recommended to set - driver behavior to ensure no conflictions occur if both `Gain` and `ReadoutMode` are used. - """ logger.debug(f"ASCOMCamera.Gain property called") return self._device.Gain @@ -425,44 +294,26 @@ def Gain(self, value): @property def GainMax(self): - """The maximum gain value supported by the camera. (`int`)""" logger.debug(f"ASCOMCamera.GainMax property called") return self._device.GainMax @property def GainMin(self): - """The minimum gain value supported by the camera. (`int`)""" logger.debug(f"ASCOMCamera.GainMin property called") return self._device.GainMin @property def Gains(self): - """ - 0-indexed array of camera gain descriptions supported by the camera. (`list` of `str`) - - Depending on implementation, the array may contain ISOs, or gain names. - """ logger.debug(f"ASCOMCamera.Gains property called") return self._device.Gains @property def HasShutter(self): - """ - Whether the camera has a mechanical shutter. (`bool`) - - If `False`, i.e. the camera has no mechanical shutter, the `StartExposure` - method will ignore the `Light` parameter. - """ logger.debug(f"ASCOMCamera.HasShutter property called") return self._device.HasShutter @property def HeatSinkTemperature(self): - """ - The current heat sink temperature in degrees Celsius. (`float`) - - The readout is only valid if `CanSetCCDTemperature` is `True`. - """ logger.debug(f"ASCOMCamera.HeatSinkTemperature property called") return self._device.HeatSinkTemperature @@ -515,27 +366,16 @@ def ImageArray(self): @property def ImageReady(self): - """ - Whether the camera has completed an exposure and the image is ready to be downloaded. (`bool`) - - If `False`, the `ImageArray` property will exit with an exception. - """ logger.debug(f"ASCOMCamera.ImageReady property called") return self._device.ImageReady @property def IsPulseGuiding(self): - """Whether the camera is currently pulse guiding. (`bool`)""" logger.debug(f"ASCOMCamera.IsPulseGuiding property called") return self._device.IsPulseGuiding @property def LastExposureDuration(self): - """ - The duration of the last exposure in seconds. (`float`) - - May differ from requested exposure time due to shutter latency, camera timing accuracy, etc. - """ logger.debug(f"ASCOMCamera.LastExposureDuration property called") last_exposure_duration = self._device.LastExposureDuration if last_exposure_duration is None or last_exposure_duration == 0: @@ -545,11 +385,6 @@ def LastExposureDuration(self): @property def LastExposureStartTime(self): - """ - The actual last exposure start time in FITS CCYY-MM-DDThh:mm:ss[.sss...] format. (`str`) - - The date string represents UTC time. - """ logger.debug(f"ASCOMCamera.LastExposureStartTime property called") last_time = self._device.LastExposureStartTime """ This code is needed to handle the case of the ASCOM ZWO driver @@ -573,33 +408,21 @@ def LastInputExposureDuration(self, value): @property def MaxADU(self): - """The maximum ADU value the camera is capable of producing. (`int`)""" logger.debug(f"ASCOMCamera.MaxADU property called") return self._device.MaxADU @property def MaxBinX(self): - """ - The maximum allowed binning factor in the X/column direction. (`int`) - - Value equivalent to `MaxBinY` if `CanAsymmetricBin` is `False`. - """ logger.debug(f"ASCOMCamera.MaxBinX property called") return self._device.MaxBinX @property def MaxBinY(self): - """ - The maximum allowed binning factor in the Y/row direction. (`int`) - - Value equivalent to `MaxBinX` if `CanAsymmetricBin` is `False`. - """ logger.debug(f"ASCOMCamera.MaxBinY property called") return self._device.MaxBinY @property def NumX(self): - """The width of the subframe in binned pixels. (`int`)""" logger.debug(f"ASCOMCamera.NumX property called") return self._device.NumX @@ -610,7 +433,6 @@ def NumX(self, value): @property def NumY(self): - """The height of the subframe in binned pixels. (`int`)""" logger.debug(f"ASCOMCamera.NumY property called") return self._device.NumY @@ -621,21 +443,6 @@ def NumY(self, value): @property def Offset(self): - """ - The camera's offset OR index of the selected camera offset description. - See below for more information. (`int`) - - Represents either the camera's offset, or the 0-index of the selected - camera offset description in the `Offsets` array. - - Depending on a camera's capabilities, the driver can support none, one, or both - representation modes, but only one mode will be active at a time. - - To determine operational mode, read the `OffsetMin`, `OffsetMax`, and `Offsets` properties. - - `ReadoutMode` may affect the gain of the camera, so it is recommended to set - driver behavior to ensure no conflictions occur if both `Gain` and `ReadoutMode` are used. - """ logger.debug(f"ASCOMCamera.Offset property called") return self._device.Offset @@ -646,52 +453,36 @@ def Offset(self, value): @property def OffsetMax(self): - """The maximum offset value supported by the camera. (`int`)""" logger.debug(f"ASCOMCamera.OffsetMax property called") return self._device.OffsetMax @property def OffsetMin(self): - """The minimum offset value supported by the camera. (`int`)""" logger.debug(f"ASCOMCamera.OffsetMin property called") return self._device.OffsetMin @property def Offsets(self): - """The array of camera offset descriptions supported by the camera. (`list` of `str`)""" logger.debug(f"ASCOMCamera.Offsets property called") return self._device.Offsets @property def PercentCompleted(self): - """ - The percentage of completion of the current operation. (`int`) - - As opposed to `CoolerPower`, this is represented as an integer - s.t. 0 <= PercentCompleted <= 100 instead of float. - """ logger.debug(f"ASCOMCamera.PercentCompleted property called") return self._device.PercentCompleted @property def PixelSizeX(self): - """The width of the CCD chip pixels in microns. (`float`)""" logger.debug(f"ASCOMCamera.PixelSizeX property called") return self._device.PixelSizeX @property def PixelSizeY(self): - """The height of the CCD chip pixels in microns. (`float`)""" logger.debug(f"ASCOMCamera.PixelSizeY property called") return self._device.PixelSizeY @property def ReadoutMode(self): - """ - Current readout mode of the camera as an index. (`int`) - - The index corresponds to the `ReadoutModes` array. - """ logger.debug(f"ASCOMCamera.ReadoutMode property called") return self._device.ReadoutMode @@ -702,17 +493,11 @@ def ReadoutMode(self, value): @property def ReadoutModes(self): - """The array of camera readout mode descriptions supported by the camera. (`list` of `str`)""" logger.debug(f"ASCOMCamera.ReadoutModes property called") return self._device.ReadoutModes @property def SensorName(self): - """ - The name of the sensor in the camera. (`str`) - - The name is the manufacturer's data sheet part number. - """ logger.debug(f"ASCOMCamera.SensorName property called") return self._device.SensorName @@ -735,12 +520,6 @@ def SensorType(self): @property def SetCCDTemperature(self): - """ - The set-target CCD temperature in degrees Celsius. (`float`) - - Contrary to `CCDTemperature`, which is the current CCD temperature, - this property is the target temperature for the cooler to reach. - """ logger.debug(f"ASCOMCamera.SetCCDTemperature property called") return self._device.SetCCDTemperature @@ -751,7 +530,6 @@ def SetCCDTemperature(self, value): @property def StartX(self): - """The set X/column position of the start subframe in binned pixels. (`int`)""" logger.debug(f"ASCOMCamera.StartX property called") return self._device.StartX @@ -762,7 +540,6 @@ def StartX(self, value): @property def StartY(self): - """The set Y/row position of the start subframe in binned pixels. (`int`)""" logger.debug(f"ASCOMCamera.StartY property called") return self._device.StartY @@ -773,7 +550,6 @@ def StartY(self, value): @property def SubExposureDuration(self): - """The duration of the subframe exposure interval in seconds. (`float`)""" logger.debug(f"ASCOMCamera.SubExposureDuration property called") return self._device.SubExposureDuration diff --git a/pyscope/observatory/ascom_device.py b/pyscope/observatory/ascom_device.py index 94a2d830..b39cbba3 100644 --- a/pyscope/observatory/ascom_device.py +++ b/pyscope/observatory/ascom_device.py @@ -9,6 +9,23 @@ class ASCOMDevice(Device): def __init__(self, identifier, alpaca=False, device_type="Device", **kwargs): + """ + Represents a generic ASCOM device. + + Provides a common interface to interact with ASCOM-compatible devices. + Supports both Alpaca and COM-based ASCOM devices, allowing for cross-platform compatibility. + + Parameters + ---------- + identifier : `str` + The unique identifier for the ASCOM device. This can be the ProgID for COM devices or the device number for Alpaca devices. + alpaca : `bool`, default : `False`, optional + Whether the device is an Alpaca device and should use the appropriate communication protocol. + device_type : `str`, default : "Device", optional + The type of the ASCOM device (e.g. "Telescope", "Camera", etc.) + **kwargs : `dict`, optional + Additional keyword arguments to pass to the device constructor. + """ logger.debug(f"ASCOMDevice.__init__({identifier}, alpaca={alpaca}, {kwargs})") self._identifier = identifier self._device = None @@ -30,13 +47,42 @@ def __init__(self, identifier, alpaca=False, device_type="Device", **kwargs): raise ObservatoryException("If you are not on Windows, you must use Alpaca") def Action(self, ActionName, *ActionParameters): # pragma: no cover + """ + Invokes the device-specific custom action on the device. + + Parameters + ---------- + ActionName : `str` + The name of the action to invoke. Action names are either specified by the device driver or are well known names agreed upon and constructed by interested parties. + ActionParameters : `list` + The required parameters for the given action. Empty string if none are required. + + Returns + ------- + `str` + The result of the action. The return value is dependent on the action being invoked and the representations are set by the driver author. + + Notes + ----- + See `SupportedActions` for a list of supported actions set up by the driver author. + Action names are case-insensitive, so be aware when creating new actions. + """ logger.debug(f"ASCOMDevice.Action({ActionName}, {ActionParameters})") return self._device.Action(ActionName, *ActionParameters) def CommandBlind(self, Command, Raw): # pragma: no cover """ + Sends a command to the device and does not wait for a response. + .. deprecated:: 0.1.1 ASCOM is deprecating this method. + + Parameters + ---------- + Command : `str` + The command string to send to the device. + Raw : `bool` + If `True`, the command is set as-is. If `False`, protocol framing characters may be added onto the command. """ logger.debug(f"ASCOMDevice.CommandBlind({Command}, {Raw})") @@ -44,8 +90,22 @@ def CommandBlind(self, Command, Raw): # pragma: no cover def CommandBool(self, Command, Raw): # pragma: no cover """ + Sends a command to the device and waits for a boolean response. + .. deprecated:: 0.1.1 ASCOM is deprecating this method. + + Parameters + ---------- + Command : `str` + The command string to send to the device. + Raw : `bool` + If `True`, the command is set as-is. If `False`, protocol framing characters may be added onto the command. + + Returns + ------- + `bool` + The boolean response from the device. """ logger.debug(f"ASCOMDevice.CommandBool({Command}, {Raw})") @@ -53,8 +113,22 @@ def CommandBool(self, Command, Raw): # pragma: no cover def CommandString(self, Command, Raw): # pragma: no cover """ + Sends a command to the device and waits for a string response. + .. deprecated:: 0.1.1 ASCOM is deprecating this method. + + Parameters + ---------- + Command : `str` + The command string to send to the device. + Raw : `bool` + If `True`, the command is set as-is. If `False`, protocol framing characters may be added onto the command. + + Returns + ------- + `str` + The string response from the device. """ logger.debug(f"ASCOMDevice.CommandString({Command}, {Raw})") @@ -72,21 +146,41 @@ def Connected(self, value): @property def Description(self): + """ + The description of the device such as the manufacturer and model number. (`str`) + + Description should be limited to 64 characters so that it can be used in FITS headers. + """ logger.debug(f"ASCOMDevice.Description property") return self._device.Description @property def DriverInfo(self): + """ + Description and version information about this ASCOM driver. (`str`) + + Length of info can contain line endings and may be up to thousands of characters long. + Version data and copyright data should be included. + See `Description` for information on the device itself. + To get the version number in a parseable string, use `DriverVersion`. + """ logger.debug(f"ASCOMDevice.DriverInfo property") return self._device.DriverInfo @property def DriverVersion(self): + """ + The driver version number, containing only the major and minor version numbers. (`str`) + + The format is "n.n" where "n" is a number. + Not to be confused with `InterfaceVersion`, which is the version of the specification supported by the driver. + """ logger.debug(f"ASCOMDevice.DriverVersion property") return self._device.DriverVersion @property def InterfaceVersion(self): + """Interface version number that this device supports. (`int`)""" logger.debug(f"ASCOMDevice.InterfaceVersion property") return self._device.InterfaceVersion @@ -97,5 +191,6 @@ def Name(self): @property def SupportedActions(self): + """List of custom action names supported by this driver. (`list`)""" logger.debug(f"ASCOMDevice.SupportedActions property") return self._device.SupportedActions diff --git a/pyscope/observatory/camera.py b/pyscope/observatory/camera.py index 1052ebff..51c36b35 100644 --- a/pyscope/observatory/camera.py +++ b/pyscope/observatory/camera.py @@ -6,37 +6,123 @@ class Camera(ABC, metaclass=_DocstringInheritee): @abstractmethod def __init__(self, *args, **kwargs): + """ + Abstract class for camera devices. + + The class defines the interface for camera devices, including methods for controlling + exposures, guiding, and retrieving camera properties. Subclasses must implement + the abstract methods defined in this class. + + Parameters + ---------- + *args : `tuple` + Variable length argument list. + **kwargs : `dict` + Arbitrary keyword arguments. + """ pass @abstractmethod def AbortExposure(self): + """ + Abort the current exposure immediately and return camera to idle. + See `CanAbortExposure` for support and possible reasons to abort. + + Parameters + ---------- + None + + Returns + ------- + None + """ pass @abstractmethod def PulseGuide(self, Direction, Duration): + """ + Moves the scope in the given direction for the specified duration. + + Parameters + ---------- + Direction : `int` + The direction in which to move the scope. + Value representations for direction are up to the camera manufacturer, + or in case of a lack of manufacturer specification, the developer. + See :py:meth:`ASCOMCamera.PulseGuide` for an example. + Duration : `int` + The duration of the guide pulse in milliseconds. + + Returns + ------- + None + """ pass @abstractmethod def StartExposure(self, Duration, Light): + """ + Starts an exposure with a given duration and light status. Check `ImageReady` for operation completion. + + Parameters + ---------- + Duration : `float` + The exposure duration in seconds. Can be zero if `Light` is `False`. + + Light : `bool` + Whether the exposure is a light frame (`True`) or a dark frame (`False`). + + Returns + ------- + None + + Notes + ----- + `Duration` can be shorter than `ExposureMin` if used for dark frame or bias exposure. + Bias frame also allows a `Duration` of zero. + """ pass @abstractmethod def StopExposure(self): + """ + Stops the current exposure gracefully. + + Parameters + ---------- + None + + Returns + ------- + None + + Notes + ----- + Readout process will initiate if stop is called during an exposure. + Ignored if readout is already in process. + """ pass @property @abstractmethod def BayerOffsetX(self): + """The X/column offset of the Bayer filter array matrix. (`int`)""" pass @property @abstractmethod def BayerOffsetY(self): + """The Y/row offset of the Bayer filter array matrix. (`int`)""" pass @property @abstractmethod def BinX(self): + """ + The binning factor in the X/column direction. (`int`) + + Default is 1 after camera connection is established. + """ pass @BinX.setter @@ -47,6 +133,11 @@ def BinX(self, value): @property @abstractmethod def BinY(self): + """ + The binning factor in the Y/row direction. (`int`) + + Default is 1 after camera connection is established. + """ pass @BinY.setter @@ -57,61 +148,101 @@ def BinY(self, value): @property @abstractmethod def CameraState(self): + """ + The current operational state of the camera. (`enum`) + + Possible values are at the discretion of the camera manufacturer specification. + In case of a lack of one, discretion is at the developer. + See :py:attr:`ASCOMCamera.CameraState` for an example. + """ pass @property @abstractmethod def CameraXSize(self): + """The width of the CCD chip in unbinned pixels. (`int`)""" pass @property @abstractmethod def CameraYSize(self): + """The height of the CCD chip in unbinned pixels. (`int`)""" pass @property @abstractmethod def CanAbortExposure(self): + """ + Whether the camera can abort exposures imminently. (`bool`) + + Aborting is not synonymous with stopping an exposure. + Aborting immediately stops the exposure and discards the data. + Used for urgent situations such as errors or temperature concerns. + See `CanStopExposure` for gracious cancellation of an exposure. + """ pass @property @abstractmethod def CanAsymmetricBin(self): + """ + Whether the camera supports asymmetric binning such that + `BinX` != `BinY`. (`bool`) + """ pass @property @abstractmethod def CanFastReadout(self): + """Whether the camera supports fast readout mode. (`bool`)""" pass @property @abstractmethod def CanGetCoolerPower(self): + """Whether the camera's cooler power setting can be read. (`bool`)""" pass @property @abstractmethod def CanPulseGuide(self): + """Whether the camera supports pulse guiding. (`bool`)""" pass @property @abstractmethod def CanSetCCDTemperature(self): + """ + Whether the camera's CCD temperature can be set. (`bool`) + + A false means either the camera uses an open-loop cooling system or + does not support adjusting the CCD temperature from software. + """ pass @property @abstractmethod def CanStopExposure(self): + """ + Whether the camera can stop exposures graciously. (`bool`) + + Stopping is not synonymous with aborting an exposure. + Stopping allows the camera to complete the current exposure cycle, then stop. + Image data up to the point of stopping is typically still available. + See `CanAbortExposure` for instant cancellation of an exposure. + """ pass @property @abstractmethod def CCDTemperature(self): + """The current CCD temperature in degrees Celsius. (`float`)""" pass @property @abstractmethod def CoolerOn(self): + """Whether the camera's cooler is on. (`bool`)""" pass @CoolerOn.setter @@ -122,31 +253,53 @@ def CoolerOn(self, value): @property @abstractmethod def CoolerPower(self): + """The current cooler power level as a percentage. (`float`)""" pass @property @abstractmethod def ElectronsPerADU(self): + """Gain of the camera in photoelectrons per analog-to-digital-unit. (`float`)""" pass @property @abstractmethod def ExposureMax(self): + """The maximum exposure duration supported by `StartExposure` in seconds. (`float`)""" pass @property @abstractmethod def ExposureMin(self): + """ + The minimum exposure duration supported by `StartExposure` in seconds. (`float`) + + Non-zero number, except for bias frame acquisition, where an exposure < ExposureMin + may be possible. + """ pass @property @abstractmethod def ExposureResolution(self): + """ + The smallest increment in exposure duration supported by `StartExposure`. (`float`) + + This property could be useful if one wants to implement a 'spin control' interface + for fine-tuning exposure durations. + + Providing a `Duration` to `StartExposure` that is not a multiple of `ExposureResolution` + will choose the closest available value. + + A value of 0.0 indicates no minimum resolution increment, except that imposed by the + floating-point precision of `float` itself. + """ pass @property @abstractmethod def FastReadout(self): + """Whether the camera is in fast readout mode. (`bool`)""" pass @FastReadout.setter @@ -157,11 +310,30 @@ def FastReadout(self, value): @property @abstractmethod def FullWellCapacity(self): + """ + The full well capacity of the camera in electrons with the + current camera settings. (`float`) + """ pass @property @abstractmethod def Gain(self): + """ + The camera's gain OR index of the selected camera gain description. + See below for more information. (`int`) + + Represents either the camera's gain in photoelectrons per analog-to-digital-unit, + or the 0-index of the selected camera gain description in the `Gains` array. + + Depending on a camera's capabilities, the driver can support none, one, or both + representation modes, but only one mode will be active at a time. + + To determine operational mode, read the `GainMin`, `GainMax`, and `Gains` properties. + + `ReadoutMode` may affect the gain of the camera, so it is recommended to set + driver behavior to ensure no conflictions occur if both `Gain` and `ReadoutMode` are used. + """ pass @Gain.setter @@ -172,71 +344,135 @@ def Gain(self, value): @property @abstractmethod def GainMax(self): + """The maximum gain value supported by the camera. (`int`)""" pass @property @abstractmethod def GainMin(self): + """The minimum gain value supported by the camera. (`int`)""" pass @property @abstractmethod def Gains(self): + """ + 0-indexed array of camera gain descriptions supported by the camera. (`list` of `str`) + + Depending on implementation, the array may contain ISOs, or gain names. + """ pass @property @abstractmethod def HasShutter(self): + """ + Whether the camera has a mechanical shutter. (`bool`) + + If `False`, i.e. the camera has no mechanical shutter, the `StartExposure` + method will ignore the `Light` parameter. + """ pass @property @abstractmethod def HeatSinkTemperature(self): + """ + The current heat sink temperature in degrees Celsius. (`float`) + + The readout is only valid if `CanSetCCDTemperature` is `True`. + """ pass @property @abstractmethod def ImageArray(self): + """ + Retrieve the image data captured by the camera as a numpy array. + + The image array contains the pixel data from the camera sensor, formatted + as a 2D or 3D numpy array depending on the camera's capabilities and settings. + The data type and shape of the array may vary based on the camera's configuration. + + Returns + ------- + numpy.ndarray + The image data captured by the camera. + + Notes + ----- + The exact format and data type of the returned array should be documented + by the specific camera implementation. This method should handle any necessary + data type conversions and ensure the array is in a standard orientation. + """ pass @property @abstractmethod def ImageReady(self): + """ + Whether the camera has completed an exposure and the image is ready to be downloaded. (`bool`) + + If `False`, the `ImageArray` property will exit with an exception. + """ pass @property @abstractmethod def IsPulseGuiding(self): + """Whether the camera is currently pulse guiding. (`bool`)""" pass @property @abstractmethod def LastExposureDuration(self): + """ + The duration of the last exposure in seconds. (`float`) + + May differ from requested exposure time due to shutter latency, camera timing accuracy, etc. + """ pass @property @abstractmethod def LastExposureStartTime(self): + """ + The actual last exposure start time in FITS CCYY-MM-DDThh:mm:ss[.sss...] format. (`str`) + + The date string represents UTC time. + """ pass @property @abstractmethod def MaxADU(self): + """The maximum ADU value the camera is capable of producing. (`int`)""" pass @property @abstractmethod def MaxBinX(self): + """ + The maximum allowed binning factor in the X/column direction. (`int`) + + Value equivalent to `MaxBinY` if `CanAsymmetricBin` is `False`. + """ pass @property @abstractmethod def MaxBinY(self): + """ + The maximum allowed binning factor in the Y/row direction. (`int`) + + Value equivalent to `MaxBinX` if `CanAsymmetricBin` is `False`. + """ pass @property @abstractmethod def NumX(self): + """The width of the subframe in binned pixels. (`int`)""" pass @NumX.setter @@ -247,6 +483,7 @@ def NumX(self, value): @property @abstractmethod def NumY(self): + """The height of the subframe in binned pixels. (`int`)""" pass @NumY.setter @@ -257,6 +494,21 @@ def NumY(self, value): @property @abstractmethod def Offset(self): + """ + The camera's offset OR index of the selected camera offset description. + See below for more information. (`int`) + + Represents either the camera's offset, or the 0-index of the selected + camera offset description in the `Offsets` array. + + Depending on a camera's capabilities, the driver can support none, one, or both + representation modes, but only one mode will be active at a time. + + To determine operational mode, read the `OffsetMin`, `OffsetMax`, and `Offsets` properties. + + `ReadoutMode` may affect the gain of the camera, so it is recommended to set + driver behavior to ensure no conflictions occur if both `Gain` and `ReadoutMode` are used. + """ pass @Offset.setter @@ -267,36 +519,52 @@ def Offset(self, value): @property @abstractmethod def OffsetMax(self): + """The maximum offset value supported by the camera. (`int`)""" pass @property @abstractmethod def OffsetMin(self): + """The minimum offset value supported by the camera. (`int`)""" pass @property @abstractmethod def Offsets(self): + """The array of camera offset descriptions supported by the camera. (`list` of `str`)""" pass @property @abstractmethod def PercentCompleted(self): + """ + The percentage of completion of the current operation. (`int`) + + As opposed to `CoolerPower`, this is represented as an integer + s.t. 0 <= PercentCompleted <= 100 instead of float. + """ pass @property @abstractmethod def PixelSizeX(self): + """The width of the CCD chip pixels in microns. (`float`)""" pass @property @abstractmethod def PixelSizeY(self): + """The height of the CCD chip pixels in microns. (`float`)""" pass @property @abstractmethod def ReadoutMode(self): + """ + Current readout mode of the camera as an index. (`int`) + + The index corresponds to the `ReadoutModes` array. + """ pass @ReadoutMode.setter @@ -307,21 +575,40 @@ def ReadoutMode(self, value): @property @abstractmethod def ReadoutModes(self): + """The array of camera readout mode descriptions supported by the camera. (`list` of `str`)""" pass @property @abstractmethod def SensorName(self): + """ + The name of the sensor in the camera. (`str`) + + The name is the manufacturer's data sheet part number. + """ pass @property @abstractmethod def SensorType(self): + """ + The type of color information the camera sensor captures. (`enum`) + + Possible types and the corresponding values are at the discretion of the camera manufacturer. + In case of a lack of specification, discretion is at the developer. + See :py:attr:`ASCOMCamera.SensorType` for an example. + """ pass @property @abstractmethod def SetCCDTemperature(self): + """ + The set-target CCD temperature in degrees Celsius. (`float`) + + Contrary to `CCDTemperature`, which is the current CCD temperature, + this property is the target temperature for the cooler to reach. + """ pass @SetCCDTemperature.setter @@ -332,6 +619,7 @@ def SetCCDTemperature(self, value): @property @abstractmethod def StartX(self): + """The set X/column position of the start subframe in binned pixels. (`int`)""" pass @StartX.setter @@ -342,6 +630,7 @@ def StartX(self, value): @property @abstractmethod def StartY(self): + """The set Y/row position of the start subframe in binned pixels. (`int`)""" pass @StartY.setter @@ -352,6 +641,7 @@ def StartY(self, value): @property @abstractmethod def SubExposureDuration(self): + """The duration of the subframe exposure interval in seconds. (`float`)""" pass @SubExposureDuration.setter diff --git a/pyscope/observatory/device.py b/pyscope/observatory/device.py index fbfcc72f..68cabda8 100644 --- a/pyscope/observatory/device.py +++ b/pyscope/observatory/device.py @@ -6,11 +6,26 @@ class Device(ABC, metaclass=_DocstringInheritee): @abstractmethod def __init__(self, *args, **kwargs): + """ + Abstract base class for all deivce types. + + This class defines the common interface for all devices. + Includes connection status and device name properties. Subclasses must implement the + abstract methods and properties in this class. + + Parameters + ---------- + *args : `tuple` + Variable length argument list. + **kwargs : `dict` + Arbitrary keyword arguments. + """ pass @property @abstractmethod def Connected(self): + """Whether the device is connected or not. (`bool`)""" pass @Connected.setter @@ -21,4 +36,5 @@ def Connected(self, value): @property @abstractmethod def Name(self): + """The shorthand name of the device for display only. (`str`)""" pass