API
 
Loading...
Searching...
No Matches
core.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.')
47
48class visxCtrl(XDevice):
49 config : VisXConfig
50 # us
51 data_directory : str = "/opt/MagAOX/rawimages/camvisx"
52 exposure_start_ts : float = 0
53 should_cancel : bool = False
54 currently_exposing : bool = False
55 should_begin_exposure : bool = False
56 last_image_filename : Optional[str] = None
57 shmim : ISIO.Image
58 frame : np.ndarray
59 exposure_start_telem : Optional[dict] = None
60 # them
61 sdk : Optional[QHYCCDSDK] = None
62 camera : Optional[QHYCCDCamera] = None
63 exposure_time_sec : Optional[float] = None
64 camera_gain : Optional[int] = None
65 temp_target_deg_c : Optional[float] = None
66 temp_current_deg_c : Optional[float] = None
67
68 @property
70 temp_on_target = abs((self.cameracamera.temperature - self.cameracamera.target_temperature) / self.cameracamera.target_temperature) < self.config.temp_on_target_pct_diff
71 return temp_on_target
72
74 w, h = 9600, 6422
75 self.telem("telem_stdcam", {
76 "roi": {
77 "xcen": (w - 1) / 2,
78 "ycen": (h - 1) / 2,
79 "w": w,
80 "h": h,
81 "xbin": 1,
82 "ybin": 1,
83 },
86 "emGain": self.cameracamera.gain,
87 "adcSpeed": -1,
88 "tempCtrl": {
89 "temp": self.cameracamera.temperature,
90 "setpt": self.cameracamera.target_temperature,
91 "status": True,
93 "statusStr": "LOCKED" if self.cooling_on_targetcooling_on_target else "UNLOCKED",
94 },
95 "shutter": {"statusStr": None, "state": None},
96 "synchro": 0,
97 "vshift": -1,
98 "cropMode": 0
99 })
100
101 def handle_exptime(self, existing_property, new_message):
102 log.debug(f"In handle_exptime")
104 self.log.debug("Ignoring exposure time change request while currently exposing")
105 elif self.sdksdk is None:
106 self.log.debug("Ignoring exposure time change while we don't have an SDK handle")
107 elif not self.currently_exposingcurrently_exposing and 'target' in new_message and new_message['target'] != existing_property['current']:
108 existing_property['current'] = new_message['target']
109 existing_property['target'] = new_message['target']
110 self.exposure_time_secexposure_time_sec = new_message['target']
111 self.cameracamera.exposure_time = self.exposure_time_secexposure_time_sec
112 self.log.debug(f"Exposure time changed to {new_message['target']} seconds")
113 self.telem('exptime', {'exptime': self.exposure_time_secexposure_time_sec})
114 self.update_property(existing_property)
115
116 def handle_gain(self, existing_property, new_message):
117 log.debug(f"In handle_gain")
119 self.log.debug("Ignoring gain change request while currently exposing")
120 elif self.sdksdk is None:
121 self.log.debug("Ignoring gain change while we don't have an SDK handle")
122 elif not self.currently_exposingcurrently_exposing and 'target' in new_message and new_message['target'] != existing_property['current']:
123 existing_property['current'] = new_message['target']
124 existing_property['target'] = new_message['target']
125 self.camera_gaincamera_gain = new_message['target']
126 self.cameracamera.gain = self.camera_gaincamera_gain
127 self.log.debug(f"Gain changed to {new_message['target']}")
128 self.telem('emGain', {'emGain': self.camera_gaincamera_gain})
129 self.update_property(existing_property)
130
131 def handle_expose(self, existing_property, new_message):
132 if self.sdksdk is None:
133 self.log.debug("Ignoring request for exposure while we don't have an SDK handle")
134 elif 'request' in new_message and new_message['request'] is constants.SwitchState.ON:
135 self.log.debug("Exposure requested!")
137 elif 'cancel' in new_message and new_message['cancel'] is constants.SwitchState.ON:
138 self.log.debug("Exposure cancellation requested")
140 self.update_property(existing_property) # ensure the switch turns back off at the client
141
142 def handle_temp_ccd(self, existing_property, new_message):
143 if self.sdksdk is None:
144 self.log.debug("Ignoring temperature setpoint change while we don't have an SDK handle")
145 elif 'target' in new_message and new_message['target'] != existing_property['current']:
146 existing_property['current'] = new_message['target']
147 existing_property['target'] = new_message['target']
148 self.temp_target_deg_ctemp_target_deg_c = new_message['target']
149 self.log.debug(f"CCD temperature setpoint changed to {self.temp_target_deg_c} deg C")
150 self.telem('tempcontrol', {'temp_target_deg_c': self.temp_target_deg_ctemp_target_deg_c})
151 self.update_property(existing_property)
152
153 def _init_camera(self):
154 # Load SDK
155 self.sdksdk = QHYCCDSDK(dll_path=self.config.full_sdk_path)
156 if self.sdksdk.number_of_cameras < 1:
157 del self.sdksdk
158 return False
159 # Find camera
161 self.exposure_time_secexposure_time_sec = self.cameracamera.exposure_time
162 self.temp_target_deg_ctemp_target_deg_c = self.config.startup_temp
163 self.camera_gaincamera_gain = self.cameracamera.gain
164 return True
165
167 fsmstate = properties.TextVector(
168 name='fsm',
169 )
170 fsmstate.add_element(DefText(name="state", _value="NODEVICE"))
171 self.add_property(fsmstate)
172
174 name='last_frame',
175 )
176 tv.add_element(DefText(name="filename", _value=None))
177 self.add_property(tv)
179 name='expose',
182 )
183 sv.add_element(DefSwitch(name="request", _value=constants.SwitchState.OFF))
184 sv.add_element(DefSwitch(name="cancel", _value=constants.SwitchState.OFF))
185 self.add_property(sv, callback=self.handle_expose)
186
188 nv.add_element(DefNumber(
189 name='current', label='Exposure time (sec)', format='%3.1f',
190 min=0, max=1_000_000, step=0.001, _value=self.exposure_time_secexposure_time_sec
191 ))
192 nv.add_element(DefNumber(
193 name='target', label='Requested exposure time (sec)', format='%3.1f',
194 min=0, max=1_000_000, step=0.001, _value=self.exposure_time_secexposure_time_sec
195 ))
196 self.add_property(nv, callback=self.handle_exptime)
197
199 nv.add_element(DefNumber(
200 name='current', label='Gain', format='%d',
201 min=0, max=100, step=1, _value=self.camera_gaincamera_gain
202 ))
203 nv.add_element(DefNumber(
204 name='target', label='Requested gain', format='%d',
205 min=0, max=100, step=1, _value=self.camera_gaincamera_gain
206 ))
207 self.add_property(nv, callback=self.handle_gain)
208
210 nv.add_element(DefNumber(
211 name='current', label='Current temperature (deg C)', format='%3.3f',
212 min=-100, max=500, step=0.1, _value=self.temp_target_deg_ctemp_target_deg_c
213 ))
214 nv.add_element(DefNumber(
215 name='target', label='Requested temperature (deg C)', format='%3.3f',
216 min=-100, max=500, step=0.1, _value=self.temp_target_deg_ctemp_target_deg_c
217 ))
218 self.add_property(nv, callback=self.handle_temp_ccd)
219
220 nv = properties.NumberVector(name='current_exposure')
221 nv.add_element(DefNumber(
222 name='remaining_sec', label='Time remaining (sec)', format='%3.3f',
223 min=0, max=1_000_000, step=1, _value=0.0
224 ))
225 nv.add_element(DefNumber(
226 name='remaining_pct', label='Percentage remaining', format='%i',
227 min=0, max=100, step=0.1, _value=0.0
228 ))
229 self.add_property(nv)
230
232 devices = set()
233 for prop in EXTERNAL_RECORDED_PROPERTIES:
234 device = prop.split('.')[0]
235 devices.add(device)
236 self.log.debug(f"subscribe to device: {device}")
237 for fw in RECORDED_WHEELS:
238 devices.add(fw)
239 try:
240 self.client.get_properties_and_wait(devices)
241 except TimeoutError as e:
242 log.warning(f"Timed out waiting to get properties from external INDI devices: {e}")
243
244 def setup(self):
246 while self.client.status is not constants.ConnectionStatus.CONNECTED:
247 self.log.info(f"Connecting to INDI as a client to get {list(EXTERNAL_RECORDED_PROPERTIES.keys())} and {RECORDED_WHEELS}...")
248 time.sleep(1)
249 self.log.info(f"INDI client connection: {self.client.status}")
251
252 self._init_properties()
253 self.properties['fsm']['state'] = 'NOTCONNECTED'
254 self.log.debug("Set FSM prop")
255 self.update_property(self.properties['fsm'])
256 self.log.debug("Sent FSM prop")
257 self.log.info("Set up complete")
258
260 if self.cameracamera is None:
261 return
262 self.temp_target_deg_ctemp_target_deg_c = self.cameracamera.target_temperature
264 self.exposure_time_secexposure_time_sec = self.cameracamera.exposure_time
265 self.camera_gaincamera_gain = self.cameracamera.gain
266 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}")
267
269 now = time.time()
270 current = self.properties['current_exposure']
272 remaining_sec = max((self.exposure_start_tsexposure_start_ts + self.exposure_time_secexposure_time_sec) - now, 0)
273 remaining_pct = 100 * remaining_sec / self.exposure_time_secexposure_time_sec
274 self.properties['fsm']['state'] = 'OPERATING'
275 self.update_property(self.properties['fsm'])
276 else:
277 remaining_sec = 0
278 remaining_pct = 0
279 self.properties['fsm']['state'] = 'READY'
280 self.update_property(self.properties['fsm'])
281 if remaining_sec != current['remaining_sec']:
282 current['remaining_sec'] = remaining_sec
283 current['remaining_pct'] = remaining_pct
284 self.update_property(current)
285
286 self.update_from_camera()
287
288 self.properties['temp_ccd']['current'] = self.temp_current_deg_ctemp_current_deg_c
289 self.properties['temp_ccd']['target'] = self.temp_target_deg_ctemp_target_deg_c
290 self.update_property(self.properties['temp_ccd'])
291
292 self.properties['exptime']['current'] = self.exposure_time_secexposure_time_sec
293 self.properties['exptime']['target'] = self.exposure_time_secexposure_time_sec
294 self.update_property(self.properties['exptime'])
295
296 self.properties['gain']['current'] = self.camera_gaincamera_gain
297 self.properties['gain']['target'] = self.camera_gaincamera_gain
298 self.update_property(self.properties['gain'])
299
300
302 '''User code must close the loop on temperature control'''
303 if self.cameracamera is None:
304 return
305 self.cameracamera.target_temperature = self.temp_target_deg_ctemp_target_deg_c
306
308 meta = {
309 'CCDTEMP': self.cameracamera.temperature,
310 'CCDSETP': self.cameracamera.target_temperature,
311 'GAIN': self.cameracamera.gain,
312 'EXPTIME': self.cameracamera.exposure_time,
313 }
314 for indi_prop in EXTERNAL_RECORDED_PROPERTIES:
315 if EXTERNAL_RECORDED_PROPERTIES[indi_prop] is None:
316 new_kw = indi_prop.upper().replace('.', ' ')
317 else:
318 new_kw = EXTERNAL_RECORDED_PROPERTIES[indi_prop]
319 #value = self.client.get(indi_prop)
320 value = self.client[indi_prop]
321 if hasattr(value, 'value'):
322 value = value.value
323 meta[new_kw] = value
324 for fwname in RECORDED_WHEELS:
325 meta[f"{fwname.upper()} PRESET NAME"] = find_active_filter(self.client, fwname)
326 return meta
327
328 def begin_exposure(self):
333 self.cameracamera.start_exposure()
334 self.log.debug("Asking camera to begin exposure")
335
336 def finalize_exposure(self, actual_exptime_sec=None):
337 img = self.cameracamera.readout()
338 # Create FITS structure
339 hdul = fits.HDUList([
340 fits.PrimaryHDU(img)
341 ])
342 # Populate headers
343 meta = self._gather_metadata()
344 self.log.debug(f"{meta=}")
346 exposure_time = self.cameracamera.exposure_time if actual_exptime_sec is None else actual_exptime_sec
347 meta['DATE-END'] = datetime.datetime.fromtimestamp(self.exposure_start_tsexposure_start_ts + exposure_time).isoformat()
348 meta['DATE'] = datetime.datetime.utcnow().isoformat()
350 meta[f"BEGIN {key}"] = self.exposure_start_telemexposure_start_telem[key]
351 meta['INSTRUME'] = 'MagAO-X'
352 meta['CAMERA'] = 'VIS-X'
353 meta['TELESCOP'] = "Magellan Clay, Las Campanas Obs."
355 warnings.simplefilter('ignore')
356 hdul[0].header.update(meta)
357 # Note if exposure was canceled
358 if actual_exptime_sec is not None:
359 hdul[0].header['CANCELD'] = True
360 # Write to /data path
361 timestamp = datetime.datetime.now(datetime.timezone.utc).strftime("%Y-%m-%dT%H%M%S")
362 self.last_image_filenamelast_image_filename = f"camvisx_{timestamp}.fits"
363 outpath = f"{self.data_directory}/{self.last_image_filename}"
364 self.log.info(f"Saving to {outpath}")
365 try:
366 hdul.writeto(outpath)
367 except Exception:
368 self.log.exception(f"Unable to save frame!")
370
373 self.should_cancelshould_cancel = False
374 self.log.debug("Asking camera to cancel exposure")
375 # TODO actually cancel
376 actual_exptime_sec = time.time() - self.exposure_start_tsexposure_start_ts
377 self.finalize_exposure(actual_exptime_sec=actual_exptime_sec)
378
379 def loop(self):
380 if self.client.interested_properties_missing:
382 self.log.debug(f"Repeating subscription because some external devices we use for headers are not showing up")
383
384 if self.sdksdk is None:
385 self.log.debug("Initializing camera SDK...")
386 success = self._init_camera()
387 if not success:
388 self.log.debug("No camera found yet, retrying on next loop")
389 return
390 self.log.debug(f"Have camera: {self.camera}")
391 self.properties['fsm']['state'] = 'CONNECTED'
392 self.update_property(self.properties['fsm'])
393
394 now = time.time()
396 self.cancel_exposure()
398 self.begin_exposure()
401 self.log.debug("Exposure finished")
402 self.finalize_exposure()
404 self.refresh_properties()
update_from_camera(self)
Definition core.py:259
maintain_temperature_control(self)
Definition core.py:301
handle_expose(self, existing_property, new_message)
Definition core.py:131
Optional temp_current_deg_c
Definition core.py:66
float exposure_start_ts
Definition core.py:52
bool should_begin_exposure
Definition core.py:55
Optional camera_gain
Definition core.py:64
VisXConfig config
Definition core.py:49
Optional last_image_filename
Definition core.py:56
handle_gain(self, existing_property, new_message)
Definition core.py:116
handle_exptime(self, existing_property, new_message)
Definition core.py:101
Optional exposure_time_sec
Definition core.py:63
refresh_properties(self)
Definition core.py:268
Optional temp_target_deg_c
Definition core.py:65
bool currently_exposing
Definition core.py:54
emit_telem_stdcam(self)
Definition core.py:73
finalize_exposure(self, actual_exptime_sec=None)
Definition core.py:336
Optional camera
Definition core.py:62
Optional exposure_start_telem
Definition core.py:59
handle_temp_ccd(self, existing_property, new_message)
Definition core.py:142
cooling_on_target(self)
Definition core.py:69
subscribe_to_other_devices(self)
Definition core.py:231
find_active_filter(client, fwname)
Definition core.py:34