Skip to content

Commit

Permalink
Merge pull request #47 from scriptorron/dev
Browse files Browse the repository at this point in the history
Dev
  • Loading branch information
scriptorron authored Sep 15, 2023
2 parents a382f57 + b51ca26 commit ace0ac9
Show file tree
Hide file tree
Showing 11 changed files with 349 additions and 115 deletions.
8 changes: 8 additions & 0 deletions CHANGELOG
Original file line number Diff line number Diff line change
@@ -1,3 +1,11 @@
2.2.0
- fixed Bayer pattern order for HQ camera (pycamera2 or libcamera have change they way they report Bayer pattern order),
BAYERPAT in FITS does not depend on "Rotation" property anymore (force_Rotation has no effect anymore),
but Bayer pattern can now be forced with force_BayerOrder
- sorting raw modes by size and bit depth to make the mode with most pixel and most bits/pixel the default
- saving and loading configurations
- bug fix: after aborting an exposure the camera stayed busy and EKOS field solver could not start a new exposure

2.1.0
- fixed division by 0 when focal length equals 0
- improved driver exit when indiserver is closed (to avoid driver continue to run)
Expand Down
28 changes: 28 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -99,6 +99,34 @@ There are more settings, mostly to support debugging.

An example for a configuration file can be found in this repository.

## Saving a Configuration
The driver allows you to save up to 6 different configurations. The "Options" tab has 3 controls for that:
- You to select which of the 6 configurations you want to save, load or delete with control "Configs".
- "Config Name" allows you to give the configuration a name. This is optional. It only helps you to remember
what this configuration is made for.
- The buttons in "Configuration" trigger the actions:
- "Load" loads and applies the configuration.
- "Save" stores the configuration.
- "Default" restores the driver defaults. It does not overwrite the stored configuration.
- "Purge" removes the stored configuration.

When you try to load a non-existing configuration no settings will be changed.

Many clients load "Config #1" automatically. If you do not want this you must purge "Config #1".

Not all driver settings will be stored. For instance all settings which trigger an action (like "Connect",
"Expose" and "Abort") will not be stored and load. Also, "Scope Location" (your place on earth), "Eq. Coordinates"
(the scope pointing coordinates) and "Pier Side" will not be stored and loaded because these will typically set
by snooping the mount driver. Telescope focal length and aperture are stored but get overwritten immediately by
client (EKOS) when snooping. Generally all settings coming from client (EKOS) will overwrite settings you loaded
previously from a configuration file.

To save and load configurations you must be connected to a camera. The configuration will only be valid
for this particular camera. It is not recommended to load a configuration which was saved for a different type
of camera.

Configurations get stored in `~/.indi_pylibcamera/CONFIG*.json`.

## Error when restarting indiserver
When killing the indiserver sometimes the driver process continues to run. You can see this with:

Expand Down
56 changes: 20 additions & 36 deletions src/indi_pylibcamera/CameraControl.py
Original file line number Diff line number Diff line change
Expand Up @@ -287,22 +287,6 @@ def getRawCameraModes(self):
if not re.match("S[RGB]{4}[0-9]+", sensor_format):
logging.warning(f'raw mode not supported: {sensor_mode}')
continue
# it seems that self.CamProps["Rotation"] determines the orientation of the Bayer pattern
if self.CamProps["Rotation"] == 0:
# at least V1 camera has this
FITS_format = sensor_format[1:5]
elif self.CamProps["Rotation"] == 180:
# at least HQ camera has this
FITS_format = sensor_format[4:0:-1]
elif self.CamProps["Rotation"] == 90:
# don't know if there is such a camera and if the following rotation is right
FITS_format = "".join([sensor_format[2], sensor_format[4], sensor_format[1], sensor_format[3]])
elif self.CamProps["Rotation"] in [270, -90]:
# don't know if there is such a camera and if the following rotation is right
FITS_format = "".join([sensor_format[3], sensor_format[1], sensor_format[4], sensor_format[2]])
else:
logging.warning(f'Sensor rotation {self.CamProps["Rotation"]} not supported!')
FITS_format = sensor_format[1:5]
#
size = sensor_mode["size"]
# adjustments for cameras:
Expand Down Expand Up @@ -347,17 +331,16 @@ def getRawCameraModes(self):
logging.warning(f'Unsupported frame size {size} for imx708!')
# add to list of raw formats
raw_mode = {
"label": f'{size[0]}x{size[1]} {FITS_format} {sensor_mode["bit_depth"]}bit',
"label": f'{size[0]}x{size[1]} {sensor_format[1:5]} {sensor_mode["bit_depth"]}bit',
"size": size,
"true_size": true_size,
"camera_format": sensor_format,
"bit_depth": sensor_mode["bit_depth"],
"FITS_format": FITS_format,
"binning": binning,
}
raw_modes.append(raw_mode)
# sort list of raw formats by size in descending order
raw_modes.sort(key=lambda k: k["size"][0] * k["size"][1], reverse=True)
# sort list of raw formats by size and bit depth in descending order
raw_modes.sort(key=lambda k: k["size"][0] * k["size"][1] * 100 + k["bit_depth"], reverse=True)
return raw_modes

