17
17
log = logging .getLogger (__name__ )
18
18
19
19
20
- class QHYCCDCamera (BaseCamera , ICamera , IWindow , IBinning , IAbortable ):
20
+ class QHYCCDCamera (BaseCamera , ICamera , IWindow , IBinning , IAbortable , ICooling ):
21
21
"""A pyobs module for QHYCCD cameras."""
22
22
23
23
__module__ = "pyobs_qhyccd"
24
24
25
- def __init__ (self , ** kwargs : Any ):
25
+ def __init__ (self , setpoint : float = - 10 , ** kwargs : Any ):
26
26
"""Initializes a new QHYCCDCamera.
27
27
"""
28
28
BaseCamera .__init__ (self , ** kwargs )
29
29
30
- # driver
31
30
self ._driver : Optional [QHYCCDDriver ] = None
32
-
33
- # window and binning
31
+ self ._setpoint = setpoint
34
32
self ._window = (0 , 0 , 0 , 0 )
35
33
self ._binning = (1 , 1 )
36
34
@@ -39,7 +37,7 @@ async def open(self) -> None:
39
37
await BaseCamera .open (self )
40
38
41
39
# disable logs
42
- set_log_level (0 )
40
+ set_log_level (0 ) #TODO:
43
41
44
42
# get devices
45
43
devices = QHYCCDDriver .list_devices ()
@@ -72,8 +70,8 @@ async def open(self) -> None:
72
70
self ._window = self ._driver .get_effective_area ()
73
71
74
72
# set cooling
75
- # if self._temp_setpoint is not None:
76
- # await self.set_cooling(True, self._temp_setpoint )
73
+ if self ._setpoint is not None :
74
+ await self .set_cooling (True , self ._setpoint )
77
75
78
76
async def close (self ) -> None :
79
77
"""Close the module."""
@@ -142,33 +140,24 @@ async def list_binnings(self, **kwargs: Any) -> List[Tuple[int, int]]:
142
140
Returns:
143
141
List of available binnings as (x, y) tuples.
144
142
"""
145
- return [(1 , 1 ), (2 , 2 ), (3 , 3 ), (4 , 4 )]
146
-
147
- async def _expose (self , exposure_time : float , open_shutter : bool , abort_event : asyncio .Event ) -> Image :
148
- """Actually do the exposure, should be implemented by derived classes.
149
-
150
- Args:
151
- exposure_time: The requested exposure time in seconds.
152
- open_shutter: Whether to open the shutter.
153
- abort_event: Event that gets triggered when exposure should be aborted.
154
-
155
- Returns:
156
- The actual image.
157
143
158
- Raises:
159
- GrabImageError: If exposure was not successful.
160
- """
161
- # check driver
144
+ binnings = []
145
+ if self ._driver .is_control_available (Control .CAM_BIN1X1MODE ):
146
+ binnings .append ((1 , 1 ))
147
+ if self ._driver .is_control_available (Control .CAM_BIN2X2MODE ):
148
+ binnings .append ((2 , 2 ))
149
+ if self ._driver .is_control_available (Control .CAM_BIN3X3MODE ):
150
+ binnings .append ((3 , 3 ))
151
+ if self ._driver .is_control_available (Control .CAM_BIN4X4MODE ):
152
+ binnings .append ((4 , 4 ))
153
+ return binnings
154
+
155
+ async def _prepare_driver_for_exposure (self , exposure_time ) -> None :
162
156
if self ._driver is None :
163
157
raise ValueError ("No camera driver." )
164
-
165
- # set binning
166
158
log .info ("Set binning to %dx%d." , self ._binning [0 ], self ._binning [1 ])
167
159
self ._driver .set_bin_mode (* self ._binning )
168
160
169
- # set window, divide width/height by binning, from libfli:
170
- # "Note that the given lower-right coordinate must take into account the horizontal and
171
- # vertical bin factor settings, but the upper-left coordinate is absolute."
172
161
width = int (math .floor (self ._window [2 ]) / self ._binning [0 ])
173
162
height = int (math .floor (self ._window [3 ]) / self ._binning [1 ])
174
163
log .info (
@@ -181,101 +170,117 @@ async def _expose(self, exposure_time: float, open_shutter: bool, abort_event: a
181
170
self ._window [1 ],
182
171
)
183
172
self ._driver .set_resolution (self ._window [0 ], self ._window [1 ], width , height )
173
+ self ._driver .set_param (Control .CONTROL_EXPOSURE , int (exposure_time * 1000.0 * 1000.0 ))
184
174
185
- # exposure time
186
- self ._driver .set_param (Control .CONTROL_EXPOSURE , int (exposure_time * 1000.0 ))
187
-
188
- # get date obs
189
- log .info (
190
- "Starting exposure with %s shutter for %.2f seconds..." , "open" if open_shutter else "closed" , exposure_time
191
- )
192
- date_obs = datetime .now (timezone .utc ).strftime ("%Y-%m-%dT%H:%M:%S.%f" )
193
-
194
- # expose
195
- print ("before expose" , time .time ())
196
- self ._driver .expose_single_frame ()
197
- print ("after expose" , time .time ())
198
-
199
- # wait for exposure
200
- if exposure_time > 0.5 :
201
- await event_wait (abort_event , exposure_time - 0.5 )
202
- #if abort_event.is_set():
203
- # raise
204
-
205
- # get image
206
- print ("before get" , time .time ())
207
- img = self ._driver .get_single_frame ()
208
- #loop = asyncio.get_running_loop()
209
- #img = await loop.run_in_executor(None, self._driver.get_single_frame)
210
- print ("after get" , time .time ())
211
-
212
- # wait exposure
213
- await self ._wait_exposure (abort_event , exposure_time , open_shutter )
214
-
215
- # create FITS image and set header
216
- image = Image (img )
175
+ async def _get_image_with_header (self , image_data , date_obs , exposure_time ) -> Image :
176
+ image = Image (image_data )
217
177
image .header ["DATE-OBS" ] = (date_obs , "Date and time of start of exposure" )
218
178
image .header ["EXPTIME" ] = (exposure_time , "Exposure time [s]" )
219
- #image.header["DET-TEMP"] = (self._driver.get_temp(FliTemperature.CCD), "CCD temperature [C]")
220
- #image.header["DET-COOL"] = (self._driver.get_cooler_power(), "Cooler power [percent]")
221
- #image.header["DET-TSET"] = (self._temp_setpoint, "Cooler setpoint [C]")
222
-
223
- # instrument and detector
224
- #image.header["INSTRUME"] = (self._driver.name, "Name of instrument")
225
-
226
- # binning
179
+ image .header ["DET-TEMP" ] = (await self ._get_ccd_temperature (), "CCD temperature [C]" )
180
+ image .header ["DET-COOL" ] = (await self ._get_cooling_power (), "Cooler power [percent]" )
181
+ image .header ["DET-TSET" ] = (self ._setpoint , "Cooler setpoint [C]" )
182
+ # image.header["INSTRUME"] = (self._driver.name, "Name of instrument")
227
183
image .header ["XBINNING" ] = image .header ["DET-BIN1" ] = (self ._binning [0 ], "Binning factor used on X axis" )
228
184
image .header ["YBINNING" ] = image .header ["DET-BIN2" ] = (self ._binning [1 ], "Binning factor used on Y axis" )
229
-
230
- # window
231
185
image .header ["XORGSUBF" ] = (self ._window [0 ], "Subframe origin on X axis" )
232
186
image .header ["YORGSUBF" ] = (self ._window [1 ], "Subframe origin on Y axis" )
233
-
234
- # statistics
235
- image .header ["DATAMIN" ] = (float (np .min (img )), "Minimum data value" )
236
- image .header ["DATAMAX" ] = (float (np .max (img )), "Maximum data value" )
237
- image .header ["DATAMEAN" ] = (float (np .mean (img )), "Mean data value" )
187
+ image .header ["DATAMIN" ] = (float (np .min (image_data )), "Minimum data value" )
188
+ image .header ["DATAMAX" ] = (float (np .max (image_data )), "Maximum data value" )
189
+ image .header ["DATAMEAN" ] = (float (np .mean (image_data )), "Mean data value" )
238
190
239
191
# biassec/trimsec
240
- #full = self._driver.get_visible_frame()
241
- #self.set_biassec_trimsec(image.header, *full)
242
-
243
- # return FITS image
192
+ # full = self._driver.get_visible_frame()
193
+ # self.set_biassec_trimsec(image.header, *full)
244
194
log .info ("Readout finished." )
245
195
return image
246
196
247
- async def _wait_exposure (self , abort_event : asyncio . Event , exposure_time : float , open_shutter : bool ) -> None :
248
- """Wait for exposure to finish .
197
+ async def _expose (self , exposure_time : float , open_shutter : bool , abort_event : asyncio . Event ) -> Image :
198
+ """Actually do the exposure, should be implemented by derived classes .
249
199
250
- Params :
251
- abort_event: Event that aborts the exposure .
252
- exposure_time: Exp time in sec .
253
- """
200
+ Args :
201
+ exposure_time: The requested exposure time in seconds .
202
+ open_shutter: Whether to open the shutter .
203
+ abort_event: Event that gets triggered when exposure should be aborted.
254
204
255
- """
256
- while True:
257
- # aborted?
258
- if abort_event.is_set():
259
- await self._change_exposure_status(ExposureStatus.IDLE)
260
- raise InterruptedError("Aborted exposure.")
261
-
262
- # is exposure finished?
263
- if self._driver.is_exposing():
264
- break
265
- else:
266
- # sleep a little
267
- await asyncio.sleep(0.01)
205
+ Returns:
206
+ The actual image.
207
+
208
+ Raises:
209
+ GrabImageError: If exposure was not successful.
268
210
"""
269
211
212
+ await self ._prepare_driver_for_exposure (exposure_time )
213
+ log .info ("Starting exposure with %s shutter for %.2f seconds..." , "open" if open_shutter else "closed" , exposure_time )
214
+ date_obs = datetime .now (timezone .utc ).strftime ("%Y-%m-%dT%H:%M:%S.%f" )
215
+ self ._driver .expose_single_frame ()
216
+ await event_wait (abort_event , exposure_time - 0.5 )
217
+ loop = asyncio .get_running_loop ()
218
+ image_data = await loop .run_in_executor (None , self ._driver .get_single_frame )
219
+ return await self ._get_image_with_header (image_data , date_obs , exposure_time )
220
+
270
221
async def _abort_exposure (self ) -> None :
271
222
"""Abort the running exposure. Should be implemented by derived class.
272
-
273
223
Raises:
274
224
ValueError: If an error occured.
275
225
"""
276
226
if self ._driver is None :
277
227
raise ValueError ("No camera driver." )
278
228
#self._driver.cancel_exposure()
279
229
230
+ async def _get_cooling_power (self ):
231
+ return self ._driver .get_param (Control .CONTROL_CURPWM ) / 256 * 100 # TODO:
232
+
233
+ async def get_cooling (self , ** kwargs : Any ) -> Tuple [bool , float , float ]:
234
+ enabled = self ._driver .is_control_available (Control .CONTROL_COOLER )
235
+ setpoint = self ._setpoint
236
+ power = await self ._get_cooling_power ()
237
+ return enabled , setpoint , power
238
+
239
+ async def set_cooling (self , enabled : bool , setpoint : float , ** kwargs : Any ) -> None :
240
+ #if not enabled:
241
+ # self._driver.set_param(Control.CONTROL_CURPWM, 0) #TODO: einfach PWM auf 0?
242
+ self ._setpoint = setpoint
243
+ await self ._cool_stepwise (setpoint )
244
+
245
+ async def _wait_for_reaching_temperature (self , target_temperature , wait_step = 1 ):
246
+ while await self ._get_ccd_temperature () > target_temperature :
247
+ print ("Current temperature is" , await self ._get_ccd_temperature (), "Target temperature is" , target_temperature )
248
+ if await self ._cooling_bug_occured ():
249
+ break
250
+ time .sleep (wait_step )
251
+
252
+ async def _cooling_bug_occured (self ):
253
+ return (self ._driver .get_param (Control .CONTROL_CURPWM ) > 250 ) & (await self ._get_ccd_temperature () < 0 )
254
+
255
+ async def _get_ccd_temperature (self ):
256
+ return self ._driver .get_param (Control .CONTROL_CURTEMP )
257
+
258
+ async def _cool_stepwise (self , target_temperature , temperature_stepwidth = 1 ):
259
+ print ("Start stepwise cooling to " , target_temperature )
260
+ while await self ._get_ccd_temperature () - target_temperature > temperature_stepwidth :
261
+ intermediate_temperature = await self ._get_ccd_temperature () - temperature_stepwidth
262
+ print ("Set temperature to" , intermediate_temperature )
263
+ self ._driver .set_temperature (intermediate_temperature )
264
+ await self ._wait_for_reaching_temperature (intermediate_temperature )
265
+ if await self ._cooling_bug_occured ():
266
+ await self ._handle_cooling_bug (intermediate_temperature )
267
+ return
268
+ print ("Set temperature to" , target_temperature )
269
+ self ._driver .set_temperature (target_temperature )
270
+ print ("End stepwise cooling to" , target_temperature )
271
+
272
+ async def _handle_cooling_bug (self , original_target_temperature , puffer = 5 , correction_step = 1 ):
273
+ print (f"Setpoint of { original_target_temperature :.2f} °C too low for cooler. Temporarily resetting it to { await self ._get_ccd_temperature () + puffer :.2f} °C." )
274
+ while await self ._cooling_bug_occured ():
275
+ time .sleep (1 )
276
+ await self ._cool_stepwise (await self ._get_ccd_temperature () + puffer )
277
+ print ("Wait a minute" )
278
+ time .sleep (60 )
279
+ self ._setpoint = original_target_temperature + correction_step
280
+ print ("Retry stepwise cooling with new setpoint of" , self ._setpoint )
281
+ await self ._cool_stepwise (self ._setpoint )
282
+
283
+ async def get_temperatures (self , ** kwargs : Any ) -> Dict [str , float ]:
284
+ return {"CCD" : await self ._get_ccd_temperature ()}
280
285
281
286
__all__ = ["QHYCCDCamera" ]
0 commit comments