def set_parameter(self, parameter_name, value): """Sets the value of a named parameter""" if parameter_name not in self.parameters_by_name: return False parameter = self.parameters_by_name[parameter_name] try: url = 'http://' + self._ip + '/' + parameter.format_set_parameter( value) with urllib.request.urlopen(url, None, self._query_timeout) as response: response = response.read().decode('ascii') if self._last_command_failed: log.info(self._log_name, 'Restored contact with ' + self._ip) self._last_command_failed = False except Exception as exception: print('{} ERROR: failed to send HTTP command: {}' \ .format(datetime.datetime.utcnow(), str(exception))) if not self._last_command_failed: log.error(self._log_name, 'Lost contact with ' + self._ip) self._last_command_failed = True return False return response == 'Success! ' + str(value)
def query_apcaccess(self, p): try: args = [ '/usr/sbin/apcaccess', '-f', self._device, '-p', p.key, '-u' ] output = subprocess.check_output( args, universal_newlines=True, timeout=self._query_timeout).strip() if not output: return p.error_value if self._last_command_failed: log.info(self._log_name, 'Restored contact with ' + self._device) self._last_command_failed = False return p.parse_apcaccess_output(output) except Exception as exception: print('{} ERROR: failed to query {}: {}' \ .format(datetime.datetime.utcnow(), self._device, str(exception))) if not self._last_command_failed: log.error(self._log_name, 'Lost contact with ' + self._device) self._last_command_failed = True return p.error_value
def update(self): '''Queries environmentd for new data and updates flags''' was_safe = self.safe try: with daemons.observatory_environment.connect() as environment: data = environment.status() safe = True unsafe_conditions = [] for condition, watchers in self._conditions.items(): for w in watchers: w.update(data) # Condition is considered unsafe if all parameters are unknown # or if any condition is unsafe if all([w.status == ConditionStatus.Unknown for w in watchers]) or \ any([w.status == ConditionStatus.Unsafe for w in watchers]): safe = False unsafe_conditions.append(condition) internal_humidity = None for watcher in self._conditions['internal_humidity']: internal_humidity = watcher.latest(data) if internal_humidity is not None: break external_humidity = None for watcher in self._conditions['humidity']: external_humidity = watcher.latest(data) if external_humidity is not None: break with self._lock: self.safe = safe self.unsafe_conditions = unsafe_conditions self.internal_humidity = internal_humidity self.external_humidity = external_humidity self.updated = datetime.datetime.utcnow() if was_safe and not safe: message = 'Environment has become unsafe (' + ', '.join(unsafe_conditions) + ')' print(message) log.warning(self._log_name, message) elif not was_safe and safe: print('Environment trigger timed out') log.info(self._log_name, 'Environment trigger timed out') except Exception as e: with self._lock: self.safe = False for condition, watchers in self._conditions.items(): for w in watchers: w.update({}) self.unsafe_conditions = list(self._conditions.keys()) self.internal_humidity = None self.external_humidity = None self.updated = datetime.datetime.utcnow() print('error: failed to query environment:') traceback.print_exc(file=sys.stdout) log.info(self._log_name, 'Failed to query environment (' + str(e) + ')')
def status(self): """Return a dictionary of parameter values for this device""" # Query all OIDs at once for efficiency oids = [p.get_oid for p in self.parameters] args = ['/usr/bin/snmpget', '-v', '1', '-c', 'public', self._ip] + oids try: output = subprocess.check_output(args, universal_newlines=True, timeout=self._query_timeout) lines = output.strip().split('\n') if self._last_command_failed: log.info(self._log_name, 'Restored contact with ' + self._ip) self._last_command_failed = False # Return a dictionary of values keyed by parameter name return { k.name: k.parse_snmpget_output(v) for k, v in zip(self.parameters, lines) } except Exception as exception: print('{} ERROR: failed to query {}: {}' \ .format(datetime.datetime.utcnow(), self._ip, str(exception))) if not self._last_command_failed: log.error(self._log_name, 'Lost contact with ' + self._ip) self._last_command_failed = True return {k.name: k.error_value for k in self.parameters}
def status(self): """Return a dictionary of parameter values for this device""" try: url = 'http://' + self._ip + '/status.xml' with urllib.request.urlopen(url, None, self._query_timeout) as response: response = response.read().decode('ascii') xml = ET.fromstring(response) if self._last_command_failed: log.info(self._log_name, 'Restored contact with ' + self._ip) self._last_command_failed = False # Return a dictionary of values keyed by parameter name return { k.name: k.parse_status_response(xml) for k in self.parameters } except Exception as exception: print('{} ERROR: failed to query {}: {}' \ .format(datetime.datetime.utcnow(), self._ip, str(exception))) if not self._last_command_failed: log.error(self._log_name, 'Lost contact with ' + self._ip) self._last_command_failed = True return {k.name: k.error_value for k in self.parameters}
def clear_open_window(self): """Clears the times that the dome should be automatically open The dome will automatically close if it is currently within this window """ self._requested_open_date = self._requested_close_date = None print('Cleared dome window') log.info(self._log_name, 'Cleared dome window') self.__shortcut_loop_wait()
def run(self): """Main run loop""" while True: # Initial setup try: self._port = serial.Serial(self._port_name, 9600, timeout=3) print('Connected to', self._port_name) log.info(self._log_name, 'Connected to battery voltmeter') self._port_connected = True except Exception as exception: if self._port_connected: log.error(self._log_name, 'Lost connection to battery voltmeter') print('error: failed to connect to battery voltmeter') print(exception) print('Will retry in 10 seconds...') time.sleep(10.) continue try: # Flush any stale state self._port.flushInput() self._port.flushOutput() # Main run loop while True: data = self._port.readline() match = self._regex.match(data) if match: self._voltage = float(match.group('voltage')) self._voltage_sum += self._voltage self._voltage_history.append(self._voltage) if len(self._voltage_history) > 10: self._voltage_sum -= self._voltage_history.pop(0) except Exception as exception: self._port.close() if self._port_connected: log.error(self._log_name, 'Lost connection to battery voltmeter') print('error: lost connection to battery voltmeter') print(exception) self._port_connected = False print('Will retry in 10 seconds...') time.sleep(10.)
def set_parameter(self, parameter_name, value): """Sets the value of a named parameter""" if parameter_name not in self.parameters_by_name: return False parameter = self.parameters_by_name[parameter_name] # Read only parameter if not parameter.set_oid: return False try: args = [ '/usr/bin/snmpset', '-v', '1', '-c', 'private', self._ip, parameter.set_oid, 'i', parameter.format_set_value(value) ] output = subprocess.check_output(args, universal_newlines=True, timeout=self._query_timeout) if self._last_command_failed: log.info(self._log_name, 'Restored contact with ' + self._ip) self._last_command_failed = False except Exception as exception: print('{} ERROR: failed to send SNMP command: {}' \ .format(datetime.datetime.utcnow(), str(exception))) if not self._last_command_failed: log.error(self._log_name, 'Lost contact with ' + self._ip) self._last_command_failed = True return False try: return parameter.parse_snmpset_output(output) == value except Exception as exception: print('{} ERROR: failed to parse SNMP response: {}' \ .format(datetime.datetime.utcnow(), str(exception))) if not self._last_command_failed: log.error( self._log_name, 'Invalid response from ' + self._ip + ': ' + str(exception)) return False
def set_parameter(self, parameter_name, value): """Sets the value of a named parameter""" if parameter_name != self._parameter_name: return False try: with self._daemon.connect(timeout=self._query_timeout) as daemon: success = daemon.set_relay(value) if self._last_command_failed: log.info(self._log_name, 'Restored contact with ' + self._daemon.name) self._last_command_failed = False return success except Exception: if not self._last_command_failed: log.error(self._log_name, 'Lost contact with ' + self._daemon.name) self._last_command_failed = True return False
def set_open_window(self, dates): """Sets the datetimes that the dome should open and close These dates will be cleared automatically if a weather alert triggers """ if not dates or len(dates) < 2: return False if not isinstance(dates[0], datetime.datetime) or \ not isinstance(dates[1], datetime.datetime): return False with self._lock: self._requested_open_date = dates[0] self._requested_close_date = dates[1] open_str = dates[0].strftime('%Y-%m-%dT%H:%M:%SZ') close_str = dates[1].strftime('%Y-%m-%dT%H:%M:%SZ') print('Scheduled dome window ' + open_str + ' - ' + close_str) log.info(self._log_name, 'Scheduled dome window ' + open_str + ' - ' + close_str) self.__shortcut_loop_wait() return True
def get_parameter(self, parameter_name): """Returns the value of a named parameter""" if parameter_name != self._parameter_name: return False try: with self._daemon.connect(timeout=self._query_timeout) as daemon: enabled = daemon.get_relay() if self._last_command_failed: log.info(self._log_name, 'Restored contact with ' + self._daemon.name) self._last_command_failed = False return SwitchStatus.On if enabled else SwitchStatus.Off except Exception: if not self._last_command_failed: log.error(self._log_name, 'Lost contact with ' + self._daemon.name) self._last_command_failed = True return SwitchStatus.Unknown
def run(self): """Main run loop""" while True: # Initial setup try: self._port = serial.Serial(self._port_name, 9600, timeout=3) print('Connected to', self._port_name) first_query = self._enabled_date == datetime.datetime.min prefix = 'Established' if first_query else 'Restored' log.info(self._log_name, prefix + ' contact with Dehumidifier switch') self._port_connected = True except Exception as exception: if self._port_connected: log.error(self._log_name, 'Lost contact with Dehumidifier switch') print('error: failed to connect to Dehumidifier switch') print(exception) print('Will retry in 10 seconds...') time.sleep(10.) continue try: # Flush any stale state self._port.flushInput() self._port.flushOutput() # Main run loop while True: data = ord(self._port.read(1)) # First bit is set when the relay to the dehumidifier is enabled enabled = data & 0x01 if enabled != self._enabled: with self._updated_condition: self._enabled = enabled self._enabled_date = datetime.datetime.utcnow() # Wake up any threads that requested the change so it can return self._updated_condition.notify_all() # Second bit is set for one update cycle when the button is pressed if data & 0x02: light_enabled = self._power_daemon.value('light') if not self._power_daemon.switch_internal( 'light', not light_enabled): log.error(self._log_name, 'Failed to toggle dome lights') if self._desired_enabled != self._enabled: command = (0x01 if self._desired_enabled else 0) self._port.write(chr(command).encode('ascii')) except Exception as exception: self._port.close() if self._port_connected: log.error(self._log_name, 'Lost contact with dehumidifier switch') print('error: failed to connect to Dehumidifier switch') print(exception) self._port_connected = False print('Will retry in 10 seconds...') time.sleep(10.)
def run_thread(self): """Thread that runs the hardware actions""" # Query site location from the telescope ts = tel_status(self.log_name) if not ts: self.status = TelescopeActionStatus.Error return # pylint: disable=no-member location = EarthLocation(lat=ts['site_latitude']*u.rad, lon=ts['site_longitude']*u.rad, height=ts['site_elevation']*u.m) # pylint: enable=no-member while not self.aborted: waiting_for = [] sun_altitude = sun_position(location)[0] if self.config['evening']: if sun_altitude < CONFIG['min_sun_altitude']: print('AutoFlat: Sun already below minimum altitude') log.info(self.log_name, 'AutoFlat: Sun already below minimum altitude') self.status = TelescopeActionStatus.Complete return if sun_altitude < CONFIG['max_sun_altitude'] and self.dome_is_open: break if not self.dome_is_open: waiting_for.append('Dome') if sun_altitude >= CONFIG['max_sun_altitude']: waiting_for.append('Sun < {:.1f} deg'.format(CONFIG['max_sun_altitude'])) print('AutoFlat: {:.1f} > {:.1f}; dome {} - keep waiting'.format( sun_altitude, CONFIG['max_sun_altitude'], self.dome_is_open)) else: if sun_altitude > CONFIG['max_sun_altitude']: print('AutoFlat: Sun already above maximum altitude') log.info(self.log_name, 'AutoFlat: Sun already above maximum altitude') self.status = TelescopeActionStatus.Complete return if sun_altitude > CONFIG['min_sun_altitude'] and self.dome_is_open: break if not self.dome_is_open: waiting_for.append('Dome') if sun_altitude < CONFIG['min_sun_altitude']: waiting_for.append('Sun > {:.1f} deg'.format(CONFIG['min_sun_altitude'])) print('AutoFlat: {:.1f} < {:.1f}; dome {} - keep waiting'.format( sun_altitude, CONFIG['min_sun_altitude'], self.dome_is_open)) self.set_task('Waiting for ' + ', '.join(waiting_for)) with self._wait_condition: self._wait_condition.wait(CONFIG['sun_altitude_check_interval']) self.set_task('Slewing to antisolar point') # The anti-solar point is opposite the sun at 75 degrees sun_altaz = sun_position(location) print('AutoFlat: Sun position is', sun_altaz) if not tel_slew_altaz(self.log_name, math.radians(75), math.radians(sun_altaz[1] + 180), False, SLEW_TIMEOUT): if not self.aborted: print('AutoFlat: Failed to slew telescope') log.error(self.log_name, 'AutoFlat: Failed to slew telescope') self.status = TelescopeActionStatus.Error return # Last chance to bail out before starting the main logic if self.aborted: self.status = TelescopeActionStatus.Complete return # Configure pipeline and camera for flats # Archiving will be enabled when the brightness is inside the required range pipeline_config = {} pipeline_config.update(self.config['pipeline']) pipeline_config.update({ 'intstats': True, 'type': 'FLAT', }) if not configure_pipeline(self.log_name, pipeline_config): self.status = TelescopeActionStatus.Error return # Take an initial bias frame for calibration # This starts the autoflat logic, which is run # in the received_frame callbacks for arm in self._instrument_arms.values(): arm.start() # Wait until complete while True: with self._wait_condition: self._wait_condition.wait(5) codes = '' for arm in self._instrument_arms.values(): arm.check_timeout() codes += AutoFlatState.Codes[arm.state] self.set_task('Acquiring (' + ''.join(codes) + ')') if self.aborted: break if not self.dome_is_open: for arm in self._instrument_arms.values(): arm.abort() print('AutoFlat: Dome has closed') log.error(self.log_name, 'AutoFlat: Dome has closed') break # We are done once all arms are either complete or have errored if all([arm.state >= AutoFlatState.Complete for arm in self._instrument_arms.values()]): break success = self.dome_is_open and all([arm.state == AutoFlatState.Complete for arm in self._instrument_arms.values()]) if self.aborted or success: self.status = TelescopeActionStatus.Complete else: self.status = TelescopeActionStatus.Error
def received_frame(self, headers): """Callback to process an acquired frame. headers is a dictionary of header keys""" last_state = self.state delay_exposure = 0 if self.state == AutoFlatState.Bias: self.bias = headers['MEDCNTS'] print('AutoFlat: {} bias level is {:.0f} ADU'.format(self.name, self.bias)) log.info(self._log_name, 'AutoFlat: {} bias is {:.0f} ADU'.format(self.name, self.bias)) # Take the first flat image self.state = AutoFlatState.Waiting self.__take_image(self._start_exposure, delay_exposure) elif self.state == AutoFlatState.Waiting or self.state == AutoFlatState.Saving: if self.state == AutoFlatState.Saving: self._exposure_count += 1 exposure = headers['EXPTIME'] counts = headers['MEDCNTS'] - self.bias # If the count rate is too low then we scale the exposure by the maximum amount if counts > 0: new_exposure = self._scale * exposure * CONFIG['target_counts'] / counts else: new_exposure = exposure * CONFIG['max_exposure_delta'] # Clamp the exposure to a sensible range clamped_exposure = min(new_exposure, CONFIG['max_exposure'], exposure * CONFIG['max_exposure_delta']) clamped_exposure = max(clamped_exposure, CONFIG['min_exposure'], exposure / CONFIG['max_exposure_delta']) clamped_desc = ' (clamped from {:.2f}s)'.format(new_exposure) \ if new_exposure > clamped_exposure else '' print('AutoFlat: {} exposure {:.2f}s counts {:.0f} ADU -> {:.2f}s{}' .format(self.name, exposure, counts, clamped_exposure, clamped_desc)) if self._is_evening: # Sky is decreasing in brightness for min_exposure in CONFIG['evening_exposure_delays']: if new_exposure < min_exposure and counts > CONFIG['min_save_counts']: delay_exposure += CONFIG['evening_exposure_delays'][min_exposure] if delay_exposure > 0: print('AutoFlat: ' + self.name + ' waiting ' + str(delay_exposure) + \ 's for it to get darker') if clamped_exposure == CONFIG['max_exposure'] \ and counts < CONFIG['min_save_counts']: self.state = AutoFlatState.Complete elif self.state == AutoFlatState.Waiting and counts > CONFIG['min_save_counts'] \ and new_exposure > CONFIG['min_save_exposure']: self.state = AutoFlatState.Saving else: # Sky is increasing in brightness if clamped_exposure < CONFIG['min_save_exposure']: self.state = AutoFlatState.Complete elif self.state == AutoFlatState.Waiting and counts > CONFIG['min_save_counts']: self.state = AutoFlatState.Saving if self.state != last_state: if not pipeline_enable_archiving(self._log_name, self.name, self.state == AutoFlatState.Saving): self.state = AutoFlatState.Error return print('AutoFlat: ' + self.name + ' ' + AutoFlatState.Names[last_state] \ + ' -> ' + AutoFlatState.Names[self.state]) if self.state == AutoFlatState.Saving: log.info(self._log_name, 'AutoFlat: {} saving enabled'.format(self.name)) elif self.state == AutoFlatState.Complete: runtime = (datetime.datetime.utcnow() - self._start_time).total_seconds() message = 'AutoFlat: {} acquired {} flats in {:.0f} seconds'.format( self.name, self._exposure_count, runtime) print(message) log.info(self._log_name, message) if self.state != AutoFlatState.Complete: self.__take_image(clamped_exposure, delay_exposure)
def __run(self): """Main run loop""" while True: # Initial setup try: self._port = serial.Serial(self._port_path, self._port_baud, timeout=0.1) print('Connected to GPS timer on ', self._port_path) prefix = 'Restored' if self._port_error else 'Established' log.info(self._log_table, prefix + ' serial connection to GPS timer') self._port_error = False except Exception as exception: print(exception) print('Will retry in 10 seconds...') if not self._port_error: log.error(self._log_table, 'Failed to connect to GPS timer') self._port_error = True time.sleep(10.) continue try: # Flush any stale state self._port.reset_input_buffer() self._port.reset_output_buffer() self.__send_config() # Main run loop buf = b'' while True: # Read as much data as we can or block until the timeout expires buf += self._port.read(max(1, self._port.in_waiting)) # Sync on start of UBX packet start = 0 for start in range(len(buf) - 2): if buf[start] == 0xb5 and buf[start + 1] == 0x62: break if start > 0: print('timer: discarding {} junk bytes'.format(start)) buf = buf[start:] # We need at least 4 bytes to determine packet type if len(buf) < 4: continue (msg_cls, msg_id, msg_len) = struct.unpack_from('<BBh', buf, offset=2) if len(buf) < 8 + msg_len: continue # Verify message checksum chka = chkb = 0 for i in range(2, msg_len + 6): chka += buf[i] chkb += chka chka = chka & 0xFF chkb = chkb & 0xFF if chka == buf[msg_len + 6] and chkb == buf[msg_len + 7]: self.__parse_message(msg_cls, msg_id, buf) else: print('timer: discarding packet: checksum mismatch') buf = buf[8 + msg_len:] except Exception as exception: self._port.close() print(exception) print('Will retry in 10 seconds...') if not self._port_error: log.error(self._log_table, 'Lost serial connection to GPS timer') self._port_error = True time.sleep(10.)
def __loop(self): """Thread that controls dome opening/closing to match requested state""" while True: # Handle requests from the user to change between manual and automatic mode # Manual intervention is required to clear errors and return to automatic mode. # If an error does occur the dome heartbeat will timeout, and it will close itself. # Copy public facing variables to avoid race conditions with self._lock: requested_mode = self._requested_mode current_date = datetime.datetime.utcnow() requested_open = self._requested_open_date is not None and \ self._requested_close_date is not None and \ current_date > self._requested_open_date and \ current_date < self._requested_close_date and \ self._environment_safe and \ self._environment_safe_date > self._requested_open_date requested_status = DomeStatus.Open if requested_open else DomeStatus.Closed environment_safe_age = (current_date - self._environment_safe_date).total_seconds() auto_failure = self._mode == OperationsMode.Error and \ requested_mode == OperationsMode.Automatic if requested_mode != self._mode and not auto_failure: print('dome: changing mode from ' + OperationsMode.Names[self._mode] + \ ' to ' + OperationsMode.Names[requested_mode]) try: with self._daemon.connect() as dome: if requested_mode == OperationsMode.Automatic: ret = dome.set_heartbeat_timer(self._heartbeat_timeout) if ret == DomeCommandStatus.Succeeded: self.__set_mode(OperationsMode.Automatic) log.info(self._log_name, 'Dome switched to Automatic mode') else: print('error: failed to switch dome to auto with ' \ + DomeCommandStatus.message(ret)) self.__set_mode(OperationsMode.Error) log.info(self._log_name, 'Failed to switch dome to Automatic mode') else: # Switch from auto or error state back to manual ret = dome.set_heartbeat_timer(0) if ret == DomeCommandStatus.Succeeded: self.__set_mode(OperationsMode.Manual) log.info(self._log_name, 'Dome switched to Manual mode') else: print('error: failed to switch dome to manual with ' \ + DomeCommandStatus.message(ret)) self.__set_mode(OperationsMode.Error) log.error(self._log_name, 'Failed to switch dome to Manual mode') if self._daemon_error: log.info(self._log_name, 'Restored contact with Dome daemon') except Exception as e: if not self._daemon_error: log.error(self._log_name, 'Lost contact with Dome daemon') self._daemon_error = True print('error: failed to communicate with the dome daemon: ', e) self.__set_mode(OperationsMode.Error) if self._mode == OperationsMode.Automatic: try: with self._daemon.connect() as dome: status = DomeStatus.parse(dome.status()) self.__set_status(status) print('dome: is ' + DomeStatus.Names[status] + ' and wants to be ' + \ DomeStatus.Names[requested_status]) if status == DomeStatus.Timeout: print('dome: detected heartbeat timeout!') self.__set_mode(OperationsMode.Error) elif requested_status == DomeStatus.Closed and status == DomeStatus.Open: print('dome: sending heartbeat ping before closing') with self._daemon.connect() as dome: dome.set_heartbeat_timer(self._heartbeat_timeout) print('dome: closing') self.__set_status(DomeStatus.Moving) with self._daemon.connect(timeout=self._open_close_timeout) as dome: ret = dome.close_shutters(east=True, west=True) if ret == DomeCommandStatus.Succeeded: self.__set_status(DomeStatus.Closed) else: self.__set_mode(OperationsMode.Error) elif requested_status == DomeStatus.Open and status == DomeStatus.Closed: print('dome: sending heartbeat ping before opening') with self._daemon.connect() as dome: dome.set_heartbeat_timer(self._heartbeat_timeout) print('dome: opening') self.__set_status(DomeStatus.Moving) with self._daemon.connect(timeout=self._open_close_timeout) as dome: ret = dome.open_shutters(east=True, west=True) if ret == DomeCommandStatus.Succeeded: self.__set_status(DomeStatus.Open) else: self.__set_mode(OperationsMode.Error) elif requested_status == status and environment_safe_age < 30: print('dome: sending heartbeat ping') with self._daemon.connect() as dome: dome.set_heartbeat_timer(self._heartbeat_timeout) except Exception as e: if not self._daemon_error: log.error(self._log_name, 'Lost contact with Dome daemon') self._daemon_error = True print('error: failed to communicate with the dome daemon: ', e) self.__set_mode(OperationsMode.Error) # Clear the schedule if we have passed the close date if self._requested_close_date and current_date > self._requested_close_date: self.clear_open_window() # Wait for the next loop period, unless woken up early by __shortcut_loop_wait with self._wait_condition: self._wait_condition.wait(self._loop_delay)
def run_thread(self): """Thread that runs the hardware actions""" if self.config['onsky'] and not self.dome_is_open: print('Aborting: dome is not open') log.error(self.log_name, 'Aborting: dome is not open') self.status = TelescopeActionStatus.Error return self.set_task('Preparing camera') if not configure_pipeline(self.log_name, self.config['pipeline']): log.error(self.log_name, 'Aborting action') print('Aborting action') self.status = TelescopeActionStatus.Error return if not take_images(self.log_name, self._camera, self.config['count'], self.config['rasa']): log.error(self.log_name, 'Aborting action') print('Aborting action') self.status = TelescopeActionStatus.Error return while True: self.set_task('Acquiring image {} / {}'.format(self._acquired_images + 1, self.config['count'])) # The wait period rate limits the camera status check # The frame received callback will wake this up immedately # TODO: This needs to be rewritten in terms of a timeout # otherwise it may check while the pipeline is processing and fail with self._wait_condition: self._wait_condition.wait(60) if self._acquired_images == self.config['count'] or self.aborted: break if self.config['onsky'] and not self.dome_is_open: print('Aborting: dome is not open') log.error(self.log_name, 'Dome is not open') break # Check camera for error status status = get_camera_status(self.log_name, self._camera) if not status: print('Failed to query camera status') log.error(self.log_name, 'Failed to query camera status') break if status['state'] not in VALID_CAMERA_STATES: message = 'Camera is in unexpected state', CameraStatus.label(status['state']) print(message) log.error(self.log_name, message) if status['state'] == CameraStatus.Idle: remaining = self.config['count'] - self._acquired_images message = 'Restarting remaining {} exposures'.format(remaining) print(message) log.info(self.log_name, message) if not take_images(self.log_name, self._camera, remaining, self.config['rasa']): print('Aborting action') log.error(self.log_name, 'Aborting action') self.status = TelescopeActionStatus.Error return continue break if self.aborted or self._acquired_images == self.config['count']: self.status = TelescopeActionStatus.Complete else: self.status = TelescopeActionStatus.Error
def run_thread(self): """Thread that runs the hardware actions""" self.set_task('Slewing to field') if not tel_slew_radec(self.log_name, math.radians(self.config['ra']), math.radians(self.config['dec']), True, SLEW_TIMEOUT): self.__set_failed_status() return self.set_task('Preparing camera') start_time = datetime.datetime.utcnow() pipeline_config = {} pipeline_config.update(self.config.get('pipeline', {})) pipeline_config.update({ 'hfd': True, 'type': 'JUNK', 'object': 'AutoFocus', }) if not configure_pipeline(self.log_name, pipeline_config): self.__set_failed_status() return self.set_task('Sampling initial HFD') first_hfd = min_hfd = self.measure_current_hfd(COARSE_MEASURE_REPEATS) if first_hfd < 0: self.__set_failed_status() return current_focus = get_focus(self.log_name, self.config['channel']) if current_focus is None: self.__set_failed_status() return log.info(self.log_name, 'AutoFocus: HFD at {} steps is {:.1f}" ({} samples)'.format( current_focus, first_hfd, COARSE_MEASURE_REPEATS)) self.set_task('Searching v-curve position') # Step inwards until we are well defocused on the inside edge of the v curve while True: log.info(self.log_name, 'AutoFocus: Searching for position on v-curve') print('AutoFocus: Searching for position on v-curve (stepping out until hfd > target)') current_focus -= FOCUS_STEP_SIZE if not set_focus(self.log_name, self.config['channel'], current_focus, FOCUS_TIMEOUT): self.__set_failed_status() return current_hfd = self.measure_current_hfd(COARSE_MEASURE_REPEATS) if current_hfd < 0: self.__set_failed_status() return log.info(self.log_name, 'AutoFocus: HFD at {} steps is {:.1f}" ({} samples)'.format( current_focus, current_hfd, COARSE_MEASURE_REPEATS)) min_hfd = min(min_hfd, current_hfd) if current_hfd > TARGET_HFD and current_hfd > min_hfd: log.info(self.log_name, 'AutoFocus: Found position on v-curve') print('AutoFocus: on inside slope') break # We may have stepped to far inwards in the previous step # Step outwards if needed until the current HFD is closer to the target self.set_task('Searching for HFD {}'.format(TARGET_HFD)) while current_hfd > 2 * TARGET_HFD: log.info(self.log_name, 'AutoFocus: Stepping towards HFD {}'.format(TARGET_HFD)) print('AutoFocus: Stepping towards HFD {}'.format(TARGET_HFD)) current_focus -= int(current_hfd / (2 * INSIDE_FOCUS_SLOPE)) if not set_focus(self.log_name, self.config['channel'], current_focus, FOCUS_TIMEOUT): self.__set_failed_status() return current_hfd = self.measure_current_hfd(COARSE_MEASURE_REPEATS) if current_hfd < 0: self.__set_failed_status() return log.info(self.log_name, 'AutoFocus: HFD at {} steps is {:.1f}" ({} samples)'.format( current_focus, current_hfd, COARSE_MEASURE_REPEATS)) # Do a final move to (approximately) the target HFD current_focus += int((TARGET_HFD - current_hfd) / INSIDE_FOCUS_SLOPE) if not set_focus(self.log_name, self.config['channel'], current_focus, FOCUS_TIMEOUT): self.__set_failed_status() return # Take more frames to get an improved HFD estimate at the current position self.set_task('Sampling HFD for final move') current_hfd = self.measure_current_hfd(FINE_MEASURE_REPEATS) if current_hfd < 0: self.__set_failed_status() return log.info(self.log_name, 'AutoFocus: HFD at {} steps is {:.1f}" ({} samples)'.format( current_focus, current_hfd, FINE_MEASURE_REPEATS)) # Jump to target focus using calibrated parameters current_focus += int((CROSSING_HFD - current_hfd) / INSIDE_FOCUS_SLOPE) self.set_task('Moving to focus') if not set_focus(self.log_name, self.config['channel'], current_focus, FOCUS_TIMEOUT): self.__set_failed_status() return self.set_task('Sampling final HFD') current_hfd = self.measure_current_hfd(FINE_MEASURE_REPEATS) runtime = (datetime.datetime.utcnow() - start_time).total_seconds() print('AutoFocus: Achieved HFD of {:.1f}" in {:.0f} seconds'.format(current_hfd, runtime)) log.info(self.log_name, 'AutoFocus: Achieved HFD of {:.1f}" in {:.0f} seconds'.format( current_hfd, runtime)) self.status = TelescopeActionStatus.Complete
def measure_current_hfd(self, exposures=1): """ Takes a set of exposures and returns the smallest MEDHFD value Returns -1 on error """ print('AutoFocus: Sampling HFD') log.info(self.log_name, 'AutoFocus: Sampling HFD') requested = exposures failed = 0 cam_config = {} cam_config.update(self.config.get('rasa', {})) cam_config.update({ 'shutter': True, 'window': [3065, 5112, 2043, 4090] }) # Handle exposures individually # This adds a few seconds of overhead when we want to take # multiple samples, but this is the simpler/safer option for nows samples = [] while True: if len(samples) == requested: print('hfd values:', samples) return np.min(samples) if failed > 5: log.error(self.log_name, 'AutoFocus: Aborting because 5 HFD samples failed') print(failed, 'AutoFocus: Aborting because 5 HFD samples failed') return -1 if not take_images(self.log_name, self._camera, 1, cam_config, quiet=True): return -1 delay = self.config['rasa']['exposure'] + MAX_PROCESSING_TIME expected_complete = datetime.datetime.utcnow() + datetime.timedelta(seconds=delay) while True: if not self.dome_is_open: log.error(self.log_name, 'AutoFocus: Aborting because dome is not open') print(failed, 'AutoFocus: Aborting because dome is not open') return -1 if self.aborted: log.error(self.log_name, 'AutoFocus: Aborted by user') print('AutoFocus: Aborted by user') return -1 measurement = self._focus_measurement if measurement: self._focus_measurement = None if measurement[1] > MINIMUM_OBJECT_COUNT and measurement[0] > MINIMUM_HFD: samples.append(measurement[0]) else: warning = 'AutoFocus: Discarding frame with {} samples ({} HFD)'.format( measurement[1], measurement[0]) print(warning) log.warning(self.log_name, warning) failed += 1 break if datetime.datetime.utcnow() > expected_complete: print('AutoFocus: Exposure timed out - retrying') log.warning(self.log_name, 'AutoFocus: Exposure timed out - retrying') failed += 1 break with self._wait_condition: self._wait_condition.wait(10)