def openCamera(self, idx: int):
Expand All @@ -370,12 +353,6 @@ def openCamera(self, idx: int):
self.CamProps = self.picam2.camera_properties
logging.info(f'camera properties: {self.CamProps}')
# force properties with values from config file
if "Rotation" not in self.CamProps:
logging.warning("Camera properties do not have Rotation value. Need to force from config file!")
self.CamProps["Rotation"] = self.config.getint(
"driver", "force_Rotation",
fallback=self.CamProps["Rotation"] if "Rotation" in self.CamProps else 0
)
if "UnitCellSize" not in self.CamProps:
logging.warning("Camera properties do not have UnitCellSize value. Need to force from config file!")
self.CamProps["UnitCellSize"] = (
Expand Down Expand Up @@ -531,15 +508,17 @@ def createBayerFits(self, array, metadata):
with self.parent.knownVectorsLock:
# determine frame type
FrameType = self.parent.knownVectors["CCD_FRAME_TYPE"].get_OnSwitchesLabels()[0]
# determine Bayer pattern order
BayerPattern = self.picam2.camera_configuration()["raw"]["format"][1:5]
BayerPattern = self.parent.config.get("driver", "force_BayerOrder", fallback=BayerPattern)
# FITS header and metadata
FitsHeader = [
("BZERO", 2 ** (bit_pix - 1), "offset data range"),
("BSCALE", 1, "default scaling factor"),
("ROWORDER", "TOP-DOWN", "Row order"),
("INSTRUME", self.parent.device, "CCD Name"),
("TELESCOP", self.parent.knownVectors["ACTIVE_DEVICES"]["ACTIVE_TELESCOPE"].value, "Telescope name"),
("OBSERVER", self.parent.knownVectors["FITS_HEADER"]["FITS_OBSERVER"].value, "Observer name"),
("OBJECT", self.parent.knownVectors["FITS_HEADER"]["FITS_OBJECT"].value, "Object name"),
] + self.parent.knownVectors["FITS_HEADER"].get_FitsHeaderList() + [
("EXPTIME", metadata["ExposureTime"]/1e6, "Total Exposure Time (s)"),
("CCD-TEMP", metadata.get('SensorTemperature', 0), "CCD Temperature (Celsius)"),
("PIXSIZE1", self.getProp("UnitCellSize")[0] / 1e3, "Pixel Size 1 (microns)"),
Expand All @@ -553,7 +532,7 @@ def createBayerFits(self, array, metadata):
] + self.snooped_FitsHeader() + [
("XBAYROFF", 0, "X offset of Bayer array"),
("YBAYROFF", 0, "Y offset of Bayer array"),
("BAYERPAT", self.present_CameraSettings.RawMode["FITS_format"], "Bayer color pattern"),
("BAYERPAT", BayerPattern, "Bayer color pattern"),
]
FitsHeader += [("Gain", metadata.get("AnalogueGain", 0.0), "Gain"), ]
if "SensorBlackLevels" in metadata:
Expand Down Expand Up @@ -607,8 +586,7 @@ def createRgbFits(self, array, metadata):
#("ROWORDER", "TOP-DOWN", "Row Order"),
("INSTRUME", self.parent.device, "CCD Name"),
("TELESCOP", self.parent.knownVectors["ACTIVE_DEVICES"]["ACTIVE_TELESCOPE"].value, "Telescope name"),
("OBSERVER", self.parent.knownVectors["FITS_HEADER"]["FITS_OBSERVER"].value, "Observer name"),
("OBJECT", self.parent.knownVectors["FITS_HEADER"]["FITS_OBJECT"].value, "Object name"),
] + self.parent.knownVectors["FITS_HEADER"].get_FitsHeaderList() + [
("EXPTIME", metadata["ExposureTime"]/1e6, "Total Exposure Time (s)"),
("CCD-TEMP", metadata.get('SensorTemperature', 0), "CCD Temperature (Celsius)"),
("PIXSIZE1", self.getProp("UnitCellSize")[0] / 1e3, "Pixel Size 1 (microns)"),
Expand Down Expand Up @@ -639,6 +617,7 @@ def checkAbort(self):
if self.parent.knownVectors["CCD_ABORT_EXPOSURE"]["ABORT"].value == ISwitchState.ON:
self.parent.setVector("CCD_FAST_COUNT", "FRAMES", value=0, state=IVectorState.OK)
self.parent.setVector("CCD_ABORT_EXPOSURE", "ABORT", value=ISwitchState.OFF, state=IVectorState.OK)
self.parent.setVector("CCD_EXPOSURE", "CCD_EXPOSURE_VALUE", value=0, state=IVectorState.OK)
return True
return False

Expand Down Expand Up @@ -693,6 +672,7 @@ def __ExposureLoop(self):
if self.Sig_ActionExit.is_set():
# exit exposure loop
self.picam2.stop_()
self.parent.setVector("CCD_EXPOSURE", "CCD_EXPOSURE_VALUE", value=0, state=IVectorState.OK)
return
# picam2 needs to be open!
if self.picam2 is None:
Expand Down Expand Up @@ -754,6 +734,7 @@ def __ExposureLoop(self):
if self.Sig_ActionExit.is_set():
# exit exposure loop
self.picam2.stop_()
self.parent.setVector("CCD_EXPOSURE", "CCD_EXPOSURE_VALUE", value=0, state=IVectorState.OK)
return
with self.parent.knownVectorsLock:
Abort = self.checkAbort()
Expand All @@ -777,6 +758,7 @@ def __ExposureLoop(self):
if self.Sig_ActionExit.is_set():
# exit exposure loop
self.picam2.stop_()
self.parent.setVector("CCD_EXPOSURE", "CCD_EXPOSURE_VALUE", value=0, state=IVectorState.OK)
return
# allow to abort exposure
with self.parent.knownVectorsLock:
Expand All @@ -789,16 +771,18 @@ def __ExposureLoop(self):
time.sleep(PollingPeriod_s)
# get frame and its metadata
if not Abort:
(array, ), metadata = self.picam2.wait(job)
(array, ), metadata = self.picam2.wait(job)
logging.info("got exposed frame")
# inform client about progress
self.parent.setVector("CCD_EXPOSURE", "CCD_EXPOSURE_VALUE", value=0, state=IVectorState.BUSY)
# at least HQ camera reports CCD temperature in meta data
self.parent.setVector("CCD_TEMPERATURE", "CCD_TEMPERATURE_VALUE", value=metadata.get('SensorTemperature', 0))
# at least HQ camera reports CCD temperature in meta data
self.parent.setVector("CCD_TEMPERATURE", "CCD_TEMPERATURE_VALUE",
value=metadata.get('SensorTemperature', 0))
# inform client about progress
self.parent.setVector("CCD_EXPOSURE", "CCD_EXPOSURE_VALUE", value=0, state=IVectorState.BUSY)
# last chance to exit or abort before sending blob
if self.Sig_ActionExit.is_set():
# exit exposure loop
self.picam2.stop_()
self.parent.setVector("CCD_EXPOSURE", "CCD_EXPOSURE_VALUE", value=0, state=IVectorState.OK)
return
with self.parent.knownVectorsLock:
Abort = Abort or self.checkAbort()
Expand Down
48 changes: 36 additions & 12 deletions src/indi_pylibcamera/CameraInfos/Raspi_HQ_IMX477.txt
Original file line number Diff line number Diff line change
@@ -1,32 +1,39 @@
Testing numpy:
numpy 1.19.5

Testing astropy:
astropy 4.2

Found 1 cameras.

Camera 0:
{'Id': '/base/soc/i2c0mux/i2c@1/imx477@1a',
'Location': 2,
'Model': 'imx477',
'Rotation': 180}
'Rotation': 0}

