diff --git a/CHANGELOG b/CHANGELOG index ffcc8d5..0df0fd1 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -1,3 +1,10 @@ +2.7.0 +- implemented Mono frame +- scaled RGB and Mono frames have now binning-factor in FITS header +- minor code cleanup and optimization +- minimized FITS metadata to avoid trouble with plate solver +- moved folders CamerInfos and testpattern out of Python library + 2.6.5 - running old driver gets killed when started with `python3` and `python` - fixed typo in label "Disconnect" diff --git a/src/indi_pylibcamera/CameraInfos/Arducam_Pivariety_IMX462.txt b/CameraInfos/Arducam_Pivariety_IMX462.txt similarity index 100% rename from src/indi_pylibcamera/CameraInfos/Arducam_Pivariety_IMX462.txt rename to CameraInfos/Arducam_Pivariety_IMX462.txt diff --git a/src/indi_pylibcamera/CameraInfos/IMX290.txt b/CameraInfos/IMX290.txt similarity index 100% rename from src/indi_pylibcamera/CameraInfos/IMX290.txt rename to CameraInfos/IMX290.txt diff --git a/src/indi_pylibcamera/CameraInfos/Raspi_GlobalShutter_IMX296.txt b/CameraInfos/Raspi_GlobalShutter_IMX296.txt similarity index 100% rename from src/indi_pylibcamera/CameraInfos/Raspi_GlobalShutter_IMX296.txt rename to CameraInfos/Raspi_GlobalShutter_IMX296.txt diff --git a/src/indi_pylibcamera/CameraInfos/Raspi_HQ_IMX477.txt b/CameraInfos/Raspi_HQ_IMX477.txt similarity index 100% rename from src/indi_pylibcamera/CameraInfos/Raspi_HQ_IMX477.txt rename to CameraInfos/Raspi_HQ_IMX477.txt diff --git a/src/indi_pylibcamera/CameraInfos/Raspi_M3_IMX708.txt b/CameraInfos/Raspi_M3_IMX708.txt similarity index 100% rename from src/indi_pylibcamera/CameraInfos/Raspi_M3_IMX708.txt rename to CameraInfos/Raspi_M3_IMX708.txt diff --git a/src/indi_pylibcamera/CameraInfos/Raspi_V1_OV5647.txt b/CameraInfos/Raspi_V1_OV5647.txt similarity index 100% rename from src/indi_pylibcamera/CameraInfos/Raspi_V1_OV5647.txt rename to CameraInfos/Raspi_V1_OV5647.txt diff --git a/MANIFEST.in b/MANIFEST.in index 2e85e42..ef5e04c 100644 --- a/MANIFEST.in +++ b/MANIFEST.in @@ -1,5 +1,2 @@ - -include src/indi_pylibcamera/CameraInfos/* -include src/indi_pylibcamera/testpattern/* include src/indi_pylibcamera/indi_pylibcamera.ini include src/indi_pylibcamera/indi_pylibcamera.xml diff --git a/README.md b/README.md index 493f361..57adc17 100644 --- a/README.md +++ b/README.md @@ -143,6 +143,10 @@ every frame exposure can solve this issue. Valid values of this switch are: Default (if not otherwise set in INI file) is `auto`. - `enable_IERS_autoupdate` (`yes`, `no`): Allows the `astropy` library to update the IERS-A table from internet. By default this is disabled to avoid errors when the camera is not connected to internet. +- `extended_Metadata` (`yes`, `no`, `on`, `off`, `true`, `false`, `1`, `0`, default: `false`): Adds more metadata to the FITS image. For +instance it stores `SCALE` (angle of sky projected to pixel) and `XPIXSZ`/`YPIXSZ` (binned pixel size). When disabled +the pixel sizes `PIXSIZE1`/`PIXSIZE2` get adjusted to the binning. That makes the images look like from a camera +without binning and avoids many issues with plate solvers. There are more settings, mostly to support debugging. @@ -186,6 +190,21 @@ If you get a line containing `python3` and `indi_pylibcamera` in the output the kill the driver process manually before you restart the indiserver. Otherwise, you will get a libcamera error when connecting to the camera. +## Frametypes +The driver can (when supported by the camera hardware) provide these image frame types: +- `Raw` is the raw signal coming from the pixel converted to digital. Most cameras have an analog amplifier +(configurable with `Gain`) between the pixel and the A/D converter. There is no software processing of the data. +Typically, these pixel data have higher resolution but suffer from offset and gain errors. Furthermore, the pixel of +color cameras have own color filter (called Bayer pattern) and do not RGB data directly. Such raw images need +post-processing. Astro-photographs like raw images because they allow much better image optimizations. The frame size +of raw images is determined by the modes implemented in the camera hardware. +- `RGB` are images post-processed by the Image Signal Processor (ISP). The ISP corrects for offset and gain, +calculates the colors and can adjust exposure time and wide balance automatically. Drawback is the lower dynamic +(lower bit width) and additional systematic "noise" due to rounding errors. Frame size can be chosen freely because +the image scaling is done by software in the ISP. +- `Mono` is a special case of the `RGB` images, exposed with saturation=0 and transmitted with only one channel per +pixel. + ## Special handling for some cameras The driver is made as generic as possible by using the camera information provided by libcamera. For instance the raw modes and frame sizes selectable in the driver are coming from libcamera. Unfortunately some important information diff --git a/src/indi_pylibcamera/CameraControl.py b/src/indi_pylibcamera/CameraControl.py index c29d893..dd07559 100644 --- a/src/indi_pylibcamera/CameraControl.py +++ b/src/indi_pylibcamera/CameraControl.py @@ -29,6 +29,7 @@ def __init__(self): self.ExposureTime = None self.DoFastExposure = None self.DoRaw = None + self.DoMono = None self.ProcSize = None self.RawMode = None self.Binning = None @@ -38,6 +39,7 @@ def update(self, ExposureTime, knownVectors, advertised_camera_controls, has_Raw self.ExposureTime = ExposureTime self.DoFastExposure = knownVectors["CCD_FAST_TOGGLE"]["INDI_ENABLED"].value == ISwitchState.ON self.DoRaw = knownVectors["CCD_CAPTURE_FORMAT"]["INDI_RAW"].value == ISwitchState.ON if has_RawModes else False + self.DoMono = knownVectors["CCD_CAPTURE_FORMAT"]["INDI_MONO"].value == ISwitchState.ON if has_RawModes else False self.ProcSize = ( int(knownVectors["CCD_PROCFRAME"]["WIDTH"].value), int(knownVectors["CCD_PROCFRAME"]["HEIGHT"].value) @@ -136,7 +138,11 @@ def update(self, ExposureTime, knownVectors, advertised_camera_controls, has_Raw "HIGHQUALITY": controls.draft.NoiseReductionModeEnum.HighQuality, }[knownVectors["CAMCTRL_NOISEREDUCTIONMODE"].get_OnSwitches()[0]] if "Saturation" in advertised_camera_controls: - self.camera_controls["Saturation"] = knownVectors["CAMCTRL_SATURATION"]["SATURATION"].value + if self.DoMono: + # mono exposures are a special case of RGB with saturation=0 + self.camera_controls["Saturation"] = 0.0 + else: + self.camera_controls["Saturation"] = knownVectors["CAMCTRL_SATURATION"]["SATURATION"].value if "Sharpness" in advertised_camera_controls: self.camera_controls["Sharpness"] = knownVectors["CAMCTRL_SHARPNESS"]["SHARPNESS"].value @@ -391,7 +397,7 @@ def openCamera(self, idx: int): # workaround for cameras reporting max_ExposureTime=0 (IMX296) self.max_ExposureTime = self.max_ExposureTime if self.min_ExposureTime < self.max_ExposureTime else 1000.0e6 self.max_AnalogueGain = self.max_AnalogueGain if self.min_AnalogueGain < self.max_AnalogueGain else 1000.0 - # TODO + # INI switch to force camera restarts force_Restart = self.config.get("driver", "force_Restart", fallback="auto").lower() if force_Restart == "yes": logger.info("INI setting forces camera restart") @@ -415,7 +421,7 @@ def getProp(self, name): """ return self.CamProps[name] - def snooped_FitsHeader(self): + def snooped_FitsHeader(self, binnedCellSize_nm): """created FITS header data from snooped data Example: @@ -449,11 +455,14 @@ def snooped_FitsHeader(self): "APTDIA": (Aperture, "[mm] Telescope aperture/diameter") }) #### SCALE #### - if FocalLength > 0: - FitsHeader["SCALE"] = ( - 0.206265 * self.getProp("UnitCellSize")[0] * self.present_CameraSettings.Binning[0] / FocalLength, - "[arcsec/px] image scale" - ) + if self.config.getboolean("driver", "extended_Metadata", fallback=False): + # some telescope driver do not provide TELESCOPE_FOCAL_LENGTH and some capture software overwrite + # FOCALLEN without recalculating SCALE --> trouble with plate solver + if FocalLength > 0: + FitsHeader["SCALE"] = ( + 0.206265 * binnedCellSize_nm / FocalLength, + "[arcsec/px] image scale" + ) #### SITELAT, SITELONG #### Lat = self.parent.knownVectors["GEOGRAPHIC_COORD"]["LAT"].value Long = self.parent.knownVectors["GEOGRAPHIC_COORD"]["LONG"].value @@ -524,17 +533,18 @@ def createRawFits(self, array, metadata): # we expect uncompressed format here if format.count("_") > 0: raise NotImplementedError(f'got unsupported raw image format {format}') - if format[0] not in ["S", "R"]: - raise NotImplementedError(f'got unsupported raw image format {format}') - # Bayer of mono format + # Bayer or mono format if format[0] == "S": # Bayer pattern format BayerPattern = format[1:5] BayerPattern = self.parent.config.get("driver", "force_BayerOrder", fallback=BayerPattern) bit_depth = int(format[5:]) - else: + elif format[0] == "R": + # mono camera BayerPattern = None bit_depth = int(format[1:]) + else: + raise NotImplementedError(f'got unsupported raw image format {format}') # left adjust if needed if bit_depth > 8: bit_pix = 16 @@ -561,17 +571,36 @@ def createRawFits(self, array, metadata): **self.parent.knownVectors["FITS_HEADER"].FitsHeader, "EXPTIME": (metadata["ExposureTime"]/1e6, "[s] Total Exposure Time"), "CCD-TEMP": (metadata.get('SensorTemperature', 0), "[degC] CCD Temperature"), - "PIXSIZE1": (self.getProp("UnitCellSize")[0] / 1e3, "[um] Pixel Size 1"), - "PIXSIZE2": (self.getProp("UnitCellSize")[1] / 1e3, "[um] Pixel Size 2"), - "XBINNING": (self.present_CameraSettings.Binning[0], "Binning factor in width"), - "YBINNING": (self.present_CameraSettings.Binning[1], "Binning factor in height"), - "XPIXSZ": (self.getProp("UnitCellSize")[0] / 1e3 * self.present_CameraSettings.Binning[0], "[um] X binned pixel size"), - "YPIXSZ": (self.getProp("UnitCellSize")[1] / 1e3 * self.present_CameraSettings.Binning[1], "[um] Y binned pixel size"), "FRAME": (FrameType, "Frame Type"), "IMAGETYP": (FrameType+" Frame", "Frame Type"), - **self.snooped_FitsHeader(), + **self.snooped_FitsHeader(binnedCellSize_nm = self.getProp("UnitCellSize")[0] * self.present_CameraSettings.Binning[0]), "GAIN": (metadata.get("AnalogueGain", 0.0), "Gain"), } + if self.config.getboolean("driver", "extended_Metadata", fallback=False): + # This is very detailed information about the camera binning. But some plate solver ignore this and get + # trouble with a wrong field of view. + FitsHeader.update({ + "PIXSIZE1": (self.getProp("UnitCellSize")[0] / 1e3, "[um] Pixel Size 1"), + "PIXSIZE2": (self.getProp("UnitCellSize")[1] / 1e3, "[um] Pixel Size 2"), + "XBINNING": (self.present_CameraSettings.Binning[0], "Binning factor in width"), + "YBINNING": (self.present_CameraSettings.Binning[1], "Binning factor in height"), + "XPIXSZ": (self.getProp("UnitCellSize")[0] / 1e3 * self.present_CameraSettings.Binning[0], + "[um] X binned pixel size"), + "YPIXSZ": (self.getProp("UnitCellSize")[1] / 1e3 * self.present_CameraSettings.Binning[1], + "[um] Y binned pixel size"), + }) + else: + # Pretend to be a camera without binning to avoid trouble with plate solver. + FitsHeader.update({ + "PIXSIZE1": (self.getProp("UnitCellSize")[0] / 1e3 * self.present_CameraSettings.Binning[0], "[um] Pixel Size 1"), + "PIXSIZE2": (self.getProp("UnitCellSize")[1] / 1e3 * self.present_CameraSettings.Binning[1], "[um] Pixel Size 2"), + "XBINNING": (1, "Binning factor in width"), + "YBINNING": (1, "Binning factor in height"), + "XPIXSZ": (self.getProp("UnitCellSize")[0] / 1e3 * self.present_CameraSettings.Binning[0], + "[um] X binned pixel size"), + "YPIXSZ": (self.getProp("UnitCellSize")[1] / 1e3 * self.present_CameraSettings.Binning[1], + "[um] Y binned pixel size"), + }) if BayerPattern is not None: FitsHeader.update({ "XBAYROFF": (0, "[px] X offset of Bayer array"), @@ -606,7 +635,7 @@ def createRawFits(self, array, metadata): return hdul def createRgbFits(self, array, metadata): - """creates RGB FITS image from RGB frame + """creates RGB and monochrome FITS image from RGB frame Args: array: data array @@ -631,8 +660,18 @@ def createRgbFits(self, array, metadata): else: raise NotImplementedError(f'got unsupported RGB image format {format}') #self.log_FrameInformation(array=array, metadata=metadata, is_raw=False) + if self.present_CameraSettings.DoMono: + # monochrome frames are a special case of RGB: exposed with saturation=0, transmitted is R channel only + array = array[0, :, :] # convert to FITS hdu = fits.PrimaryHDU(array) + # The image scaling in the ISP works like a software-binning. + # When aspect ratio of the scaled image differs from the pixel array the ISP ignores rows (columns) on + # both sides of the pixel array to select the field of view. + ArraySize = self.getProp("PixelArraySize") + FrameSize = self.picam2.camera_configuration()["main"]["size"] + SoftwareBinning = ArraySize[1] / FrameSize[1] if (ArraySize[0] / ArraySize[1]) > (FrameSize[0] / FrameSize[1]) \ + else ArraySize[0] / FrameSize[0] # avoid access conflicts to knownVectors with self.parent.knownVectorsLock: # determine frame type @@ -650,18 +689,34 @@ def createRgbFits(self, array, metadata): **self.parent.knownVectors["FITS_HEADER"].FitsHeader, "EXPTIME": (metadata["ExposureTime"]/1e6, "[s] Total Exposure Time"), "CCD-TEMP": (metadata.get('SensorTemperature', 0), "[degC] CCD Temperature"), - "PIXSIZE1": (self.getProp("UnitCellSize")[0] / 1e3, "[um] Pixel Size 1"), - "PIXSIZE2": (self.getProp("UnitCellSize")[1] / 1e3, "[um] Pixel Size 2"), - "XBINNING": (self.present_CameraSettings.Binning[0], "Binning factor in width"), - "YBINNING": (self.present_CameraSettings.Binning[1], "Binning factor in height"), - "XPIXSZ": (self.getProp("UnitCellSize")[0] / 1e3 * self.present_CameraSettings.Binning[0], "[um] X binned pixel size"), - "YPIXSZ": (self.getProp("UnitCellSize")[1] / 1e3 * self.present_CameraSettings.Binning[1], "[um] Y binned pixel size"), "FRAME": (FrameType, "Frame Type"), "IMAGETYP": (FrameType+" Frame", "Frame Type"), - **self.snooped_FitsHeader(), + **self.snooped_FitsHeader(binnedCellSize_nm = self.getProp("UnitCellSize")[0] * SoftwareBinning), # more info from camera "GAIN": (metadata.get("AnalogueGain", 0.0), "Analog gain setting"), } + if self.config.getboolean("driver", "extended_Metadata", fallback=False): + # This is very detailed information about the camera binning. But some plate solver ignore this and get + # trouble with a wrong field of view. + FitsHeader.update({ + "PIXSIZE1": (self.getProp("UnitCellSize")[0] / 1e3, "[um] Pixel Size 1"), + "PIXSIZE2": (self.getProp("UnitCellSize")[1] / 1e3, "[um] Pixel Size 2"), + "XBINNING": (SoftwareBinning, "Binning factor in width"), + "YBINNING": (SoftwareBinning, "Binning factor in height"), + "XPIXSZ": (self.getProp("UnitCellSize")[0] / 1e3 * SoftwareBinning, "[um] X binned pixel size"), + "YPIXSZ": (self.getProp("UnitCellSize")[1] / 1e3 * SoftwareBinning, "[um] Y binned pixel size"), + }) + else: + # Pretend to be a camera without binning to avoid trouble with plate solver. + FitsHeader.update({ + "PIXSIZE1": (self.getProp("UnitCellSize")[0] / 1e3 * SoftwareBinning, "[um] Pixel Size 1"), + "PIXSIZE2": (self.getProp("UnitCellSize")[1] / 1e3 * SoftwareBinning, "[um] Pixel Size 2"), + "XBINNING": (1, "Binning factor in width"), + "YBINNING": (1, "Binning factor in height"), + "XPIXSZ": (self.getProp("UnitCellSize")[0] / 1e3 * SoftwareBinning, "[um] X binned pixel size"), + "YPIXSZ": (self.getProp("UnitCellSize")[1] / 1e3 * SoftwareBinning, "[um] Y binned pixel size"), + + }) for kw, value_comment in FitsHeader.items(): hdu.header[kw] = value_comment hdu.header.set("DATE-OBS", (datetime.datetime.fromisoformat(hdu.header["DATE-END"])-datetime.timedelta(seconds=hdu.header["EXPTIME"])).isoformat(timespec="milliseconds"), @@ -677,7 +732,7 @@ def log_FrameInformation(self, array, metadata, format): metadata: frame metadata format: format string """ - if self.parent.config.getboolean("driver", "log_FrameInformation", fallback=False): + if self.config.getboolean("driver", "log_FrameInformation", fallback=False): if array.ndim == 2: arr = array.view(np.uint16) BitUsages = list() @@ -740,7 +795,6 @@ def __ExposureLoop(self): self.Sig_ActionExpose.clear() if self.Sig_ActionExit.is_set(): # exit exposure loop - #logger.error(f'DBG vor stop (line 744): {self.picam2.started=}') # FIXME self.picam2.stop_() self.parent.setVector("CCD_EXPOSURE", "CCD_EXPOSURE_VALUE", value=0, state=IVectorState.OK) return @@ -767,7 +821,6 @@ def __ExposureLoop(self): if self.present_CameraSettings.is_ReconfigurationNeeded(NewCameraSettings) or self.needs_Restarts: logger.info(f'reconfiguring camera') # need a new camera configuration - #logger.error(f'DBG vor create_still_configuration: {self.picam2.started=}') # FIXME config = self.picam2.create_still_configuration( queue=NewCameraSettings.DoFastExposure, buffer_count=2 # 2 if NewCameraSettings.DoFastExposure else 1 # need at least 2 buffer for queueing @@ -788,19 +841,15 @@ def __ExposureLoop(self): #self.parent.setVector("CCD_FRAME", "HEIGHT", value=NewCameraSettings.ProcSize[1]) # optimize (align) configuration: small changes to some main stream configurations # (for instance: size) will fit better to hardware - #logger.error(f'DBG vor align_configuration: {self.picam2.started=}') # FIXME self.picam2.align_configuration(config) # set still configuration - #logger.error(f'DBG vor configure: {self.picam2.started=}') # FIXME self.picam2.configure(config) # changing exposure time or analogue gain needs a restart if IsRestartNeeded: # change camera controls - #logger.error(f'DBG vor set_controls: {self.picam2.started=}') # FIXME self.picam2.set_controls(NewCameraSettings.get_controls()) # start camera if not already running in Fast Exposure mode if not self.picam2.started: - #logger.error(f'DBG vor start (line 802): {self.picam2.started=}') # FIXME self.picam2.start() logger.debug(f'camera started') # camera runs now with new parameter @@ -808,7 +857,6 @@ def __ExposureLoop(self): # last chance to exit or abort before doing exposure if self.Sig_ActionExit.is_set(): # exit exposure loop - #logger.error(f'DBG vor stop (line 811): {self.picam2.started=}') # FIXME self.picam2.stop_() self.parent.setVector("CCD_EXPOSURE", "CCD_EXPOSURE_VALUE", value=0, state=IVectorState.OK) return @@ -818,7 +866,6 @@ def __ExposureLoop(self): # get (non-blocking!) frame and meta data self.Sig_CaptureDone.clear() ExpectedEndOfExposure = time.time() + self.present_CameraSettings.ExposureTime - #logger.error(f'DBG vor capture_arrays: {self.picam2.started=}') # FIXME job = self.picam2.capture_arrays( ["raw" if self.present_CameraSettings.DoRaw else "main"], wait=False, signal_function=self.on_CaptureFinished, @@ -835,14 +882,12 @@ def __ExposureLoop(self): # allow to close camera if self.Sig_ActionExit.is_set(): # exit exposure loop - #logger.error(f'DBG vor stop (line 838): {self.picam2.started=}') # FIXME self.picam2.stop_() self.parent.setVector("CCD_EXPOSURE", "CCD_EXPOSURE_VALUE", value=0, state=IVectorState.OK) return # allow to abort exposure Abort = self.Sig_ActionAbort.is_set() if Abort: - #logger.error(f'DBG vor stop (line 846): {self.picam2.started=}') # FIXME self.picam2.stop_() # stop exposure immediately self.Sig_ActionAbort.clear() break @@ -852,7 +897,6 @@ def __ExposureLoop(self): time.sleep(PollingPeriod_s) # get frame and its metadata if not Abort: - #logger.error(f'DBG vor wait: {self.picam2.started=}') # FIXME (array, ), metadata = self.picam2.wait(job) logger.info('got exposed frame') # at least HQ camera reports CCD temperature in meta data @@ -863,7 +907,6 @@ def __ExposureLoop(self): # last chance to exit or abort before sending blob if self.Sig_ActionExit.is_set(): # exit exposure loop - #logger.error(f'DBG vor stop (line 864): {self.picam2.started=}') # FIXME self.picam2.stop_() self.parent.setVector("CCD_EXPOSURE", "CCD_EXPOSURE_VALUE", value=0, state=IVectorState.OK) return @@ -875,7 +918,6 @@ def __ExposureLoop(self): FastCount_Frames = self.parent.knownVectors["CCD_FAST_COUNT"]["FRAMES"].value if not DoFastExposure: # in normal exposure mode the camera needs to be started with exposure command - #logger.error(f'DBG vor stop (line 876): {self.picam2.started=}') # FIXME self.picam2.stop() if not Abort: if DoFastExposure: @@ -885,9 +927,14 @@ def __ExposureLoop(self): if self.present_CameraSettings.DoRaw: hdul = self.createRawFits(array=array, metadata=metadata) else: + # RGB and Mono hdul = self.createRgbFits(array=array, metadata=metadata) bstream = io.BytesIO() hdul.writeto(bstream) + # free up some memory + del hdul + del array + # save and/or transmit frame size = bstream.tell() # what to do with image with self.parent.knownVectorsLock: diff --git a/src/indi_pylibcamera/__init__.py b/src/indi_pylibcamera/__init__.py index 8bf574b..a62d6dc 100644 --- a/src/indi_pylibcamera/__init__.py +++ b/src/indi_pylibcamera/__init__.py @@ -2,4 +2,4 @@ INDI driver for libcamera supported cameras """ -__version__ = "2.6.5" +__version__ = "2.7.0" diff --git a/src/indi_pylibcamera/indi_pylibcamera.py b/src/indi_pylibcamera/indi_pylibcamera.py index 7cdfe79..7b854f8 100755 --- a/src/indi_pylibcamera/indi_pylibcamera.py +++ b/src/indi_pylibcamera/indi_pylibcamera.py @@ -102,7 +102,7 @@ def __init__(self, parent): ISwitch(name="DISCONNECT", label="Disconnect", value=ISwitchState.ON), ], label="Connection", group="Main Control", - rule=ISwitchRule.ONEOFMANY, is_savable=False, + rule=ISwitchRule.ONEOFMANY, is_storable=False, ) def set_byClient(self, values: dict): @@ -146,7 +146,7 @@ def __init__(self, parent, min_exp, max_exp): INumber(name="CCD_EXPOSURE_VALUE", label="Duration (s)", min=min_exp / 1e6, max=max_exp / 1e6, step=0.001, value=1.0, format="%.3f"), ], - label="Expose", group="Main Control", is_savable=False, + label="Expose", group="Main Control", is_storable=False, ) def set_byClient(self, values: dict): @@ -233,10 +233,12 @@ def __init__(self, parent, CameraThread): elements = [ ISwitch(name="INDI_RAW", label="RAW", value=ISwitchState.ON), ISwitch(name="INDI_RGB", label="RGB", value=ISwitchState.OFF), + ISwitch(name="INDI_MONO", label="Mono", value=ISwitchState.OFF), ] else: elements = [ ISwitch(name="INDI_RGB", label="RGB", value=ISwitchState.ON), + ISwitch(name="INDI_MONO", label="Mono", value=ISwitchState.OFF), ] super().__init__( device=self.parent.device, timestamp=self.parent.timestamp, name="CCD_CAPTURE_FORMAT", @@ -286,7 +288,7 @@ def __init__(self, parent, CameraThread, do_CameraAdjustments): INumber(name="HOR_BIN", label="X", min=1, max=max_HOR_BIN, step=1, value=1, format="%2.0f"), INumber(name="VER_BIN", label="Y", min=1, max=max_VER_BIN, step=1, value=1, format="%2.0f"), ], - label="Binning", group="Image Settings", + label="Raw binning", group="Image Settings", state=IVectorState.IDLE, perm=IPermission.RW, ) @@ -374,7 +376,7 @@ def __init__(self, parent): IText(name="KEYWORD_VALUE", label="Value", value=""), IText(name="KEYWORD_COMMENT", label="Comment", value=""), ], - label="FITS Header", group="General Info", perm=IPermission.WO, is_savable=False, + label="FITS Header", group="General Info", perm=IPermission.WO, is_storable=False, ) def set_byClient(self, values: dict): @@ -418,7 +420,7 @@ def __init__(self, parent): ISwitch(name="ABORT", label="Abort", value=ISwitchState.OFF), ], label="Abort", group="Main Control", - rule=ISwitchRule.ATMOST1, is_savable=False, + rule=ISwitchRule.ATMOST1, is_storable=False, ) def set_byClient(self, values: dict): @@ -441,7 +443,7 @@ def __init__(self, parent): ISwitch(name="PRINT_SNOOPED", label="Print", value=ISwitchState.OFF), ], label="Print snooped values", group="Snooping", - rule=ISwitchRule.ATMOST1, is_savable=False, + rule=ISwitchRule.ATMOST1, is_storable=False, ) def set_byClient(self, values: dict): @@ -471,7 +473,7 @@ def __init__(self, parent): ISwitch(name="CONFIG_PURGE", label="Purge", value=ISwitchState.OFF), ], label="Configuration", group="Options", - rule=ISwitchRule.ATMOST1, is_savable=False, + rule=ISwitchRule.ATMOST1, is_storable=False, ) def set_byClient(self, values: dict): @@ -594,7 +596,7 @@ def __init__(self, config=None): ) for i in range(len(self.Cameras)) ], label="Camera", group="Main Control", - rule=ISwitchRule.ONEOFMANY, is_savable=False, + rule=ISwitchRule.ONEOFMANY, is_storable=False, ) ) self.checkin( @@ -610,7 +612,7 @@ def __init__(self, config=None): IText(name="DRIVER_INTERFACE", label="Interface", value="2"), # This is a CCD! ], label="Driver Info", group="General Info", - perm=IPermission.RO, is_savable=False, + perm=IPermission.RO, is_storable=False, ) ) self.checkin( @@ -637,7 +639,7 @@ def __init__(self, config=None): INumber(name="ELEV", label="Elevation (m)", min=-200, max=10000, step=0, value=0, format="%g"), ], label="Scope Location", group="Snooping", - perm=IPermission.RW, is_savable=False, + perm=IPermission.RW, is_storable=False, ), ) self.checkin( @@ -648,7 +650,7 @@ def __init__(self, config=None): INumber(name="DEC", label="DEC (dd:mm:ss)", min=-90, max=90, step=0, value=0, format="%010.6m"), ], label="Eq. Coordinates", group="Snooping", - perm=IPermission.RW, is_savable=False, + perm=IPermission.RW, is_storable=False, ), ) # TODO: "EQUATORIAL_COORD" (J2000 coordinates from mount) are not used! @@ -661,7 +663,7 @@ def __init__(self, config=None): INumber(name="DEC", label="DEC (dd:mm:ss)", min=-90, max=90, step=0, value=0, format="%010.6m"), ], label="Eq. J2000 Coordinates", group="Snooping", - perm=IPermission.RW, is_savable=False, + perm=IPermission.RW, is_storable=False, ), ) self.checkin( @@ -672,7 +674,7 @@ def __init__(self, config=None): ISwitch(name="PIER_EAST", value=ISwitchState.OFF, label="East (pointing west)"), ], label="Pier Side", group="Snooping", - rule=ISwitchRule.ONEOFMANY, is_savable=False, + rule=ISwitchRule.ONEOFMANY, is_storable=False, ) ) self.checkin( @@ -749,7 +751,7 @@ def openCamera(self): IText(name="CAMERA_UNITCELLSIZE", label="Pixel size", value=str(self.CameraThread.getProp("UnitCellSize"))), ], label="Camera Info", group="General Info", - state=IVectorState.OK, perm=IPermission.RO, is_savable=False, + state=IVectorState.OK, perm=IPermission.RO, is_storable=False, ), send_defVector=True, ) @@ -780,7 +782,7 @@ def openCamera(self): INumber(name="HEIGHT", label="Height", min=1, max=self.CameraThread.getProp("PixelArraySize")[1], step=0, value=self.CameraThread.getProp("PixelArraySize")[1], format="%4.0f"), ], - label="RGB format", group="Image Settings", + label="RGB, Mono", group="Image Settings", perm=IPermission.RW, ), send_defVector=True, @@ -814,7 +816,7 @@ def openCamera(self): step=0, value=self.CameraThread.getProp("PixelArraySize")[1], format="%4.0f"), ], label="Frame", group="Image Info", - perm=IPermission.RO, is_savable=False, # TODO: make it savable after implementing frame cropping + perm=IPermission.RO, is_storable=False, # TODO: make it available after implementing frame cropping ), send_defVector=True, ) @@ -827,7 +829,7 @@ def openCamera(self): ISwitch(name="RESET", label="Reset", value=ISwitchState.OFF), ], label="Frame Values", group="Image Settings", - rule=ISwitchRule.ONEOFMANY, perm=IPermission.WO, is_savable=False, + rule=ISwitchRule.ONEOFMANY, perm=IPermission.WO, is_storable=False, ), send_defVector=True, ) @@ -856,7 +858,7 @@ def openCamera(self): INumber(name="CCD_TEMPERATURE_VALUE", label="Temperature (C)", min=-50, max=50, step=0, value=0, format="%5.2f"), ], label="Temperature", group="Main Control", - state=IVectorState.IDLE, perm=IPermission.RO, is_savable=False, + state=IVectorState.IDLE, perm=IPermission.RO, is_storable=False, ), send_defVector=True, ) @@ -881,7 +883,7 @@ def openCamera(self): value=8 if len(self.CameraThread.RawModes) < 1 else self.CameraThread.RawModes[0]["bit_depth"], format="%.f"), ], label="CCD Information", group="Image Info", - state=IVectorState.IDLE, perm=IPermission.RO, is_savable=False, + state=IVectorState.IDLE, perm=IPermission.RO, is_storable=False, ), send_defVector=True, ) @@ -913,7 +915,7 @@ def openCamera(self): IBlob(name="CCD1", label="Image"), ], label="Image Data", group="Image Info", - state=IVectorState.OK, perm=IPermission.RO, is_savable=False, + state=IVectorState.OK, perm=IPermission.RO, is_storable=False, ), send_defVector=True, ) @@ -983,7 +985,7 @@ def openCamera(self): elements=[ INumber(name="FRAMES", label="Frames", min=0, max=100000, step=1, value=1, format="%.f"), ], - label="Fast Count", group="Main Control", is_savable=False, + label="Fast Count", group="Main Control", is_storable=False, ), send_defVector=True, ) diff --git a/src/indi_pylibcamera/indi_pylibcamera.xml b/src/indi_pylibcamera/indi_pylibcamera.xml index 18c298c..8d294fe 100644 --- a/src/indi_pylibcamera/indi_pylibcamera.xml +++ b/src/indi_pylibcamera/indi_pylibcamera.xml @@ -2,7 +2,7 @@ indi_pylibcamera - 2.6.5 + 2.7.0 diff --git a/src/indi_pylibcamera/indidevice.py b/src/indi_pylibcamera/indidevice.py index 7072f4b..0d3a4b7 100755 --- a/src/indi_pylibcamera/indidevice.py +++ b/src/indi_pylibcamera/indidevice.py @@ -154,7 +154,7 @@ def __init__( label: str = None, group: str = "", state: str = IVectorState.IDLE, perm: str = IPermission.RW, timeout: int = 60, timestamp: bool = False, message: str = None, - is_savable: bool = True, + is_storable: bool = True, ): """constructor @@ -169,7 +169,7 @@ def __init__( timeout: timeout timestamp: send messages with (True) or without (False) timestamp message: message send to client - is_savable: can be saved + is_storable: can be saved """ self._vectorType = "NotSet" self.device = device @@ -186,7 +186,7 @@ def __init__( self.timeout = timeout self.timestamp = timestamp self.message = message - self.is_savable = is_savable + self.is_storable = is_storable def __str__(self) -> str: return f"" @@ -341,7 +341,7 @@ def save(self): dict with Vector state """ state = None - if self.is_savable: + if self.is_storable: state = dict() state["name"] = self.name state["values"] = {element.name: element.value for element in self.elements} @@ -350,7 +350,7 @@ def save(self): def restore_DriverDefault(self): """restore driver defaults for savable vector """ - if self.is_savable: + if self.is_storable: self.set_byClient(self.driver_default) @@ -390,11 +390,11 @@ def __init__( label: str = None, group: str = "", state: str = IVectorState.IDLE, perm: str = IPermission.RW, timeout: int = 60, timestamp: bool = False, message: str = None, - is_savable: bool = True, + is_storable: bool = True, ): super().__init__( device=device, name=name, elements=elements, label=label, group=group, - state=state, perm=perm, timeout=timeout, timestamp=timestamp, message=message, is_savable=is_savable, + state=state, perm=perm, timeout=timeout, timestamp=timestamp, message=message, is_storable=is_storable, ) self._vectorType = "TextVector" @@ -444,11 +444,11 @@ def __init__( label: str = None, group: str = "", state: str = IVectorState.IDLE, perm: str = IPermission.RW, timeout: int = 60, timestamp: bool = False, message: str = None, - is_savable: bool = True, + is_storable: bool = True, ): super().__init__( device=device, name=name, elements=elements, label=label, group=group, - state=state, perm=perm, timeout=timeout, timestamp=timestamp, message=message, is_savable=is_savable, + state=state, perm=perm, timeout=timeout, timestamp=timestamp, message=message, is_storable=is_storable, ) self._vectorType = "NumberVector" @@ -490,11 +490,11 @@ def __init__( state: str = IVectorState.IDLE, perm: str = IPermission.RW, rule: str = ISwitchRule.ONEOFMANY, timeout: int = 60, timestamp: bool = False, message: str = None, - is_savable: bool = True, + is_storable: bool = True, ): super().__init__( device=device, name=name, elements=elements, label=label, group=group, - state=state, perm=perm, timeout=timeout, timestamp=timestamp, message=message, is_savable=is_savable, + state=state, perm=perm, timeout=timeout, timestamp=timestamp, message=message, is_storable=is_storable, ) self._vectorType = "SwitchVector" self.rule = rule @@ -629,11 +629,11 @@ def __init__( label: str = None, group: str = "", state: str = IVectorState.IDLE, perm: str = IPermission.RO, timeout: int = 60, timestamp: bool = False, message: str = None, - is_savable: bool = True, + is_storable: bool = True, ): super().__init__( device=device, name=name, elements=elements, label=label, group=group, - state=state, perm=perm, timeout=timeout, timestamp=timestamp, message=message, is_savable=is_savable, + state=state, perm=perm, timeout=timeout, timestamp=timestamp, message=message, is_storable=is_storable, ) self._vectorType = "BLOBVector" diff --git a/src/indi_pylibcamera/testpattern/RBG_testpattern.png b/testpattern/RBG_testpattern.png similarity index 100% rename from src/indi_pylibcamera/testpattern/RBG_testpattern.png rename to testpattern/RBG_testpattern.png