예제 #1
0
    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)
예제 #2
0
    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
예제 #3
0
    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) + ')')
예제 #4
0
    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}
예제 #5
0
    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}
예제 #6
0
 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()
예제 #7
0
    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.)
예제 #8
0
    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
예제 #9
0
    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
예제 #10
0
    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
예제 #11
0
    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
예제 #12
0
    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.)
예제 #13
0
    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
예제 #14
0
    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)
예제 #15
0
    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.)
예제 #16
0
    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)
예제 #17
0
    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
예제 #18
0
    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
예제 #19
0
    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)