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 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 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 __take_image(self, exposure, delay): """Tells the camera to take an exposure. if exposure is 0 then it will reset the camera configuration and take a bias with the shutter closed """ self._expected_complete = datetime.datetime.utcnow() \ + datetime.timedelta(seconds=exposure + delay + CONFIG['max_processing_time']) try: # Need to communicate directly with camera daemon # to allow set_exposure_delay and set_exosure with self._daemon.connect() as cam: if exposure == 0: # .configure will reset all other parameters to their default values cam_config = {} cam_config.update(self._camera_config) cam_config.update({ 'shutter': False, 'exposure': 0 }) cam.configure(cam_config, quiet=True) else: cam.set_exposure_delay(delay, quiet=True) cam.set_exposure(exposure, quiet=True) cam.set_shutter(True, quiet=True) cam.start_sequence(1, quiet=True) except Pyro4.errors.CommunicationError: print('AutoFlat: Failed to communicate with ' + self.name + ' camera daemon') log.error(self._log_name, 'Failed to communicate with ' + self.name + ' camera daemon') self.state = AutoFlatState.Error except Exception: print('AutoFlat: Unknown error with ' + self.name + ' camera') traceback.print_exc(file=sys.stdout) log.error(self._log_name, 'Unknown error with ' + self.name + ' camera') self.state = AutoFlatState.Error
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 check_timeout(self): """Sets error state if an expected frame is more than 30 seconds late""" if self.state not in [AutoFlatState.Waiting, AutoFlatState.Saving]: return if datetime.datetime.utcnow() > self._expected_complete: print('AutoFlat: ' + self.name + ' camera exposure timed out') log.error(self._log_name, 'AutoFlat: ' + self.name + ' camera exposure timed out') self.state = AutoFlatState.Error
def __run_thread_wrapper(self): """Wrapper that catches exceptions thrown in run_thread implementations and sets the error status """ try: self.run_thread() except: print('error: exception in action run thread:') traceback.print_exc(file=sys.stdout) log.error(self.log_name, 'Exception in action run thread') self.status = TelescopeActionStatus.Error
def stop_camera(log_name, daemon): """Aborts any active exposure sequences""" try: with daemon.connect() as camd: return camd.stop_sequence() == CamCommandStatus.Succeeded except Pyro4.errors.CommunicationError: print('Failed to communicate with camera daemon') log.error(log_name, 'Failed to communicate with camera daemon') return False except Exception: print('Unknown error while stopping camera') traceback.print_exc(file=sys.stdout)
def __initialize_telescope(self): """Initializes and homes the telescope""" try: self.set_task('Initializing Telescope') with daemons.rasa_telescope.connect(timeout=HOME_TIMEOUT) as teld: status = teld.initialize() if status not in [TelCommandStatus.Succeeded, TelCommandStatus.TelescopeNotDisabled]: print('Failed to initialize telescope') log.error(self.log_name, 'Failed to initialize telescope') return False self.set_task('Slewing to park position') with daemons.rasa_telescope.connect(timeout=STOW_TIMEOUT) as teld: status = teld.slew_altaz(STOW_ALTAZ[0], STOW_ALTAZ[1]) if status != TelCommandStatus.Succeeded: print('Failed to park telescope') log.error(self.log_name, 'Failed to park telescope') return False except Pyro4.errors.CommunicationError: print('Failed to communicate with telescope daemon') log.error(self.log_name, 'Failed to communicate with telescope daemon') return False except Exception as e: print('Unknown error with telescope') print(e) log.error(self.log_name, 'Unknown error with telescope') return False return True
def get_camera_status(log_name, daemon): """Returns the status dictionary for the camera""" try: with daemon.connect() as camd: return camd.report_status() except Pyro4.errors.CommunicationError: print('Failed to communicate with camera daemon') log.error(log_name, 'Failed to communicate with camera daemon') return None except Exception: print('Unknown error while querying camera status') traceback.print_exc(file=sys.stdout) log.error(log_name, 'Unknown error with camera') return None
def __initialize_camera(self, name, daemon): """Initializes a given camera and enables cooling""" try: self.set_task('Initializing Cameras') with daemon.connect(timeout=CAM_INIT_TIMEOUT) as cam: status = cam.initialize() if status not in [CamCommandStatus.Succeeded, CamCommandStatus.CameraNotUninitialized]: print('Failed to initialize ' + name + ' camera') log.error(self.log_name, 'Failed to initialize ' + name + ' camera') return False # Calling configure with an empty dictionary resets everything to defaults if cam.configure({}, quiet=True) != CamCommandStatus.Succeeded: print('Failed to reset ' + name + ' camera to defaults') log.error(self.log_name, 'Failed to reset ' + name + ' camera to defaults') return False except Pyro4.errors.CommunicationError: print('Failed to communicate with ' + name + ' camera daemon') log.error(self.log_name, 'Failed to communicate with ' + name + ' camera daemon') return False except Exception as e: print('Unknown error with ' + name + ' camera') print(e) log.error(self.log_name, 'Unknown error with ' + name + ' camera') return False return True
def tel_status(log_name): """Returns the telescope status dict or None on error""" try: with daemons.rasa_telescope.connect() as teld: return teld.report_status() except Pyro4.errors.CommunicationError: print('Failed to communicate with telescope daemon') log.error(log_name, 'Failed to communicate with telescope daemon') return None except Exception: print('Unknown error while querying telescope status') traceback.print_exc(file=sys.stdout) log.error(log_name, 'Unknown error while querying telescope status') return None
def stop_focus(log_name, channel): """Stop the focuser movement""" try: with daemons.rasa_focus.connect() as focusd: focusd.stop_channel(channel) return True except Pyro4.errors.CommunicationError: print('Failed to communicate with focuser daemon') log.error(log_name, 'Failed to communicate with focuser daemon') return False except Exception: print('Unknown error while stopping focuser') traceback.print_exc(file=sys.stdout) log.error(log_name, 'Unknown error while stopping focuser') return False
def tel_stop(log_name): """Stop the telescope tracking or movement""" try: with daemons.rasa_telescope.connect() as teld: teld.stop() return True except Pyro4.errors.CommunicationError: print('Failed to communicate with telescope daemon') log.error(log_name, 'Failed to communicate with telescope daemon') return False except Exception: print('Unknown error while stopping telescope') traceback.print_exc(file=sys.stdout) log.error(log_name, 'Unknown error while stopping telescope') return False
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 take_images(log_name, daemon, count=1, config=None, quiet=False): """Start an exposure sequence with count images If config is non-None it is assumed to contain a dictionary of camera configuration that has been validated by the camera schema, which is applied before starting the sequence. """ try: with daemon.connect() as cam: if config: status = cam.configure(config, quiet=quiet) if not config or status == CamCommandStatus.Succeeded: status = cam.start_sequence(count, quiet=quiet) if status != CamCommandStatus.Succeeded: print('Failed to start exposure sequence') log.error(log_name, 'Failed to start exposure sequence') return False return True except Pyro4.errors.CommunicationError: print('Failed to communicate with camera daemon') log.error(log_name, 'Failed to communicate with camera daemon') return False except Exception: print('Unknown error with camera') traceback.print_exc(file=sys.stdout) log.error(log_name, 'Unknown error with camera') 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 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 get_parameter(self, parameter_name): """Returns 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: args = [ '/usr/bin/snmpget', '-v', '1', '-c', 'public', self._ip, parameter.get_oid ] output = subprocess.check_output(args, universal_newlines=True, timeout=self._query_timeout) return parameter.parse_snmpget_output(output) 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 parameter.error_value
def tel_offset_radec(log_name, ra, dec, timeout): """Offset the telescope by a given RA, Dec""" try: with daemons.rasa_telescope.connect(timeout=timeout) as teld: status = teld.offset_radec(ra, dec) if status != TelCommandStatus.Succeeded: print('Failed to offset telescope position') log.error(log_name, 'Failed to offset telescope position') return False return True except Pyro4.errors.CommunicationError: print('Failed to communicate with telescope daemon') log.error(log_name, 'Failed to communicate with telescope daemon') return False except Exception: print('Unknown error while offsetting telescope') traceback.print_exc(file=sys.stdout) log.error(log_name, 'Unknown error while offsetting telescope') return False
def set_focus(log_name, channel, position, timeout): """Set the given focuser channel to the given position""" try: with daemons.rasa_focus.connect(timeout=timeout) as focusd: print('moving focus {} to {}'.format(channel, position)) status = focusd.set_focus(channel, position) if status != FocCommandStatus.Succeeded: print('Failed to set focuser position') log.error(log_name, 'Failed to set focuser position') return False return True except Pyro4.errors.CommunicationError: print('Failed to communicate with focuser daemon') log.error(log_name, 'Failed to communicate with focuser daemon') return False except Exception: print('Unknown error while configuring focuser') traceback.print_exc(file=sys.stdout) log.error(log_name, 'Unknown error while configuring focuser') return False
def tel_slew_altaz(log_name, alt, az, tracking, timeout): """Slew the telescope to a given Alt, Az""" try: with daemons.rasa_telescope.connect(timeout=timeout) as teld: if tracking: status = teld.track_altaz(alt, az) else: status = teld.slew_altaz(alt, az) if status != TelCommandStatus.Succeeded: print('Failed to slew telescope') log.error(log_name, 'Failed to slew telescope') return False return True except Pyro4.errors.CommunicationError: print('Failed to communicate with telescope daemon') log.error(log_name, 'Failed to communicate with telescope daemon') return False except Exception: print('Unknown error while slewing telescope') traceback.print_exc(file=sys.stdout) log.error(log_name, 'Unknown error while slewing telescope') return False
def get_focus(log_name, channel): """Returns the requested focuser position or None on error Requires focuser to be idle """ try: with daemons.rasa_focus.connect() as focusd: # TODO: Hacking channel id to channel array index is bogus # Maybe push this into focusd itself status = focusd.report_status()['channels'][channel-1] if status['status'] != FocuserStatus.Idle: print('Focuser status is not idle') log.error(log_name, 'Focuser status is not idle') return None return status['current_steps'] except Pyro4.errors.CommunicationError: print('Failed to communicate with focuser daemon') log.error(log_name, 'Failed to communicate with focuser daemon') return None except Exception: print('Unknown error while querying focuser position') traceback.print_exc(file=sys.stdout) log.error(log_name, 'Unknown error while querying focuser position') return None
def __wait_for_temperature_lock(self): """Waits until both cameras have reached their target temperature Returns True on success, False on error """ # Wait for cameras to cool if required self.set_task('Cooling cameras') locked = {k: False for k in self._camera_daemons} while not self.aborted: try: for arm in self._camera_daemons: with self._camera_daemons[arm].connect() as cam: status = cam.report_status() if 'temperature_locked' not in status: print('Failed to check tempearture on ' + arm + ' camera') log.error(self.log_name, 'Failed to check temperature on ' + arm + ' camera') return False locked[arm] = status['temperature_locked'] except Pyro4.errors.CommunicationError: print('Failed to communicate with ' + arm + ' camera daemon') log.error(self.log_name, 'Failed to communicate with ' + arm + ' camera daemon') return False except Exception as e: print('Unknown error with ' + arm + ' camera') print(e) log.error(self.log_name, 'Unknown error with ' + arm + ' camera') return False if all([locked[k] for k in locked]): break with self._cooling_condition: self._cooling_condition.wait(CAMERA_CHECK_INTERVAL) return not self.aborted
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 __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 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 __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 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)
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