Camera properties:
{'ColorFilterArrangement': 0,
'Location': 2,
'Model': 'imx477',
'PixelArrayActiveAreas': (8, 16, 4056, 3040),
'PixelArrayActiveAreas': [(8, 16, 4056, 3040)],
'PixelArraySize': (4056, 3040),
'Rotation': 180,
'Rotation': 0,
'ScalerCropMaximum': (0, 0, 0, 0),
'SystemDevices': (20750, 20751, 20737, 20738, 20739),
'UnitCellSize': (1550, 1550)}

Raw sensor modes:
[{'bit_depth': 10,
'crop_limits': (696, 528, 2664, 1980),
'exposure_limits': (31, 667234896, None),
'exposure_limits': (31, None),
'format': SRGGB10_CSI2P,
'fps': 120.05,
'size': (1332, 990),
'unpacked': 'SRGGB10'},
{'bit_depth': 12,
'crop_limits': (0, 440, 4056, 2160),
'exposure_limits': (60, 674181621, None),
'exposure_limits': (60, 667244877, None),
'format': SRGGB12_CSI2P,
'fps': 50.03,
'size': (2028, 1080),
Expand All @@ -40,12 +47,32 @@ Raw sensor modes:
'unpacked': 'SRGGB12'},
{'bit_depth': 12,
'crop_limits': (0, 0, 4056, 3040),
'exposure_limits': (114, 694422939, None),
'exposure_limits': (114, 674191602, None),
'format': SRGGB12_CSI2P,
'fps': 10.0,
'size': (4056, 3040),
'unpacked': 'SRGGB12'}]

