4 from typing
import Optional
6 from astropy.io
import fits
8 from purepyindi2
import device, properties, constants
9 from purepyindi2.messages
import DefNumber, DefSwitch, DefText, DefLight
10 from magaox.indi.device
import XDevice, BaseConfig
11 import ImageStreamIOWrap
as ISIO
16 from .qhyccd
import QHYCCDSDK, QHYCCDCamera
18 log = logging.getLogger(__name__)
20 EXTERNAL_RECORDED_PROPERTIES = {
21 'tcsi.catalog.object':
'OBJECT',
22 'tcsi.catdata.ra':
None,
23 'tcsi.catdata.dec':
None,
24 'tcsi.catdata.epoch':
None,
25 'observers.current_observer.full_name':
'OBSERVER',
26 'tcsi.teldata.pa':
'PARANG',
27 'flipacq.presetName.in':
None,
30 RECORDED_WHEELS = (
'fwfpm',
'fwlyot')
32 CAMERA_CONNECT_RETRY_SEC = 5
35 fwelems = client[f
"{fwname}.filterName"]
39 if fwelems[elem] == constants.SwitchState.ON:
44 full_sdk_path : str = xconf.field(default=
'/usr/local/lib/libqhyccd.so')
45 temp_on_target_pct_diff : float = xconf.field(default=0.05, help=
"Absolute percent difference between temperature setpoint and currently reported value")
46 startup_temp : float = xconf.field(default=-15, help=
'Startup temperature of the camera.')
50 data_directory : str =
"/opt/MagAOX/rawimages/camvisx"
51 exposure_start_ts : float = 0
52 should_cancel : bool =
False
53 currently_exposing : bool =
False
54 should_begin_exposure : bool =
False
55 last_image_filename : Optional[str] =
None
58 exposure_start_telem : Optional[dict] =
None
60 sdk : Optional[QHYCCDSDK] =
None
61 camera : Optional[QHYCCDCamera] =
None
62 exposure_time_sec : Optional[float] =
None
63 camera_gain : Optional[int] =
None
64 temp_target_deg_c : Optional[float] =
None
65 temp_current_deg_c : Optional[float] =
None
69 temp_on_target = abs((self.
cameracamera.temperature - self.
cameracamera.target_temperature) / self.
cameracamera.target_temperature) < self.config.temp_on_target_pct_diff
74 self.telem(
"telem_stdcam", {
85 "emGain": self.
cameracamera.gain,
88 "temp": self.
cameracamera.temperature,
89 "setpt": self.
cameracamera.target_temperature,
94 "shutter": {
"statusStr":
None,
"state":
None},
101 log.debug(f
"In handle_exptime")
103 self.log.debug(
"Ignoring exposure time change request while currently exposing")
104 elif self.
sdksdk
is None:
105 self.log.debug(
"Ignoring exposure time change while we don't have an SDK handle")
106 elif not self.
currently_exposingcurrently_exposing
and 'target' in new_message
and new_message[
'target'] != existing_property[
'current']:
107 existing_property[
'current'] = new_message[
'target']
108 existing_property[
'target'] = new_message[
'target']
111 self.log.debug(f
"Exposure time changed to {new_message['target']} seconds")
113 self.update_property(existing_property)
116 log.debug(f
"In handle_gain")
118 self.log.debug(
"Ignoring gain change request while currently exposing")
119 elif self.
sdksdk
is None:
120 self.log.debug(
"Ignoring gain change while we don't have an SDK handle")
121 elif not self.
currently_exposingcurrently_exposing
and 'target' in new_message
and new_message[
'target'] != existing_property[
'current']:
122 existing_property[
'current'] = new_message[
'target']
123 existing_property[
'target'] = new_message[
'target']
126 self.log.debug(f
"Gain changed to {new_message['target']}")
127 self.telem(
'emGain', {
'emGain': self.
camera_gaincamera_gain})
128 self.update_property(existing_property)
131 if self.
sdksdk
is None:
132 self.log.debug(
"Ignoring request for exposure while we don't have an SDK handle")
133 elif 'request' in new_message
and new_message[
'request']
is constants.SwitchState.ON:
134 self.log.debug(
"Exposure requested!")
136 elif 'cancel' in new_message
and new_message[
'cancel']
is constants.SwitchState.ON:
137 self.log.debug(
"Exposure cancellation requested")
139 self.update_property(existing_property)
142 if self.
sdksdk
is None:
143 self.log.debug(
"Ignoring temperature setpoint change while we don't have an SDK handle")
144 elif 'target' in new_message
and new_message[
'target'] != existing_property[
'current']:
145 existing_property[
'current'] = new_message[
'target']
146 existing_property[
'target'] = new_message[
'target']
148 self.log.debug(f
"CCD temperature setpoint changed to {self.temp_target_deg_c} deg C")
149 self.telem(
'tempcontrol', {
'temp_target_deg_c': self.
temp_target_deg_ctemp_target_deg_c})
150 self.update_property(existing_property)
155 if self.
sdksdk.number_of_cameras < 1:
166 fsmstate = properties.TextVector(
169 fsmstate.add_element(DefText(name=
"state", _value=
"NODEVICE"))
170 self.add_property(fsmstate)
172 tv = properties.TextVector(
175 tv.add_element(DefText(name=
"filename", _value=
None))
176 self.add_property(tv)
177 sv = properties.SwitchVector(
179 rule=constants.SwitchRule.ONE_OF_MANY,
180 perm=constants.PropertyPerm.READ_WRITE,
182 sv.add_element(DefSwitch(name=
"request", _value=constants.SwitchState.OFF))
183 sv.add_element(DefSwitch(name=
"cancel", _value=constants.SwitchState.OFF))
184 self.add_property(sv, callback=self.
handle_exposehandle_expose)
186 nv = properties.NumberVector(name=
'exptime', perm=constants.PropertyPerm.READ_WRITE)
187 nv.add_element(DefNumber(
188 name=
'current', label=
'Exposure time (sec)', format=
'%3.1f',
191 nv.add_element(DefNumber(
192 name=
'target', label=
'Requested exposure time (sec)', format=
'%3.1f',
197 nv = properties.NumberVector(name=
'gain', perm=constants.PropertyPerm.READ_WRITE)
198 nv.add_element(DefNumber(
199 name=
'current', label=
'Gain', format=
'%d',
200 min=0, max=100, step=1, _value=self.
camera_gaincamera_gain
202 nv.add_element(DefNumber(
203 name=
'target', label=
'Requested gain', format=
'%d',
204 min=0, max=100, step=1, _value=self.
camera_gaincamera_gain
206 self.add_property(nv, callback=self.
handle_gainhandle_gain)
208 nv = properties.NumberVector(name=
'temp_ccd', perm=constants.PropertyPerm.READ_WRITE)
209 nv.add_element(DefNumber(
210 name=
'current', label=
'Current temperature (deg C)', format=
'%3.3f',
213 nv.add_element(DefNumber(
214 name=
'target', label=
'Requested temperature (deg C)', format=
'%3.3f',
219 nv = properties.NumberVector(name=
'current_exposure')
220 nv.add_element(DefNumber(
221 name=
'remaining_sec', label=
'Time remaining (sec)', format=
'%3.3f',
222 min=0, max=1_000_000, step=1, _value=0.0
224 nv.add_element(DefNumber(
225 name=
'remaining_pct', label=
'Percentage remaining', format=
'%i',
226 min=0, max=100, step=0.1, _value=0.0
228 self.add_property(nv)
232 for prop
in EXTERNAL_RECORDED_PROPERTIES:
233 device = prop.split(
'.')[0]
235 self.log.debug(f
"subscribe to device: {device}")
236 for fw
in RECORDED_WHEELS:
239 self.client.get_properties_and_wait(devices)
240 except TimeoutError
as e:
241 log.warning(f
"Timed out waiting to get properties from external INDI devices: {e}")
244 os.makedirs(self.data_directory, exist_ok=
True)
245 while self.client.status
is not constants.ConnectionStatus.CONNECTED:
246 self.log.info(f
"Connecting to INDI as a client to get {list(EXTERNAL_RECORDED_PROPERTIES.keys())} and {RECORDED_WHEELS}...")
248 self.log.info(f
"INDI client connection: {self.client.status}")
252 self.properties[
'fsm'][
'state'] =
'NOTCONNECTED'
253 self.log.debug(
"Set FSM prop")
254 self.update_property(self.properties[
'fsm'])
255 self.log.debug(
"Sent FSM prop")
256 self.log.info(
"Set up complete")
259 if self.
cameracamera
is None:
265 self.log.debug(f
"Read from camera: target = {self.temp_target_deg_c} deg C, current = {self.temp_current_deg_c} deg C, exptime = {self.exposure_time_sec} s, gain = {self.camera_gain}")
269 current = self.properties[
'current_exposure']
273 self.properties[
'fsm'][
'state'] =
'OPERATING'
274 self.update_property(self.properties[
'fsm'])
278 self.properties[
'fsm'][
'state'] =
'READY'
279 self.update_property(self.properties[
'fsm'])
280 if remaining_sec != current[
'remaining_sec']:
281 current[
'remaining_sec'] = remaining_sec
282 current[
'remaining_pct'] = remaining_pct
283 self.update_property(current)
289 self.update_property(self.properties[
'temp_ccd'])
293 self.update_property(self.properties[
'exptime'])
295 self.properties[
'gain'][
'current'] = self.
camera_gaincamera_gain
296 self.properties[
'gain'][
'target'] = self.
camera_gaincamera_gain
297 self.update_property(self.properties[
'gain'])
301 '''User code must close the loop on temperature control'''
302 if self.
cameracamera
is None:
308 'CCDTEMP': self.
cameracamera.temperature,
309 'CCDSETP': self.
cameracamera.target_temperature,
310 'GAIN': self.
cameracamera.gain,
311 'EXPTIME': self.
cameracamera.exposure_time,
313 for indi_prop
in EXTERNAL_RECORDED_PROPERTIES:
314 if EXTERNAL_RECORDED_PROPERTIES[indi_prop]
is None:
315 new_kw = indi_prop.upper().replace(
'.',
' ')
317 new_kw = EXTERNAL_RECORDED_PROPERTIES[indi_prop]
319 value = self.client[indi_prop]
320 if hasattr(value,
'value'):
323 for fwname
in RECORDED_WHEELS:
332 self.
cameracamera.start_exposure()
333 self.log.debug(
"Asking camera to begin exposure")
336 img = self.
cameracamera.readout()
338 hdul = fits.HDUList([
343 self.log.debug(f
"{meta=}")
344 meta[
'DATE-OBS'] = datetime.datetime.fromtimestamp(self.
exposure_start_tsexposure_start_ts).isoformat()
345 exposure_time = self.
cameracamera.exposure_time
if actual_exptime_sec
is None else actual_exptime_sec
346 meta[
'DATE-END'] = datetime.datetime.fromtimestamp(self.
exposure_start_tsexposure_start_ts + exposure_time).isoformat()
347 meta[
'DATE'] = datetime.datetime.utcnow().isoformat()
350 meta[
'INSTRUME'] =
'MagAO-X'
351 meta[
'CAMERA'] =
'VIS-X'
352 meta[
'TELESCOP'] =
"Magellan Clay, Las Campanas Obs."
353 with warnings.catch_warnings():
354 warnings.simplefilter(
'ignore')
355 hdul[0].header.update(meta)
357 if actual_exptime_sec
is not None:
358 hdul[0].header[
'CANCELD'] =
True
360 timestamp = datetime.datetime.utcnow().strftime(
"%Y-%m-%dT%H%M%S")
362 outpath = f
"{self.data_directory}/{self.last_image_filename}"
363 self.log.info(f
"Saving to {outpath}")
365 hdul.writeto(outpath)
367 self.log.exception(f
"Unable to save frame!")
373 self.log.debug(
"Asking camera to cancel exposure")
379 if self.client.interested_properties_missing:
381 self.log.debug(f
"Repeating subscription because some external devices we use for headers are not showing up")
383 if self.
sdksdk
is None:
384 self.log.debug(
"Initializing camera SDK...")
387 self.log.debug(
"No camera found yet, retrying on next loop")
389 self.log.debug(f
"Have camera: {self.camera}")
390 self.properties[
'fsm'][
'state'] =
'CONNECTED'
391 self.update_property(self.properties[
'fsm'])
400 self.log.debug(
"Exposure finished")
def handle_expose(self, existing_property, new_message)
def subscribe_to_other_devices(self)
def finalize_exposure(self, actual_exptime_sec=None)
def maintain_temperature_control(self)
def cooling_on_target(self)
def _gather_metadata(self)
def cancel_exposure(self)
def emit_telem_stdcam(self)
def update_from_camera(self)
def refresh_properties(self)
def handle_temp_ccd(self, existing_property, new_message)
def handle_gain(self, existing_property, new_message)
def _init_properties(self)
def handle_exptime(self, existing_property, new_message)
def find_active_filter(client, fwname)