API
 
Loading...
Searching...
No Matches
xapp.py
Go to the documentation of this file.
1import xconf
2import numpy as np
3import warnings
4from typing import Optional
5import datetime
6from astropy.io import fits
7import os
8from purepyindi2 import device, properties, constants
9from purepyindi2.messages import DefNumber, DefSwitch, DefText, DefLight
10from magaox.indi.device import XDevice, BaseConfig
11import ImageStreamIOWrap as ISIO
12import logging
13import time
14import sys
15
16from .qhyccd import QHYCCDSDK, QHYCCDCamera
17
18log = logging.getLogger(__name__)
19
20EXTERNAL_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,
28}
29
30RECORDED_WHEELS = ('fwfpm', 'fwlyot')
31
32CAMERA_CONNECT_RETRY_SEC = 5
33
34def find_active_filter(client, fwname):
35 fwelems = client[f"{fwname}.filterName"]
36 if fwelems is None:
37 return
38 for elem in fwelems:
39 if fwelems[elem] == constants.SwitchState.ON:
40 return elem
41
42@xconf.config
43class VisXConfig(BaseConfig):
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.')
47class VisX(XDevice):
48 config : VisXConfig
49 # us
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
56 shmim : ISIO.Image
57 frame : np.ndarray
58 exposure_start_telem : Optional[dict] = None
59 # them
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
66
67 @property
69 temp_on_target = abs((self.cameracamera.temperature - self.cameracamera.target_temperature) / self.cameracamera.target_temperature) < self.config.temp_on_target_pct_diff
70 return temp_on_target
71
73 w, h = 9600, 6422
74 self.telem("telem_stdcam", {
75 "roi": {
76 "xcen": (w - 1) / 2,
77 "ycen": (h - 1) / 2,
78 "w": w,
79 "h": h,
80 "xbin": 1,
81 "ybin": 1,
82 },
85 "emGain": self.cameracamera.gain,
86 "adcSpeed": -1,
87 "tempCtrl": {
88 "temp": self.cameracamera.temperature,
89 "setpt": self.cameracamera.target_temperature,
90 "status": True,
92 "statusStr": "LOCKED" if self.cooling_on_targetcooling_on_target else "UNLOCKED",
93 },
94 "shutter": {"statusStr": None, "state": None},
95 "synchro": 0,
96 "vshift": -1,
97 "cropMode": 0
98 })
99
100 def handle_exptime(self, existing_property, new_message):
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']
109 self.exposure_time_secexposure_time_sec = new_message['target']
110 self.cameracamera.exposure_time = self.exposure_time_secexposure_time_sec
111 self.log.debug(f"Exposure time changed to {new_message['target']} seconds")
112 self.telem('exptime', {'exptime': self.exposure_time_secexposure_time_sec})
113 self.update_property(existing_property)
114
115 def handle_gain(self, existing_property, new_message):
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']
124 self.camera_gaincamera_gain = new_message['target']
125 self.cameracamera.gain = self.camera_gaincamera_gain
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)
129
130 def handle_expose(self, existing_property, new_message):
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) # ensure the switch turns back off at the client
140
141 def handle_temp_ccd(self, existing_property, new_message):
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']
147 self.temp_target_deg_ctemp_target_deg_c = 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)
151
152 def _init_camera(self):
153 # Load SDK
154 self.sdksdk = QHYCCDSDK(dll_path=self.config.full_sdk_path)
155 if self.sdksdk.number_of_cameras < 1:
156 del self.sdksdk
157 return False
158 # Find camera
160 self.exposure_time_secexposure_time_sec = self.cameracamera.exposure_time
161 self.temp_target_deg_ctemp_target_deg_c = self.config.startup_temp
162 self.camera_gaincamera_gain = self.cameracamera.gain
163 return True
164
166 fsmstate = properties.TextVector(
167 name='fsm',
168 )
169 fsmstate.add_element(DefText(name="state", _value="NODEVICE"))
170 self.add_property(fsmstate)
171
173 name='last_frame',
174 )
175 tv.add_element(DefText(name="filename", _value=None))
176 self.add_property(tv)
178 name='expose',
181 )
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_expose)
185
187 nv.add_element(DefNumber(
188 name='current', label='Exposure time (sec)', format='%3.1f',
189 min=0, max=1_000_000, step=0.001, _value=self.exposure_time_secexposure_time_sec
190 ))
191 nv.add_element(DefNumber(
192 name='target', label='Requested exposure time (sec)', format='%3.1f',
193 min=0, max=1_000_000, step=0.001, _value=self.exposure_time_secexposure_time_sec
194 ))
195 self.add_property(nv, callback=self.handle_exptime)
196
198 nv.add_element(DefNumber(
199 name='current', label='Gain', format='%d',
200 min=0, max=100, step=1, _value=self.camera_gaincamera_gain
201 ))
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
205 ))
206 self.add_property(nv, callback=self.handle_gain)
207
209 nv.add_element(DefNumber(
210 name='current', label='Current temperature (deg C)', format='%3.3f',
211 min=-100, max=500, step=0.1, _value=self.temp_target_deg_ctemp_target_deg_c
212 ))
213 nv.add_element(DefNumber(
214 name='target', label='Requested temperature (deg C)', format='%3.3f',
215 min=-100, max=500, step=0.1, _value=self.temp_target_deg_ctemp_target_deg_c
216 ))
217 self.add_property(nv, callback=self.handle_temp_ccd)
218
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
223 ))
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
227 ))
228 self.add_property(nv)
229
231 devices = set()
232 for prop in EXTERNAL_RECORDED_PROPERTIES:
233 device = prop.split('.')[0]
234 devices.add(device)
235 self.log.debug(f"subscribe to device: {device}")
236 for fw in RECORDED_WHEELS:
237 devices.add(fw)
238 try:
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}")
242
243 def setup(self):
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}...")
247 time.sleep(1)
248 self.log.info(f"INDI client connection: {self.client.status}")
250
251 self._init_properties()
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")
257
259 if self.cameracamera is None:
260 return
261 self.temp_target_deg_ctemp_target_deg_c = self.cameracamera.target_temperature
263 self.exposure_time_secexposure_time_sec = self.cameracamera.exposure_time
264 self.camera_gaincamera_gain = self.cameracamera.gain
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}")
266
268 now = time.time()
269 current = self.properties['current_exposure']
271 remaining_sec = max((self.exposure_start_tsexposure_start_ts + self.exposure_time_secexposure_time_sec) - now, 0)
272 remaining_pct = 100 * remaining_sec / self.exposure_time_secexposure_time_sec
273 self.properties['fsm']['state'] = 'OPERATING'
274 self.update_property(self.properties['fsm'])
275 else:
276 remaining_sec = 0
277 remaining_pct = 0
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)
284
285 self.update_from_camera()
286
287 self.properties['temp_ccd']['current'] = self.temp_current_deg_ctemp_current_deg_c
288 self.properties['temp_ccd']['target'] = self.temp_target_deg_ctemp_target_deg_c
289 self.update_property(self.properties['temp_ccd'])
290
291 self.properties['exptime']['current'] = self.exposure_time_secexposure_time_sec
292 self.properties['exptime']['target'] = self.exposure_time_secexposure_time_sec
293 self.update_property(self.properties['exptime'])
294
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'])
298
299
301 '''User code must close the loop on temperature control'''
302 if self.cameracamera is None:
303 return
304 self.cameracamera.target_temperature = self.temp_target_deg_ctemp_target_deg_c
305
307 meta = {
308 'CCDTEMP': self.cameracamera.temperature,
309 'CCDSETP': self.cameracamera.target_temperature,
310 'GAIN': self.cameracamera.gain,
311 'EXPTIME': self.cameracamera.exposure_time,
312 }
313 for indi_prop in EXTERNAL_RECORDED_PROPERTIES:
314 if EXTERNAL_RECORDED_PROPERTIES[indi_prop] is None:
315 new_kw = indi_prop.upper().replace('.', ' ')
316 else:
317 new_kw = EXTERNAL_RECORDED_PROPERTIES[indi_prop]
318 #value = self.client.get(indi_prop)
319 value = self.client[indi_prop]
320 if hasattr(value, 'value'):
321 value = value.value
322 meta[new_kw] = value
323 for fwname in RECORDED_WHEELS:
324 meta[f"{fwname.upper()} PRESET NAME"] = find_active_filter(self.client, fwname)
325 return meta
326
327 def begin_exposure(self):
332 self.cameracamera.start_exposure()
333 self.log.debug("Asking camera to begin exposure")
334
335 def finalize_exposure(self, actual_exptime_sec=None):
336 img = self.cameracamera.readout()
337 # Create FITS structure
338 hdul = fits.HDUList([
339 fits.PrimaryHDU(img)
340 ])
341 # Populate headers
342 meta = self._gather_metadata()
343 self.log.debug(f"{meta=}")
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()
349 meta[f"BEGIN {key}"] = self.exposure_start_telemexposure_start_telem[key]
350 meta['INSTRUME'] = 'MagAO-X'
351 meta['CAMERA'] = 'VIS-X'
352 meta['TELESCOP'] = "Magellan Clay, Las Campanas Obs."
354 warnings.simplefilter('ignore')
355 hdul[0].header.update(meta)
356 # Note if exposure was canceled
357 if actual_exptime_sec is not None:
358 hdul[0].header['CANCELD'] = True
359 # Write to /data path
360 timestamp = datetime.datetime.utcnow().strftime("%Y-%m-%dT%H%M%S")
361 self.last_image_filenamelast_image_filename = f"camvisx_{timestamp}.fits"
362 outpath = f"{self.data_directory}/{self.last_image_filename}"
363 self.log.info(f"Saving to {outpath}")
364 try:
365 hdul.writeto(outpath)
366 except Exception:
367 self.log.exception(f"Unable to save frame!")
369
372 self.should_cancelshould_cancel = False
373 self.log.debug("Asking camera to cancel exposure")
374 # TODO actually cancel
375 actual_exptime_sec = time.time() - self.exposure_start_tsexposure_start_ts
376 self.finalize_exposure(actual_exptime_sec=actual_exptime_sec)
377
378 def loop(self):
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")
382
383 if self.sdksdk is None:
384 self.log.debug("Initializing camera SDK...")
385 success = self._init_camera()
386 if not success:
387 self.log.debug("No camera found yet, retrying on next loop")
388 return
389 self.log.debug(f"Have camera: {self.camera}")
390 self.properties['fsm']['state'] = 'CONNECTED'
391 self.update_property(self.properties['fsm'])
392
393 now = time.time()
395 self.cancel_exposure()
397 self.begin_exposure()
400 self.log.debug("Exposure finished")
401 self.finalize_exposure()
403 self.refresh_properties()
cancel_exposure(self)
Definition xapp.py:370
float exposure_start_ts
Definition xapp.py:51
Optional temp_current_deg_c
Definition xapp.py:65
finalize_exposure(self, actual_exptime_sec=None)
Definition xapp.py:335
handle_expose(self, existing_property, new_message)
Definition xapp.py:130
bool currently_exposing
Definition xapp.py:53
Optional exposure_start_telem
Definition xapp.py:58
_init_camera(self)
Definition xapp.py:152
Optional sdk
Definition xapp.py:60
handle_temp_ccd(self, existing_property, new_message)
Definition xapp.py:141
handle_gain(self, existing_property, new_message)
Definition xapp.py:115
emit_telem_stdcam(self)
Definition xapp.py:72
Optional camera
Definition xapp.py:61
cooling_on_target(self)
Definition xapp.py:68
Optional exposure_time_sec
Definition xapp.py:62
bool should_begin_exposure
Definition xapp.py:54
_init_properties(self)
Definition xapp.py:165
maintain_temperature_control(self)
Definition xapp.py:300
Optional last_image_filename
Definition xapp.py:55
refresh_properties(self)
Definition xapp.py:267
Optional camera_gain
Definition xapp.py:63
update_from_camera(self)
Definition xapp.py:258
str data_directory
Definition xapp.py:50
bool should_cancel
Definition xapp.py:52
subscribe_to_other_devices(self)
Definition xapp.py:230
begin_exposure(self)
Definition xapp.py:327
VisXConfig config
Definition xapp.py:48
Optional temp_target_deg_c
Definition xapp.py:64
_gather_metadata(self)
Definition xapp.py:306
handle_exptime(self, existing_property, new_message)
Definition xapp.py:100
find_active_filter(client, fwname)
Definition xapp.py:34