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