Camera configuration:
{'buffer_count': 4,
'colour_space': <libcamera.ColorSpace 'sYCC'>,
'controls': {'FrameDurationLimits': (100, 83333),
'NoiseReductionMode': <NoiseReductionModeEnum.Minimal: 3>},
'display': 'main',
'encode': 'main',
'lores': None,
'main': {'format': 'XBGR8888',
'framesize': 1228800,
'size': (640, 480),
'stride': 2560},
'queue': True,
'raw': {'format': 'SBGGR12_CSI2P',
'framesize': 18580480,
'size': (4056, 3040),
'stride': 6112},
'transform': <libcamera.Transform 'identity'>,
'use_case': 'preview'}

Camera controls:
{'AeConstraintMode': (0, 3, 0),
'AeEnable': (False, True, None),
Expand All @@ -55,21 +82,18 @@ Camera controls:
'AwbEnable': (False, True, None),
'AwbMode': (0, 7, 0),
'Brightness': (-1.0, 1.0, 0.0),
'ColourCorrectionMatrix': (-16.0, 16.0, None),
'ColourGains': (0.0, 32.0, None),
'Contrast': (0.0, 32.0, 1.0),
'ExposureTime': (114, 694422939, None),
'ExposureTime': (114, 674191602, None),
'ExposureValue': (-8.0, 8.0, 0.0),
'FrameDurationLimits': (100000, 694434742, None),
'NoiseReductionMode': (0, 4, 0),
'Saturation': (0.0, 32.0, 1.0),
'ScalerCrop': (libcamera.Rectangle(0, 0, 64, 64),
libcamera.Rectangle(0, 0, 4056, 3040),
None),
'ScalerCrop': ((0, 0, 64, 64), (0, 0, 4056, 3040), (2, 0, 4053, 3040)),
'Sharpness': (0.0, 16.0, 1.0)}

Exposure time:
min: 114, max: 694422939, default: None
min: 114, max: 674191602, default: None

AnalogGain:
min: 1.0, max: 22.2608699798584, default: None
Expand Down
Loading

0 comments on commit ace0ac9

Please sign in to comment.