diff --git a/.gitignore b/.gitignore index 0ba60be8..84d4a9e0 100755 --- a/.gitignore +++ b/.gitignore @@ -140,3 +140,4 @@ dmypy.json *OmniSim* !coverage.xml docs/source/api/auto_api/ +pgHardware \ No newline at end of file diff --git a/pyscope/observatory/ascom_camera.py b/pyscope/observatory/ascom_camera.py index a2acf305..f01eec37 100644 --- a/pyscope/observatory/ascom_camera.py +++ b/pyscope/observatory/ascom_camera.py @@ -1,5 +1,8 @@ import logging +import numpy as np +from astropy.time import Time + from .ascom_device import ASCOMDevice from .camera import Camera @@ -15,17 +18,54 @@ def __init__(self, identifier, alpaca=False, device_number=0, protocol="http"): device_number=device_number, protocol=protocol, ) + self._last_exposure_duration = None + self._last_exposure_start_time = None + self._image_data_type = None + self._DoTranspose = True + self._camera_time = True def AbortExposure(self): logger.debug(f"ASCOMCamera.AbortExposure() called") self._device.AbortExposure() + def SetImageDataType(self): + """Determine the data type of the image array based on the MaxADU property. + + This method is called automatically when the ImageArray property is called + if it has not already been set (initializes to `None`). + It will choose from the following data types based on the MaxADU property: + + - numpy.uint8 : (if MaxADU <= 255) + - numpy.uint16 : (default if MaxADU is not defined, or if MaxADU <= 65535) + - numpy.uint32 : (if MaxADU > 65535) + + See Also + -------- + numpy.uint8 + numpy.uint16 + numpy.uint32 + MaxADU : ASCOM Camera interface property `ASCOM Documentation `_ + """ + logger.debug(f"ASCOMCamera.SetImageDataType() called") + try: + max_adu = self.MaxADU + if max_adu <= 255: + self._image_data_type = np.uint8 + elif max_adu <= 65535: + self._image_data_type = np.uint16 + else: + self._image_data_type = np.uint32 + except: + self._image_data_type = np.uint16 + def PulseGuide(self, Direction, Duration): logger.debug(f"ASCOMCamera.PulseGuide({Direction}, {Duration}) called") self._device.PulseGuide(Direction, Duration) def StartExposure(self, Duration, Light): 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): @@ -85,6 +125,11 @@ def CameraYSize(self): logger.debug(f"ASCOMCamera.CameraYSize property called") return self._device.CameraYSize + @property + def CameraTime(self): + logger.debug(f"ASCOMCamera.CameraTime property called") + return self._camera_time + @property def CanAbortExposure(self): logger.debug(f"ASCOMCamera.CanAbortExposure property called") @@ -213,7 +258,14 @@ def HeatSinkTemperature(self): @property def ImageArray(self): logger.debug(f"ASCOMCamera.ImageArray property called") - return self._device.ImageArray + img_array = self._device.ImageArray + # Convert to numpy array and check if it is the correct data type + if self._image_data_type is None: + self.SetImageDataType() + img_array = np.array(img_array, dtype=self._image_data_type) + if self._DoTranspose: + img_array = np.transpose(img_array) + return img_array @property def ImageReady(self): @@ -228,12 +280,34 @@ def IsPulseGuiding(self): @property def LastExposureDuration(self): logger.debug(f"ASCOMCamera.LastExposureDuration property called") - return self._device.LastExposureDuration + last_exposure_duration = self._device.LastExposureDuration + if last_exposure_duration is None or last_exposure_duration == 0: + last_exposure_duration = self.LastInputExposureDuration + self._camera_time = False + return last_exposure_duration @property def LastExposureStartTime(self): logger.debug(f"ASCOMCamera.LastExposureStartTime property called") - return self._device.LastExposureStartTime + last_time = self._device.LastExposureStartTime + """ This code is needed to handle the case of the ASCOM ZWO driver + which returns an empty string instead of None if the camera does not + support the property """ + return ( + last_time + if last_time != "" and last_time != None + else self._last_exposure_start_time + ) + + @property + def LastInputExposureDuration(self): + logger.debug(f"ASCOMCamera.LastInputExposureDuration property called") + return self._last_exposure_duration + + @LastInputExposureDuration.setter + def LastInputExposureDuration(self, value): + logger.debug(f"ASCOMCamera.LastInputExposureDuration property set to {value}") + self._last_exposure_duration = value @property def MaxADU(self): diff --git a/pyscope/observatory/maxim.py b/pyscope/observatory/maxim.py index caf1a6b9..3fa703f9 100755 --- a/pyscope/observatory/maxim.py +++ b/pyscope/observatory/maxim.py @@ -2,6 +2,8 @@ import platform import time +from astropy.time import Time + from .autofocus import Autofocus from .camera import Camera from .device import Device @@ -124,7 +126,7 @@ def PulseGuide(self, Direction, Duration): def StartExposure(self, Duration, Light): logger.debug(f"StartExposure called with Duration={Duration}, Light={Light}") self._last_exposure_duration = Duration - self._last_exposure_start_time = time.time() + self._last_exposure_start_time = str(Time.now()) self._com_object.Expose(Duration, Light) def StopExposure(self): diff --git a/pyscope/observatory/observatory.py b/pyscope/observatory/observatory.py index aafee518..e526962d 100644 --- a/pyscope/observatory/observatory.py +++ b/pyscope/observatory/observatory.py @@ -1169,11 +1169,10 @@ def save_last_image( logger.exception("Image is not ready, cannot be saved") return False - if ( - self.camera.ImageArray is None - or len(self.camera.ImageArray) == 0 - or len(self.camera.ImageArray[0]) == 0 - ): + # Read out the image array + img_array = self.camera.ImageArray + + if img_array is None or len(img_array) == 0 or len(img_array) == 0: logger.exception("Image array is empty, cannot be saved") return False @@ -1182,9 +1181,9 @@ def save_last_image( hdr["SIMPLE"] = True hdr["BITPIX"] = (16, "8 unsigned int, 16 & 32 int, -32 & -64 real") hdr["NAXIS"] = (2, "number of axes") - hdr["NAXIS1"] = (len(self.camera.ImageArray), "fastest changing axis") + hdr["NAXIS1"] = (len(img_array), "fastest changing axis") hdr["NAXIS2"] = ( - len(self.camera.ImageArray[0]), + len(img_array[0]), "next to fastest changing axis", ) hdr["BSCALE"] = (1, "physical=BZERO + BSCALE*array_value") @@ -1224,7 +1223,7 @@ def save_last_image( for hist in history: hdr["HISTORY"] = hist - hdu = fits.PrimaryHDU(self.camera.ImageArray, header=hdr) + hdu = fits.PrimaryHDU(img_array, header=hdr) hdu.writeto(filename, overwrite=overwrite) if do_fwhm: @@ -2375,13 +2374,17 @@ def _read_out_kwargs(self, dictionary): "filter_focus_offsets", self.filter_focus_offsets ) - self.rotator_reverse = dictionary.get("rotator_reverse", self.rotator_reverse) - self.rotator_min_angle = dictionary.get( - "rotator_min_angle", self.rotator_min_angle - ) - self.rotator_max_angle = dictionary.get( - "rotator_max_angle", self.rotator_max_angle - ) + # Not sure if this if statement is a good idea here... + if dictionary.get("rotator_driver", self.rotator_driver) is not None: + self.rotator_reverse = dictionary.get( + "rotator_reverse", self.rotator_reverse + ) + self.rotator_min_angle = dictionary.get( + "rotator_min_angle", self.rotator_min_angle + ) + self.rotator_max_angle = dictionary.get( + "rotator_max_angle", self.rotator_max_angle + ) self.min_altitude = dictionary.get("min_altitude", self.min_altitude) self.settle_time = dictionary.get("settle_time", self.settle_time) @@ -2422,6 +2425,7 @@ def camera_info(self): "JD": (None, "Julian date"), "MJD": (None, "Modified Julian date"), "MJD-OBS": (None, "Modified Julian date"), + "CAMTIME": (None, "Exposure time from camera (T) or user (F)"), "EXPTIME": (None, "Exposure time [seconds]"), "EXPOSURE": (None, "Exposure time [seconds]"), "SUBEXP": (None, "Subexposure time [seconds]"), @@ -2518,8 +2522,13 @@ def camera_info(self): except: pass try: - info["EXPTIME"] = (self.camera.ExposureTime, info["EXPTIME"][1]) - info["EXPOSURE"] = (self.camera.ExposureTime, info["EXPOSURE"][1]) + last_exposure_duration = self.camera.LastExposureDuration + info["EXPTIME"] = (last_exposure_duration, info["EXPTIME"][1]) + info["EXPOSURE"] = (last_exposure_duration, info["EXPOSURE"][1]) + except: + pass + try: + info["CAMTIME"] = (self.camera.CameraTime, info["CAMTIME"][1]) except: pass try: @@ -3684,7 +3693,9 @@ def latitude(self, value): self._latitude = ( coord.Latitude(value) if value is not None or value != "" else None ) - self.telescope.SiteLatitude = self._latitude.deg + # If connected, set the telescope site latitude + if self.telescope.Connected: + self.telescope.SiteLatitude = self._latitude.deg self._config["site"]["latitude"] = ( self._latitude.to_string(unit=u.degree, sep="dms", precision=5) if self._latitude is not None @@ -3704,7 +3715,8 @@ def longitude(self, value): if value is not None or value != "" else None ) - self.telescope.SiteLongitude = self._longitude.deg + if self.telescope.Connected: + self.telescope.SiteLongitude = self._longitude.deg self._config["site"]["longitude"] = ( self._longitude.to_string(unit=u.degree, sep="dms", precision=5) if self._longitude is not None @@ -3855,7 +3867,7 @@ def cover_calibrator_alt(self): def cover_calibrator_alt(self, value): logger.debug(f"Observatory.cover_calibrator_alt = {value} called") self._cover_calibrator_alt = ( - min(max(float(value), 0), 90) if value is not None or value != "" else None + min(max(float(value), 0), 90) if value is not None and value != "" else None ) self._config["cover_calibrator"]["cover_calibrator_alt"] = ( str(self._cover_calibrator_alt) @@ -3872,7 +3884,9 @@ def cover_calibrator_az(self): def cover_calibrator_az(self, value): logger.debug(f"Observatory.cover_calibrator_az = {value} called") self._cover_calibrator_az = ( - min(max(float(value), 0), 360) if value is not None or value != "" else None + min(max(float(value), 0), 360) + if value is not None and value != "" + else None ) self._config["cover_calibrator"]["cover_calibrator_az"] = ( str(self._cover_calibrator_az)